Projet

Général

Profil

Paste
Télécharger (91,4 ko) Statistiques
| Branche: | Révision:

root / drupal7 / sites / all / modules / rules / includes / rules.core.inc @ 76e2e7c3

1
<?php
2

    
3
/**
4
 * @file
5
 * Rules base classes and interfaces needed for any rule evaluation.
6
 */
7

    
8
// This is not necessary as the classes are autoloaded via the registry. However
9
// it saves some possible update headaches until the registry is rebuilt.
10
// @todo
11
//   Remove for a future release.
12
require_once dirname(__FILE__) . '/faces.inc';
13

    
14
/**
15
 * Make sure loaded rule configs are instantiated right.
16
 */
17
class RulesEntityController extends EntityAPIControllerExportable {
18

    
19
  /**
20
   * Overridden.
21
   *
22
   * @see EntityAPIController::create()
23
   */
24
  public function create(array $values = array()) {
25
    // Default to rules as owning module.
26
    $values += array('owner' => 'rules');
27
    return parent::create($values);
28
  }
29

    
30
  /**
31
   * Overridden.
32
   * @see DrupalDefaultEntityController::attachLoad()
33
   */
34
  protected function attachLoad(&$queried_entities, $revision_id = FALSE) {
35
    // Retrieve stdClass records and turn them in rules objects stored in 'data'
36
    $ids = array_keys($queried_entities);
37
    $result = db_select('rules_tags')
38
      ->fields('rules_tags', array('id', 'tag'))
39
      ->condition('id', $ids, 'IN')
40
      ->execute();
41
    foreach ($result as $row) {
42
      $tags[$row->id][] = $row->tag;
43
    }
44
    $result = db_select('rules_dependencies')
45
      ->fields('rules_dependencies', array('id', 'module'))
46
      ->condition('id', $ids, 'IN')
47
      ->execute();
48
    foreach ($result as $row) {
49
      $modules[$row->id][] = $row->module;
50
    }
51

    
52
    $entities = array();
53
    foreach ($queried_entities as $record) {
54
      $entity = $record->data;
55
      // Set the values of the other columns.
56
      foreach ($this->entityInfo['schema_fields_sql']['base table'] as $field) {
57
        $entity->$field = $record->$field;
58
      }
59
      unset($entity->data, $entity->plugin);
60
      // Add any tags or dependencies.
61
      $entity->dependencies = isset($modules[$entity->id]) ? $modules[$entity->id] : array();
62
      $entity->tags = isset($tags[$entity->id]) ? $tags[$entity->id] : array();
63
      $entities[$entity->id] = $entity;
64
    }
65
    $queried_entities = $entities;
66
    parent::attachLoad($queried_entities, $revision_id);
67
  }
68

    
69
  /**
70
   * Override to support having events and tags as conditions.
71
   * @see EntityAPIController::applyConditions($entities, $conditions)
72
   * @see rules_query_rules_config_load_multiple_alter()
73
   */
74
  protected function applyConditions($entities, $conditions = array()) {
75
    if (isset($conditions['event']) || isset($conditions['plugin'])) {
76
      foreach ($entities as $key => $entity) {
77
        if (isset($conditions['event']) && (!($entity instanceof RulesTriggerableInterface) || !in_array($conditions['event'], $entity->events()))) {
78
          unset($entities[$key]);
79
        }
80
        if (isset($conditions['plugin']) && !is_array($conditions['plugin'])) {
81
          $conditions['plugin'] = array($conditions['plugin']);
82
        }
83
        if (isset($conditions['plugin']) && !in_array($entity->plugin(), $conditions['plugin'])) {
84
          unset($entities[$key]);
85
        }
86
      }
87
      unset($conditions['event'], $conditions['plugin']);
88
    }
89
    if (!empty($conditions['tags'])) {
90
      foreach ($entities as $key => $entity) {
91
        foreach ($conditions['tags'] as $tag) {
92
          if (in_array($tag, $entity->tags)) {
93
            continue 2;
94
          }
95
        }
96
        unset($entities[$key]);
97
      }
98
      unset($conditions['tags']);
99
    }
100
    return parent::applyConditions($entities, $conditions);
101
  }
102

    
103
  /**
104
   * Overridden to work with Rules' custom export format.
105
   *
106
   * @param $export
107
   *   A serialized string in JSON format as produced by the
108
   *   RulesPlugin::export() method, or the PHP export as usual PHP array.
109
   */
110
  public function import($export, &$error_msg = '') {
111
    $export = is_array($export) ? $export : drupal_json_decode($export);
112
    if (!is_array($export)) {
113
      $error_msg = t('Unable to parse the pasted export.');
114
      return FALSE;
115
    }
116
    // The key ist the configuration name and the value the actual export.
117
    list($name, $export) = each($export);
118
    if (!isset($export['PLUGIN'])) {
119
      $error_msg = t('Export misses plugin information.');
120
      return FALSE;
121
    }
122
    // Create an empty configuration, re-set basic keys and import.
123
    $config = rules_plugin_factory($export['PLUGIN']);
124
    $config->name = $name;
125
    foreach (array('label', 'active', 'weight', 'tags', 'access_exposed', 'owner') as $key) {
126
      if (isset($export[strtoupper($key)])) {
127
        $config->$key = $export[strtoupper($key)];
128
      }
129
    }
130
    if (!empty($export['REQUIRES'])) {
131
      foreach ($export['REQUIRES'] as $module) {
132
        if (!module_exists($module)) {
133
          $error_msg = t('Missing the required module %module.', array('%module' => $module));
134
          return FALSE;
135
        }
136
      }
137
      $config->dependencies = $export['REQUIRES'];
138
    }
139
    $config->import($export);
140
    return $config;
141
  }
142

    
143
  public function save($rules_config, DatabaseTransaction $transaction = NULL) {
144
    $transaction = isset($transaction) ? $transaction : db_transaction();
145

    
146
    // Load the stored entity, if any.
147
    if (!isset($rules_config->original) && $rules_config->{$this->idKey}) {
148
      $rules_config->original = entity_load_unchanged($this->entityType, $rules_config->{$this->idKey});
149
    }
150
    $original = isset($rules_config->original) ? $rules_config->original : NULL;
151

    
152
    $return = parent::save($rules_config, $transaction);
153
    $this->storeTags($rules_config);
154
    if ($rules_config instanceof RulesTriggerableInterface) {
155
      $this->storeEvents($rules_config);
156
    }
157
    $this->storeDependencies($rules_config);
158

    
159
    // See if there are any events that have been removed.
160
    if ($original && $rules_config->plugin == 'reaction rule') {
161
      foreach (array_diff($original->events(), $rules_config->events()) as $event_name) {
162
        // Check if the event handler implements the event dispatcher interface.
163
        $handler = rules_get_event_handler($event_name, $rules_config->getEventSettings($event_name));
164
        if (!$handler instanceof RulesEventDispatcherInterface) {
165
          continue;
166
        }
167

    
168
        // Only stop an event dispatcher if there are no rules for it left.
169
        if (!rules_config_load_multiple(FALSE, array('event' => $event_name, 'plugin' => 'reaction rule', 'active' => TRUE)) && $handler->isWatching()) {
170
          $handler->stopWatching();
171
        }
172
      }
173
    }
174

    
175
    return $return;
176
  }
177

    
178
  /**
179
   * Save tagging information to the rules_tags table.
180
   */
181
  protected function storeTags($rules_config) {
182
    db_delete('rules_tags')
183
      ->condition('id', $rules_config->id)
184
      ->execute();
185
    if (!empty($rules_config->tags)) {
186
      foreach ($rules_config->tags as $tag) {
187
        db_insert('rules_tags')
188
          ->fields(array('id', 'tag'), array($rules_config->id, $tag))
189
          ->execute();
190
      }
191
    }
192
  }
193

    
194
  /**
195
   * Save event information to the rules_trigger table.
196
   */
197
  protected function storeEvents(RulesTriggerableInterface $rules_config) {
198
    db_delete('rules_trigger')
199
      ->condition('id', $rules_config->id)
200
      ->execute();
201
    foreach ($rules_config->events() as $event) {
202
      db_insert('rules_trigger')
203
        ->fields(array(
204
        'id' => $rules_config->id,
205
        'event' => $event,
206
      ))
207
      ->execute();
208
    }
209
  }
210

    
211
  protected function storeDependencies($rules_config) {
212
    db_delete('rules_dependencies')
213
      ->condition('id', $rules_config->id)
214
      ->execute();
215
    if (!empty($rules_config->dependencies)) {
216
      foreach ($rules_config->dependencies as $dependency) {
217
        db_insert('rules_dependencies')
218
          ->fields(array(
219
          'id' => $rules_config->id,
220
          'module' => $dependency,
221
        ))
222
        ->execute();
223
      }
224
    }
225
  }
226

    
227
  /**
228
   * Overridden to support tags and events in $conditions.
229
   * @see EntityAPIControllerExportable::buildQuery()
230
   */
231
  protected function buildQuery($ids, $conditions = array(), $revision_id = FALSE) {
232
    $query = parent::buildQuery($ids, $conditions, $revision_id);
233
    $query_conditions =& $query->conditions();
234
    foreach ($query_conditions as &$condition) {
235
      // One entry in $query_conditions is a string with key '#conjunction'.
236
      // @see QueryConditionInterface::conditions().
237
      if (is_array($condition)) {
238
        // Support using 'tags' => array('tag1', 'tag2') as condition.
239
        if ($condition['field'] == 'base.tags') {
240
          $query->join('rules_tags', 'rt', 'base.id = rt.id');
241
          $condition['field'] = 'rt.tag';
242
        }
243
        // Support using 'event' => $name as condition.
244
        if ($condition['field'] == 'base.event') {
245
          $query->join('rules_trigger', 'tr', "base.id = tr.id");
246
          $condition['field'] = 'tr.event';
247
          // Use like operator to support % wildcards also.
248
          $condition['operator'] = 'LIKE';
249
        }
250
      }
251
    }
252
    return $query;
253
  }
254

    
255
  /**
256
   * Overridden to also delete tags and events.
257
   * @see EntityAPIControllerExportable::delete()
258
   */
259
  public function delete($ids, DatabaseTransaction $transaction = NULL) {
260
    $transaction = isset($transaction) ? $transaction : db_transaction();
261
    // Use entity-load as ids may be the names as well as the ids.
262
    $configs = $ids ? entity_load('rules_config', $ids) : array();
263
    if ($configs) {
264
      foreach ($configs as $config) {
265
        db_delete('rules_trigger')
266
          ->condition('id', $config->id)
267
          ->execute();
268
        db_delete('rules_tags')
269
          ->condition('id', $config->id)
270
          ->execute();
271
        db_delete('rules_dependencies')
272
          ->condition('id', $config->id)
273
          ->execute();
274
      }
275
    }
276
    $return = parent::delete($ids, $transaction);
277

    
278
    // Stop event dispatchers when deleting the last rule of an event set.
279
    $processed = array();
280
    foreach ($configs as $config) {
281
      if ($config->getPluginName() != 'reaction rule') {
282
        continue;
283
      }
284

    
285
      foreach ($config->events() as $event_name) {
286
        // Only process each event once.
287
        if (!empty($processed[$event_name])) {
288
          continue;
289
        }
290
        $processed[$event_name] = TRUE;
291

    
292
        // Check if the event handler implements the event dispatcher interface.
293
        $handler = rules_get_event_handler($event_name, $config->getEventSettings($event_name));
294
        if (!$handler instanceof RulesEventDispatcherInterface) {
295
          continue;
296
        }
297

    
298
        // Only stop an event dispatcher if there are no rules for it left.
299
        if ($handler->isWatching() && !rules_config_load_multiple(FALSE, array('event' => $event_name, 'plugin' => 'reaction rule', 'active' => TRUE))) {
300
          $handler->stopWatching();
301
        }
302
      }
303
    }
304

    
305
    return $return;
306
  }
307
}
308

    
309
/**
310
 * The RulesExtendable uses the rules cache to setup the defined extenders
311
 * and overrides automatically.
312
 * As soon faces is used the faces information is autoloaded using setUp().
313
 */
314
abstract class RulesExtendable extends FacesExtendable {
315

    
316
  /**
317
   * The name of the info definitions associated with info about this class.
318
   * This would be defined abstract, if possible. Common rules hooks with class
319
   * info are e.g. plugin_info and data_info.
320
   */
321
  protected $hook;
322

    
323
  /**
324
   * The name of the item this class represents in the info hook.
325
   */
326
  protected $itemName;
327

    
328
  protected $cache, $itemInfo = array();
329

    
330

    
331
  public function __construct() {
332
    $this->setUp();
333
  }
334

    
335
  protected function setUp() {
336
    // Keep a reference on the cache, so elements created during cache
337
    // rebuilding end up with a complete cache in the end too.
338
    $this->cache = &rules_get_cache();
339
    if (isset($this->cache[$this->hook][$this->itemName])) {
340
      $this->itemInfo = &$this->cache[$this->hook][$this->itemName];
341
    }
342
    // Set up the Faces Extenders
343
    if (!empty($this->itemInfo['faces_cache'])) {
344
      list($this->facesMethods, $this->facesIncludes, $this->faces) = $this->itemInfo['faces_cache'];
345
    }
346
  }
347

    
348
  /**
349
   * Force the object to be setUp, this executes setUp() if not done yet.
350
   */
351
  public function forceSetUp() {
352
    if (!isset($this->cache) || (!empty($this->itemInfo['faces_cache']) && !$this->faces)) {
353
      $this->setUp();
354
    }
355
  }
356

    
357
  /**
358
   * Magic method: Invoke the dynamically implemented methods.
359
   */
360
  public function __call($name, $arguments = array()) {
361
    $this->forceSetUp();
362
    return parent::__call($name, $arguments);
363
  }
364

    
365
  public function facesAs($interface = NULL) {
366
    $this->forceSetUp();
367
    return parent::facesAs($interface);
368
  }
369

    
370
  /**
371
   * Allows items to add something to the rules cache.
372
   */
373
  public function rebuildCache(&$itemInfo, &$cache) {
374
    // Speed up setting up items by caching the faces methods.
375
    if (!empty($itemInfo['extenders'])) {
376
      // Apply extenders and overrides.
377
      $itemInfo += array('overrides' => array());
378
      foreach ($itemInfo['extenders'] as $face => $data) {
379
        $data += array('file' => array());
380
        if (isset($data['class'])) {
381
          $this->extendByClass($face, $data['class'], $data['file']);
382
        }
383
        elseif (isset($data['methods'])) {
384
          $this->extend($face, $data['methods'], $data['file']);
385
        }
386
      }
387
      foreach ($itemInfo['overrides'] as $data) {
388
        $data += array('file' => array());
389
        $this->override($data['methods'], $data['file']);
390
      }
391
      $itemInfo['faces_cache'] = array($this->facesMethods, $this->facesIncludes, $this->faces);
392
      // We don't need that any more.
393
      unset($itemInfo['extenders'], $itemInfo['overrides']);
394
    }
395
  }
396

    
397
  /**
398
   * Returns whether the a RuleExtendable supports the given interface.
399
   *
400
   * @param $itemInfo
401
   *   The info about the item as specified in the hook.
402
   * @param $interface
403
   *   The interface to check for.
404
   * @return
405
   *   Whether it supports the given interface.
406
   */
407
  public static function itemFacesAs(&$itemInfo, $interface) {
408
    return in_array($interface, class_implements($itemInfo['class'])) || isset($itemInfo['faces_cache'][2][$interface]);
409
  }
410
}
411

    
412
/**
413
 * Base class for rules plugins.
414
 *
415
 * We cannot inherit from EntityDB at the same time, so we implement our own
416
 * entity related methods. Any CRUD related actions performed on contained
417
 * plugins are applied and the root element representing the configuration is
418
 * saved.
419
 */
420
abstract class RulesPlugin extends RulesExtendable {
421

    
422
  /**
423
   * If this is a configuration saved to the db, the id of it.
424
   */
425
  public $id = NULL;
426
  public $weight = 0;
427
  public $name = NULL;
428

    
429
  /**
430
   * An array of settings for this element.
431
   */
432
  public $settings = array();
433

    
434
  /**
435
   * Info about this element. Usage depends on the plugin.
436
   */
437
  protected $info = array();
438

    
439
  /**
440
   * The parent element, if any.
441
   * @var RulesContainerPlugin
442
   */
443
  protected $parent = NULL;
444

    
445
  protected $cache = NULL, $hook = 'plugin_info';
446

    
447
  /**
448
   * Identifies an element inside a configuration.
449
   */
450
  protected $elementId = NULL;
451

    
452
  /**
453
   * Static cache for availableVariables().
454
   */
455
  protected $availableVariables;
456

    
457

    
458
  /**
459
   * Sets a new parent element.
460
   */
461
  public function setParent(RulesContainerPlugin $parent) {
462
    if ($this->parent == $parent) {
463
      return;
464
    }
465
    if (isset($this->parent) && ($key = array_search($this, $this->parent->children)) !== FALSE) {
466
      // Remove element from any previous parent.
467
      unset($this->parent->children[$key]);
468
      $this->parent->resetInternalCache();
469
    }
470
    // Make sure the interface matches the type of the container.
471
    if (($parent instanceof RulesActionContainer && $this instanceof RulesActionInterface) ||
472
       ($parent instanceof RulesConditionContainer && $this instanceof RulesConditionInterface)) {
473

    
474
      $this->parent = $parent;
475
      $parent->children[] = $this;
476
      $this->parent->resetInternalCache();
477
    }
478
    else {
479
      throw new RulesEvaluationException('The given container is incompatible with this element.', array(), $this, RulesLog::ERROR);
480
    }
481
  }
482

    
483
  /**
484
   * Gets the root element of the configuration.
485
   */
486
  public function root() {
487
    $element = $this;
488
    while (!$element->isRoot()) {
489
      $element = $element->parent;
490
    }
491
    return $element;
492
  }
493

    
494
  /**
495
   * Returns whether the element is the root of the configuration.
496
   */
497
  public function isRoot() {
498
    return empty($this->parent) || isset($this->name);
499
  }
500

    
501
  /**
502
   * Returns the element's parent.
503
   */
504
  public function parentElement() {
505
    return $this->parent;
506
  }
507

    
508
  /**
509
   * Returns the element id, which identifies the element inside the config.
510
   */
511
  public function elementId() {
512
    if (!isset($this->elementId)) {
513
      $this->elementMap()->index();
514
    }
515
    return $this->elementId;
516
  }
517

    
518
  /**
519
   * Gets the element map helper object, which helps mapping elements to ids.
520
   *
521
   * @return RulesElementMap
522
   */
523
  public function elementMap() {
524
    $config = $this->root();
525
    if (empty($config->map)) {
526
      $config->map = new RulesElementMap($config);
527
    }
528
    return $config->map;
529
  }
530

    
531
  /**
532
   * Iterate over all elements nested below the current element.
533
   *
534
   * This helper can be used to recursively iterate over all elements of a
535
   * configuration. To iterate over the children only, just regulary iterate
536
   * over the object.
537
   *
538
   * @param $mode
539
   *   (optional) The iteration mode used. See
540
   *   RecursiveIteratorIterator::construct(). Defaults to SELF_FIRST.
541
   *
542
   * @return RecursiveIteratorIterator
543
   */
544
  public function elements($mode = RecursiveIteratorIterator::SELF_FIRST) {
545
    return new RecursiveIteratorIterator($this, $mode);
546
  }
547

    
548
  /**
549
   * Do a deep clone.
550
   */
551
  public function __clone() {
552
    // Make sure the element map is cleared.
553
    // @see self::elementMap()
554
    unset($this->map);
555
  }
556

    
557
  /**
558
   * Returns the depth of this element in the configuration.
559
   */
560
  public function depth() {
561
    $element = $this;
562
    $i = 0;
563
    while (!empty($element->parent)) {
564
      $element = $element->parent;
565
      $i++;
566
    }
567
    return $i;
568
  }
569

    
570
  /**
571
   * Execute the configuration.
572
   *
573
   * @param ...
574
   *   Arguments to pass to the configuration.
575
   */
576
  public function execute() {
577
    return $this->executeByArgs(func_get_args());
578
  }
579

    
580
  /**
581
   * Execute the configuration by passing arguments in a single array.
582
   */
583
  abstract public function executeByArgs($args = array());
584

    
585
  /**
586
   * Evaluate the element on a given rules evaluation state.
587
   */
588
  abstract function evaluate(RulesState $state);
589

    
590
  protected static function compare(RulesPlugin $a, RulesPlugin $b) {
591
    if ($a->weight == $b->weight) {
592
      return 0;
593
    }
594
    return ($a->weight < $b->weight) ? -1 : 1;
595
  }
596

    
597
  /**
598
   * Returns info about parameters needed by the plugin.
599
   *
600
   * Note that not necessarily all parameters are needed when executing the
601
   * plugin, as values for the parameter might have been already configured via
602
   * the element settings.
603
   *
604
   * @see self::parameterInfo()
605
   */
606
  public function pluginParameterInfo() {
607
    return isset($this->info['parameter']) ? $this->info['parameter'] : array();
608
  }
609

    
610
  /**
611
   * Returns info about parameters needed for executing the configured plugin.
612
   *
613
   * @param $optional
614
   *   Whether optional parameters should be included.
615
   *
616
   * @see self::pluginParameterInfo()
617
   */
618
  public function parameterInfo($optional = FALSE) {
619
    // We have to filter out parameters that are already configured.
620
    foreach ($this->pluginParameterInfo() as $name => $info) {
621
      if (!isset($this->settings[$name . ':select']) && !isset($this->settings[$name]) && ($optional || (empty($info['optional']) && $info['type'] != 'hidden'))) {
622
        $vars[$name] = $info;
623
      }
624
    }
625
    return isset($vars) ? $vars : array();
626
  }
627

    
628
  /**
629
   * Returns the about variables the plugin provides for later evaluated elements.
630
   *
631
   * Note that this method returns info about the provided variables as defined
632
   * by the plugin. Thus this resembles the original info, which may be
633
   * adapted via configuration.
634
   *
635
   * @see self::providesVariables()
636
   */
637
  public function pluginProvidesVariables() {
638
    return isset($this->info['provides']) ? $this->info['provides'] : array();
639
  }
640

    
641
  /**
642
   * Returns info about all variables provided for later evaluated elements.
643
   *
644
   * @see self::pluginProvidesVariables()
645
   */
646
  public function providesVariables() {
647
    foreach ($this->pluginProvidesVariables() as $name => $info) {
648
      $info['source name'] = $name;
649
      $info['label'] = isset($this->settings[$name . ':label']) ? $this->settings[$name . ':label'] : $info['label'];
650
      if (isset($this->settings[$name . ':var'])) {
651
        $name = $this->settings[$name . ':var'];
652
      }
653
      $provides[$name] = $info;
654
    }
655
    return isset($provides) ? $provides : array();
656
  }
657

    
658
  /**
659
   * Returns the info of the plugin.
660
   */
661
  public function info() {
662
    return $this->info;
663
  }
664

    
665
  /**
666
   * When converted to a string, just use the export format.
667
   */
668
  public function __toString() {
669
    return $this->isRoot() ? $this->export() : entity_var_json_export($this->export());
670
  }
671

    
672
  /**
673
   * Gets variables to return once the configuration has been executed.
674
   */
675
  protected function returnVariables(RulesState $state, $result = NULL) {
676
    $var_info = $this->providesVariables();
677
    foreach ($var_info as $name => $info) {
678
      try {
679
        $vars[$name] = $this->getArgument($name, $info, $state);
680
      }
681
      catch (RulesEvaluationException $e) {
682
        // Ignore not existing variables.
683
        $vars[$name] = NULL;
684
      }
685
      $var_info[$name] += array('allow null' => TRUE);
686
    }
687
    return isset($vars) ? array_values(rules_unwrap_data($vars, $var_info)) : array();
688
  }
689

    
690
  /**
691
   * Sets up the execution state for the given arguments.
692
   */
693
  public function setUpState(array $args) {
694
    $state = new RulesState();
695
    $vars = $this->setUpVariables();
696
    // Fix numerically indexed args to start with 0.
697
    if (!isset($args[rules_array_key($vars)])) {
698
      $args = array_values($args);
699
    }
700
    $offset = 0;
701
    foreach (array_keys($vars) as $i => $name) {
702
      $info = $vars[$name];
703
      if (!empty($info['handler']) || (isset($info['parameter']) && $info['parameter'] === FALSE)) {
704
        $state->addVariable($name, NULL, $info);
705
        // Count the variables that are not passed as parameters.
706
        $offset++;
707
      }
708
      // Support numerically indexed arrays as well as named parameter style.
709
      // The index is reduced to exclude non-parameter variables.
710
      elseif (isset($args[$i - $offset])) {
711
        $state->addVariable($name, $args[$i - $offset], $info);
712
      }
713
      elseif (isset($args[$name])) {
714
        $state->addVariable($name, $args[$name], $info);
715
      }
716
      elseif (empty($info['optional']) && $info['type'] != 'hidden') {
717
        throw new RulesEvaluationException('Argument %name is missing.', array('%name' => $name), $this, RulesLog::ERROR);
718
      }
719
    }
720
    return $state;
721
  }
722

    
723
  /**
724
   * Returns info about all variables that have to be setup in the state.
725
   */
726
  protected function setUpVariables() {
727
    return $this->parameterInfo(TRUE);
728
  }
729

    
730
  /**
731
   * Returns info about variables available to be used as arguments for this element.
732
   *
733
   * As this is called very often, e.g. during integrity checks, we statically
734
   * cache the results.
735
   *
736
   * @see RulesPlugin::resetInternalCache()
737
   */
738
  public function availableVariables() {
739
    if (!isset($this->availableVariables)) {
740
      $this->availableVariables = !$this->isRoot() ? $this->parent->stateVariables($this) : RulesState::defaultVariables();
741
    }
742
    return $this->availableVariables;
743
  }
744

    
745
  /**
746
   * Returns asserted additions to the available variable info. Any returned
747
   * info is merged into the variable info, in case the execution flow passes
748
   * the element.
749
   * E.g. this is used to assert the content type of a node if the condition
750
   * is met, such that the per node type properties are available.
751
   */
752
  protected function variableInfoAssertions() {
753
    return array();
754
  }
755

    
756
  /**
757
   * Get the name of this plugin instance. The returned name should identify
758
   * the code which drives this plugin.
759
   */
760
  public function getPluginName() {
761
    return $this->itemName;
762
  }
763

    
764
  /**
765
   * Calculates an array of required modules.
766
   *
767
   * You can use $this->dependencies to access dependencies for saved
768
   * configurations.
769
   */
770
  public function dependencies() {
771
    $this->processSettings();
772
    $modules = isset($this->itemInfo['module']) && $this->itemInfo['module'] != 'rules' ? array($this->itemInfo['module'] => 1) : array();
773
    foreach ($this->pluginParameterInfo() as $name => $info) {
774
      if (isset($this->settings[$name . ':process']) && $this->settings[$name . ':process'] instanceof RulesDataProcessor) {
775
        $modules += array_flip($this->settings[$name . ':process']->dependencies());
776
      }
777
    }
778
    return array_keys($modules);
779
  }
780

    
781
  /**
782
   * Whether the currently logged in user has access to all configured elements.
783
   *
784
   * Note that this only checks whether the current user has permission to all
785
   * configured elements, but not whether a user has access to configure Rule
786
   * configurations in general. Use rules_config_access() for that.
787
   *
788
   * Use this to determine access permissions for configuring or triggering the
789
   * execution of certain configurations independent of the Rules UI.
790
   *
791
   * @see rules_config_access()
792
   */
793
  public function access() {
794
    $this->processSettings();
795
    foreach ($this->pluginParameterInfo() as $name => $info) {
796
      if (isset($this->settings[$name . ':select']) && $wrapper = $this->applyDataSelector($this->settings[$name . ':select'])) {
797
        if ($wrapper->access('view') === FALSE) {
798
          return FALSE;
799
        }
800
      }
801
      // Incorporate access checks for data processors and input evaluators.
802
      if (isset($this->settings[$name . ':process']) && $this->settings[$name . ':process'] instanceof RulesDataProcessor && !$this->settings[$name . ':process']->editAccess()) {
803
        return FALSE;
804
      }
805
    }
806
    return TRUE;
807
  }
808

    
809
  /**
810
   * Processes the settings e.g. to prepare input evaluators.
811
   *
812
   * Usually settings get processed automatically, however if $this->settings
813
   * has been altered manually after element construction, it needs to be
814
   * invoked explicitly with $force set to TRUE.
815
   *
816
   */
817
  public function processSettings($force = FALSE) {
818
    // Process if not done yet.
819
    if ($force || !empty($this->settings['#_needs_processing'])) {
820
      $var_info = $this->availableVariables();
821
      foreach ($this->pluginParameterInfo() as $name => $info) {
822
        // Prepare input evaluators.
823
        if (isset($this->settings[$name])) {
824
          $this->settings[$name . ':process'] = $this->settings[$name];
825
          RulesDataInputEvaluator::prepareSetting($this->settings[$name . ':process'], $info, $var_info);
826
        }
827
        // Prepare data processors.
828
        elseif (isset($this->settings[$name . ':select']) && !empty($this->settings[$name . ':process'])) {
829
          RulesDataProcessor::prepareSetting($this->settings[$name . ':process'], $info, $var_info);
830
        }
831
        // Clean up.
832
        if (empty($this->settings[$name . ':process'])) {
833
          unset($this->settings[$name . ':process']);
834
        }
835
      }
836
      unset($this->settings['#_needs_processing']);
837
    }
838
  }
839

    
840
  /**
841
   * Makes sure the plugin is configured right, e.g. all needed variables
842
   * are available in the element's scope and dependent modules are enabled.
843
   *
844
   * @return RulesPlugin
845
   *   Returns $this to support chained usage.
846
   *
847
   * @throws RulesIntegrityException
848
   *   In case of a failed integraty check, a RulesIntegrityException exception
849
   *   is thrown.
850
   */
851
  public function integrityCheck() {
852
    // First process the settings if not done yet.
853
    $this->processSettings();
854
    // Check dependencies using the pre-calculated dependencies stored in
855
    // $this->dependencies. Fail back to calculation them on the fly, e.g.
856
    // during creation.
857
    $dependencies = empty($this->dependencies) ? $this->dependencies() : $this->dependencies;
858
    foreach ($dependencies as $module) {
859
      if (!module_exists($module)) {
860
        throw new RulesDependencyException(t('Missing required module %name.', array('%name' => $module)));
861
      }
862
    }
863
    // Check the parameter settings.
864
    $this->checkParameterSettings();
865
    // Check variable names for provided variables to be valid.
866
    foreach ($this->pluginProvidesVariables() as $name => $info) {
867
      if (isset($this->settings[$name . ':var'])) {
868
        $this->checkVarName($this->settings[$name . ':var']);
869
      }
870
    }
871
    return $this;
872
  }
873

    
874
  protected function checkVarName($name) {
875
    if (!preg_match('/^[0-9a-zA-Z_]*$/', $name)) {
876
      throw new RulesIntegrityException(t('%plugin: The variable name %name contains not allowed characters.', array('%plugin' => $this->getPluginName(), '%name' => $name)), $this);
877
    }
878
  }
879

    
880
  /**
881
   * Checks whether parameters are correctly configured.
882
   */
883
  protected function checkParameterSettings() {
884
    foreach ($this->pluginParameterInfo() as $name => $info) {
885
      if (isset($info['restriction']) && $info['restriction'] == 'selector' && isset($this->settings[$name])) {
886
        throw new RulesIntegrityException(t("The parameter %name may only be configured using a selector.", array('%name' => $name)), array($this, 'parameter', $name));
887
      }
888
      elseif (isset($info['restriction']) && $info['restriction'] == 'input' && isset($this->settings[$name . ':select'])) {
889
        throw new RulesIntegrityException(t("The parameter %name may not be configured using a selector.", array('%name' => $name)), array($this, 'parameter', $name));
890
      }
891
      elseif (!empty($this->settings[$name . ':select']) && !$this->applyDataSelector($this->settings[$name . ':select'])) {
892
        throw new RulesIntegrityException(t("Data selector %selector for parameter %name is invalid.", array('%selector' => $this->settings[$name . ':select'], '%name' => $name)), array($this, 'parameter', $name));
893
      }
894
      elseif ($arg_info = $this->getArgumentInfo($name)) {
895
        // If we have enough metadata, check whether the types match.
896
        if (!RulesData::typesMatch($arg_info, $info)) {
897
          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));
898
        }
899
      }
900
      elseif (!$this->isRoot() && !isset($this->settings[$name]) && empty($info['optional']) && $info['type'] != 'hidden') {
901
        throw new RulesIntegrityException(t('Missing configuration for parameter %name.', array('%name' => $name)), array($this, 'parameter', $name));
902
      }
903
      //TODO: Make sure used values are allowed. (key/value pairs + allowed values)
904
    }
905
  }
906

    
907
  /**
908
   * Returns the argument as configured in the element settings for the
909
   * parameter $name described with $info.
910
   *
911
   * @param $name
912
   *   The name of the parameter for which to get the argument.
913
   * @param $info
914
   *   Info about the parameter.
915
   * @param RulesState $state
916
   *   The current evaluation state.
917
   * @param $langcode
918
   *   (optional) The language code used to get the argument value if the
919
   *   argument value should be translated. By default (NULL) the current
920
   *   interface language will be used.
921
   *
922
   * @return
923
   *   The argument, possibly wrapped.
924
   *
925
   * @throws RulesEvaluationException
926
   *   In case the argument cannot be retrieved an exception is thrown.
927
   */
928
  protected function getArgument($name, $info, RulesState $state, $langcode = NULL) {
929
    // Only apply the langcode if the parameter has been marked translatable.
930
    if (empty($info['translatable'])) {
931
      $langcode = LANGUAGE_NONE;
932
    }
933
    elseif (!isset($langcode)) {
934
      $langcode = $GLOBALS['language']->language;
935
    }
936

    
937
    if (!empty($this->settings[$name . ':select'])) {
938
      $arg = $state->applyDataSelector($this->settings[$name . ':select'], $langcode);
939
    }
940
    elseif (isset($this->settings[$name])) {
941
      $arg = rules_wrap_data($this->settings[$name], $info);
942
      // We don't sanitize directly specified values.
943
      $skip_sanitize = TRUE;
944
    }
945
    elseif ($state->varinfo($name)) {
946
      $arg = $state->get($name);
947
    }
948
    elseif (empty($info['optional']) && $info['type'] != 'hidden') {
949
      throw new RulesEvaluationException('Required parameter %name is missing.', array('%name' => $name), $this, RulesLog::ERROR);
950
    }
951
    else {
952
      $arg = isset($info['default value']) ? $info['default value'] : NULL;
953
      $skip_sanitize = TRUE;
954
      $info['allow null'] = TRUE;
955
    }
956
    // Make sure the given value is set if required (default).
957
    if (!isset($arg) && empty($info['allow null'])) {
958
      throw new RulesEvaluationException('The provided argument for parameter %name is empty.', array('%name' => $name), $this);
959
    }
960

    
961
    // Support passing already sanitized values.
962
    if ($info['type'] == 'text' && !isset($skip_sanitize) && !empty($info['sanitize']) && !($arg instanceof EntityMetadataWrapper)) {
963
      $arg = check_plain((string) $arg);
964
    }
965

    
966
    // Apply any configured data processors.
967
    if (!empty($this->settings[$name . ':process'])) {
968
      // For processing, make sure the data is unwrapped now.
969
      $return = rules_unwrap_data(array($arg), array($info));
970
      // @todo for Drupal 8: Refactor to add the name and language code as
971
      // separate parameter to process().
972
      $info['#name'] = $name;
973
      $info['#langcode'] = $langcode;
974
      return isset($return[0]) ? $this->settings[$name . ':process']->process($return[0], $info, $state, $this) : NULL;
975
    }
976
    return $arg;
977
  }
978

    
979
  /**
980
   * Gets the right arguments for executing the element.
981
   *
982
   * @throws RulesEvaluationException
983
   *   If case an argument cannot be retrieved an exception is thrown.
984
   */
985
  protected function getExecutionArguments(RulesState $state) {
986
    $parameters = $this->pluginParameterInfo();
987
    // If there is language parameter, get its value first so it can be used
988
    // for getting other translatable values.
989
    $langcode = NULL;
990
    if (isset($parameters['language'])) {
991
      $lang_arg = $this->getArgument('language', $parameters['language'], $state);
992
      $langcode = $lang_arg instanceof EntityMetadataWrapper ? $lang_arg->value() : $lang_arg;
993
    }
994
    // Now get all arguments.
995
    foreach ($parameters as $name => $info) {
996
      $args[$name] = $name == 'language' ? $lang_arg : $this->getArgument($name, $info, $state, $langcode);
997
    }
998
    // Append the settings and the execution state. Faces will append $this.
999
    $args['settings'] = $this->settings;
1000
    $args['state'] = $state;
1001
    // Make the wrapped variables for the arguments available in the state.
1002
    $state->currentArguments = $args;
1003
    return rules_unwrap_data($args, $parameters);
1004
  }
1005

    
1006
  /**
1007
   * Apply the given data selector by using the info about available variables.
1008
   * Thus it doesn't require an actual evaluation state.
1009
   *
1010
   * @param $selector
1011
   *   The selector string, e.g. "node:author:mail".
1012
   *
1013
   * @return EntityMetadataWrapper
1014
   *   An empty wrapper for the given selector or FALSE if the selector couldn't
1015
   *   be applied.
1016
   */
1017
  public function applyDataSelector($selector) {
1018
    $parts = explode(':', str_replace('-', '_', $selector), 2);
1019
    if (($vars = $this->availableVariables()) && isset($vars[$parts[0]]['type'])) {
1020
      $wrapper = rules_wrap_data(NULL, $vars[$parts[0]], TRUE);
1021
      if (count($parts) > 1 && $wrapper instanceof EntityMetadataWrapper) {
1022
        try {
1023
          foreach (explode(':', $parts[1]) as $name) {
1024
            if ($wrapper instanceof EntityListWrapper || $wrapper instanceof EntityStructureWrapper) {
1025
              $wrapper = $wrapper->get($name);
1026
            }
1027
            else {
1028
              return FALSE;
1029
            }
1030
          }
1031
        }
1032
        // In case of an exception or we were unable to get a wrapper, return FALSE.
1033
        catch (EntityMetadataWrapperException $e) {
1034
          return FALSE;
1035
        }
1036
      }
1037
    }
1038
    return isset($wrapper) ? $wrapper : FALSE;
1039
  }
1040

    
1041
  /**
1042
   * Returns info about the configured argument.
1043
   *
1044
   * @return
1045
   *   The determined info. If it's not known NULL is returned.
1046
   */
1047
  public function getArgumentInfo($name) {
1048
    $vars = $this->availableVariables();
1049
    if (!empty($this->settings[$name . ':select']) && !empty($vars[$this->settings[$name . ':select']])) {
1050
      return $vars[$this->settings[$name . ':select']];
1051
    }
1052
    elseif (!empty($this->settings[$name . ':select'])) {
1053
      if ($wrapper = $this->applyDataSelector($this->settings[$name . ':select'])) {
1054
        return $wrapper->info();
1055
      }
1056
      return;
1057
    }
1058
    elseif (isset($this->settings[$name . ':type'])) {
1059
      return array('type' => $this->settings[$name . ':type']);
1060
    }
1061
    elseif (!isset($this->settings[$name]) && isset($vars[$name])) {
1062
      return $vars[$name];
1063
    }
1064
  }
1065

    
1066
  /**
1067
   * Saves the configuration to the database, regardless whether this is invoked
1068
   * on the rules configuration or a contained rule element.
1069
   */
1070
  public function save($name = NULL, $module = 'rules') {
1071
    if (isset($this->parent)) {
1072
      $this->parent->sortChildren();
1073
      return $this->parent->save($name, $module);
1074
    }
1075
    else {
1076
      // Update the dirty flag before saving.
1077
      // However, this operation depends on a fully built Rules-cache, so skip
1078
      // it when entities in code are imported to the database.
1079
      // @see _rules_rebuild_cache()
1080
      if (empty($this->is_rebuild)) {
1081
        rules_config_update_dirty_flag($this, FALSE);
1082
        // In case the config is not dirty, pre-calculate the dependencies for
1083
        // later checking. Note that this also triggers processing settings if
1084
        // necessary.
1085
        // @see rules_modules_enabled()
1086
        if (empty($this->dirty)) {
1087
          $this->dependencies = $this->dependencies();
1088
        }
1089
      }
1090

    
1091
      $this->plugin = $this->itemName;
1092
      $this->name = isset($name) ? $name : $this->name;
1093
      // Module stores the module via which the rule is configured and is used
1094
      // for generating machine names with the right prefix. However, for
1095
      // default configurations 'module' points to the module providing the
1096
      // default configuration, so the module via which the rules is configured
1097
      // is stored in the "owner" property.
1098
      // @todo: For Drupal 8 use "owner" for generating machine names also and
1099
      // module only for the modules providing default configurations.
1100
      $this->module = !isset($this->module) || $module != 'rules' ? $module : $this->module;
1101
      if (!isset($this->owner)) {
1102
        $this->owner = 'rules';
1103
      }
1104
      $this->ensureNameExists();
1105
      $this->data = $this;
1106
      $return = entity_get_controller('rules_config')->save($this);
1107
      unset($this->data);
1108

    
1109
      // Care about clearing necessary caches.
1110
      if (!empty($this->is_rebuild)) {
1111
        rules_clear_cache();
1112
      }
1113
      else {
1114
        $plugin_info = $this->pluginInfo();
1115
        if (!empty($plugin_info['component'])) {
1116
          // When component variables changes rebuild the complete cache so the
1117
          // changes to the provided action/condition take affect.
1118
          if (empty($this->original) || $this->componentVariables() != $this->original->componentVariables()) {
1119
            rules_clear_cache();
1120
          }
1121
          // Clear components cached for evaluation.
1122
          cache_clear_all('comp_', 'cache_rules', TRUE);
1123
        }
1124
        elseif ($this->plugin == 'reaction rule') {
1125
          // Clear event sets cached for evaluation.
1126
          cache_clear_all('event_', 'cache_rules', TRUE);
1127
          variable_del('rules_event_whitelist');
1128
        }
1129
        drupal_static_reset('rules_get_cache');
1130
        drupal_static_reset('rules_config_update_dirty_flag');
1131
      }
1132

    
1133
      return $return;
1134
    }
1135
  }
1136

    
1137
  /**
1138
   * Ensure the configuration has a name. If not, generate one.
1139
   */
1140
  protected function ensureNameExists() {
1141
    if (!isset($this->module)) {
1142
      $this->module = 'rules';
1143
    }
1144
    if (!isset($this->name)) {
1145
      // Find a unique name for this configuration.
1146
      $this->name = $this->module . '_';
1147
      for ($i = 0; $i < 8; $i++) {
1148
        // Alphanumeric name generation.
1149
        $rnd = mt_rand(97, 122);
1150
        $this->name .= chr($rnd);
1151
      }
1152
    }
1153
  }
1154

    
1155
  public function __sleep() {
1156
    // Keep the id always as we need it for the recursion prevention.
1157
    $array = drupal_map_assoc(array('parent', 'id', 'elementId', 'weight', 'settings'));
1158
    // Keep properties related to configurations if they are there.
1159
    $info = entity_get_info('rules_config');
1160
    $fields = array_merge($info['schema_fields_sql']['base table'], array('recursion', 'tags'));
1161
    foreach ($fields as $key) {
1162
      if (isset($this->$key)) {
1163
        $array[$key] = $key;
1164
      }
1165
    }
1166
    return $array;
1167
  }
1168

    
1169
  /**
1170
   * Optimizes a rule configuration in order to speed up evaluation.
1171
   *
1172
   * Additional optimization methods may be inserted by an extender
1173
   * implementing the RulesOptimizationInterface. By default, there is no
1174
   * optimization extender.
1175
   *
1176
   * An optimization method may rearrange the internal structure of a
1177
   * configuration in order to speed up the evaluation. As the configuration may
1178
   * change optimized configurations should not be saved permanently, except
1179
   * when saving it temporary, for later execution only.
1180
   *
1181
   * @see RulesOptimizationInterface
1182
   */
1183
  public function optimize() {
1184
    // Make sure settings are processed before configs are cached.
1185
    $this->processSettings();
1186
    if ($this->facesAs('RulesOptimizationInterface')) {
1187
      $this->__call('optimize');
1188
    }
1189
  }
1190

    
1191
  /**
1192
   * If invoked on a rules configuration it is deleted from database. If
1193
   * invoked on a contained rule element, it's removed from the configuration.
1194
   */
1195
  public function delete() {
1196
    if (isset($this->parent)) {
1197
      foreach ($this->parent->children as $key => $child) {
1198
        if ($child === $this) {
1199
          unset($this->parent->children[$key]);
1200
          break;
1201
        }
1202
      }
1203
    }
1204
    elseif (isset($this->id)) {
1205
      entity_get_controller('rules_config')->delete(array($this->name));
1206
      rules_clear_cache();
1207
    }
1208
  }
1209

    
1210
  public function internalIdentifier() {
1211
    return isset($this->id) ? $this->id : NULL;
1212
  }
1213

    
1214
  /**
1215
   * Returns the config name.
1216
   */
1217
  public function identifier() {
1218
    return isset($this->name) ? $this->name : NULL;
1219
  }
1220

    
1221
  public function entityInfo() {
1222
    return entity_get_info('rules_config');
1223
  }
1224

    
1225
  public function entityType() {
1226
    return 'rules_config';
1227
  }
1228

    
1229
  /**
1230
   * Checks if the configuration has a certain exportable status.
1231
   *
1232
   * @param $status
1233
   *   A status constant, i.e. one of ENTITY_CUSTOM, ENTITY_IN_CODE,
1234
   *   ENTITY_OVERRIDDEN or ENTITY_FIXED.
1235
   *
1236
   * @return
1237
   *   TRUE if the configuration has the status, else FALSE.
1238
   *
1239
   * @see entity_has_status()
1240
   */
1241
  public function hasStatus($status) {
1242
    return $this->isRoot() && isset($this->status) && ($this->status & $status) == $status;
1243
  }
1244

    
1245
  /**
1246
   * Remove circular object references so the PHP garbage collector does its
1247
   * work.
1248
   */
1249
  public function destroy() {
1250
    parent::destroy();
1251
    $this->parent = NULL;
1252
  }
1253

    
1254
  /**
1255
   * Seamlessy invokes the method implemented via faces without having to think
1256
   * about references.
1257
   */
1258
  public function form(&$form, &$form_state, array $options = array()) {
1259
    $this->__call('form', array(&$form, &$form_state, $options));
1260
  }
1261

    
1262
  public function form_validate($form, &$form_state) {
1263
    $this->__call('form_validate', array($form, &$form_state));
1264
  }
1265

    
1266
  public function form_submit($form, &$form_state) {
1267
    $this->__call('form_submit', array($form, &$form_state));
1268
  }
1269

    
1270
  /**
1271
   * Returns the label of the element.
1272
   */
1273
  public function label() {
1274
    if (!empty($this->label) && $this->label != t('unlabeled')) {
1275
      return $this->label;
1276
    }
1277
    $info = $this->info();
1278
    return isset($info['label']) ? $info['label'] : (!empty($this->name) ? $this->name : t('unlabeled'));
1279
  }
1280

    
1281
  /**
1282
   * Returns the name of the element's plugin.
1283
   */
1284
  public function plugin() {
1285
    return $this->itemName;
1286
  }
1287

    
1288
  /**
1289
   * Returns info about the element's plugin.
1290
   */
1291
  public function pluginInfo() {
1292
    $this->forceSetUp();
1293
    return $this->itemInfo;
1294
  }
1295

    
1296
  /**
1297
   * Applies the given export.
1298
   */
1299
  public function import(array $export) {
1300
    $this->importSettings($export[strtoupper($this->plugin())]);
1301
  }
1302

    
1303
  protected function importSettings($export) {
1304
    // Import parameter settings.
1305
    $export += array('USING' => array(), 'PROVIDE' => array());
1306
    foreach ($export['USING'] as $name => $param_export) {
1307
      $this->importParameterSetting($name, $param_export);
1308
    }
1309
    foreach ($export['PROVIDE'] as $name => $var_export) {
1310
      // The key of $var_export is the variable name, the value the label.
1311
      $this->settings[$name . ':var'] = rules_array_key($var_export);
1312
      $this->settings[$name . ':label'] = reset($var_export);
1313
    }
1314
  }
1315

    
1316
  protected function importParameterSetting($name, $export) {
1317
    if (is_array($export) && isset($export['select'])) {
1318
      $this->settings[$name . ':select'] = $export['select'];
1319
      if (count($export) > 1) {
1320
        // Add in processor settings.
1321
        unset($export['select']);
1322
        $this->settings[$name . ':process'] = $export;
1323
      }
1324
    }
1325
    // Convert back the [selector] strings being an array with one entry.
1326
    elseif (is_array($export) && count($export) == 1 && isset($export[0])) {
1327
      $this->settings[$name . ':select'] = $export[0];
1328
    }
1329
    elseif (is_array($export) && isset($export['value'])) {
1330
      $this->settings[$name] = $export['value'];
1331
    }
1332
    else {
1333
      $this->settings[$name] = $export;
1334
    }
1335
  }
1336

    
1337
  /**
1338
   * Exports a rule configuration.
1339
   *
1340
   * @param $prefix
1341
   *   An optional prefix for each line.
1342
   * @param $php
1343
   *   Optional. Set to TRUE to format the export using PHP arrays. By default
1344
   *   JSON is used.
1345
   * @return
1346
   *   The exported confiugration.
1347
   *
1348
   * @see rules_import()
1349
   */
1350
  public function export($prefix = '', $php = FALSE) {
1351
    $export = $this->exportToArray();
1352
    return $this->isRoot() ? $this->returnExport($export, $prefix, $php) : $export;
1353
  }
1354

    
1355
  protected function exportToArray() {
1356
    $export[strtoupper($this->plugin())] = $this->exportSettings();
1357
    return $export;
1358
  }
1359

    
1360
  protected function exportSettings() {
1361
    $export = array();
1362
    if (!$this->isRoot()) {
1363
      foreach ($this->pluginParameterInfo() as $name => $info) {
1364
        if (($return = $this->exportParameterSetting($name, $info)) !== NULL) {
1365
          $export['USING'][$name] = $return;
1366
        }
1367
      }
1368
      foreach ($this->providesVariables() as $name => $info) {
1369
        if (!empty($info['source name'])) {
1370
          $export['PROVIDE'][$info['source name']][$name] = $info['label'];
1371
        }
1372
      }
1373
    }
1374
    return $export;
1375
  }
1376

    
1377
  protected function exportParameterSetting($name, $info) {
1378
    if (isset($this->settings[$name]) && (empty($info['optional']) || !isset($info['default value']) || $this->settings[$name] != $info['default value'])) {
1379
      // In case of an array-value wrap the value into another array, such that
1380
      // the value cannot be confused with an exported data selector.
1381
      return is_array($this->settings[$name]) ? array('value' => $this->settings[$name]) : $this->settings[$name];
1382
    }
1383
    elseif (isset($this->settings[$name . ':select'])) {
1384
      if (isset($this->settings[$name . ':process']) && $processor = $this->settings[$name . ':process']) {
1385
        $export['select'] = $this->settings[$name . ':select'];
1386
        $export += $processor instanceof RulesDataProcessor ? $processor->getChainSettings() : $processor;
1387
        return $export;
1388
      }
1389
      // If there is no processor use a simple array to abbreviate this usual
1390
      // case. In JSON this turns to a nice [selector] string.
1391
      return array($this->settings[$name . ':select']);
1392
    }
1393
  }
1394

    
1395
  /**
1396
   * Finalizies the configuration export by adding general attributes regarding
1397
   * the configuration and returns it in the right format.
1398
   */
1399
  protected function returnExport($export, $prefix = '', $php = FALSE) {
1400
    $this->ensureNameExists();
1401
    if (!empty($this->label) && $this->label != t('unlabeled')) {
1402
      $export_cfg[$this->name]['LABEL'] = $this->label;
1403
    }
1404
    $export_cfg[$this->name]['PLUGIN'] = $this->plugin();
1405
    if (!empty($this->weight)) {
1406
      $export_cfg[$this->name]['WEIGHT'] = $this->weight;
1407
    }
1408
    if (isset($this->active) && !$this->active) {
1409
      $export_cfg[$this->name]['ACTIVE'] = FALSE;
1410
    }
1411
    if (!empty($this->owner)) {
1412
      $export_cfg[$this->name]['OWNER'] = $this->owner;
1413
    }
1414
    if (!empty($this->tags)) {
1415
      $export_cfg[$this->name]['TAGS'] = $this->tags;
1416
    }
1417
    if ($modules = $this->dependencies()) {
1418
      $export_cfg[$this->name]['REQUIRES'] = $modules;
1419
    }
1420
    if (!empty($this->access_exposed)) {
1421
      $export_cfg[$this->name]['ACCESS_EXPOSED'] = $this->access_exposed;
1422
    };
1423
    $export_cfg[$this->name] += $export;
1424
    return $php ? entity_var_export($export_cfg, $prefix) : entity_var_json_export($export_cfg, $prefix);
1425
  }
1426

    
1427
  /**
1428
   * Resets any internal static caches.
1429
   *
1430
   * This function does not reset regular caches as retrieved via
1431
   * rules_get_cache(). Usually, it's invoked automatically when a Rules
1432
   * configuration is modified.
1433
   *
1434
   * Static caches are reset for the element and any elements down the tree. To
1435
   * clear static caches of the whole configuration, invoke the function at the
1436
   * root.
1437
   *
1438
   * @see RulesPlugin::availableVariables()
1439
   */
1440
  public function resetInternalCache() {
1441
    $this->availableVariables = NULL;
1442
  }
1443
}
1444

    
1445
/**
1446
 * Defines a common base class for so called "Abstract Plugins" like actions.
1447
 * Thus modules have to provide the concrete plugin implementation.
1448
 */
1449
abstract class RulesAbstractPlugin extends RulesPlugin {
1450

    
1451
  protected $elementName;
1452
  protected $info = array('parameter' => array(), 'provides' => array());
1453
  protected $infoLoaded = FALSE;
1454

    
1455
  /**
1456
   * @param $name
1457
   *   The plugin implementation's name.
1458
   * @param $info
1459
   *   Further information provided about the plugin. Optional.
1460
   * @throws RulesException
1461
   *   If validation of the passed settings fails RulesExceptions are thrown.
1462
   */
1463
  function __construct($name = NULL, $settings = array()) {
1464
    $this->elementName = $name;
1465
    $this->settings = (array) $settings + array('#_needs_processing' => TRUE);
1466
    $this->setUp();
1467
  }
1468

    
1469
  protected function setUp() {
1470
    parent::setUp();
1471
    if (isset($this->cache[$this->itemName . '_info'][$this->elementName])) {
1472
      $this->info = $this->cache[$this->itemName . '_info'][$this->elementName];
1473
      // Remember that the info has been correctly setup.
1474
      // @see self::forceSetup().
1475
      $this->infoLoaded = TRUE;
1476

    
1477
      // Register the defined class, if any.
1478
      if (isset($this->info['class'])) {
1479
        $this->faces['RulesPluginImplInterface'] = 'RulesPluginImplInterface';
1480
        $face_methods = get_class_methods('RulesPluginImplInterface');
1481
        $class_info = array(1 => $this->info['class']);
1482
        foreach ($face_methods as $method) {
1483
          $this->facesMethods[$method] = $class_info;
1484
        }
1485
      }
1486
      // Add in per-plugin implementation callbacks if any.
1487
      if (!empty($this->info['faces_cache'])) {
1488
        foreach ($this->info['faces_cache'] as $face => $data) {
1489
          list($methods, $file_names) = $data;
1490
          foreach ($methods as $method => $callback) {
1491
            $this->facesMethods[$method] = $callback;
1492
          }
1493
          foreach ((array) $file_names as $method => $name) {
1494
            $this->facesIncludes[$method] = array('module' => $this->info['module'], 'name' => $name);
1495
          }
1496
        }
1497
        // Invoke the info_alter callback, but only if it has been implemented.
1498
        if ($this->facesMethods['info_alter'] != $this->itemInfo['faces_cache'][0]['info_alter']) {
1499
          $this->__call('info_alter', array(&$this->info));
1500
        }
1501
      }
1502
    }
1503
    elseif (!empty($this->itemInfo['faces_cache']) && function_exists($this->elementName)) {
1504
      // We don't have any info, so just add the name as execution callback.
1505
      $this->override(array('execute' => $this->elementName));
1506
    }
1507
  }
1508

    
1509
  public function forceSetUp() {
1510
    if (!isset($this->cache) || (!empty($this->itemInfo['faces_cache']) && !$this->faces)) {
1511
      $this->setUp();
1512
    }
1513
    // In case we have element specific information, which is not loaded yet,
1514
    // do so now. This might happen if the element has been initially loaded
1515
    // with an incomplete cache, i.e. during cache rebuilding.
1516
    elseif (!$this->infoLoaded && isset($this->cache[$this->itemName . '_info'][$this->elementName])) {
1517
      $this->setUp();
1518
    }
1519
  }
1520

    
1521
  /**
1522
   * Returns the label of the element.
1523
   */
1524
  public function label() {
1525
    $info = $this->info();
1526
    return isset($info['label']) ? $info['label'] : t('@plugin "@name"', array('@name' => $this->elementName, '@plugin' => $this->plugin()));
1527
  }
1528

    
1529
  public function access() {
1530
    $info = $this->info();
1531
    $this->loadBasicInclude();
1532
    if (!empty($info['access callback']) && !call_user_func($info['access callback'], $this->itemName, $this->getElementName())) {
1533
      return FALSE;
1534
    }
1535
    return parent::access() && $this->__call('access');
1536
  }
1537

    
1538
  public function integrityCheck() {
1539
    // Do the usual integrity check first so the implementation's validation
1540
    // handler can rely on that already.
1541
    parent::integrityCheck();
1542
    // Make sure the element is known.
1543
    $this->forceSetUp();
1544
    if (!isset($this->cache[$this->itemName . '_info'][$this->elementName])) {
1545
      throw new RulesIntegrityException(t('Unknown @plugin %name.', array('@plugin' => $this->plugin(), '%name' => $this->elementName)));
1546
    }
1547
    $this->validate();
1548
    return $this;
1549
  }
1550

    
1551
  public function processSettings($force = FALSE) {
1552
    // Process if not done yet.
1553
    if ($force || !empty($this->settings['#_needs_processing'])) {
1554
      $this->resetInternalCache();
1555
      // In case the element implements the info alteration callback, (re-)run
1556
      // the alteration so that any settings depending info alterations are
1557
      // applied.
1558
      if ($this->facesMethods && $this->facesMethods['info_alter'] != $this->itemInfo['faces_cache'][0]['info_alter']) {
1559
        $this->__call('info_alter', array(&$this->info));
1560
      }
1561
      // First let the plugin implementation do processing, so data types of the
1562
      // parameters are fixed when we process the settings.
1563
      $this->process();
1564
      parent::processSettings($force);
1565
    }
1566
  }
1567

    
1568
  public function pluginParameterInfo() {
1569
    // Ensure the info alter callback has been executed.
1570
    $this->forceSetup();
1571
    return parent::pluginParameterInfo();
1572
  }
1573

    
1574
  public function pluginProvidesVariables() {
1575
    // Ensure the info alter callback has been executed.
1576
    $this->forceSetup();
1577
    return parent::pluginProvidesVariables();
1578
  }
1579

    
1580
  public function info() {
1581
    // Ensure the info alter callback has been executed.
1582
    $this->forceSetup();
1583
    return $this->info;
1584
  }
1585

    
1586
  protected function variableInfoAssertions() {
1587
    // Get the implementation's assertions and map them to the variable names.
1588
    if ($assertions = $this->__call('assertions')) {
1589
      foreach ($assertions as $param_name => $data) {
1590
        $name = isset($this->settings[$param_name . ':select']) ? $this->settings[$param_name . ':select'] : $param_name;
1591
        $return[$name] = $data;
1592
      }
1593
      return $return;
1594
    }
1595
  }
1596

    
1597
  public function import(array $export) {
1598
    // The key is the element name and the value the actual export.
1599
    $this->elementName = rules_array_key($export);
1600
    $export = reset($export);
1601

    
1602
    // After setting the element name, setup the element again so the right
1603
    // element info is loaded.
1604
    $this->setUp();
1605

    
1606
    if (!isset($export['USING']) && !isset($export['PROVIDES']) && !empty($export)) {
1607
      // The export has been abbreviated to skip "USING".
1608
      $export = array('USING' => $export);
1609
    }
1610
    $this->importSettings($export);
1611
  }
1612

    
1613
  protected function exportToArray() {
1614
    $export = $this->exportSettings();
1615
    if (!$this->providesVariables()) {
1616
      // Abbreviate the export making "USING" implicit.
1617
      $export = isset($export['USING']) ? $export['USING'] : array();
1618
    }
1619
    return array($this->elementName => $export);
1620
  }
1621

    
1622
  public function dependencies() {
1623
    $modules = array_flip(parent::dependencies());
1624
    $modules += array_flip((array) $this->__call('dependencies'));
1625
    return array_keys($modules + (!empty($this->info['module']) ? array($this->info['module'] => 1) : array()));
1626
  }
1627

    
1628
  public function executeByArgs($args = array()) {
1629
    $replacements = array('%label' => $this->label(), '@plugin' => $this->itemName);
1630
    rules_log('Executing @plugin %label.', $replacements, RulesLog::INFO, $this, TRUE);
1631
    $this->processSettings();
1632
    // If there is no element info, just pass through the passed arguments.
1633
    // That way we support executing actions without any info at all.
1634
    if ($this->info()) {
1635
      $state = $this->setUpState($args);
1636
      module_invoke_all('rules_config_execute', $this);
1637

    
1638
      $result = $this->evaluate($state);
1639
      $return = $this->returnVariables($state, $result);
1640
    }
1641
    else {
1642
      rules_log('Unable to execute @plugin %label.', $replacements, RulesLog::ERROR, $this);
1643
    }
1644
    $state->cleanUp();
1645
    rules_log('Finished executing of @plugin %label.', $replacements, RulesLog::INFO, $this, FALSE);
1646
    return $return;
1647
  }
1648

    
1649
  /**
1650
   * Execute the configured execution callback and log that.
1651
   */
1652
  abstract protected function executeCallback(array $args, RulesState $state = NULL);
1653

    
1654

    
1655
  public function evaluate(RulesState $state) {
1656
    $this->processSettings();
1657
    try {
1658
      // Get vars as needed for execute and call it.
1659
      return $this->executeCallback($this->getExecutionArguments($state), $state);
1660
    }
1661
    catch (RulesEvaluationException $e) {
1662
      rules_log($e->msg, $e->args, $e->severity);
1663
      rules_log('Unable to evaluate %name.', array('%name' => $this->getPluginName()), RulesLog::WARN, $this);
1664
    }
1665
    // Catch wrapper exceptions that might occur due to failures loading an
1666
    // entity or similar.
1667
    catch (EntityMetadataWrapperException $e) {
1668
      rules_log('Unable to get a data value. Error: !error', array('!error' => $e->getMessage()), RulesLog::WARN);
1669
      rules_log('Unable to evaluate %name.', array('%name' => $this->getPluginName()), RulesLog::WARN, $this);
1670
    }
1671
  }
1672

    
1673
  public function __sleep() {
1674
    return parent::__sleep() + array('elementName' => 'elementName');
1675
  }
1676

    
1677
  public function getPluginName() {
1678
    return $this->itemName ." ". $this->elementName;
1679
  }
1680

    
1681
  /**
1682
   * Gets the name of the configured action or condition.
1683
   */
1684
  public function getElementName() {
1685
    return $this->elementName;
1686
  }
1687

    
1688
  /**
1689
   * Add in the data provided by the info hooks to the cache.
1690
   */
1691
  public function rebuildCache(&$itemInfo, &$cache) {
1692
    parent::rebuildCache($itemInfo, $cache);
1693

    
1694
    // Include all declared files so we can find all implementations.
1695
    self::includeFiles();
1696

    
1697
    // Get the plugin's own info data.
1698
    $cache[$this->itemName .'_info'] = rules_fetch_data($this->itemName .'_info');
1699
    foreach ($cache[$this->itemName .'_info'] as $name => &$info) {
1700
      $info += array(
1701
        'parameter' => isset($info['arguments']) ? $info['arguments'] : array(),
1702
        'provides' => isset($info['new variables']) ? $info['new variables'] : array(),
1703
        'base' => $name,
1704
        'callbacks' => array(),
1705
      );
1706
      unset($info['arguments'], $info['new variables']);
1707

    
1708
      if (function_exists($info['base'])) {
1709
        $info['callbacks'] += array('execute' => $info['base']);
1710
      }
1711

    
1712
      // We do not need to build a faces cache for RulesPluginHandlerInterface,
1713
      // which gets added in automatically as its a parent of
1714
      // RulesPluginImplInterface.
1715
      unset($this->faces['RulesPluginHandlerInterface']);
1716

    
1717
      // Build up the per plugin implementation faces cache.
1718
      foreach ($this->faces as $interface) {
1719
        $methods = $file_names = array();
1720
        $includes = self::getIncludeFiles($info['module']);
1721

    
1722
        foreach (get_class_methods($interface) as $method) {
1723
          if (isset($info['callbacks'][$method]) && ($function = $info['callbacks'][$method])) {
1724
            $methods[$method][0] = $function;
1725
            $file_names[$method] = $this->getFileName($function, $includes);
1726
          }
1727
          // Note that this skips RulesPluginImplInterface, which is not
1728
          // implemented by plugin handlers.
1729
          elseif (isset($info['class']) && is_subclass_of($info['class'], $interface)) {
1730
            $methods[$method][1] = $info['class'];
1731
          }
1732
          elseif (function_exists($function = $info['base'] . '_' . $method)) {
1733
            $methods[$method][0] = $function;
1734
            $file_names[$method] = $this->getFileName($function, $includes);
1735
          }
1736
        }
1737
        // Cache only the plugin implementation specific callbacks.
1738
        $info['faces_cache'][$interface] = array($methods, array_filter($file_names));
1739
      }
1740
      // Filter out interfaces with no overriden methods.
1741
      $info['faces_cache'] = rules_filter_array($info['faces_cache'], 0, TRUE);
1742
      // We don't need that any more.
1743
      unset($info['callbacks'], $info['base']);
1744
    }
1745
  }
1746

    
1747
  /**
1748
   * Makes sure the providing modules' .rules.inc file is included as diverse
1749
   * callbacks may reside in that file.
1750
   */
1751
  protected function loadBasicInclude() {
1752
    static $included = array();
1753

    
1754
    if (isset($this->info['module']) && !isset($included[$this->info['module']])) {
1755
      $module = $this->info['module'];
1756
      module_load_include('inc', $module, $module . '.rules');
1757
      $included[$module] = TRUE;
1758
    }
1759
  }
1760

    
1761
  /**
1762
   * Make sure all supported destinations are included.
1763
   */
1764
  public static function includeFiles() {
1765
    static $included;
1766

    
1767
    if (!isset($included)) {
1768
      foreach (module_implements('rules_file_info') as $module) {
1769
        // rules.inc are already included thanks to the rules_hook_info() group.
1770
        foreach (self::getIncludeFiles($module, FALSE) as $name) {
1771
          module_load_include('inc', $module, $name);
1772
        }
1773
      }
1774
      $dirs = array();
1775
      foreach (module_implements('rules_directory') as $module) {
1776
        // Include all files once, so the discovery can find them.
1777
        $result = module_invoke($module, 'rules_directory');
1778
        if (!is_array($result)) {
1779
          $result = array($module => $result);
1780
        }
1781
        $dirs += $result;
1782
      }
1783
      foreach ($dirs as $module => $directory) {
1784
        foreach (glob(drupal_get_path('module', $module) . "/$directory/*.{inc,php}", GLOB_BRACE) as $filename) {
1785
          include_once $filename;
1786
        }
1787
      }
1788
      $included = TRUE;
1789
    }
1790
  }
1791

    
1792
  /**
1793
   * Returns all include files for a module. If $all is set to FALSE the
1794
   * $module.rules.inc file isn't added.
1795
   */
1796
  protected static function getIncludeFiles($module, $all = TRUE) {
1797
    $files = (array)module_invoke($module, 'rules_file_info');
1798
    // Automatically add "$module.rules_forms.inc" and "$module.rules.inc".
1799
    $files[] = $module . '.rules_forms';
1800
    if ($all) {
1801
      $files[] = $module . '.rules';
1802
    }
1803
    return $files;
1804
  }
1805

    
1806
  protected function getFileName($function, $includes) {
1807
    $reflector = new ReflectionFunction($function);
1808
    // On windows the path contains backslashes instead of slashes, fix that.
1809
    $file = str_replace('\\', '/', $reflector->getFileName());
1810
    foreach ($includes as $include) {
1811
      $pos = strpos($file, $include . '.inc');
1812
      // Test whether the file ends with the given filename.inc.
1813
      if ($pos !== FALSE && strlen($file) - $pos == strlen($include) + 4) {
1814
        return $include;
1815
      }
1816
    }
1817
  }
1818
}
1819

    
1820
/**
1821
 * Interface for objects that can be used as action.
1822
 */
1823
interface RulesActionInterface {
1824

    
1825
  /**
1826
   * @return As specified.
1827
   *
1828
   * @throws RulesEvaluationException
1829
   *   Throws an exception if not all necessary arguments have been provided.
1830
   */
1831
  public function execute();
1832
}
1833

    
1834
/**
1835
 * Interface for objects that can be used as condition.
1836
 */
1837
interface RulesConditionInterface {
1838

    
1839
  /**
1840
   * @return Boolean.
1841
   *
1842
   * @throws RulesEvaluationException
1843
   *   Throws an exception if not all necessary arguments have been provided.
1844
   */
1845
  public function execute();
1846

    
1847
  /**
1848
   * Negate the result.
1849
   */
1850
  public function negate($negate = TRUE);
1851

    
1852
  /**
1853
   * Returns whether the element is configured to negate the result.
1854
   */
1855
  public function isNegated();
1856
}
1857

    
1858
interface RulesTriggerableInterface {
1859

    
1860
  /**
1861
   * Returns the array of (configured) event names associated with this object.
1862
   */
1863
  public function events();
1864

    
1865
  /**
1866
   * Removes an event from the rule configuration.
1867
   *
1868
   * @param $event
1869
   *   The name of the (configured) event to remove.
1870
   * @return RulesTriggerableInterface
1871
   *   The object instance itself, to allow chaining.
1872
   */
1873
  public function removeEvent($event_name);
1874

    
1875
  /**
1876
   * Adds the specified event.
1877
   *
1878
   * @param string $event_name
1879
   *   The base name of the event to add.
1880
   * @param array $settings
1881
   *   (optional) The event settings. If there are no event settings, pass an
1882
   *   empty array (default).
1883
   *
1884
   * @return RulesTriggerableInterface
1885
   */
1886
  public function event($event_name, array $settings = array());
1887

    
1888
  /**
1889
   * Gets the event settings associated with the given (configured) event.
1890
   *
1891
   * @param $event_name
1892
   *   The (configured) event's name.
1893
   *
1894
   * @return array|null
1895
   *   The array of event settings, or NULL if there are no settings.
1896
   */
1897
  public function getEventSettings($event_name);
1898

    
1899
}
1900

    
1901
/**
1902
 * Provides the base interface for implementing abstract plugins via classes.
1903
 */
1904
interface RulesPluginHandlerInterface {
1905

    
1906
  /**
1907
   * Validates $settings independent from a form submission.
1908
   *
1909
   * @throws RulesIntegrityException
1910
   *   In case of validation errors, RulesIntegrityExceptions are thrown.
1911
   */
1912
  public function validate();
1913

    
1914
  /**
1915
   * Processes settings independent from a form submission.
1916
   *
1917
   * Processing results may be stored and accessed on execution time in $settings.
1918
   */
1919
  public function process();
1920

    
1921
  /**
1922
   * Allows altering of the element's action/condition info.
1923
   *
1924
   * Note that this method is also invoked on evaluation time, thus any costly
1925
   * operations should be avoided.
1926
   *
1927
   * @param $element_info
1928
   *   A reference on the element's info as returned by RulesPlugin::info().
1929
   */
1930
  public function info_alter(&$element_info);
1931

    
1932
  /**
1933
   * Checks whether the user has access to configure this element.
1934
   *
1935
   * Note that this only covers access for already created elements. In order to
1936
   * control access for creating or using elements specify an 'access callback'
1937
   * in the element's info array.
1938
   *
1939
   * @see hook_rules_action_info()
1940
   */
1941
  public function access();
1942

    
1943
  /**
1944
   * Returns an array of required modules.
1945
   */
1946
  public function dependencies();
1947

    
1948
  /**
1949
   * Alter the generated configuration form of the element.
1950
   *
1951
   * Validation and processing of the settings should be untied from the form
1952
   * and implemented in validate() and process() wherever it makes sense.
1953
   * For the remaining cases where form tied validation and processing is needed
1954
   * make use of the form API #element_validate and #value_callback properties.
1955
   */
1956
  public function form_alter(&$form, $form_state, $options);
1957

    
1958

    
1959
  /**
1960
   * Optionally returns an array of info assertions for the specified
1961
   * parameters. This allows conditions to assert additional metadata, such as
1962
   * info about the fields of a bundle.
1963
   *
1964
   * @see RulesPlugin::variableInfoAssertions()
1965
   */
1966
  public function assertions();
1967

    
1968
}
1969

    
1970
/**
1971
 * Interface for implementing conditions via classes.
1972
 *
1973
 * In addition to the interface an execute() and a static getInfo() method must
1974
 * be implemented. The static getInfo() method has to return the info as
1975
 * returned by hook_rules_condition_info() but including an additional 'name'
1976
 * key, specifying the plugin name.
1977
 * The execute method is the equivalent to the usual execution callback and
1978
 * gets the parameters passed as specified in the info array.
1979
 *
1980
 * See RulesNodeConditionType for an example and rules_discover_plugins()
1981
 * for information about class discovery.
1982
 */
1983
interface RulesConditionHandlerInterface extends RulesPluginHandlerInterface {}
1984

    
1985
/**
1986
 * Interface for implementing actions via classes.
1987
 *
1988
 * In addition to the interface an execute() and a static getInfo() method must
1989
 * be implemented. The static getInfo() method has to return the info as
1990
 * returned by hook_rules_action_info() but including an additional 'name' key,
1991
 * specifying the plugin name.
1992
 * The execute method is the equivalent to the usual execution callback and
1993
 * gets the parameters passed as specified in the info array.
1994
 *
1995
 * See RulesNodeConditionType for an example and rules_discover_plugins()
1996
 * for information about class discovery.
1997
 */
1998
interface RulesActionHandlerInterface extends RulesPluginHandlerInterface {}
1999

    
2000
/**
2001
 *
2002
 * Provides the interface used for implementing an abstract plugin by using
2003
 * the Faces extension mechanism.
2004
 */
2005
interface RulesPluginImplInterface extends RulesPluginHandlerInterface {
2006

    
2007
  /**
2008
   * Execute the action or condition making use of the parameters as specified.
2009
   */
2010
  public function execute();
2011
}
2012

    
2013
/**
2014
 * Interface for optimizing evaluation.
2015
 *
2016
 * @see RulesContainerPlugin::optimize()
2017
 */
2018
interface RulesOptimizationInterface {
2019
  /**
2020
   * Optimizes a rule configuration in order to speed up evaluation.
2021
   */
2022
  public function optimize();
2023
}
2024

    
2025
/**
2026
 * Base class for implementing abstract plugins via classes.
2027
 */
2028
abstract class RulesPluginHandlerBase extends FacesExtender implements RulesPluginHandlerInterface {
2029

    
2030
  /**
2031
   * @var RulesAbstractPlugin
2032
   */
2033
  protected $element;
2034

    
2035
  /**
2036
   * Overridden to provide $this->element to make the code more meaningful.
2037
   */
2038
  public function __construct(FacesExtendable $object) {
2039
    $this->object = $object;
2040
    $this->element = $object;
2041
  }
2042

    
2043
  /**
2044
   * Implements RulesPluginImplInterface.
2045
   */
2046
  public function access() {
2047
    return TRUE;
2048
  }
2049

    
2050
  public function validate() {}
2051
  public function process() {}
2052
  public function info_alter(&$element_info) {}
2053
  public function dependencies() {}
2054
  public function form_alter(&$form, $form_state, $options) {}
2055
  public function assertions() {}
2056
}
2057

    
2058
/**
2059
 * Base class for implementing conditions via classes.
2060
 */
2061
abstract class RulesConditionHandlerBase extends RulesPluginHandlerBase implements RulesConditionHandlerInterface {}
2062

    
2063
/**
2064
 * Base class for implementing actions via classes.
2065
 */
2066
abstract class RulesActionHandlerBase extends RulesPluginHandlerBase implements RulesActionHandlerInterface {}
2067

    
2068
/**
2069
 * Class providing default implementations of the methods of the RulesPluginImplInterface.
2070
 *
2071
 * If a plugin implementation does not provide a function for a method, the
2072
 * default method of this class will be invoked.
2073
 *
2074
 * @see RulesPluginImplInterface
2075
 * @see RulesAbstractPlugin
2076
 */
2077
class RulesAbstractPluginDefaults extends RulesPluginHandlerBase implements RulesPluginImplInterface {
2078

    
2079
  public function execute() {
2080
    throw new RulesEvaluationException($this->object->getPluginName() .": Execution implementation is missing.", array(), $this->object, RulesLog::ERROR);
2081
  }
2082
}
2083

    
2084
/**
2085
 * A RecursiveIterator for rule elements.
2086
 */
2087
class RulesRecursiveElementIterator extends ArrayIterator implements RecursiveIterator {
2088

    
2089
   public function getChildren() {
2090
     return $this->current()->getIterator();
2091
   }
2092

    
2093
   public function hasChildren() {
2094
      return $this->current() instanceof IteratorAggregate;
2095
   }
2096
}
2097

    
2098
/**
2099
 * Base class for ContainerPlugins like Rules, Logical Operations or Loops.
2100
 */
2101
abstract class RulesContainerPlugin extends RulesPlugin implements IteratorAggregate {
2102

    
2103
  protected $children = array();
2104

    
2105
  public function __construct($variables = array()) {
2106
    $this->setUp();
2107
    if (!empty($variables) && $this->isRoot()) {
2108
      $this->info['variables'] = $variables;
2109
    }
2110
  }
2111

    
2112
  /**
2113
   * Returns the specified variables, in case the plugin is used as component.
2114
   */
2115
  public function &componentVariables() {
2116
    if ($this->isRoot()) {
2117
      $this->info += array('variables' => array());
2118
      return $this->info['variables'];
2119
    }
2120
    // We have to return a reference in any case.
2121
    $return = NULL;
2122
    return $return;
2123
  }
2124

    
2125
  /**
2126
   * Allow access to the children through the iterator.
2127
   *
2128
   * @return RulesRecursiveElementIterator
2129
   */
2130
  public function getIterator() {
2131
    return new RulesRecursiveElementIterator($this->children);
2132
  }
2133

    
2134
  /**
2135
   * @return RulesContainerPlugin
2136
   */
2137
  public function integrityCheck() {
2138
    if (!empty($this->info['variables']) && !$this->isRoot()) {
2139
      throw new RulesIntegrityException(t('%plugin: Specifying state variables is not possible for child elements.', array('%plugin' => $this->getPluginName())), $this);
2140
    }
2141
    parent::integrityCheck();
2142
    foreach ($this->children as $child) {
2143
      $child->integrityCheck();
2144
    }
2145
    return $this;
2146
  }
2147

    
2148
  public function dependencies() {
2149
    $modules = array_flip(parent::dependencies());
2150
    foreach ($this->children as $child) {
2151
      $modules += array_flip($child->dependencies());
2152
    }
2153
    return array_keys($modules);
2154
  }
2155

    
2156
  public function parameterInfo($optional = FALSE) {
2157
    $params = parent::parameterInfo($optional);
2158
    if (isset($this->info['variables'])) {
2159
      foreach ($this->info['variables'] as $name => $var_info) {
2160
        if (empty($var_info['handler']) && (!isset($var_info['parameter']) || $var_info['parameter'])) {
2161
          $params[$name] = $var_info;
2162
          // For lists allow empty variables by default.
2163
          if (entity_property_list_extract_type($var_info['type'])) {
2164
            $params[$name] += array('allow null' => TRUE);
2165
          }
2166
        }
2167
      }
2168
    }
2169
    return $params;
2170
  }
2171

    
2172
  public function availableVariables() {
2173
    if (!isset($this->availableVariables)) {
2174
      if ($this->isRoot()) {
2175
        $this->availableVariables = RulesState::defaultVariables();
2176
        if (isset($this->info['variables'])) {
2177
          $this->availableVariables += $this->info['variables'];
2178
        }
2179
      }
2180
      else {
2181
        $this->availableVariables = $this->parent->stateVariables($this);
2182
      }
2183
    }
2184
    return $this->availableVariables;
2185
  }
2186

    
2187
  /**
2188
   * Returns info about variables available in the evaluation state for any
2189
   * children elements or if given for a special child element.
2190
   *
2191
   * @param $element
2192
   *   The element for which the available state variables should be returned.
2193
   *   If NULL is given, the variables available before any children are invoked
2194
   *   are returned. If set to TRUE, the variables available after evaluating
2195
   *   all children will be returned.
2196
   */
2197
  protected function stateVariables($element = NULL) {
2198
    $vars = $this->availableVariables();
2199
    if (isset($element)) {
2200
      // Add in variables provided by siblings executed before the element.
2201
      foreach ($this->children as $child) {
2202
        if ($child === $element) {
2203
          break;
2204
        }
2205
        $vars += $child->providesVariables();
2206
        // Take variable info assertions into account.
2207
        if ($assertions = $child->variableInfoAssertions()) {
2208
          $vars = RulesData::addMetadataAssertions($vars, $assertions);
2209
        }
2210
      }
2211
    }
2212
    return $vars;
2213
  }
2214

    
2215
  protected function variableInfoAssertions() {
2216
    $assertions = array();
2217
    foreach ($this->children as $child) {
2218
      if ($add = $child->variableInfoAssertions()) {
2219
        $assertions = rules_update_array($assertions, $add);
2220
      }
2221
    }
2222
    return $assertions;
2223
  }
2224

    
2225
  protected function setUpVariables() {
2226
    return isset($this->info['variables']) ? parent::parameterInfo(TRUE) + $this->info['variables'] : $this->parameterInfo(TRUE);
2227
  }
2228

    
2229
  /**
2230
   * Condition containers just return a boolean while action containers return
2231
   * the configured provided variables as an array of variables.
2232
   */
2233
  public function executeByArgs($args = array()) {
2234
    $replacements = array('%label' => $this->label(), '@plugin' => $this->itemName);
2235
    rules_log('Executing @plugin %label.', $replacements, RulesLog::INFO, $this, TRUE);
2236
    $this->processSettings();
2237
    $state = $this->setUpState($args);
2238

    
2239
    // Handle recursion prevention.
2240
    if ($state->isBlocked($this)) {
2241
      return rules_log('Not evaluating @plugin %label to prevent recursion.', array('%label' => $this->label(), '@plugin' => $this->plugin()), RulesLog::INFO);
2242
    }
2243
    // Block the config to prevent any future recursion.
2244
    $state->block($this);
2245

    
2246
    module_invoke_all('rules_config_execute', $this);
2247
    $result = $this->evaluate($state);
2248
    $return = $this->returnVariables($state, $result);
2249

    
2250
    $state->unblock($this);
2251
    $state->cleanUp();
2252
    rules_log('Finished executing of @plugin %label.', $replacements, RulesLog::INFO, $this, FALSE);
2253
    return $return;
2254
  }
2255

    
2256
  public function access() {
2257
    foreach ($this->children as $key => $child) {
2258
      if (!$child->access()) {
2259
        return FALSE;
2260
      }
2261
    }
2262
    return TRUE;
2263
  }
2264

    
2265
  public function destroy() {
2266
    foreach ($this->children as $key => $child) {
2267
      $child->destroy();
2268
    }
2269
    parent::destroy();
2270
  }
2271

    
2272
  /**
2273
   * By default we do a deep clone.
2274
   */
2275
  public function __clone() {
2276
    parent::__clone();
2277
    foreach ($this->children as $key => $child) {
2278
      $this->children[$key] = clone $child;
2279
      $this->children[$key]->parent = $this;
2280
    }
2281
  }
2282

    
2283
  /**
2284
   * Override delete to keep the children alive, if possible.
2285
   */
2286
  public function delete($keep_children = TRUE) {
2287
    if (isset($this->parent) && $keep_children) {
2288
      foreach ($this->children as $child) {
2289
        $child->setParent($this->parent);
2290
      }
2291
    }
2292
    parent::delete();
2293
  }
2294

    
2295
  public function __sleep() {
2296
    return parent::__sleep() + array('children' => 'children', 'info' => 'info');
2297
  }
2298

    
2299
  /**
2300
   * Sorts all child elements by their weight.
2301
   *
2302
   * @param $deep
2303
   *   If enabled a deep sort is performed, thus the whole element tree below
2304
   *   this element is sorted.
2305
   */
2306
  public function sortChildren($deep = FALSE) {
2307
    // Make sure the array order is kept in case two children have the same
2308
    // weight by ensuring later childrens would have higher weights.
2309
    foreach (array_values($this->children) as $i => $child) {
2310
      $child->weight += $i / 1000;
2311
    }
2312
    usort($this->children, array('RulesPlugin', 'compare'));
2313

    
2314
    // Fix up the weights afterwards to be unique integers.
2315
    foreach (array_values($this->children) as $i => $child) {
2316
      $child->weight = $i;
2317
    }
2318

    
2319
    if ($deep) {
2320
      foreach (new ParentIterator($this->getIterator()) as $child) {
2321
        $child->sortChildren(TRUE);
2322
      }
2323
    }
2324
    $this->resetInternalCache();
2325
  }
2326

    
2327
  protected function exportChildren($key = NULL) {
2328
    $key = isset($key) ? $key : strtoupper($this->plugin());
2329
    $export[$key] = array();
2330
    foreach ($this->children as $child) {
2331
      $export[$key][] = $child->export();
2332
    }
2333
    return $export;
2334
  }
2335

    
2336
  /**
2337
   * Determines whether the element should be exported in flat style. Flat style
2338
   * means that the export keys are written directly into the export array,
2339
   * whereas else the export is written into a sub-array.
2340
   */
2341
  protected function exportFlat() {
2342
    // By default we always use flat style for plugins without any parameters
2343
    // or provided variables, as then only children have to be exported. E.g.
2344
    // this applies to the OR and AND plugins.
2345
    return $this->isRoot() || (!$this->pluginParameterInfo() && !$this->providesVariables());
2346
  }
2347

    
2348
  protected function exportToArray() {
2349
    $export = array();
2350
    if (!empty($this->info['variables'])) {
2351
      $export['USES VARIABLES'] = $this->info['variables'];
2352
    }
2353
    if ($this->exportFlat()) {
2354
      $export += $this->exportSettings() + $this->exportChildren();
2355
    }
2356
    else {
2357
      $export[strtoupper($this->plugin())] = $this->exportSettings() + $this->exportChildren();
2358
    }
2359
    return $export;
2360
  }
2361

    
2362
  public function import(array $export) {
2363
    if (!empty($export['USES VARIABLES'])) {
2364
      $this->info['variables'] = $export['USES VARIABLES'];
2365
    }
2366
    // Care for exports having the export array nested in a sub-array.
2367
    if (!$this->exportFlat()) {
2368
      $export = reset($export);
2369
    }
2370
    $this->importSettings($export);
2371
    $this->importChildren($export);
2372
  }
2373

    
2374
  protected function importChildren($export, $key = NULL) {
2375
    $key = isset($key) ? $key : strtoupper($this->plugin());
2376
    foreach ($export[$key] as $child_export) {
2377
      $plugin = _rules_import_get_plugin(rules_array_key($child_export), $this instanceof RulesActionInterface ? 'action' : 'condition');
2378
      $child = rules_plugin_factory($plugin);
2379
      $child->setParent($this);
2380
      $child->import($child_export);
2381
    }
2382
  }
2383

    
2384
  public function resetInternalCache() {
2385
    $this->availableVariables = NULL;
2386
    foreach ($this->children as $child) {
2387
      $child->resetInternalCache();
2388
    }
2389
  }
2390

    
2391
  /**
2392
   * Overrides optimize().
2393
   */
2394
  public function optimize() {
2395
    parent::optimize();
2396
    // Now let the children optimize itself.
2397
    foreach ($this as $element) {
2398
      $element->optimize();
2399
    }
2400
  }
2401
}
2402

    
2403
/**
2404
 * Base class for all action containers.
2405
 */
2406
abstract class RulesActionContainer extends RulesContainerPlugin implements RulesActionInterface {
2407

    
2408
  public function __construct($variables = array(), $providesVars = array()) {
2409
    parent::__construct($variables);
2410
    // The provided vars of a component are the names of variables, which should
2411
    // be provided to the caller. See rule().
2412
    if ($providesVars) {
2413
      $this->info['provides'] = $providesVars;
2414
    }
2415
  }
2416

    
2417
  /**
2418
   * Add an action. Pass either an instance of the RulesActionInterface
2419
   * or the arguments as needed by rules_action().
2420
   *
2421
   * @return RulesActionContainer
2422
   *   Returns $this to support chained usage.
2423
   */
2424
  public function action($name, $settings = array()) {
2425
    $action = (is_object($name) && $name instanceof RulesActionInterface) ? $name : rules_action($name, $settings);
2426
    $action->setParent($this);
2427
    return $this;
2428
  }
2429

    
2430
  /**
2431
   * Evaluate, whereas by default new vars are visible in the parent's scope.
2432
   */
2433
  public function evaluate(RulesState $state) {
2434
    foreach ($this->children as $action) {
2435
      $action->evaluate($state);
2436
    }
2437
  }
2438

    
2439
  public function pluginProvidesVariables() {
2440
    return array();
2441
  }
2442

    
2443
  public function providesVariables() {
2444
    $provides = parent::providesVariables();
2445
    if (isset($this->info['provides']) && $vars = $this->componentVariables()) {
2446
      // Determine the full variable info for the provided variables. Note that
2447
      // we only support providing variables list in the component vars.
2448
      $provides += array_intersect_key($vars, array_flip($this->info['provides']));
2449
    }
2450
    return $provides;
2451
  }
2452

    
2453
  /**
2454
   * Returns an array of variable names, which are provided by passing through
2455
   * the provided variables of the children.
2456
   */
2457
  public function &componentProvidesVariables() {
2458
    $this->info += array('provides' => array());
2459
    return $this->info['provides'];
2460
  }
2461

    
2462
  protected function exportToArray() {
2463
    $export = parent::exportToArray();
2464
    if (!empty($this->info['provides'])) {
2465
      $export['PROVIDES VARIABLES'] = $this->info['provides'];
2466
    }
2467
    return $export;
2468
  }
2469

    
2470
  public function import(array $export) {
2471
    parent::import($export);
2472
    if (!empty($export['PROVIDES VARIABLES'])) {
2473
      $this->info['provides'] = $export['PROVIDES VARIABLES'];
2474
    }
2475
  }
2476
}
2477

    
2478
/**
2479
 * Base class for all condition containers.
2480
 */
2481
abstract class RulesConditionContainer extends RulesContainerPlugin implements RulesConditionInterface {
2482

    
2483
  protected $negate = FALSE;
2484

    
2485
  /**
2486
   * Add a condition. Pass either an instance of the RulesConditionInterface
2487
   * or the arguments as needed by rules_condition().
2488
   *
2489
   * @return RulesConditionContainer
2490
   *   Returns $this to support chained usage.
2491
   */
2492
  public function condition($name, $settings = array()) {
2493
    $condition = (is_object($name) && $name instanceof RulesConditionInterface) ? $name : rules_condition($name, $settings);
2494
    $condition->setParent($this);
2495
    return $this;
2496
  }
2497

    
2498
  /**
2499
   * Negate this condition.
2500
   *
2501
   * @return RulesConditionContainer
2502
   */
2503
  public function negate($negate = TRUE) {
2504
    $this->negate = (bool) $negate;
2505
    return $this;
2506
  }
2507

    
2508
  public function isNegated() {
2509
    return $this->negate;
2510
  }
2511

    
2512
  public function __sleep() {
2513
    return parent::__sleep() + array('negate' => 'negate');
2514
  }
2515

    
2516
  /**
2517
   * Just return the condition container's result.
2518
   */
2519
  protected function returnVariables(RulesState $state, $result = NULL) {
2520
    return $result;
2521
  }
2522

    
2523
  protected function exportChildren($key = NULL) {
2524
    $key = isset($key) ? $key : strtoupper($this->plugin());
2525
    return parent::exportChildren($this->negate ? 'NOT ' . $key : $key);
2526
  }
2527

    
2528
  protected function importChildren($export, $key = NULL) {
2529
    $key = isset($key) ? $key : strtoupper($this->plugin());
2530
    // Care for negated elements.
2531
    if (!isset($export[$key]) && isset($export['NOT ' . $key])) {
2532
      $this->negate = TRUE;
2533
      $key = 'NOT ' . $key;
2534
    }
2535
    parent::importChildren($export, $key);
2536
  }
2537

    
2538
  /**
2539
   * Overridden to exclude variable assertions of negated conditions.
2540
   */
2541
  protected function stateVariables($element = NULL) {
2542
    $vars = $this->availableVariables();
2543
    if (isset($element)) {
2544
      // Add in variables provided by siblings executed before the element.
2545
      foreach ($this->children as $child) {
2546
        if ($child === $element) {
2547
          break;
2548
        }
2549
        $vars += $child->providesVariables();
2550
        // Take variable info assertions into account.
2551
        if (!$this->negate && !$child->isNegated() && ($assertions = $child->variableInfoAssertions())) {
2552
          $vars = RulesData::addMetadataAssertions($vars, $assertions);
2553
        }
2554
      }
2555
    }
2556
    return $vars;
2557
  }
2558
}
2559

    
2560
/**
2561
 * The rules default logging class.
2562
 */
2563
class RulesLog {
2564

    
2565
  const INFO  = 1;
2566
  const WARN  = 2;
2567
  const ERROR = 3;
2568

    
2569
  static protected $logger;
2570

    
2571
  /**
2572
   * @return RulesLog
2573
   *   Returns the rules logger instance.
2574
   */
2575
  static function logger() {
2576
    if (!isset(self::$logger)) {
2577
      $class = __CLASS__;
2578
      self::$logger = new $class(variable_get('rules_log_level', self::INFO));
2579
    }
2580
    return self::$logger;
2581
  }
2582

    
2583
  protected $log = array();
2584
  protected $logLevel, $line = 0;
2585

    
2586
  /**
2587
   * This is a singleton.
2588
   */
2589
  protected function __construct($logLevel = self::WARN) {
2590
    $this->logLevel = $logLevel;
2591
  }
2592

    
2593
  public function __clone() {
2594
    throw new Exception("Cannot clone the logger.");
2595
  }
2596

    
2597
  /**
2598
   * Logs a log message.
2599
   *
2600
   * @see rules_log()
2601
   */
2602
  public function log($msg, $args = array(), $logLevel = self::INFO, $scope = NULL, $path = NULL) {
2603
    if ($logLevel >= $this->logLevel) {
2604
      $this->log[] = array($msg, $args, $logLevel, microtime(TRUE), $scope, $path);
2605
    }
2606
  }
2607

    
2608
  /**
2609
   * Checks the log and throws an exception if there were any problems.
2610
   */
2611
  function checkLog($logLevel = self::WARN) {
2612
    foreach ($this->log as $entry) {
2613
      if ($entry[2] >= $logLevel) {
2614
        throw new Exception($this->render());
2615
      }
2616
    }
2617
  }
2618

    
2619
  /**
2620
   * Checks the log for (error) messages with a log level equal or higher than the given one.
2621
   *
2622
   * @return
2623
   *   Whether the an error has been logged.
2624
   */
2625
  public function hasErrors($logLevel = self::WARN) {
2626
    foreach ($this->log as $entry) {
2627
      if ($entry[2] >= $logLevel) {
2628
        return TRUE;
2629
      }
2630
    }
2631
    return FALSE;
2632
  }
2633

    
2634
  /**
2635
   * Gets an array of logged messages.
2636
   */
2637
  public function get() {
2638
    return $this->log;
2639
  }
2640

    
2641
  /**
2642
   * Renders the whole log.
2643
   */
2644
  public function render() {
2645
    $line = 0;
2646
    $output = array();
2647
    while (isset($this->log[$line])) {
2648
      $vars['head'] = t($this->log[$line][0], $this->log[$line][1]);
2649
      $vars['log'] = $this->renderHelper($line);
2650
      $output[] = theme('rules_debug_element', $vars);
2651
      $line++;
2652
    }
2653
    return implode('', $output);
2654
  }
2655

    
2656
  /**
2657
   * Renders the log of one event invocation.
2658
   */
2659
  protected function renderHelper(&$line = 0) {
2660
    $startTime = isset($this->log[$line][3]) ? $this->log[$line][3] : 0;
2661
    $output = array();
2662
    while ($line < count($this->log)) {
2663
      if ($output && !empty($this->log[$line][4])) {
2664
        // The next entry stems from another evaluated set, add in its log
2665
        // messages here.
2666
        $vars['head'] = t($this->log[$line][0], $this->log[$line][1]);
2667
        if (isset($this->log[$line][5])) {
2668
          $vars['link'] = '[' . l('edit', $this->log[$line][5]) . ']';
2669
        }
2670
        $vars['log'] = $this->renderHelper($line);
2671
        $output[] = theme('rules_debug_element', $vars);
2672
      }
2673
      else {
2674
        $formatted_diff = round(($this->log[$line][3] - $startTime) * 1000, 3) .' ms';
2675
        $msg = $formatted_diff .' '. t($this->log[$line][0], $this->log[$line][1]);
2676
        if ($this->log[$line][2] >= RulesLog::WARN) {
2677
          $level = $this->log[$line][2] == RulesLog::WARN ? 'warn' : 'error';
2678
          $msg = '<span class="rules-debug-' . $level . '">'. $msg .'</span>';
2679
        }
2680
        if (isset($this->log[$line][5]) && !isset($this->log[$line][4])) {
2681
          $msg .= ' [' . l('edit', $this->log[$line][5]) . ']';
2682
        }
2683
        $output[] = $msg;
2684

    
2685
        if (isset($this->log[$line][4]) && !$this->log[$line][4]) {
2686
          // This was the last log entry of this set.
2687
          return theme('item_list', array('items' => $output));
2688
        }
2689
      }
2690
      $line++;
2691
    }
2692
    return theme('item_list', array('items' => $output));
2693
  }
2694

    
2695
  /**
2696
   * Clears the logged messages.
2697
   */
2698
  public function clear() {
2699
    $this->log = array();
2700
  }
2701
}
2702

    
2703
/**
2704
 * A common exception for Rules.
2705
 *
2706
 * This class can be used to catch all exceptions thrown by Rules.
2707
 */
2708
abstract class RulesException extends Exception {}
2709

    
2710
/**
2711
 * An exception that is thrown during evaluation.
2712
 *
2713
 * Messages are prepared to be logged to the watchdog, thus not yet translated.
2714
 *
2715
 * @see watchdog()
2716
 */
2717
class RulesEvaluationException extends RulesException {
2718

    
2719
  public $msg, $args, $severity, $element, $keys = array();
2720

    
2721
  /**
2722
   * @param $msg
2723
   *   The exception message containing placeholder as t().
2724
   * @param $args
2725
   *   Replacement arguments such as for t().
2726
   * @param $element
2727
   *   The element of a configuration causing the exception or an array
2728
   *   consisting of the element and keys specifying a setting value causing
2729
   *   the exception.
2730
   * @param $severity
2731
   *   The RulesLog severity. Defaults to RulesLog::WARN.
2732
   */
2733
  function __construct($msg, array $args = array(), $element = NULL, $severity = RulesLog::WARN) {
2734
    $this->element = is_array($element) ? array_shift($element) : $element;
2735
    $this->keys = is_array($element) ? $element : array();
2736
    $this->msg = $msg;
2737
    $this->args = $args;
2738
    $this->severity = $severity;
2739
    // If an error happened, run the integrity check on the rules configuration
2740
    // and mark it as dirty if it the check fails.
2741
    if ($severity == RulesLog::ERROR && isset($this->element)) {
2742
      $rules_config = $this->element->root();
2743
      rules_config_update_dirty_flag($rules_config);
2744
      // If we discovered a broken configuration, exclude it in future.
2745
      if ($rules_config->dirty) {
2746
        rules_clear_cache();
2747
      }
2748
    }
2749
    // @todo fix _drupal_decode_exception() to use __toString() and override it.
2750
    $this->message = t($this->msg, $this->args);
2751
  }
2752
}
2753

    
2754
/**
2755
 * An exception that is thrown for Rules configurations that fail the integrity check.
2756
 *
2757
 * @see RulesPlugin::integrityCheck()
2758
 */
2759
class RulesIntegrityException extends RulesException {
2760

    
2761
  public $msg, $element, $keys = array();
2762

    
2763
  /**
2764
   * @param string $msg
2765
   *   The exception message, already translated.
2766
   * @param $element
2767
   *   The element of a configuration causing the exception or an array
2768
   *   consisting of the element and keys specifying a parameter or provided
2769
   *   variable causing the exception, e.g.
2770
   *   @code array($element, 'parameter', 'node') @endcode.
2771
   */
2772
  function __construct($msg, $element = NULL) {
2773
    $this->element = is_array($element) ? array_shift($element) : $element;
2774
    $this->keys = is_array($element) ? $element : array();
2775
    parent::__construct($msg);
2776
  }
2777
}
2778

    
2779
/**
2780
 * An exception that is thrown for missing module dependencies.
2781
 */
2782
class RulesDependencyException extends RulesIntegrityException {}
2783

    
2784
/**
2785
 * Determines the plugin to be used for importing a child element.
2786
 *
2787
 * @param $key
2788
 *   The key to look for, e.g. 'OR' or 'DO'.
2789
 * @param $default
2790
 *   The default to return if no special plugin can be found.
2791
 */
2792
function _rules_import_get_plugin($key, $default = 'action') {
2793
  $map = &drupal_static(__FUNCTION__);
2794
  if (!isset($map)) {
2795
    $cache = rules_get_cache();
2796
    foreach ($cache['plugin_info'] as $name => $info) {
2797
      if (!empty($info['embeddable'])) {
2798
        $info += array('import keys' => array(strtoupper($name)));
2799
        foreach ($info['import keys'] as $k) {
2800
          $map[$k] = $name;
2801
        }
2802
      }
2803
    }
2804
  }
2805
  // Cut of any leading NOT from the key.
2806
  if (strpos($key, 'NOT ') === 0) {
2807
    $key = substr($key, 4);
2808
  }
2809
  if (isset($map[$key])) {
2810
    return $map[$key];
2811
  }
2812
  return $default;
2813
}