You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2933 lines
93 KiB
2933 lines
93 KiB
<?php |
|
|
|
/** |
|
* @file |
|
* Rules base classes and interfaces needed for any rule evaluation. |
|
*/ |
|
|
|
// This is not necessary as the classes are autoloaded via the registry. However |
|
// it saves some possible update headaches until the registry is rebuilt. |
|
// @todo Remove for a future release. |
|
require_once dirname(__FILE__) . '/faces.inc'; |
|
|
|
/** |
|
* Make sure loaded rule configs are instantiated right. |
|
*/ |
|
class RulesEntityController extends EntityAPIControllerExportable { |
|
|
|
/** |
|
* Overridden. |
|
* |
|
* @see EntityAPIController::create() |
|
*/ |
|
public function create(array $values = array()) { |
|
// Default to rules as owning module. |
|
$values += array('owner' => 'rules'); |
|
return parent::create($values); |
|
} |
|
|
|
/** |
|
* Overridden. |
|
* |
|
* @see DrupalDefaultEntityController::attachLoad() |
|
*/ |
|
protected function attachLoad(&$queried_entities, $revision_id = FALSE) { |
|
// Retrieve stdClass records and store them as rules objects in 'data'. |
|
$ids = array_keys($queried_entities); |
|
$result = db_select('rules_tags') |
|
->fields('rules_tags', array('id', 'tag')) |
|
->condition('id', $ids, 'IN') |
|
->execute(); |
|
foreach ($result as $row) { |
|
$tags[$row->id][] = $row->tag; |
|
} |
|
$result = db_select('rules_dependencies') |
|
->fields('rules_dependencies', array('id', 'module')) |
|
->condition('id', $ids, 'IN') |
|
->execute(); |
|
foreach ($result as $row) { |
|
$modules[$row->id][] = $row->module; |
|
} |
|
|
|
$entities = array(); |
|
foreach ($queried_entities as $record) { |
|
$entity = $record->data; |
|
// Set the values of the other columns. |
|
foreach ($this->entityInfo['schema_fields_sql']['base table'] as $field) { |
|
$entity->$field = $record->$field; |
|
} |
|
unset($entity->data, $entity->plugin); |
|
// Add any tags or dependencies. |
|
$entity->dependencies = isset($modules[$entity->id]) ? $modules[$entity->id] : array(); |
|
$entity->tags = isset($tags[$entity->id]) ? $tags[$entity->id] : array(); |
|
$entities[$entity->id] = $entity; |
|
} |
|
$queried_entities = $entities; |
|
parent::attachLoad($queried_entities, $revision_id); |
|
} |
|
|
|
/** |
|
* Override to support having events and tags as conditions. |
|
* |
|
* @see EntityAPIController::applyConditions() |
|
* @see rules_query_rules_config_load_multiple_alter() |
|
*/ |
|
protected function applyConditions($entities, $conditions = array()) { |
|
if (isset($conditions['event']) || isset($conditions['plugin'])) { |
|
foreach ($entities as $key => $entity) { |
|
if (isset($conditions['event']) && (!($entity instanceof RulesTriggerableInterface) || !in_array($conditions['event'], $entity->events()))) { |
|
unset($entities[$key]); |
|
} |
|
if (isset($conditions['plugin']) && !is_array($conditions['plugin'])) { |
|
$conditions['plugin'] = array($conditions['plugin']); |
|
} |
|
if (isset($conditions['plugin']) && !in_array($entity->plugin(), $conditions['plugin'])) { |
|
unset($entities[$key]); |
|
} |
|
} |
|
unset($conditions['event'], $conditions['plugin']); |
|
} |
|
if (!empty($conditions['tags'])) { |
|
foreach ($entities as $key => $entity) { |
|
foreach ($conditions['tags'] as $tag) { |
|
if (in_array($tag, $entity->tags)) { |
|
continue 2; |
|
} |
|
} |
|
unset($entities[$key]); |
|
} |
|
unset($conditions['tags']); |
|
} |
|
return parent::applyConditions($entities, $conditions); |
|
} |
|
|
|
/** |
|
* Overridden to work with Rules' custom export format. |
|
* |
|
* @param string $export |
|
* A serialized string in JSON format as produced by the |
|
* RulesPlugin::export() method, or the PHP export as usual PHP array. |
|
* @param string $error_msg |
|
* The error message. |
|
*/ |
|
public function import($export, &$error_msg = '') { |
|
$export = is_array($export) ? $export : drupal_json_decode($export); |
|
if (!is_array($export)) { |
|
$error_msg = t('Unable to parse the pasted export.'); |
|
return FALSE; |
|
} |
|
// The key is the configuration name and the value the actual export. |
|
$name = key($export); |
|
$export = current($export); |
|
if (!isset($export['PLUGIN'])) { |
|
$error_msg = t('Export misses plugin information.'); |
|
return FALSE; |
|
} |
|
// Create an empty configuration, re-set basic keys and import. |
|
$config = rules_plugin_factory($export['PLUGIN']); |
|
$config->name = $name; |
|
foreach (array('label', 'active', 'weight', 'tags', 'access_exposed', 'owner') as $key) { |
|
if (isset($export[strtoupper($key)])) { |
|
$config->$key = $export[strtoupper($key)]; |
|
} |
|
} |
|
if (!empty($export['REQUIRES'])) { |
|
foreach ($export['REQUIRES'] as $module) { |
|
if (!module_exists($module)) { |
|
$error_msg = t('Missing the required module %module.', array('%module' => $module)); |
|
return FALSE; |
|
} |
|
} |
|
$config->dependencies = $export['REQUIRES']; |
|
} |
|
$config->import($export); |
|
return $config; |
|
} |
|
|
|
public function save($rules_config, DatabaseTransaction $transaction = NULL) { |
|
$transaction = isset($transaction) ? $transaction : db_transaction(); |
|
|
|
// Load the stored entity, if any. |
|
if (!isset($rules_config->original) && $rules_config->{$this->idKey}) { |
|
$rules_config->original = entity_load_unchanged($this->entityType, $rules_config->{$this->idKey}); |
|
} |
|
$original = isset($rules_config->original) ? $rules_config->original : NULL; |
|
|
|
$return = parent::save($rules_config, $transaction); |
|
$this->storeTags($rules_config); |
|
if ($rules_config instanceof RulesTriggerableInterface) { |
|
$this->storeEvents($rules_config); |
|
} |
|
$this->storeDependencies($rules_config); |
|
|
|
// See if there are any events that have been removed. |
|
if ($original && $rules_config->plugin == 'reaction rule') { |
|
foreach (array_diff($original->events(), $rules_config->events()) as $event_name) { |
|
// Check if the event handler implements the event dispatcher interface. |
|
$handler = rules_get_event_handler($event_name, $rules_config->getEventSettings($event_name)); |
|
if (!$handler instanceof RulesEventDispatcherInterface) { |
|
continue; |
|
} |
|
|
|
// Only stop an event dispatcher if there are no rules for it left. |
|
if (!rules_config_load_multiple(FALSE, array('event' => $event_name, 'plugin' => 'reaction rule', 'active' => TRUE)) && $handler->isWatching()) { |
|
$handler->stopWatching(); |
|
} |
|
} |
|
} |
|
|
|
return $return; |
|
} |
|
|
|
/** |
|
* Save tagging information to the rules_tags table. |
|
*/ |
|
protected function storeTags($rules_config) { |
|
db_delete('rules_tags') |
|
->condition('id', $rules_config->id) |
|
->execute(); |
|
if (!empty($rules_config->tags)) { |
|
foreach ($rules_config->tags as $tag) { |
|
db_insert('rules_tags') |
|
->fields(array('id', 'tag'), array($rules_config->id, $tag)) |
|
->execute(); |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* Save event information to the rules_trigger table. |
|
*/ |
|
protected function storeEvents(RulesTriggerableInterface $rules_config) { |
|
db_delete('rules_trigger') |
|
->condition('id', $rules_config->id) |
|
->execute(); |
|
foreach ($rules_config->events() as $event) { |
|
db_insert('rules_trigger') |
|
->fields(array( |
|
'id' => $rules_config->id, |
|
'event' => $event, |
|
)) |
|
->execute(); |
|
} |
|
} |
|
|
|
protected function storeDependencies($rules_config) { |
|
db_delete('rules_dependencies') |
|
->condition('id', $rules_config->id) |
|
->execute(); |
|
if (!empty($rules_config->dependencies)) { |
|
foreach ($rules_config->dependencies as $dependency) { |
|
db_insert('rules_dependencies') |
|
->fields(array( |
|
'id' => $rules_config->id, |
|
'module' => $dependency, |
|
)) |
|
->execute(); |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* Overridden to support tags and events in $conditions. |
|
* |
|
* @see EntityAPIControllerExportable::buildQuery() |
|
*/ |
|
protected function buildQuery($ids, $conditions = array(), $revision_id = FALSE) { |
|
$query = parent::buildQuery($ids, $conditions, $revision_id); |
|
$query_conditions =& $query->conditions(); |
|
foreach ($query_conditions as &$condition) { |
|
// One entry in $query_conditions is a string with key '#conjunction'. |
|
// @see QueryConditionInterface::conditions() |
|
if (is_array($condition)) { |
|
// Support using 'tags' => array('tag1', 'tag2') as condition. |
|
if ($condition['field'] == 'base.tags') { |
|
$query->join('rules_tags', 'rt', 'base.id = rt.id'); |
|
$condition['field'] = 'rt.tag'; |
|
} |
|
// Support using 'event' => $name as condition. |
|
if ($condition['field'] == 'base.event') { |
|
$query->join('rules_trigger', 'tr', "base.id = tr.id"); |
|
$condition['field'] = 'tr.event'; |
|
// Use like operator to support % wildcards also. |
|
$condition['operator'] = 'LIKE'; |
|
} |
|
} |
|
} |
|
return $query; |
|
} |
|
|
|
/** |
|
* Overridden to also delete tags and events. |
|
* |
|
* @see EntityAPIControllerExportable::delete() |
|
*/ |
|
public function delete($ids, DatabaseTransaction $transaction = NULL) { |
|
$transaction = isset($transaction) ? $transaction : db_transaction(); |
|
// Use entity-load as ids may be the names as well as the ids. |
|
$configs = $ids ? entity_load('rules_config', $ids) : array(); |
|
if ($configs) { |
|
foreach ($configs as $config) { |
|
db_delete('rules_trigger') |
|
->condition('id', $config->id) |
|
->execute(); |
|
db_delete('rules_tags') |
|
->condition('id', $config->id) |
|
->execute(); |
|
db_delete('rules_dependencies') |
|
->condition('id', $config->id) |
|
->execute(); |
|
} |
|
} |
|
$return = parent::delete($ids, $transaction); |
|
|
|
// Stop event dispatchers when deleting the last rule of an event set. |
|
$processed = array(); |
|
foreach ($configs as $config) { |
|
if ($config->getPluginName() != 'reaction rule') { |
|
continue; |
|
} |
|
|
|
foreach ($config->events() as $event_name) { |
|
// Only process each event once. |
|
if (!empty($processed[$event_name])) { |
|
continue; |
|
} |
|
$processed[$event_name] = TRUE; |
|
|
|
// Check if the event handler implements the event dispatcher interface. |
|
$handler = rules_get_event_handler($event_name, $config->getEventSettings($event_name)); |
|
if (!$handler instanceof RulesEventDispatcherInterface) { |
|
continue; |
|
} |
|
|
|
// Only stop an event dispatcher if there are no rules for it left. |
|
if ($handler->isWatching() && !rules_config_load_multiple(FALSE, array('event' => $event_name, 'plugin' => 'reaction rule', 'active' => TRUE))) { |
|
$handler->stopWatching(); |
|
} |
|
} |
|
} |
|
|
|
return $return; |
|
} |
|
|
|
} |
|
|
|
/** |
|
* Base class for RulesExtendables. |
|
* |
|
* The RulesExtendable uses the rules cache to setup the defined extenders |
|
* and overrides automatically. |
|
* As soon faces is used the faces information is autoloaded using setUp(). |
|
*/ |
|
abstract class RulesExtendable extends FacesExtendable { |
|
|
|
/** |
|
* The name of the info definitions associated with info about this class. |
|
* |
|
* This would be defined abstract, if possible. Common rules hooks with class |
|
* info are e.g. plugin_info and data_info. |
|
*/ |
|
protected $hook; |
|
|
|
/** |
|
* The name of the item this class represents in the info hook. |
|
* |
|
* @var string |
|
*/ |
|
protected $itemName; |
|
|
|
protected $cache; |
|
|
|
/** |
|
* @var array |
|
*/ |
|
protected $itemInfo = array(); |
|
|
|
public function __construct() { |
|
$this->setUp(); |
|
} |
|
|
|
protected function setUp() { |
|
// Keep a reference on the cache, so elements created during cache |
|
// rebuilding end up with a complete cache in the end too. |
|
$this->cache = &rules_get_cache(); |
|
if (isset($this->cache[$this->hook][$this->itemName])) { |
|
$this->itemInfo = &$this->cache[$this->hook][$this->itemName]; |
|
} |
|
// Set up the Faces Extenders. |
|
if (!empty($this->itemInfo['faces_cache'])) { |
|
list($this->facesMethods, $this->facesIncludes, $this->faces) = $this->itemInfo['faces_cache']; |
|
} |
|
} |
|
|
|
/** |
|
* Forces the object to be setUp, this executes setUp() if not done yet. |
|
*/ |
|
public function forceSetUp() { |
|
if (!isset($this->cache) || (!empty($this->itemInfo['faces_cache']) && !$this->faces)) { |
|
$this->setUp(); |
|
} |
|
} |
|
|
|
/** |
|
* Magic method: Invoke the dynamically implemented methods. |
|
*/ |
|
public function __call($name, $arguments = array()) { |
|
$this->forceSetUp(); |
|
return parent::__call($name, $arguments); |
|
} |
|
|
|
public function facesAs($interface = NULL) { |
|
$this->forceSetUp(); |
|
return parent::facesAs($interface); |
|
} |
|
|
|
/** |
|
* Allows items to add something to the rules cache. |
|
*/ |
|
public function rebuildCache(&$itemInfo, &$cache) { |
|
// Speed up setting up items by caching the faces methods. |
|
if (!empty($itemInfo['extenders'])) { |
|
// Apply extenders and overrides. |
|
$itemInfo += array('overrides' => array()); |
|
foreach ($itemInfo['extenders'] as $face => $data) { |
|
$data += array('file' => array()); |
|
if (isset($data['class'])) { |
|
$this->extendByClass($face, $data['class'], $data['file']); |
|
} |
|
elseif (isset($data['methods'])) { |
|
$this->extend($face, $data['methods'], $data['file']); |
|
} |
|
} |
|
foreach ($itemInfo['overrides'] as $data) { |
|
$data += array('file' => array()); |
|
$this->override($data['methods'], $data['file']); |
|
} |
|
$itemInfo['faces_cache'] = array($this->facesMethods, $this->facesIncludes, $this->faces); |
|
// We don't need that any more. |
|
unset($itemInfo['extenders'], $itemInfo['overrides']); |
|
} |
|
} |
|
|
|
/** |
|
* Returns whether the a RuleExtendable supports the given interface. |
|
* |
|
* @param $itemInfo |
|
* The info about the item as specified in the hook. |
|
* @param $interface |
|
* The interface to check for. |
|
* |
|
* @return bool |
|
* Whether it supports the given interface. |
|
*/ |
|
public static function itemFacesAs(&$itemInfo, $interface) { |
|
return in_array($interface, class_implements($itemInfo['class'])) || isset($itemInfo['faces_cache'][2][$interface]); |
|
} |
|
|
|
} |
|
|
|
/** |
|
* Base class for rules plugins. |
|
* |
|
* We cannot inherit from EntityDB at the same time, so we implement our own |
|
* entity related methods. Any CRUD related actions performed on contained |
|
* plugins are applied and the root element representing the configuration is |
|
* saved. |
|
*/ |
|
abstract class RulesPlugin extends RulesExtendable { |
|
|
|
/** |
|
* If this is a configuration saved to the db, the id of it. |
|
*/ |
|
public $id = NULL; |
|
public $weight = 0; |
|
public $name = NULL; |
|
|
|
/** |
|
* An array of settings for this element. |
|
* |
|
* @var array |
|
*/ |
|
public $settings = array(); |
|
|
|
/** |
|
* Info about this element. Usage depends on the plugin. |
|
* |
|
* @var array |
|
*/ |
|
protected $info = array(); |
|
|
|
/** |
|
* The parent element, if any. |
|
* |
|
* @var RulesContainerPlugin |
|
*/ |
|
protected $parent = NULL; |
|
|
|
protected $cache = NULL; |
|
|
|
/** |
|
* @var array |
|
*/ |
|
protected $hook = 'plugin_info'; |
|
|
|
/** |
|
* Identifies an element inside a configuration. |
|
*/ |
|
protected $elementId = NULL; |
|
|
|
/** |
|
* Static cache for availableVariables(). |
|
*/ |
|
protected $availableVariables; |
|
|
|
/** |
|
* Sets a new parent element. |
|
*/ |
|
public function setParent(RulesContainerPlugin $parent) { |
|
if ($this->parent == $parent) { |
|
return; |
|
} |
|
if (isset($this->parent) && ($key = array_search($this, $this->parent->children)) !== FALSE) { |
|
// Remove element from any previous parent. |
|
unset($this->parent->children[$key]); |
|
$this->parent->resetInternalCache(); |
|
} |
|
// Make sure the interface matches the type of the container. |
|
if (($parent instanceof RulesActionContainer && $this instanceof RulesActionInterface) || |
|
($parent instanceof RulesConditionContainer && $this instanceof RulesConditionInterface)) { |
|
|
|
$this->parent = $parent; |
|
$parent->children[] = $this; |
|
$this->parent->resetInternalCache(); |
|
} |
|
else { |
|
throw new RulesEvaluationException('The given container is incompatible with this element.', array(), $this, RulesLog::ERROR); |
|
} |
|
} |
|
|
|
/** |
|
* Gets the root element of the configuration. |
|
*/ |
|
public function root() { |
|
$element = $this; |
|
while (!$element->isRoot()) { |
|
$element = $element->parent; |
|
} |
|
return $element; |
|
} |
|
|
|
/** |
|
* Returns whether the element is the root of the configuration. |
|
*/ |
|
public function isRoot() { |
|
return empty($this->parent) || isset($this->name); |
|
} |
|
|
|
/** |
|
* Returns the element's parent. |
|
*/ |
|
public function parentElement() { |
|
return $this->parent; |
|
} |
|
|
|
/** |
|
* Returns the element id, which identifies the element inside the config. |
|
*/ |
|
public function elementId() { |
|
if (!isset($this->elementId)) { |
|
$this->elementMap()->index(); |
|
} |
|
return $this->elementId; |
|
} |
|
|
|
/** |
|
* Gets the element map helper object, which helps mapping elements to ids. |
|
* |
|
* @return RulesElementMap |
|
*/ |
|
public function elementMap() { |
|
$config = $this->root(); |
|
if (empty($config->map)) { |
|
$config->map = new RulesElementMap($config); |
|
} |
|
return $config->map; |
|
} |
|
|
|
/** |
|
* Iterate over all elements nested below the current element. |
|
* |
|
* This helper can be used to recursively iterate over all elements of a |
|
* configuration. To iterate over the children only, just regularly iterate |
|
* over the object. |
|
* |
|
* @param int $mode |
|
* (optional) The iteration mode used. See |
|
* RecursiveIteratorIterator::construct(). Defaults to SELF_FIRST. |
|
* |
|
* @return RecursiveIteratorIterator |
|
*/ |
|
public function elements($mode = RecursiveIteratorIterator::SELF_FIRST) { |
|
return new RecursiveIteratorIterator($this, $mode); |
|
} |
|
|
|
/** |
|
* Do a deep clone. |
|
*/ |
|
public function __clone() { |
|
// Make sure the element map is cleared. |
|
// @see self::elementMap() |
|
unset($this->map); |
|
} |
|
|
|
/** |
|
* Returns the depth of this element in the configuration. |
|
*/ |
|
public function depth() { |
|
$element = $this; |
|
$i = 0; |
|
while (!empty($element->parent)) { |
|
$element = $element->parent; |
|
$i++; |
|
} |
|
return $i; |
|
} |
|
|
|
/** |
|
* Execute the configuration. |
|
* |
|
* @param ... |
|
* Arguments to pass to the configuration. |
|
*/ |
|
public function execute() { |
|
return $this->executeByArgs(func_get_args()); |
|
} |
|
|
|
/** |
|
* Execute the configuration by passing arguments in a single array. |
|
*/ |
|
abstract public function executeByArgs($args = array()); |
|
|
|
/** |
|
* Evaluate the element on a given rules evaluation state. |
|
*/ |
|
abstract public function evaluate(RulesState $state); |
|
|
|
protected static function compare(RulesPlugin $a, RulesPlugin $b) { |
|
if ($a->weight == $b->weight) { |
|
return 0; |
|
} |
|
return ($a->weight < $b->weight) ? -1 : 1; |
|
} |
|
|
|
/** |
|
* Returns info about parameters needed by the plugin. |
|
* |
|
* Note that not necessarily all parameters are needed when executing the |
|
* plugin, as values for the parameter might have been already configured via |
|
* the element settings. |
|
* |
|
* @see self::parameterInfo() |
|
*/ |
|
public function pluginParameterInfo() { |
|
return isset($this->info['parameter']) ? $this->info['parameter'] : array(); |
|
} |
|
|
|
/** |
|
* Returns info about parameters needed for executing the configured plugin. |
|
* |
|
* @param bool $optional |
|
* Whether optional parameters should be included. |
|
* |
|
* @see self::pluginParameterInfo() |
|
*/ |
|
public function parameterInfo($optional = FALSE) { |
|
// We have to filter out parameters that are already configured. |
|
foreach ($this->pluginParameterInfo() as $name => $info) { |
|
if (!isset($this->settings[$name . ':select']) && !isset($this->settings[$name]) && ($optional || (empty($info['optional']) && $info['type'] != 'hidden'))) { |
|
$vars[$name] = $info; |
|
} |
|
} |
|
return isset($vars) ? $vars : array(); |
|
} |
|
|
|
/** |
|
* Returns info about variables 'provided' by the plugin. |
|
* |
|
* Note that this method returns info about the provided variables as defined |
|
* by the plugin. Thus this resembles the original info, which may be |
|
* adapted via configuration. |
|
* |
|
* @see self::providesVariables() |
|
*/ |
|
public function pluginProvidesVariables() { |
|
return isset($this->info['provides']) ? $this->info['provides'] : array(); |
|
} |
|
|
|
/** |
|
* Returns info about all variables provided for later evaluated elements. |
|
* |
|
* @see self::pluginProvidesVariables() |
|
*/ |
|
public function providesVariables() { |
|
foreach ($this->pluginProvidesVariables() as $name => $info) { |
|
$info['source name'] = $name; |
|
$info['label'] = isset($this->settings[$name . ':label']) ? $this->settings[$name . ':label'] : $info['label']; |
|
if (isset($this->settings[$name . ':var'])) { |
|
$name = $this->settings[$name . ':var']; |
|
} |
|
$provides[$name] = $info; |
|
} |
|
return isset($provides) ? $provides : array(); |
|
} |
|
|
|
/** |
|
* Returns the info of the plugin. |
|
*/ |
|
public function info() { |
|
return $this->info; |
|
} |
|
|
|
/** |
|
* When converted to a string, just use the export format. |
|
*/ |
|
public function __toString() { |
|
return $this->isRoot() ? $this->export() : entity_var_json_export($this->export()); |
|
} |
|
|
|
/** |
|
* Gets variables to return once the configuration has been executed. |
|
*/ |
|
protected function returnVariables(RulesState $state, $result = NULL) { |
|
$var_info = $this->providesVariables(); |
|
foreach ($var_info as $name => $info) { |
|
try { |
|
$vars[$name] = $this->getArgument($name, $info, $state); |
|
} |
|
catch (RulesEvaluationException $e) { |
|
// Ignore not existing variables. |
|
$vars[$name] = NULL; |
|
} |
|
$var_info[$name] += array('allow null' => TRUE); |
|
} |
|
return isset($vars) ? array_values(rules_unwrap_data($vars, $var_info)) : array(); |
|
} |
|
|
|
/** |
|
* Sets up the execution state for the given arguments. |
|
*/ |
|
public function setUpState(array $args) { |
|
$state = new RulesState(); |
|
$vars = $this->setUpVariables(); |
|
// Fix numerically indexed args to start with 0. |
|
if (!isset($args[rules_array_key($vars)])) { |
|
$args = array_values($args); |
|
} |
|
$offset = 0; |
|
foreach (array_keys($vars) as $i => $name) { |
|
$info = $vars[$name]; |
|
if (!empty($info['handler']) || (isset($info['parameter']) && $info['parameter'] === FALSE)) { |
|
$state->addVariable($name, NULL, $info); |
|
// Count the variables that are not passed as parameters. |
|
$offset++; |
|
} |
|
// Support numerically indexed arrays as well as named parameter style. |
|
// The index is reduced to exclude non-parameter variables. |
|
elseif (isset($args[$i - $offset])) { |
|
$state->addVariable($name, $args[$i - $offset], $info); |
|
} |
|
elseif (isset($args[$name])) { |
|
$state->addVariable($name, $args[$name], $info); |
|
} |
|
elseif (empty($info['optional']) && $info['type'] != 'hidden') { |
|
throw new RulesEvaluationException('Argument %name is missing.', array('%name' => $name), $this, RulesLog::ERROR); |
|
} |
|
} |
|
return $state; |
|
} |
|
|
|
/** |
|
* Returns info about all variables that have to be setup in the state. |
|
*/ |
|
protected function setUpVariables() { |
|
return $this->parameterInfo(TRUE); |
|
} |
|
|
|
/** |
|
* Returns info about variables available to be used as arguments for this element. |
|
* |
|
* As this is called very often, e.g. during integrity checks, we statically |
|
* cache the results. |
|
* |
|
* @see RulesPlugin::resetInternalCache() |
|
*/ |
|
public function availableVariables() { |
|
if (!isset($this->availableVariables)) { |
|
$this->availableVariables = !$this->isRoot() ? $this->parent->stateVariables($this) : RulesState::defaultVariables(); |
|
} |
|
return $this->availableVariables; |
|
} |
|
|
|
/** |
|
* Returns asserted additions to the available variable info. |
|
* |
|
* Any returned info is merged into the variable info, in case the execution |
|
* flow passes the element. |
|
* E.g. this is used to assert the content type of a node if the condition |
|
* is met, such that the per-node type properties are available. |
|
*/ |
|
protected function variableInfoAssertions() { |
|
return array(); |
|
} |
|
|
|
/** |
|
* Gets the name of this plugin instance. |
|
* |
|
* The returned name should identify the code which drives this plugin. |
|
*/ |
|
public function getPluginName() { |
|
return $this->itemName; |
|
} |
|
|
|
/** |
|
* Calculates an array of required modules. |
|
* |
|
* You can use $this->dependencies to access dependencies for saved |
|
* configurations. |
|
*/ |
|
public function dependencies() { |
|
$this->processSettings(); |
|
$modules = isset($this->itemInfo['module']) && $this->itemInfo['module'] != 'rules' ? array($this->itemInfo['module'] => 1) : array(); |
|
foreach ($this->pluginParameterInfo() as $name => $info) { |
|
if (isset($this->settings[$name . ':process']) && $this->settings[$name . ':process'] instanceof RulesDataProcessor) { |
|
$modules += array_flip($this->settings[$name . ':process']->dependencies()); |
|
} |
|
} |
|
return array_keys($modules); |
|
} |
|
|
|
/** |
|
* Whether the currently logged in user has access to all configured elements. |
|
* |
|
* Note that this only checks whether the current user has permission to all |
|
* configured elements, but not whether a user has access to configure Rule |
|
* configurations in general. Use rules_config_access() for that. |
|
* |
|
* Use this to determine access permissions for configuring or triggering the |
|
* execution of certain configurations independent of the Rules UI. |
|
* |
|
* @see rules_config_access() |
|
*/ |
|
public function access() { |
|
$this->processSettings(); |
|
foreach ($this->pluginParameterInfo() as $name => $info) { |
|
if (isset($this->settings[$name . ':select']) && $wrapper = $this->applyDataSelector($this->settings[$name . ':select'])) { |
|
if ($wrapper->access('view') === FALSE) { |
|
return FALSE; |
|
} |
|
} |
|
// Incorporate access checks for data processors and input evaluators. |
|
if (isset($this->settings[$name . ':process']) && $this->settings[$name . ':process'] instanceof RulesDataProcessor && !$this->settings[$name . ':process']->editAccess()) { |
|
return FALSE; |
|
} |
|
} |
|
return TRUE; |
|
} |
|
|
|
/** |
|
* Processes the settings e.g. to prepare input evaluators. |
|
* |
|
* Usually settings get processed automatically, however if $this->settings |
|
* has been altered manually after element construction, it needs to be |
|
* invoked explicitly with $force set to TRUE. |
|
*/ |
|
public function processSettings($force = FALSE) { |
|
// Process if not done yet. |
|
if ($force || !empty($this->settings['#_needs_processing'])) { |
|
$var_info = $this->availableVariables(); |
|
foreach ($this->pluginParameterInfo() as $name => $info) { |
|
// Prepare input evaluators. |
|
if (isset($this->settings[$name])) { |
|
$this->settings[$name . ':process'] = $this->settings[$name]; |
|
RulesDataInputEvaluator::prepareSetting($this->settings[$name . ':process'], $info, $var_info); |
|
} |
|
// Prepare data processors. |
|
elseif (isset($this->settings[$name . ':select']) && !empty($this->settings[$name . ':process'])) { |
|
RulesDataProcessor::prepareSetting($this->settings[$name . ':process'], $info, $var_info); |
|
} |
|
// Clean up. |
|
if (empty($this->settings[$name . ':process'])) { |
|
unset($this->settings[$name . ':process']); |
|
} |
|
} |
|
unset($this->settings['#_needs_processing']); |
|
} |
|
} |
|
|
|
/** |
|
* Makes sure the plugin is configured right. |
|
* |
|
* "Configured right" means all needed variables are available in the |
|
* element's scope and dependent modules are enabled. |
|
* |
|
* @return $this |
|
* |
|
* @throws RulesIntegrityException |
|
* In case of a failed integrity check, a RulesIntegrityException exception |
|
* is thrown. |
|
*/ |
|
public function integrityCheck() { |
|
// First process the settings if not done yet. |
|
$this->processSettings(); |
|
// Check dependencies using the pre-calculated dependencies stored in |
|
// $this->dependencies. Fail back to calculation them on the fly, e.g. |
|
// during creation. |
|
$dependencies = empty($this->dependencies) ? $this->dependencies() : $this->dependencies; |
|
foreach ($dependencies as $module) { |
|
if (!module_exists($module)) { |
|
throw new RulesDependencyException(t('Missing required module %name.', array('%name' => $module))); |
|
} |
|
} |
|
// Check the parameter settings. |
|
$this->checkParameterSettings(); |
|
// Check variable names for provided variables to be valid. |
|
foreach ($this->pluginProvidesVariables() as $name => $info) { |
|
if (isset($this->settings[$name . ':var'])) { |
|
$this->checkVarName($this->settings[$name . ':var']); |
|
} |
|
} |
|
return $this; |
|
} |
|
|
|
protected function checkVarName($name) { |
|
if (!preg_match('/^[0-9a-zA-Z_]*$/', $name)) { |
|
throw new RulesIntegrityException(t('%plugin: The variable name %name contains not allowed characters.', array('%plugin' => $this->getPluginName(), '%name' => $name)), $this); |
|
} |
|
} |
|
|
|
/** |
|
* Checks whether parameters are correctly configured. |
|
*/ |
|
protected function checkParameterSettings() { |
|
foreach ($this->pluginParameterInfo() as $name => $info) { |
|
if (isset($info['restriction']) && $info['restriction'] == 'selector' && isset($this->settings[$name])) { |
|
throw new RulesIntegrityException(t("The parameter %name may only be configured using a selector.", array('%name' => $name)), array($this, 'parameter', $name)); |
|
} |
|
elseif (isset($info['restriction']) && $info['restriction'] == 'input' && isset($this->settings[$name . ':select'])) { |
|
throw new RulesIntegrityException(t("The parameter %name may not be configured using a selector.", array('%name' => $name)), array($this, 'parameter', $name)); |
|
} |
|
elseif (!empty($this->settings[$name . ':select']) && !$this->applyDataSelector($this->settings[$name . ':select'])) { |
|
throw new RulesIntegrityException(t("Data selector %selector for parameter %name is invalid.", array('%selector' => $this->settings[$name . ':select'], '%name' => $name)), array($this, 'parameter', $name)); |
|
} |
|
elseif ($arg_info = $this->getArgumentInfo($name)) { |
|
// If we have enough metadata, check whether the types match. |
|
if (!RulesData::typesMatch($arg_info, $info)) { |
|
throw new RulesIntegrityException(t("The data type of the configured argument does not match the parameter's %name requirement.", array('%name' => $name)), array($this, 'parameter', $name)); |
|
} |
|
} |
|
elseif (!$this->isRoot() && !isset($this->settings[$name]) && empty($info['optional']) && $info['type'] != 'hidden') { |
|
throw new RulesIntegrityException(t('Missing configuration for parameter %name.', array('%name' => $name)), array($this, 'parameter', $name)); |
|
} |
|
// @todo Make sure used values are allowed. |
|
// (key/value pairs + allowed values). |
|
} |
|
} |
|
|
|
/** |
|
* Returns the argument for the parameter $name described with $info. |
|
* |
|
* Returns the argument as configured in the element settings for the |
|
* parameter $name described with $info. |
|
* |
|
* @param string $name |
|
* The name of the parameter for which to get the argument. |
|
* @param $info |
|
* Info about the parameter. |
|
* @param RulesState $state |
|
* The current evaluation state. |
|
* @param string $langcode |
|
* (optional) The language code used to get the argument value if the |
|
* argument value should be translated. By default (NULL) the current |
|
* interface language will be used. |
|
* |
|
* @return |
|
* The argument, possibly wrapped. |
|
* |
|
* @throws RulesEvaluationException |
|
* In case the argument cannot be retrieved an exception is thrown. |
|
*/ |
|
protected function getArgument($name, $info, RulesState $state, $langcode = NULL) { |
|
// Only apply the langcode if the parameter has been marked translatable. |
|
if (empty($info['translatable'])) { |
|
$langcode = LANGUAGE_NONE; |
|
} |
|
elseif (!isset($langcode)) { |
|
$langcode = $GLOBALS['language']->language; |
|
} |
|
|
|
if (!empty($this->settings[$name . ':select'])) { |
|
$arg = $state->applyDataSelector($this->settings[$name . ':select'], $langcode); |
|
} |
|
elseif (isset($this->settings[$name])) { |
|
$arg = rules_wrap_data($this->settings[$name], $info); |
|
// We don't sanitize directly specified values. |
|
$skip_sanitize = TRUE; |
|
} |
|
elseif ($state->varinfo($name)) { |
|
$arg = $state->get($name); |
|
} |
|
elseif (empty($info['optional']) && $info['type'] != 'hidden') { |
|
throw new RulesEvaluationException('Required parameter %name is missing.', array('%name' => $name), $this, RulesLog::ERROR); |
|
} |
|
else { |
|
$arg = isset($info['default value']) ? $info['default value'] : NULL; |
|
$skip_sanitize = TRUE; |
|
$info['allow null'] = TRUE; |
|
} |
|
// Make sure the given value is set if required (default). |
|
if (!isset($arg) && empty($info['allow null'])) { |
|
throw new RulesEvaluationException('The provided argument for parameter %name is empty.', array('%name' => $name), $this); |
|
} |
|
|
|
// Support passing already sanitized values. |
|
if ($info['type'] == 'text' && !isset($skip_sanitize) && !empty($info['sanitize']) && !($arg instanceof EntityMetadataWrapper)) { |
|
$arg = check_plain((string) $arg); |
|
} |
|
|
|
// Apply any configured data processors. |
|
if (!empty($this->settings[$name . ':process'])) { |
|
// For processing, make sure the data is unwrapped now. |
|
$return = rules_unwrap_data(array($arg), array($info)); |
|
// @todo For Drupal 8: Refactor to add the name and language code as |
|
// separate parameter to process(). |
|
$info['#name'] = $name; |
|
$info['#langcode'] = $langcode; |
|
return isset($return[0]) ? $this->settings[$name . ':process']->process($return[0], $info, $state, $this) : NULL; |
|
} |
|
return $arg; |
|
} |
|
|
|
/** |
|
* Gets the right arguments for executing the element. |
|
* |
|
* @throws RulesEvaluationException |
|
* If case an argument cannot be retrieved an exception is thrown. |
|
*/ |
|
protected function getExecutionArguments(RulesState $state) { |
|
$parameters = $this->pluginParameterInfo(); |
|
// If there is language parameter, get its value first so it can be used |
|
// for getting other translatable values. |
|
$langcode = NULL; |
|
if (isset($parameters['language'])) { |
|
$lang_arg = $this->getArgument('language', $parameters['language'], $state); |
|
$langcode = $lang_arg instanceof EntityMetadataWrapper ? $lang_arg->value() : $lang_arg; |
|
} |
|
// Now get all arguments. |
|
foreach ($parameters as $name => $info) { |
|
$args[$name] = $name == 'language' ? $lang_arg : $this->getArgument($name, $info, $state, $langcode); |
|
} |
|
// Append the settings and the execution state. Faces will append $this. |
|
$args['settings'] = $this->settings; |
|
$args['state'] = $state; |
|
// Make the wrapped variables for the arguments available in the state. |
|
$state->currentArguments = $args; |
|
return rules_unwrap_data($args, $parameters); |
|
} |
|
|
|
/** |
|
* Applies the given data selector. |
|
* |
|
* Applies the given data selector by using the info about available |
|
* variables. Thus it doesn't require an actual evaluation state. |
|
* |
|
* @param string $selector |
|
* The selector string, e.g. "node:author:mail". |
|
* |
|
* @return EntityMetadataWrapper |
|
* An empty wrapper for the given selector or FALSE if the selector couldn't |
|
* be applied. |
|
*/ |
|
public function applyDataSelector($selector) { |
|
$parts = explode(':', str_replace('-', '_', $selector), 2); |
|
if (($vars = $this->availableVariables()) && isset($vars[$parts[0]]['type'])) { |
|
$wrapper = rules_wrap_data(NULL, $vars[$parts[0]], TRUE); |
|
if (count($parts) > 1 && $wrapper instanceof EntityMetadataWrapper) { |
|
try { |
|
foreach (explode(':', $parts[1]) as $name) { |
|
if ($wrapper instanceof EntityListWrapper || $wrapper instanceof EntityStructureWrapper) { |
|
$wrapper = $wrapper->get($name); |
|
} |
|
else { |
|
return FALSE; |
|
} |
|
} |
|
} |
|
// Return FALSE if there is no wrappper or we get an exception. |
|
catch (EntityMetadataWrapperException $e) { |
|
return FALSE; |
|
} |
|
} |
|
} |
|
return isset($wrapper) ? $wrapper : FALSE; |
|
} |
|
|
|
/** |
|
* Returns info about the configured argument. |
|
* |
|
* @return |
|
* The determined info. If it's not known NULL is returned. |
|
*/ |
|
public function getArgumentInfo($name) { |
|
$vars = $this->availableVariables(); |
|
if (!empty($this->settings[$name . ':select']) && !empty($vars[$this->settings[$name . ':select']])) { |
|
return $vars[$this->settings[$name . ':select']]; |
|
} |
|
elseif (!empty($this->settings[$name . ':select'])) { |
|
if ($wrapper = $this->applyDataSelector($this->settings[$name . ':select'])) { |
|
return $wrapper->info(); |
|
} |
|
return; |
|
} |
|
elseif (isset($this->settings[$name . ':type'])) { |
|
return array('type' => $this->settings[$name . ':type']); |
|
} |
|
elseif (!isset($this->settings[$name]) && isset($vars[$name])) { |
|
return $vars[$name]; |
|
} |
|
} |
|
|
|
/** |
|
* Saves the configuration to the database. |
|
* |
|
* The configuration is saved regardless whether this method is invoked on |
|
* the rules configuration or a contained rule element. |
|
*/ |
|
public function save($name = NULL, $module = 'rules') { |
|
if (isset($this->parent)) { |
|
$this->parent->sortChildren(); |
|
return $this->parent->save($name, $module); |
|
} |
|
else { |
|
// Update the dirty flag before saving. |
|
// However, this operation depends on a fully built Rules-cache, so skip |
|
// it when entities in code are imported to the database. |
|
// @see _rules_rebuild_cache() |
|
if (empty($this->is_rebuild)) { |
|
rules_config_update_dirty_flag($this, FALSE); |
|
// In case the config is not dirty, pre-calculate the dependencies for |
|
// later checking. Note that this also triggers processing settings if |
|
// necessary. |
|
// @see rules_modules_enabled() |
|
if (empty($this->dirty)) { |
|
$this->dependencies = $this->dependencies(); |
|
} |
|
} |
|
|
|
$this->plugin = $this->itemName; |
|
$this->name = isset($name) ? $name : $this->name; |
|
// Module stores the module via which the rule is configured and is used |
|
// for generating machine names with the right prefix. However, for |
|
// default configurations 'module' points to the module providing the |
|
// default configuration, so the module via which the rules is configured |
|
// is stored in the "owner" property. |
|
// @todo For Drupal 8 use "owner" for generating machine names also and |
|
// module only for the modules providing default configurations. |
|
$this->module = !isset($this->module) || $module != 'rules' ? $module : $this->module; |
|
if (!isset($this->owner)) { |
|
$this->owner = 'rules'; |
|
} |
|
$this->ensureNameExists(); |
|
$this->data = $this; |
|
$return = entity_get_controller('rules_config')->save($this); |
|
unset($this->data); |
|
|
|
// Care about clearing necessary caches. |
|
if (!empty($this->is_rebuild)) { |
|
rules_clear_cache(); |
|
} |
|
else { |
|
$plugin_info = $this->pluginInfo(); |
|
if (!empty($plugin_info['component'])) { |
|
// When component variables changes rebuild the complete cache so the |
|
// changes to the provided action/condition take affect. |
|
if (empty($this->original) || $this->componentVariables() != $this->original->componentVariables()) { |
|
rules_clear_cache(); |
|
} |
|
// Clear components cached for evaluation. |
|
cache_clear_all('comp_', 'cache_rules', TRUE); |
|
} |
|
elseif ($this->plugin == 'reaction rule') { |
|
// Clear event sets cached for evaluation. |
|
cache_clear_all('event_', 'cache_rules', TRUE); |
|
// Clear event whitelist for rebuild. |
|
cache_clear_all('rules_event_whitelist', 'cache_rules', TRUE); |
|
} |
|
drupal_static_reset('rules_get_cache'); |
|
drupal_static_reset('rules_config_update_dirty_flag'); |
|
} |
|
|
|
return $return; |
|
} |
|
} |
|
|
|
/** |
|
* Ensure the configuration has a name. If not, generate one. |
|
*/ |
|
protected function ensureNameExists() { |
|
if (!isset($this->module)) { |
|
$this->module = 'rules'; |
|
} |
|
if (!isset($this->name)) { |
|
// Find a unique name for this configuration. |
|
$this->name = $this->module . '_'; |
|
for ($i = 0; $i < 8; $i++) { |
|
// Alphanumeric name generation. |
|
$rnd = mt_rand(97, 122); |
|
$this->name .= chr($rnd); |
|
} |
|
} |
|
} |
|
|
|
public function __sleep() { |
|
// Keep the id always as we need it for the recursion prevention. |
|
$array = drupal_map_assoc(array('parent', 'id', 'elementId', 'weight', 'settings')); |
|
// Keep properties related to configurations if they are there. |
|
$info = entity_get_info('rules_config'); |
|
$fields = array_merge($info['schema_fields_sql']['base table'], array('recursion', 'tags')); |
|
foreach ($fields as $key) { |
|
if (isset($this->$key)) { |
|
$array[$key] = $key; |
|
} |
|
} |
|
return $array; |
|
} |
|
|
|
/** |
|
* Optimizes a rule configuration in order to speed up evaluation. |
|
* |
|
* Additional optimization methods may be inserted by an extender |
|
* implementing the RulesOptimizationInterface. By default, there is no |
|
* optimization extender. |
|
* |
|
* An optimization method may rearrange the internal structure of a |
|
* configuration in order to speed up the evaluation. As the configuration may |
|
* change optimized configurations should not be saved permanently, except |
|
* when saving it temporary, for later execution only. |
|
* |
|
* @see RulesOptimizationInterface |
|
*/ |
|
public function optimize() { |
|
// Make sure settings are processed before configs are cached. |
|
$this->processSettings(); |
|
if ($this->facesAs('RulesOptimizationInterface')) { |
|
$this->__call('optimize'); |
|
} |
|
} |
|
|
|
/** |
|
* Deletes configuration from database. |
|
* |
|
* If invoked on a rules configuration it is deleted from database. If |
|
* invoked on a contained rule element, it's removed from the configuration. |
|
*/ |
|
public function delete() { |
|
if (isset($this->parent)) { |
|
foreach ($this->parent->children as $key => $child) { |
|
if ($child === $this) { |
|
unset($this->parent->children[$key]); |
|
break; |
|
} |
|
} |
|
} |
|
elseif (isset($this->id)) { |
|
entity_get_controller('rules_config')->delete(array($this->name)); |
|
rules_clear_cache(); |
|
} |
|
} |
|
|
|
public function internalIdentifier() { |
|
return isset($this->id) ? $this->id : NULL; |
|
} |
|
|
|
/** |
|
* Returns the config name. |
|
*/ |
|
public function identifier() { |
|
return isset($this->name) ? $this->name : NULL; |
|
} |
|
|
|
public function entityInfo() { |
|
return entity_get_info('rules_config'); |
|
} |
|
|
|
public function entityType() { |
|
return 'rules_config'; |
|
} |
|
|
|
/** |
|
* Checks if the configuration has a certain exportable status. |
|
* |
|
* @param $status |
|
* A status constant, i.e. one of ENTITY_CUSTOM, ENTITY_IN_CODE, |
|
* ENTITY_OVERRIDDEN or ENTITY_FIXED. |
|
* |
|
* @return bool |
|
* TRUE if the configuration has the status, else FALSE. |
|
* |
|
* @see entity_has_status() |
|
*/ |
|
public function hasStatus($status) { |
|
return $this->isRoot() && isset($this->status) && ($this->status & $status) == $status; |
|
} |
|
|
|
/** |
|
* Removes circular object references so PHP garbage collector can work. |
|
*/ |
|
public function destroy() { |
|
parent::destroy(); |
|
$this->parent = NULL; |
|
} |
|
|
|
/** |
|
* Seamlessly invokes the method implemented via faces. |
|
* |
|
* Frees the caller from having to think about references. |
|
*/ |
|
public function form(&$form, &$form_state, array $options = array()) { |
|
$this->__call('form', array(&$form, &$form_state, $options)); |
|
} |
|
|
|
public function form_validate($form, &$form_state) { |
|
$this->__call('form_validate', array($form, &$form_state)); |
|
} |
|
|
|
public function form_submit($form, &$form_state) { |
|
$this->__call('form_submit', array($form, &$form_state)); |
|
} |
|
|
|
/** |
|
* Returns the label of the element. |
|
*/ |
|
public function label() { |
|
if (!empty($this->label) && $this->label != t('unlabeled')) { |
|
return $this->label; |
|
} |
|
$info = $this->info(); |
|
return isset($info['label']) ? $info['label'] : (!empty($this->name) ? $this->name : t('unlabeled')); |
|
} |
|
|
|
/** |
|
* Returns the name of the element's plugin. |
|
*/ |
|
public function plugin() { |
|
return $this->itemName; |
|
} |
|
|
|
/** |
|
* Returns info about the element's plugin. |
|
*/ |
|
public function pluginInfo() { |
|
$this->forceSetUp(); |
|
return $this->itemInfo; |
|
} |
|
|
|
/** |
|
* Applies the given export. |
|
*/ |
|
public function import(array $export) { |
|
$this->importSettings($export[strtoupper($this->plugin())]); |
|
} |
|
|
|
protected function importSettings($export) { |
|
// Import parameter settings. |
|
$export += array('USING' => array(), 'PROVIDE' => array()); |
|
foreach ($export['USING'] as $name => $param_export) { |
|
$this->importParameterSetting($name, $param_export); |
|
} |
|
foreach ($export['PROVIDE'] as $name => $var_export) { |
|
// The key of $var_export is the variable name, the value the label. |
|
$this->settings[$name . ':var'] = rules_array_key($var_export); |
|
$this->settings[$name . ':label'] = reset($var_export); |
|
} |
|
} |
|
|
|
protected function importParameterSetting($name, $export) { |
|
if (is_array($export) && isset($export['select'])) { |
|
$this->settings[$name . ':select'] = $export['select']; |
|
if (count($export) > 1) { |
|
// Add in processor settings. |
|
unset($export['select']); |
|
$this->settings[$name . ':process'] = $export; |
|
} |
|
} |
|
// Convert back the [selector] strings being an array with one entry. |
|
elseif (is_array($export) && count($export) == 1 && isset($export[0])) { |
|
$this->settings[$name . ':select'] = $export[0]; |
|
} |
|
elseif (is_array($export) && isset($export['value'])) { |
|
$this->settings[$name] = $export['value']; |
|
} |
|
else { |
|
$this->settings[$name] = $export; |
|
} |
|
} |
|
|
|
/** |
|
* Exports a rule configuration. |
|
* |
|
* @param string $prefix |
|
* An optional prefix for each line. |
|
* @param bool $php |
|
* (optional) Set to TRUE to format the export using PHP arrays. By default |
|
* JSON is used. |
|
* |
|
* @return |
|
* The exported configuration. |
|
* |
|
* @see rules_import() |
|
*/ |
|
public function export($prefix = '', $php = FALSE) { |
|
$export = $this->exportToArray(); |
|
return $this->isRoot() ? $this->returnExport($export, $prefix, $php) : $export; |
|
} |
|
|
|
protected function exportToArray() { |
|
$export[strtoupper($this->plugin())] = $this->exportSettings(); |
|
return $export; |
|
} |
|
|
|
protected function exportSettings() { |
|
$export = array(); |
|
if (!$this->isRoot()) { |
|
foreach ($this->pluginParameterInfo() as $name => $info) { |
|
if (($return = $this->exportParameterSetting($name, $info)) !== NULL) { |
|
$export['USING'][$name] = $return; |
|
} |
|
} |
|
foreach ($this->providesVariables() as $name => $info) { |
|
if (!empty($info['source name'])) { |
|
$export['PROVIDE'][$info['source name']][$name] = $info['label']; |
|
} |
|
} |
|
} |
|
return $export; |
|
} |
|
|
|
protected function exportParameterSetting($name, $info) { |
|
if (isset($this->settings[$name]) && (empty($info['optional']) || !isset($info['default value']) || $this->settings[$name] != $info['default value'])) { |
|
// In case of an array-value wrap the value into another array, such that |
|
// the value cannot be confused with an exported data selector. |
|
return is_array($this->settings[$name]) ? array('value' => $this->settings[$name]) : $this->settings[$name]; |
|
} |
|
elseif (isset($this->settings[$name . ':select'])) { |
|
if (isset($this->settings[$name . ':process']) && $processor = $this->settings[$name . ':process']) { |
|
$export['select'] = $this->settings[$name . ':select']; |
|
$export += $processor instanceof RulesDataProcessor ? $processor->getChainSettings() : $processor; |
|
return $export; |
|
} |
|
// If there is no processor use a simple array to abbreviate this usual |
|
// case. In JSON this turns to a nice [selector] string. |
|
return array($this->settings[$name . ':select']); |
|
} |
|
} |
|
|
|
/** |
|
* Finalizes the configuration export. |
|
* |
|
* Adds general attributes regarding the configuration and returns it in the |
|
* right format for export. |
|
* |
|
* @param $export |
|
* @param string $prefix |
|
* An optional prefix for each line. |
|
* @param bool $php |
|
* (optional) Set to TRUE to format the export using PHP arrays. By default |
|
* JSON is used. |
|
*/ |
|
protected function returnExport($export, $prefix = '', $php = FALSE) { |
|
$this->ensureNameExists(); |
|
if (!empty($this->label) && $this->label != t('unlabeled')) { |
|
$export_cfg[$this->name]['LABEL'] = $this->label; |
|
} |
|
$export_cfg[$this->name]['PLUGIN'] = $this->plugin(); |
|
if (!empty($this->weight)) { |
|
$export_cfg[$this->name]['WEIGHT'] = $this->weight; |
|
} |
|
if (isset($this->active) && !$this->active) { |
|
$export_cfg[$this->name]['ACTIVE'] = FALSE; |
|
} |
|
if (!empty($this->owner)) { |
|
$export_cfg[$this->name]['OWNER'] = $this->owner; |
|
} |
|
if (!empty($this->tags)) { |
|
$export_cfg[$this->name]['TAGS'] = $this->tags; |
|
} |
|
if ($modules = $this->dependencies()) { |
|
$export_cfg[$this->name]['REQUIRES'] = $modules; |
|
} |
|
if (!empty($this->access_exposed)) { |
|
$export_cfg[$this->name]['ACCESS_EXPOSED'] = $this->access_exposed; |
|
}; |
|
$export_cfg[$this->name] += $export; |
|
return $php ? entity_var_export($export_cfg, $prefix) : entity_var_json_export($export_cfg, $prefix); |
|
} |
|
|
|
/** |
|
* Resets any internal static caches. |
|
* |
|
* This function does not reset regular caches as retrieved via |
|
* rules_get_cache(). Usually, it's invoked automatically when a Rules |
|
* configuration is modified. |
|
* |
|
* Static caches are reset for the element and any elements down the tree. To |
|
* clear static caches of the whole configuration, invoke the function at the |
|
* root. |
|
* |
|
* @see RulesPlugin::availableVariables() |
|
*/ |
|
public function resetInternalCache() { |
|
$this->availableVariables = NULL; |
|
} |
|
|
|
} |
|
|
|
/** |
|
* Defines a common base class for so-called "Abstract Plugins" like actions. |
|
* |
|
* Modules have to provide the concrete plugin implementation. |
|
*/ |
|
abstract class RulesAbstractPlugin extends RulesPlugin { |
|
|
|
protected $elementName; |
|
protected $info = array('parameter' => array(), 'provides' => array()); |
|
protected $infoLoaded = FALSE; |
|
|
|
/** |
|
* @param string $name |
|
* The plugin implementation's name. |
|
* @param $settings |
|
* (optional) Further information provided about the plugin. |
|
* @throws RulesException |
|
* If validation of the passed settings fails RulesExceptions are thrown. |
|
*/ |
|
public function __construct($name = NULL, $settings = array()) { |
|
$this->elementName = $name; |
|
$this->settings = (array) $settings + array('#_needs_processing' => TRUE); |
|
$this->setUp(); |
|
} |
|
|
|
protected function setUp() { |
|
parent::setUp(); |
|
if (isset($this->cache[$this->itemName . '_info'][$this->elementName])) { |
|
$this->info = $this->cache[$this->itemName . '_info'][$this->elementName]; |
|
// Remember that the info has been correctly setup. |
|
// @see self::forceSetup() |
|
$this->infoLoaded = TRUE; |
|
|
|
// Register the defined class, if any. |
|
if (isset($this->info['class'])) { |
|
$this->faces['RulesPluginImplInterface'] = 'RulesPluginImplInterface'; |
|
$face_methods = get_class_methods('RulesPluginImplInterface'); |
|
$class_info = array(1 => $this->info['class']); |
|
foreach ($face_methods as $method) { |
|
$this->facesMethods[$method] = $class_info; |
|
} |
|
} |
|
// Add in per-plugin implementation callbacks if any. |
|
if (!empty($this->info['faces_cache'])) { |
|
foreach ($this->info['faces_cache'] as $face => $data) { |
|
list($methods, $file_names) = $data; |
|
foreach ($methods as $method => $callback) { |
|
$this->facesMethods[$method] = $callback; |
|
} |
|
foreach ((array) $file_names as $method => $name) { |
|
$this->facesIncludes[$method] = array('module' => $this->info['module'], 'name' => $name); |
|
} |
|
} |
|
// Invoke the info_alter callback, but only if it has been implemented. |
|
if ($this->facesMethods['info_alter'] != $this->itemInfo['faces_cache'][0]['info_alter']) { |
|
$this->__call('info_alter', array(&$this->info)); |
|
} |
|
} |
|
} |
|
elseif (!empty($this->itemInfo['faces_cache']) && function_exists($this->elementName)) { |
|
// We don't have any info, so just add the name as execution callback. |
|
$this->override(array('execute' => $this->elementName)); |
|
} |
|
} |
|
|
|
public function forceSetUp() { |
|
if (!isset($this->cache) || (!empty($this->itemInfo['faces_cache']) && !$this->faces)) { |
|
$this->setUp(); |
|
} |
|
// In case we have element specific information, which is not loaded yet, |
|
// do so now. This might happen if the element has been initially loaded |
|
// with an incomplete cache, i.e. during cache rebuilding. |
|
elseif (!$this->infoLoaded && isset($this->cache[$this->itemName . '_info'][$this->elementName])) { |
|
$this->setUp(); |
|
} |
|
} |
|
|
|
/** |
|
* Returns the label of the element. |
|
*/ |
|
public function label() { |
|
$info = $this->info(); |
|
return isset($info['label']) ? $info['label'] : t('@plugin "@name"', array('@name' => $this->elementName, '@plugin' => $this->plugin())); |
|
} |
|
|
|
public function access() { |
|
$info = $this->info(); |
|
$this->loadBasicInclude(); |
|
if (!empty($info['access callback']) && !call_user_func($info['access callback'], $this->itemName, $this->getElementName())) { |
|
return FALSE; |
|
} |
|
return parent::access() && $this->__call('access'); |
|
} |
|
|
|
public function integrityCheck() { |
|
// Do the usual integrity check first so the implementation's validation |
|
// handler can rely on that already. |
|
parent::integrityCheck(); |
|
// Make sure the element is known. |
|
$this->forceSetUp(); |
|
if (!isset($this->cache[$this->itemName . '_info'][$this->elementName])) { |
|
throw new RulesIntegrityException(t('Unknown @plugin %name.', array('@plugin' => $this->plugin(), '%name' => $this->elementName))); |
|
} |
|
$this->validate(); |
|
return $this; |
|
} |
|
|
|
public function processSettings($force = FALSE) { |
|
// Process if not done yet. |
|
if ($force || !empty($this->settings['#_needs_processing'])) { |
|
$this->resetInternalCache(); |
|
// In case the element implements the info alteration callback, (re-)run |
|
// the alteration so that any settings depending info alterations are |
|
// applied. |
|
if ($this->facesMethods && $this->facesMethods['info_alter'] != $this->itemInfo['faces_cache'][0]['info_alter']) { |
|
$this->__call('info_alter', array(&$this->info)); |
|
} |
|
// First let the plugin implementation do processing, so data types of the |
|
// parameters are fixed when we process the settings. |
|
$this->process(); |
|
parent::processSettings($force); |
|
} |
|
} |
|
|
|
public function pluginParameterInfo() { |
|
// Ensure the info alter callback has been executed. |
|
$this->forceSetup(); |
|
return parent::pluginParameterInfo(); |
|
} |
|
|
|
public function pluginProvidesVariables() { |
|
// Ensure the info alter callback has been executed. |
|
$this->forceSetup(); |
|
return parent::pluginProvidesVariables(); |
|
} |
|
|
|
public function info() { |
|
// Ensure the info alter callback has been executed. |
|
$this->forceSetup(); |
|
return $this->info; |
|
} |
|
|
|
protected function variableInfoAssertions() { |
|
// Get the implementation's assertions and map them to the variable names. |
|
if ($assertions = $this->__call('assertions')) { |
|
foreach ($assertions as $param_name => $data) { |
|
$name = isset($this->settings[$param_name . ':select']) ? $this->settings[$param_name . ':select'] : $param_name; |
|
$return[$name] = $data; |
|
} |
|
return $return; |
|
} |
|
} |
|
|
|
public function import(array $export) { |
|
// The key is the element name and the value the actual export. |
|
$this->elementName = rules_array_key($export); |
|
$export = reset($export); |
|
|
|
// After setting the element name, setup the element again so the right |
|
// element info is loaded. |
|
$this->setUp(); |
|
|
|
if (!isset($export['USING']) && !isset($export['PROVIDES']) && !empty($export)) { |
|
// The export has been abbreviated to skip "USING". |
|
$export = array('USING' => $export); |
|
} |
|
$this->importSettings($export); |
|
} |
|
|
|
protected function exportToArray() { |
|
$export = $this->exportSettings(); |
|
if (!$this->providesVariables()) { |
|
// Abbreviate the export making "USING" implicit. |
|
$export = isset($export['USING']) ? $export['USING'] : array(); |
|
} |
|
return array($this->elementName => $export); |
|
} |
|
|
|
public function dependencies() { |
|
$modules = array_flip(parent::dependencies()); |
|
$modules += array_flip((array) $this->__call('dependencies')); |
|
return array_keys($modules + (!empty($this->info['module']) ? array($this->info['module'] => 1) : array())); |
|
} |
|
|
|
public function executeByArgs($args = array()) { |
|
$replacements = array('%label' => $this->label(), '@plugin' => $this->itemName); |
|
rules_log('Executing @plugin %label.', $replacements, RulesLog::INFO, $this, TRUE); |
|
$this->processSettings(); |
|
// If there is no element info, just pass through the passed arguments. |
|
// That way we support executing actions without any info at all. |
|
if ($this->info()) { |
|
$state = $this->setUpState($args); |
|
module_invoke_all('rules_config_execute', $this); |
|
|
|
$result = $this->evaluate($state); |
|
$return = $this->returnVariables($state, $result); |
|
} |
|
else { |
|
rules_log('Unable to execute @plugin %label.', $replacements, RulesLog::ERROR, $this); |
|
} |
|
$state->cleanUp(); |
|
rules_log('Finished executing of @plugin %label.', $replacements, RulesLog::INFO, $this, FALSE); |
|
return $return; |
|
} |
|
|
|
/** |
|
* Execute the configured execution callback and log that. |
|
*/ |
|
abstract protected function executeCallback(array $args, RulesState $state = NULL); |
|
|
|
public function evaluate(RulesState $state) { |
|
$this->processSettings(); |
|
try { |
|
// Get vars as needed for execute and call it. |
|
return $this->executeCallback($this->getExecutionArguments($state), $state); |
|
} |
|
catch (RulesEvaluationException $e) { |
|
rules_log($e->msg, $e->args, $e->severity); |
|
rules_log('Unable to evaluate %name.', array('%name' => $this->getPluginName()), RulesLog::WARN, $this); |
|
} |
|
// Catch wrapper exceptions that might occur due to failures loading an |
|
// entity or similar. |
|
catch (EntityMetadataWrapperException $e) { |
|
rules_log('Unable to get a data value. Error: !error', array('!error' => $e->getMessage()), RulesLog::WARN); |
|
rules_log('Unable to evaluate %name.', array('%name' => $this->getPluginName()), RulesLog::WARN, $this); |
|
} |
|
} |
|
|
|
public function __sleep() { |
|
return parent::__sleep() + array('elementName' => 'elementName'); |
|
} |
|
|
|
public function getPluginName() { |
|
return $this->itemName . " " . $this->elementName; |
|
} |
|
|
|
/** |
|
* Gets the name of the configured action or condition. |
|
*/ |
|
public function getElementName() { |
|
return $this->elementName; |
|
} |
|
|
|
/** |
|
* Add in the data provided by the info hooks to the cache. |
|
*/ |
|
public function rebuildCache(&$itemInfo, &$cache) { |
|
parent::rebuildCache($itemInfo, $cache); |
|
|
|
// Include all declared files so we can find all implementations. |
|
self::includeFiles(); |
|
|
|
// Get the plugin's own info data. |
|
$cache[$this->itemName . '_info'] = rules_fetch_data($this->itemName . '_info'); |
|
foreach ($cache[$this->itemName . '_info'] as $name => &$info) { |
|
$info += array( |
|
'parameter' => isset($info['arguments']) ? $info['arguments'] : array(), |
|
'provides' => isset($info['new variables']) ? $info['new variables'] : array(), |
|
'base' => $name, |
|
'callbacks' => array(), |
|
); |
|
unset($info['arguments'], $info['new variables']); |
|
|
|
if (function_exists($info['base'])) { |
|
$info['callbacks'] += array('execute' => $info['base']); |
|
} |
|
|
|
// We do not need to build a faces cache for RulesPluginHandlerInterface, |
|
// which gets added in automatically as its a parent of |
|
// RulesPluginImplInterface. |
|
unset($this->faces['RulesPluginHandlerInterface']); |
|
|
|
// Build up the per-plugin implementation faces cache. |
|
foreach ($this->faces as $interface) { |
|
$methods = $file_names = array(); |
|
$includes = self::getIncludeFiles($info['module']); |
|
|
|
foreach (get_class_methods($interface) as $method) { |
|
if (isset($info['callbacks'][$method]) && ($function = $info['callbacks'][$method])) { |
|
$methods[$method][0] = $function; |
|
$file_names[$method] = $this->getFileName($function, $includes); |
|
} |
|
// Note that this skips RulesPluginImplInterface, which is not |
|
// implemented by plugin handlers. |
|
elseif (isset($info['class']) && is_subclass_of($info['class'], $interface)) { |
|
$methods[$method][1] = $info['class']; |
|
} |
|
elseif (function_exists($function = $info['base'] . '_' . $method)) { |
|
$methods[$method][0] = $function; |
|
$file_names[$method] = $this->getFileName($function, $includes); |
|
} |
|
} |
|
// Cache only the plugin implementation specific callbacks. |
|
$info['faces_cache'][$interface] = array($methods, array_filter($file_names)); |
|
} |
|
// Filter out interfaces with no overridden methods. |
|
$info['faces_cache'] = rules_filter_array($info['faces_cache'], 0, TRUE); |
|
// We don't need that any more. |
|
unset($info['callbacks'], $info['base']); |
|
} |
|
} |
|
|
|
/** |
|
* Loads this module's .rules.inc file. |
|
* |
|
* Makes sure the providing modules' .rules.inc file is included, as diverse |
|
* callbacks may reside in that file. |
|
*/ |
|
protected function loadBasicInclude() { |
|
static $included = array(); |
|
|
|
if (isset($this->info['module']) && !isset($included[$this->info['module']])) { |
|
$module = $this->info['module']; |
|
module_load_include('inc', $module, $module . '.rules'); |
|
$included[$module] = TRUE; |
|
} |
|
} |
|
|
|
/** |
|
* Makes sure all supported destinations are included. |
|
*/ |
|
public static function includeFiles() { |
|
static $included; |
|
|
|
if (!isset($included)) { |
|
foreach (module_implements('rules_file_info') as $module) { |
|
// rules.inc are already included thanks to the rules_hook_info() group. |
|
foreach (self::getIncludeFiles($module, FALSE) as $name) { |
|
module_load_include('inc', $module, $name); |
|
} |
|
} |
|
$dirs = array(); |
|
foreach (module_implements('rules_directory') as $module) { |
|
// Include all files once, so the discovery can find them. |
|
$result = module_invoke($module, 'rules_directory'); |
|
if (!is_array($result)) { |
|
$result = array($module => $result); |
|
} |
|
$dirs += $result; |
|
} |
|
foreach ($dirs as $module => $directory) { |
|
$module_path = drupal_get_path('module', $module); |
|
foreach (array('inc', 'php') as $extension) { |
|
foreach (glob("$module_path/$directory/*.$extension") as $filename) { |
|
include_once $filename; |
|
} |
|
} |
|
} |
|
$included = TRUE; |
|
} |
|
} |
|
|
|
/** |
|
* Returns all include files for a module. |
|
* |
|
* @param string $module |
|
* The module name. |
|
* @param bool $all |
|
* If FALSE, the $module.rules.inc file isn't added. |
|
* |
|
* @return string[] |
|
* An array containing the names of all the include files for a module. |
|
*/ |
|
protected static function getIncludeFiles($module, $all = TRUE) { |
|
$files = (array) module_invoke($module, 'rules_file_info'); |
|
// Automatically add "$module.rules_forms.inc" and "$module.rules.inc". |
|
$files[] = $module . '.rules_forms'; |
|
if ($all) { |
|
$files[] = $module . '.rules'; |
|
} |
|
return $files; |
|
} |
|
|
|
protected function getFileName($function, $includes) { |
|
static $filenames; |
|
if (!isset($filenames) || !array_key_exists($function, $filenames)) { |
|
$filenames[$function] = NULL; |
|
$reflector = new ReflectionFunction($function); |
|
// On windows the path contains backslashes instead of slashes, fix that. |
|
$file = str_replace('\\', '/', $reflector->getFileName()); |
|
foreach ($includes as $include) { |
|
$pos = strpos($file, $include . '.inc'); |
|
// Test whether the file ends with the given filename.inc. |
|
if ($pos !== FALSE && strlen($file) - $pos == strlen($include) + 4) { |
|
$filenames[$function] = $include; |
|
return $include; |
|
} |
|
} |
|
} |
|
return $filenames[$function]; |
|
} |
|
|
|
} |
|
|
|
/** |
|
* Interface for objects that can be used as actions. |
|
*/ |
|
interface RulesActionInterface { |
|
|
|
/** |
|
* @return |
|
* As specified. |
|
* |
|
* @throws RulesEvaluationException |
|
* Throws an exception if not all necessary arguments have been provided. |
|
*/ |
|
public function execute(); |
|
|
|
} |
|
|
|
/** |
|
* Interface for objects that can be used as conditions. |
|
*/ |
|
interface RulesConditionInterface { |
|
|
|
/** |
|
* @return bool |
|
* |
|
* @throws RulesEvaluationException |
|
* Throws an exception if not all necessary arguments have been provided. |
|
*/ |
|
public function execute(); |
|
|
|
/** |
|
* Negate the result. |
|
*/ |
|
public function negate($negate = TRUE); |
|
|
|
/** |
|
* Returns whether the element is configured to negate the result. |
|
*/ |
|
public function isNegated(); |
|
|
|
} |
|
|
|
/** |
|
* Interface for objects that are triggerable. |
|
*/ |
|
interface RulesTriggerableInterface { |
|
|
|
/** |
|
* Returns the array of (configured) event names associated with this object. |
|
*/ |
|
public function events(); |
|
|
|
/** |
|
* Removes an event from the rule configuration. |
|
* |
|
* @param string $event_name |
|
* The name of the (configured) event to remove. |
|
* |
|
* @return RulesTriggerableInterface |
|
* The object instance itself, to allow chaining. |
|
*/ |
|
public function removeEvent($event_name); |
|
|
|
/** |
|
* Adds the specified event. |
|
* |
|
* @param string $event_name |
|
* The base name of the event to add. |
|
* @param array $settings |
|
* (optional) The event settings. If there are no event settings, pass an |
|
* empty array (default). |
|
* |
|
* @return RulesTriggerableInterface |
|
*/ |
|
public function event($event_name, array $settings = array()); |
|
|
|
/** |
|
* Gets the event settings associated with the given (configured) event. |
|
* |
|
* @param string $event_name |
|
* The (configured) event's name. |
|
* |
|
* @return array|null |
|
* The array of event settings, or NULL if there are no settings. |
|
*/ |
|
public function getEventSettings($event_name); |
|
|
|
} |
|
|
|
/** |
|
* Provides the base interface for implementing abstract plugins via classes. |
|
*/ |
|
interface RulesPluginHandlerInterface { |
|
|
|
/** |
|
* Validates $settings independent from a form submission. |
|
* |
|
* @throws RulesIntegrityException |
|
* In case of validation errors, RulesIntegrityExceptions are thrown. |
|
*/ |
|
public function validate(); |
|
|
|
/** |
|
* Processes settings independent from a form submission. |
|
* |
|
* Processing results may be stored and accessed on execution time |
|
* in $settings. |
|
*/ |
|
public function process(); |
|
|
|
/** |
|
* Allows altering of the element's action/condition info. |
|
* |
|
* Note that this method is also invoked on evaluation time, thus any costly |
|
* operations should be avoided. |
|
* |
|
* @param $element_info |
|
* A reference on the element's info as returned by RulesPlugin::info(). |
|
*/ |
|
public function info_alter(&$element_info); |
|
|
|
/** |
|
* Checks whether the user has access to configure this element. |
|
* |
|
* Note that this only covers access for already created elements. In order to |
|
* control access for creating or using elements specify an 'access callback' |
|
* in the element's info array. |
|
* |
|
* @see hook_rules_action_info() |
|
*/ |
|
public function access(); |
|
|
|
/** |
|
* Returns an array of required modules. |
|
*/ |
|
public function dependencies(); |
|
|
|
/** |
|
* Alters the generated configuration form of the element. |
|
* |
|
* Validation and processing of the settings should be untied from the form |
|
* and implemented in validate() and process() wherever it makes sense. |
|
* For the remaining cases where form tied validation and processing is needed |
|
* make use of the form API #element_validate and #value_callback properties. |
|
*/ |
|
public function form_alter(&$form, $form_state, $options); |
|
|
|
/** |
|
* Returns an array of info assertions for the specified parameters. |
|
* |
|
* This allows conditions to assert additional metadata, such as info about |
|
* the fields of a bundle. |
|
* |
|
* @see RulesPlugin::variableInfoAssertions() |
|
*/ |
|
public function assertions(); |
|
|
|
} |
|
|
|
/** |
|
* Interface for implementing conditions via classes. |
|
* |
|
* In addition to the interface an execute() and a static getInfo() method must |
|
* be implemented. The static getInfo() method has to return the info as |
|
* returned by hook_rules_condition_info() but including an additional 'name' |
|
* key, specifying the plugin name. |
|
* The execute method is the equivalent to the usual execution callback and |
|
* gets the parameters passed as specified in the info array. |
|
* |
|
* See RulesNodeConditionType for an example and rules_discover_plugins() |
|
* for information about class discovery. |
|
*/ |
|
interface RulesConditionHandlerInterface extends RulesPluginHandlerInterface {} |
|
|
|
/** |
|
* Interface for implementing actions via classes. |
|
* |
|
* In addition to the interface an execute() and a static getInfo() method must |
|
* be implemented. The static getInfo() method has to return the info as |
|
* returned by hook_rules_action_info() but including an additional 'name' key, |
|
* specifying the plugin name. |
|
* The execute method is the equivalent to the usual execution callback and |
|
* gets the parameters passed as specified in the info array. |
|
* |
|
* See RulesNodeConditionType for an example and rules_discover_plugins() |
|
* for information about class discovery. |
|
*/ |
|
interface RulesActionHandlerInterface extends RulesPluginHandlerInterface {} |
|
|
|
/** |
|
* Interface used for implementing an abstract plugin via Faces. |
|
* |
|
* Provides the interface used for implementing an abstract plugin by using |
|
* the Faces extension mechanism. |
|
*/ |
|
interface RulesPluginImplInterface extends RulesPluginHandlerInterface { |
|
|
|
/** |
|
* Executes the action or condition making use of the parameters as specified. |
|
*/ |
|
public function execute(); |
|
|
|
} |
|
|
|
/** |
|
* Interface for optimizing evaluation. |
|
* |
|
* @see RulesContainerPlugin::optimize() |
|
*/ |
|
interface RulesOptimizationInterface { |
|
|
|
/** |
|
* Optimizes a rule configuration in order to speed up evaluation. |
|
*/ |
|
public function optimize(); |
|
|
|
} |
|
|
|
/** |
|
* Base class for implementing abstract plugins via classes. |
|
*/ |
|
abstract class RulesPluginHandlerBase extends FacesExtender implements RulesPluginHandlerInterface { |
|
|
|
/** |
|
* @var RulesAbstractPlugin |
|
*/ |
|
protected $element; |
|
|
|
/** |
|
* Overridden to provide $this->element to make the code more meaningful. |
|
*/ |
|
public function __construct(FacesExtendable $object) { |
|
$this->object = $object; |
|
$this->element = $object; |
|
} |
|
|
|
/** |
|
* Implements RulesPluginImplInterface::access(). |
|
*/ |
|
public function access() { |
|
return TRUE; |
|
} |
|
|
|
public function validate() {} |
|
|
|
public function process() {} |
|
|
|
public function info_alter(&$element_info) {} |
|
|
|
public function dependencies() {} |
|
|
|
public function form_alter(&$form, $form_state, $options) {} |
|
|
|
public function assertions() {} |
|
|
|
} |
|
|
|
/** |
|
* Base class for implementing conditions via classes. |
|
*/ |
|
abstract class RulesConditionHandlerBase extends RulesPluginHandlerBase implements RulesConditionHandlerInterface {} |
|
|
|
/** |
|
* Base class for implementing actions via classes. |
|
*/ |
|
abstract class RulesActionHandlerBase extends RulesPluginHandlerBase implements RulesActionHandlerInterface {} |
|
|
|
/** |
|
* Provides default implementations of all RulesPluginImplInterface methods. |
|
* |
|
* If a plugin implementation does not provide a function for a method, the |
|
* default method of this class will be invoked. |
|
* |
|
* @see RulesPluginImplInterface |
|
* @see RulesAbstractPlugin |
|
*/ |
|
class RulesAbstractPluginDefaults extends RulesPluginHandlerBase implements RulesPluginImplInterface { |
|
|
|
public function execute() { |
|
throw new RulesEvaluationException($this->object->getPluginName() . ": Execution implementation is missing.", array(), $this->object, RulesLog::ERROR); |
|
} |
|
|
|
} |
|
|
|
/** |
|
* A RecursiveIterator for rule elements. |
|
*/ |
|
class RulesRecursiveElementIterator extends ArrayIterator implements RecursiveIterator { |
|
|
|
public function getChildren() { |
|
return $this->current()->getIterator(); |
|
} |
|
|
|
public function hasChildren() { |
|
return $this->current() instanceof IteratorAggregate; |
|
} |
|
|
|
} |
|
|
|
/** |
|
* Base class for ContainerPlugins like Rules, Logical Operations or Loops. |
|
*/ |
|
abstract class RulesContainerPlugin extends RulesPlugin implements IteratorAggregate { |
|
|
|
protected $children = array(); |
|
|
|
public function __construct($variables = array()) { |
|
$this->setUp(); |
|
if (!empty($variables) && $this->isRoot()) { |
|
$this->info['variables'] = $variables; |
|
} |
|
} |
|
|
|
/** |
|
* Returns the specified variables, in case the plugin is used as component. |
|
*/ |
|
public function &componentVariables() { |
|
if ($this->isRoot()) { |
|
$this->info += array('variables' => array()); |
|
return $this->info['variables']; |
|
} |
|
// We have to return a reference in any case. |
|
$return = NULL; |
|
return $return; |
|
} |
|
|
|
/** |
|
* Allows access to the children through the iterator. |
|
* |
|
* @return RulesRecursiveElementIterator |
|
*/ |
|
public function getIterator() { |
|
return new RulesRecursiveElementIterator($this->children); |
|
} |
|
|
|
/** |
|
* @return RulesContainerPlugin |
|
*/ |
|
public function integrityCheck() { |
|
if (!empty($this->info['variables']) && !$this->isRoot()) { |
|
throw new RulesIntegrityException(t('%plugin: Specifying state variables is not possible for child elements.', array('%plugin' => $this->getPluginName())), $this); |
|
} |
|
parent::integrityCheck(); |
|
foreach ($this->children as $child) { |
|
$child->integrityCheck(); |
|
} |
|
return $this; |
|
} |
|
|
|
public function dependencies() { |
|
$modules = array_flip(parent::dependencies()); |
|
foreach ($this->children as $child) { |
|
$modules += array_flip($child->dependencies()); |
|
} |
|
return array_keys($modules); |
|
} |
|
|
|
public function parameterInfo($optional = FALSE) { |
|
$params = parent::parameterInfo($optional); |
|
if (isset($this->info['variables'])) { |
|
foreach ($this->info['variables'] as $name => $var_info) { |
|
if (empty($var_info['handler']) && (!isset($var_info['parameter']) || $var_info['parameter'])) { |
|
$params[$name] = $var_info; |
|
// For lists allow empty variables by default. |
|
if (entity_property_list_extract_type($var_info['type'])) { |
|
$params[$name] += array('allow null' => TRUE); |
|
} |
|
} |
|
} |
|
} |
|
return $params; |
|
} |
|
|
|
public function availableVariables() { |
|
if (!isset($this->availableVariables)) { |
|
if ($this->isRoot()) { |
|
$this->availableVariables = RulesState::defaultVariables(); |
|
if (isset($this->info['variables'])) { |
|
$this->availableVariables += $this->info['variables']; |
|
} |
|
} |
|
else { |
|
$this->availableVariables = $this->parent->stateVariables($this); |
|
} |
|
} |
|
return $this->availableVariables; |
|
} |
|
|
|
/** |
|
* Returns available state variables for an element. |
|
* |
|
* Returns info about variables available in the evaluation state for any |
|
* children elements or if given for a special child element. |
|
* |
|
* @param $element |
|
* The element for which the available state variables should be returned. |
|
* If NULL is given, the variables available before any children are invoked |
|
* are returned. If set to TRUE, the variables available after evaluating |
|
* all children will be returned. |
|
*/ |
|
protected function stateVariables($element = NULL) { |
|
$vars = $this->availableVariables(); |
|
if (isset($element)) { |
|
// Add in variables provided by siblings executed before the element. |
|
foreach ($this->children as $child) { |
|
if ($child === $element) { |
|
break; |
|
} |
|
$vars += $child->providesVariables(); |
|
// Take variable info assertions into account. |
|
if ($assertions = $child->variableInfoAssertions()) { |
|
$vars = RulesData::addMetadataAssertions($vars, $assertions); |
|
} |
|
} |
|
} |
|
return $vars; |
|
} |
|
|
|
protected function variableInfoAssertions() { |
|
$assertions = array(); |
|
foreach ($this->children as $child) { |
|
if ($add = $child->variableInfoAssertions()) { |
|
$assertions = rules_update_array($assertions, $add); |
|
} |
|
} |
|
return $assertions; |
|
} |
|
|
|
protected function setUpVariables() { |
|
return isset($this->info['variables']) ? parent::parameterInfo(TRUE) + $this->info['variables'] : $this->parameterInfo(TRUE); |
|
} |
|
|
|
/** |
|
* Executes container with the given arguments. |
|
* |
|
* Condition containers just return a boolean while action containers return |
|
* the configured provided variables as an array of variables. |
|
*/ |
|
public function executeByArgs($args = array()) { |
|
$replacements = array('%label' => $this->label(), '@plugin' => $this->itemName); |
|
rules_log('Executing @plugin %label.', $replacements, RulesLog::INFO, $this, TRUE); |
|
$this->processSettings(); |
|
$state = $this->setUpState($args); |
|
|
|
// Handle recursion prevention. |
|
if ($state->isBlocked($this)) { |
|
return rules_log('Not evaluating @plugin %label to prevent recursion.', array('%label' => $this->label(), '@plugin' => $this->plugin()), RulesLog::INFO); |
|
} |
|
// Block the config to prevent any future recursion. |
|
$state->block($this); |
|
|
|
module_invoke_all('rules_config_execute', $this); |
|
$result = $this->evaluate($state); |
|
$return = $this->returnVariables($state, $result); |
|
|
|
$state->unblock($this); |
|
$state->cleanUp(); |
|
rules_log('Finished executing of @plugin %label.', $replacements, RulesLog::INFO, $this, FALSE); |
|
return $return; |
|
} |
|
|
|
public function access() { |
|
foreach ($this->children as $key => $child) { |
|
if (!$child->access()) { |
|
return FALSE; |
|
} |
|
} |
|
return TRUE; |
|
} |
|
|
|
public function destroy() { |
|
foreach ($this->children as $key => $child) { |
|
$child->destroy(); |
|
} |
|
parent::destroy(); |
|
} |
|
|
|
/** |
|
* By default we do a deep clone. |
|
*/ |
|
public function __clone() { |
|
parent::__clone(); |
|
foreach ($this->children as $key => $child) { |
|
$this->children[$key] = clone $child; |
|
$this->children[$key]->parent = $this; |
|
} |
|
} |
|
|
|
/** |
|
* Overrides delete to keep the children alive, if possible. |
|
*/ |
|
public function delete($keep_children = TRUE) { |
|
if (isset($this->parent) && $keep_children) { |
|
foreach ($this->children as $child) { |
|
$child->setParent($this->parent); |
|
} |
|
} |
|
parent::delete(); |
|
} |
|
|
|
public function __sleep() { |
|
return parent::__sleep() + array('children' => 'children', 'info' => 'info'); |
|
} |
|
|
|
/** |
|
* Sorts all child elements by their weight. |
|
* |
|
* @param bool $deep |
|
* If enabled a deep sort is performed, thus the whole element tree below |
|
* this element is sorted. |
|
*/ |
|
public function sortChildren($deep = FALSE) { |
|
// Make sure the array order is kept in case two children have the same |
|
// weight by ensuring later children would have higher weights. |
|
foreach (array_values($this->children) as $i => $child) { |
|
$child->weight += $i / 1000; |
|
} |
|
usort($this->children, array('RulesPlugin', 'compare')); |
|
|
|
// Fix up the weights afterwards to be unique integers. |
|
foreach (array_values($this->children) as $i => $child) { |
|
$child->weight = $i; |
|
} |
|
|
|
if ($deep) { |
|
foreach (new ParentIterator($this->getIterator()) as $child) { |
|
$child->sortChildren(TRUE); |
|
} |
|
} |
|
$this->resetInternalCache(); |
|
} |
|
|
|
protected function exportChildren($key = NULL) { |
|
$key = isset($key) ? $key : strtoupper($this->plugin()); |
|
$export[$key] = array(); |
|
foreach ($this->children as $child) { |
|
$export[$key][] = $child->export(); |
|
} |
|
return $export; |
|
} |
|
|
|
/** |
|
* Determines whether the element should be exported in flat style. |
|
* |
|
* Flat style means that the export keys are written directly into the export |
|
* array, whereas else the export is written into a sub-array. |
|
*/ |
|
protected function exportFlat() { |
|
// By default we always use flat style for plugins without any parameters |
|
// or provided variables, as then only children have to be exported. E.g. |
|
// this applies to the OR and AND plugins. |
|
return $this->isRoot() || (!$this->pluginParameterInfo() && !$this->providesVariables()); |
|
} |
|
|
|
protected function exportToArray() { |
|
$export = array(); |
|
if (!empty($this->info['variables'])) { |
|
$export['USES VARIABLES'] = $this->info['variables']; |
|
} |
|
if ($this->exportFlat()) { |
|
$export += $this->exportSettings() + $this->exportChildren(); |
|
} |
|
else { |
|
$export[strtoupper($this->plugin())] = $this->exportSettings() + $this->exportChildren(); |
|
} |
|
return $export; |
|
} |
|
|
|
public function import(array $export) { |
|
if (!empty($export['USES VARIABLES'])) { |
|
$this->info['variables'] = $export['USES VARIABLES']; |
|
} |
|
// Care for exports having the export array nested in a sub-array. |
|
if (!$this->exportFlat()) { |
|
$export = reset($export); |
|
} |
|
$this->importSettings($export); |
|
$this->importChildren($export); |
|
} |
|
|
|
protected function importChildren($export, $key = NULL) { |
|
$key = isset($key) ? $key : strtoupper($this->plugin()); |
|
foreach ($export[$key] as $child_export) { |
|
$plugin = _rules_import_get_plugin(rules_array_key($child_export), $this instanceof RulesActionInterface ? 'action' : 'condition'); |
|
$child = rules_plugin_factory($plugin); |
|
$child->setParent($this); |
|
$child->import($child_export); |
|
} |
|
} |
|
|
|
public function resetInternalCache() { |
|
$this->availableVariables = NULL; |
|
foreach ($this->children as $child) { |
|
$child->resetInternalCache(); |
|
} |
|
} |
|
|
|
/** |
|
* Overrides optimize(). |
|
*/ |
|
public function optimize() { |
|
parent::optimize(); |
|
// Now let the children optimize itself. |
|
foreach ($this as $element) { |
|
$element->optimize(); |
|
} |
|
} |
|
|
|
} |
|
|
|
/** |
|
* Base class for all action containers. |
|
*/ |
|
abstract class RulesActionContainer extends RulesContainerPlugin implements RulesActionInterface { |
|
|
|
public function __construct($variables = array(), $providesVars = array()) { |
|
parent::__construct($variables); |
|
// The provided vars of a component are the names of variables, which should |
|
// be provided to the caller. See rule(). |
|
if ($providesVars) { |
|
$this->info['provides'] = $providesVars; |
|
} |
|
} |
|
|
|
/** |
|
* Adds an action to the container. |
|
* |
|
* Pass in either an instance of the RulesActionInterface or the arguments |
|
* as needed by rules_action(). |
|
* |
|
* @return $this |
|
*/ |
|
public function action($name, $settings = array()) { |
|
$action = (is_object($name) && $name instanceof RulesActionInterface) ? $name : rules_action($name, $settings); |
|
$action->setParent($this); |
|
return $this; |
|
} |
|
|
|
/** |
|
* Evaluate, whereas by default new vars are visible in the parent's scope. |
|
*/ |
|
public function evaluate(RulesState $state) { |
|
foreach ($this->children as $action) { |
|
$action->evaluate($state); |
|
} |
|
} |
|
|
|
public function pluginProvidesVariables() { |
|
return array(); |
|
} |
|
|
|
public function providesVariables() { |
|
$provides = parent::providesVariables(); |
|
if (isset($this->info['provides']) && $vars = $this->componentVariables()) { |
|
// Determine the full variable info for the provided variables. Note that |
|
// we only support providing variables list in the component vars. |
|
$provides += array_intersect_key($vars, array_flip($this->info['provides'])); |
|
} |
|
return $provides; |
|
} |
|
|
|
/** |
|
* Returns an array of provided variable names. |
|
* |
|
* Returns an array of variable names, which are provided by passing through |
|
* the provided variables of the children. |
|
*/ |
|
public function &componentProvidesVariables() { |
|
$this->info += array('provides' => array()); |
|
return $this->info['provides']; |
|
} |
|
|
|
protected function exportToArray() { |
|
$export = parent::exportToArray(); |
|
if (!empty($this->info['provides'])) { |
|
$export['PROVIDES VARIABLES'] = $this->info['provides']; |
|
} |
|
return $export; |
|
} |
|
|
|
public function import(array $export) { |
|
parent::import($export); |
|
if (!empty($export['PROVIDES VARIABLES'])) { |
|
$this->info['provides'] = $export['PROVIDES VARIABLES']; |
|
} |
|
} |
|
|
|
} |
|
|
|
/** |
|
* Base class for all condition containers. |
|
*/ |
|
abstract class RulesConditionContainer extends RulesContainerPlugin implements RulesConditionInterface { |
|
|
|
protected $negate = FALSE; |
|
|
|
/** |
|
* Adds a condition to the container. |
|
* |
|
* Pass in either an instance of the RulesConditionInterface or the arguments |
|
* as needed by rules_condition(). |
|
* |
|
* @return $this |
|
*/ |
|
public function condition($name, $settings = array()) { |
|
$condition = (is_object($name) && $name instanceof RulesConditionInterface) ? $name : rules_condition($name, $settings); |
|
$condition->setParent($this); |
|
return $this; |
|
} |
|
|
|
/** |
|
* Negate this condition. |
|
* |
|
* @return RulesConditionContainer |
|
*/ |
|
public function negate($negate = TRUE) { |
|
$this->negate = (bool) $negate; |
|
return $this; |
|
} |
|
|
|
public function isNegated() { |
|
return $this->negate; |
|
} |
|
|
|
public function __sleep() { |
|
return parent::__sleep() + array('negate' => 'negate'); |
|
} |
|
|
|
/** |
|
* Just return the condition container's result. |
|
*/ |
|
protected function returnVariables(RulesState $state, $result = NULL) { |
|
return $result; |
|
} |
|
|
|
protected function exportChildren($key = NULL) { |
|
$key = isset($key) ? $key : strtoupper($this->plugin()); |
|
return parent::exportChildren($this->negate ? 'NOT ' . $key : $key); |
|
} |
|
|
|
protected function importChildren($export, $key = NULL) { |
|
$key = isset($key) ? $key : strtoupper($this->plugin()); |
|
// Care for negated elements. |
|
if (!isset($export[$key]) && isset($export['NOT ' . $key])) { |
|
$this->negate = TRUE; |
|
$key = 'NOT ' . $key; |
|
} |
|
parent::importChildren($export, $key); |
|
} |
|
|
|
/** |
|
* Overridden to exclude variable assertions of negated conditions. |
|
*/ |
|
protected function stateVariables($element = NULL) { |
|
$vars = $this->availableVariables(); |
|
if (isset($element)) { |
|
// Add in variables provided by siblings executed before the element. |
|
foreach ($this->children as $child) { |
|
if ($child === $element) { |
|
break; |
|
} |
|
$vars += $child->providesVariables(); |
|
// Take variable info assertions into account. |
|
if (!$this->negate && !$child->isNegated() && ($assertions = $child->variableInfoAssertions())) { |
|
$vars = RulesData::addMetadataAssertions($vars, $assertions); |
|
} |
|
} |
|
} |
|
return $vars; |
|
} |
|
|
|
} |
|
|
|
/** |
|
* The rules default logging class. |
|
*/ |
|
class RulesLog { |
|
|
|
const INFO = 1; |
|
const WARN = 2; |
|
const ERROR = 3; |
|
|
|
static protected $logger; |
|
|
|
/** |
|
* @return RulesLog |
|
* Returns the rules logger instance. |
|
*/ |
|
public static function logger() { |
|
if (!isset(self::$logger)) { |
|
$class = __CLASS__; |
|
self::$logger = new $class(variable_get('rules_log_level', self::INFO)); |
|
} |
|
return self::$logger; |
|
} |
|
|
|
protected $log = array(); |
|
protected $logLevel; |
|
protected $line = 0; |
|
|
|
/** |
|
* This is a singleton. |
|
*/ |
|
protected function __construct($logLevel = self::WARN) { |
|
$this->logLevel = $logLevel; |
|
} |
|
|
|
public function __clone() { |
|
throw new Exception("Cannot clone the logger."); |
|
} |
|
|
|
/** |
|
* Logs a log message. |
|
* |
|
* @see rules_log() |
|
*/ |
|
public function log($msg, $args = array(), $logLevel = self::INFO, $scope = NULL, $path = NULL) { |
|
if ($logLevel >= $this->logLevel) { |
|
$this->log[] = array($msg, $args, $logLevel, microtime(TRUE), $scope, $path); |
|
} |
|
} |
|
|
|
/** |
|
* Gets an array of logged messages. |
|
*/ |
|
public function get() { |
|
return $this->log; |
|
} |
|
|
|
/** |
|
* Clears the logged messages. |
|
*/ |
|
public function clear() { |
|
$this->log = array(); |
|
} |
|
|
|
/** |
|
* Checks the log and throws an exception if there were any problems. |
|
*/ |
|
public function checkLog($logLevel = self::WARN) { |
|
foreach ($this->log as $entry) { |
|
if ($entry[2] >= $logLevel) { |
|
throw new Exception($this->render()); |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* Checks the log for error messages. |
|
* |
|
* @param int $logLevel |
|
* Lowest log level to return. Values lower than $logLevel will not be |
|
* returned. |
|
* |
|
* @return bool |
|
* Whether the an error has been logged. |
|
*/ |
|
public function hasErrors($logLevel = self::WARN) { |
|
foreach ($this->log as $entry) { |
|
if ($entry[2] >= $logLevel) { |
|
return TRUE; |
|
} |
|
} |
|
return FALSE; |
|
} |
|
|
|
/** |
|
* Renders the whole log. |
|
*/ |
|
public function render() { |
|
$line = 0; |
|
$output = array(); |
|
while (isset($this->log[$line])) { |
|
$vars['head'] = t($this->log[$line][0], $this->log[$line][1]); |
|
$vars['log'] = $this->renderHelper($line); |
|
$output[] = theme('rules_debug_element', $vars); |
|
$line++; |
|
} |
|
return implode('', $output); |
|
} |
|
|
|
/** |
|
* Renders the log of one event invocation. |
|
*/ |
|
protected function renderHelper(&$line = 0) { |
|
$startTime = isset($this->log[$line][3]) ? $this->log[$line][3] : 0; |
|
$output = array(); |
|
while ($line < count($this->log)) { |
|
if ($output && !empty($this->log[$line][4])) { |
|
// The next entry stems from another evaluated set, add in its log |
|
// messages here. |
|
$vars['head'] = t($this->log[$line][0], $this->log[$line][1]); |
|
if (isset($this->log[$line][5])) { |
|
$vars['link'] = '[' . l(t('edit'), $this->log[$line][5]) . ']'; |
|
} |
|
$vars['log'] = $this->renderHelper($line); |
|
$output[] = theme('rules_debug_element', $vars); |
|
} |
|
else { |
|
$formatted_diff = round(($this->log[$line][3] - $startTime) * 1000, 3) . ' ms'; |
|
$msg = $formatted_diff . ' ' . t($this->log[$line][0], $this->log[$line][1]); |
|
if ($this->log[$line][2] >= RulesLog::WARN) { |
|
$level = $this->log[$line][2] == RulesLog::WARN ? 'warn' : 'error'; |
|
$msg = '<span class="rules-debug-' . $level . '">' . $msg . '</span>'; |
|
} |
|
if (isset($this->log[$line][5]) && !isset($this->log[$line][4])) { |
|
$msg .= ' [' . l(t('edit'), $this->log[$line][5]) . ']'; |
|
} |
|
$output[] = $msg; |
|
|
|
if (isset($this->log[$line][4]) && !$this->log[$line][4]) { |
|
// This was the last log entry of this set. |
|
return theme('item_list', array('items' => $output)); |
|
} |
|
} |
|
$line++; |
|
} |
|
return theme('item_list', array('items' => $output)); |
|
} |
|
|
|
} |
|
|
|
/** |
|
* A base exception class for Rules. |
|
* |
|
* This class can be used to catch all exceptions thrown by Rules, and it |
|
* may be subclassed to describe more specific exceptions. |
|
*/ |
|
abstract class RulesException extends Exception {} |
|
|
|
/** |
|
* An exception that is thrown during evaluation. |
|
* |
|
* Messages are prepared to be logged to the watchdog, thus not yet translated. |
|
* |
|
* @see watchdog() |
|
*/ |
|
class RulesEvaluationException extends RulesException { |
|
|
|
public $msg; |
|
public $args; |
|
public $severity; |
|
public $element; |
|
public $keys = array(); |
|
|
|
/** |
|
* Constructor. |
|
* |
|
* @param string $msg |
|
* The exception message containing placeholder as t(). |
|
* @param array $args |
|
* Replacement arguments such as for t(). |
|
* @param $element |
|
* The element of a configuration causing the exception or an array |
|
* consisting of the element and keys specifying a setting value causing |
|
* the exception. |
|
* @param int $severity |
|
* The RulesLog severity. Defaults to RulesLog::WARN. |
|
*/ |
|
public function __construct($msg, array $args = array(), $element = NULL, $severity = RulesLog::WARN) { |
|
$this->element = is_array($element) ? array_shift($element) : $element; |
|
$this->keys = is_array($element) ? $element : array(); |
|
$this->msg = $msg; |
|
$this->args = $args; |
|
$this->severity = $severity; |
|
// If an error happened, run the integrity check on the rules configuration |
|
// and mark it as dirty if it the check fails. |
|
if ($severity == RulesLog::ERROR && isset($this->element)) { |
|
$rules_config = $this->element->root(); |
|
rules_config_update_dirty_flag($rules_config); |
|
// If we discovered a broken configuration, exclude it in future. |
|
if ($rules_config->dirty) { |
|
rules_clear_cache(); |
|
} |
|
} |
|
// @todo Fix _drupal_decode_exception() to use __toString() and override it. |
|
$this->message = t($this->msg, $this->args); |
|
} |
|
|
|
} |
|
|
|
/** |
|
* Indicates the Rules configuration failed the integrity check. |
|
* |
|
* @see RulesPlugin::integrityCheck() |
|
*/ |
|
class RulesIntegrityException extends RulesException { |
|
|
|
public $msg; |
|
public $element; |
|
public $keys = array(); |
|
|
|
/** |
|
* Constructs a RulesIntegrityException object. |
|
* |
|
* @param string $msg |
|
* The exception message, already translated. |
|
* @param $element |
|
* The element of a configuration causing the exception or an array |
|
* consisting of the element and keys specifying a parameter or provided |
|
* variable causing the exception, e.g. |
|
* @code array($element, 'parameter', 'node') @endcode |
|
*/ |
|
public function __construct($msg, $element = NULL) { |
|
$this->element = is_array($element) ? array_shift($element) : $element; |
|
$this->keys = is_array($element) ? $element : array(); |
|
parent::__construct($msg); |
|
} |
|
|
|
} |
|
|
|
/** |
|
* An exception that is thrown for missing module dependencies. |
|
*/ |
|
class RulesDependencyException extends RulesIntegrityException {} |
|
|
|
/** |
|
* Determines the plugin to be used for importing a child element. |
|
* |
|
* @param string $key |
|
* The key to look for, e.g. 'OR' or 'DO'. |
|
* @param string $default |
|
* The default to return if no special plugin can be found. |
|
*/ |
|
function _rules_import_get_plugin($key, $default = 'action') { |
|
$map = &drupal_static(__FUNCTION__); |
|
if (!isset($map)) { |
|
$cache = rules_get_cache(); |
|
foreach ($cache['plugin_info'] as $name => $info) { |
|
if (!empty($info['embeddable'])) { |
|
$info += array('import keys' => array(strtoupper($name))); |
|
foreach ($info['import keys'] as $k) { |
|
$map[$k] = $name; |
|
} |
|
} |
|
} |
|
} |
|
// Cut off any leading NOT from the key. |
|
if (strpos($key, 'NOT ') === 0) { |
|
$key = substr($key, 4); |
|
} |
|
if (isset($map[$key])) { |
|
return $map[$key]; |
|
} |
|
return $default; |
|
}
|
|
|