Projet

Général

Profil

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

root / drupal7 / sites / all / modules / rules / includes / rules.core.inc @ 73ab1d0a

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
          // Clear event whitelist for rebuild.
1128
          cache_clear_all('rules_event_whitelist', 'cache_rules', TRUE);
1129
        }
1130
        drupal_static_reset('rules_get_cache');
1131
        drupal_static_reset('rules_config_update_dirty_flag');
1132
      }
1133

    
1134
      return $return;
1135
    }
1136
  }
1137

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1655

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1827
/**
1828
 * Interface for objects that can be used as action.
1829
 */
1830
interface RulesActionInterface {
1831

    
1832
  /**
1833
   * @return As specified.
1834
   *
1835
   * @throws RulesEvaluationException
1836
   *   Throws an exception if not all necessary arguments have been provided.
1837
   */
1838
  public function execute();
1839
}
1840

    
1841
/**
1842
 * Interface for objects that can be used as condition.
1843
 */
1844
interface RulesConditionInterface {
1845

    
1846
  /**
1847
   * @return Boolean.
1848
   *
1849
   * @throws RulesEvaluationException
1850
   *   Throws an exception if not all necessary arguments have been provided.
1851
   */
1852
  public function execute();
1853

    
1854
  /**
1855
   * Negate the result.
1856
   */
1857
  public function negate($negate = TRUE);
1858

    
1859
  /**
1860
   * Returns whether the element is configured to negate the result.
1861
   */
1862
  public function isNegated();
1863
}
1864

    
1865
interface RulesTriggerableInterface {
1866

    
1867
  /**
1868
   * Returns the array of (configured) event names associated with this object.
1869
   */
1870
  public function events();
1871

    
1872
  /**
1873
   * Removes an event from the rule configuration.
1874
   *
1875
   * @param $event
1876
   *   The name of the (configured) event to remove.
1877
   * @return RulesTriggerableInterface
1878
   *   The object instance itself, to allow chaining.
1879
   */
1880
  public function removeEvent($event_name);
1881

    
1882
  /**
1883
   * Adds the specified event.
1884
   *
1885
   * @param string $event_name
1886
   *   The base name of the event to add.
1887
   * @param array $settings
1888
   *   (optional) The event settings. If there are no event settings, pass an
1889
   *   empty array (default).
1890
   *
1891
   * @return RulesTriggerableInterface
1892
   */
1893
  public function event($event_name, array $settings = array());
1894

    
1895
  /**
1896
   * Gets the event settings associated with the given (configured) event.
1897
   *
1898
   * @param $event_name
1899
   *   The (configured) event's name.
1900
   *
1901
   * @return array|null
1902
   *   The array of event settings, or NULL if there are no settings.
1903
   */
1904
  public function getEventSettings($event_name);
1905

    
1906
}
1907

    
1908
/**
1909
 * Provides the base interface for implementing abstract plugins via classes.
1910
 */
1911
interface RulesPluginHandlerInterface {
1912

    
1913
  /**
1914
   * Validates $settings independent from a form submission.
1915
   *
1916
   * @throws RulesIntegrityException
1917
   *   In case of validation errors, RulesIntegrityExceptions are thrown.
1918
   */
1919
  public function validate();
1920

    
1921
  /**
1922
   * Processes settings independent from a form submission.
1923
   *
1924
   * Processing results may be stored and accessed on execution time in $settings.
1925
   */
1926
  public function process();
1927

    
1928
  /**
1929
   * Allows altering of the element's action/condition info.
1930
   *
1931
   * Note that this method is also invoked on evaluation time, thus any costly
1932
   * operations should be avoided.
1933
   *
1934
   * @param $element_info
1935
   *   A reference on the element's info as returned by RulesPlugin::info().
1936
   */
1937
  public function info_alter(&$element_info);
1938

    
1939
  /**
1940
   * Checks whether the user has access to configure this element.
1941
   *
1942
   * Note that this only covers access for already created elements. In order to
1943
   * control access for creating or using elements specify an 'access callback'
1944
   * in the element's info array.
1945
   *
1946
   * @see hook_rules_action_info()
1947
   */
1948
  public function access();
1949

    
1950
  /**
1951
   * Returns an array of required modules.
1952
   */
1953
  public function dependencies();
1954

    
1955
  /**
1956
   * Alter the generated configuration form of the element.
1957
   *
1958
   * Validation and processing of the settings should be untied from the form
1959
   * and implemented in validate() and process() wherever it makes sense.
1960
   * For the remaining cases where form tied validation and processing is needed
1961
   * make use of the form API #element_validate and #value_callback properties.
1962
   */
1963
  public function form_alter(&$form, $form_state, $options);
1964

    
1965

    
1966
  /**
1967
   * Optionally returns an array of info assertions for the specified
1968
   * parameters. This allows conditions to assert additional metadata, such as
1969
   * info about the fields of a bundle.
1970
   *
1971
   * @see RulesPlugin::variableInfoAssertions()
1972
   */
1973
  public function assertions();
1974

    
1975
}
1976

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

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

    
2007
/**
2008
 *
2009
 * Provides the interface used for implementing an abstract plugin by using
2010
 * the Faces extension mechanism.
2011
 */
2012
interface RulesPluginImplInterface extends RulesPluginHandlerInterface {
2013

    
2014
  /**
2015
   * Execute the action or condition making use of the parameters as specified.
2016
   */
2017
  public function execute();
2018
}
2019

    
2020
/**
2021
 * Interface for optimizing evaluation.
2022
 *
2023
 * @see RulesContainerPlugin::optimize()
2024
 */
2025
interface RulesOptimizationInterface {
2026
  /**
2027
   * Optimizes a rule configuration in order to speed up evaluation.
2028
   */
2029
  public function optimize();
2030
}
2031

    
2032
/**
2033
 * Base class for implementing abstract plugins via classes.
2034
 */
2035
abstract class RulesPluginHandlerBase extends FacesExtender implements RulesPluginHandlerInterface {
2036

    
2037
  /**
2038
   * @var RulesAbstractPlugin
2039
   */
2040
  protected $element;
2041

    
2042
  /**
2043
   * Overridden to provide $this->element to make the code more meaningful.
2044
   */
2045
  public function __construct(FacesExtendable $object) {
2046
    $this->object = $object;
2047
    $this->element = $object;
2048
  }
2049

    
2050
  /**
2051
   * Implements RulesPluginImplInterface.
2052
   */
2053
  public function access() {
2054
    return TRUE;
2055
  }
2056

    
2057
  public function validate() {}
2058
  public function process() {}
2059
  public function info_alter(&$element_info) {}
2060
  public function dependencies() {}
2061
  public function form_alter(&$form, $form_state, $options) {}
2062
  public function assertions() {}
2063
}
2064

    
2065
/**
2066
 * Base class for implementing conditions via classes.
2067
 */
2068
abstract class RulesConditionHandlerBase extends RulesPluginHandlerBase implements RulesConditionHandlerInterface {}
2069

    
2070
/**
2071
 * Base class for implementing actions via classes.
2072
 */
2073
abstract class RulesActionHandlerBase extends RulesPluginHandlerBase implements RulesActionHandlerInterface {}
2074

    
2075
/**
2076
 * Class providing default implementations of the methods of the RulesPluginImplInterface.
2077
 *
2078
 * If a plugin implementation does not provide a function for a method, the
2079
 * default method of this class will be invoked.
2080
 *
2081
 * @see RulesPluginImplInterface
2082
 * @see RulesAbstractPlugin
2083
 */
2084
class RulesAbstractPluginDefaults extends RulesPluginHandlerBase implements RulesPluginImplInterface {
2085

    
2086
  public function execute() {
2087
    throw new RulesEvaluationException($this->object->getPluginName() .": Execution implementation is missing.", array(), $this->object, RulesLog::ERROR);
2088
  }
2089
}
2090

    
2091
/**
2092
 * A RecursiveIterator for rule elements.
2093
 */
2094
class RulesRecursiveElementIterator extends ArrayIterator implements RecursiveIterator {
2095

    
2096
   public function getChildren() {
2097
     return $this->current()->getIterator();
2098
   }
2099

    
2100
   public function hasChildren() {
2101
      return $this->current() instanceof IteratorAggregate;
2102
   }
2103
}
2104

    
2105
/**
2106
 * Base class for ContainerPlugins like Rules, Logical Operations or Loops.
2107
 */
2108
abstract class RulesContainerPlugin extends RulesPlugin implements IteratorAggregate {
2109

    
2110
  protected $children = array();
2111

    
2112
  public function __construct($variables = array()) {
2113
    $this->setUp();
2114
    if (!empty($variables) && $this->isRoot()) {
2115
      $this->info['variables'] = $variables;
2116
    }
2117
  }
2118

    
2119
  /**
2120
   * Returns the specified variables, in case the plugin is used as component.
2121
   */
2122
  public function &componentVariables() {
2123
    if ($this->isRoot()) {
2124
      $this->info += array('variables' => array());
2125
      return $this->info['variables'];
2126
    }
2127
    // We have to return a reference in any case.
2128
    $return = NULL;
2129
    return $return;
2130
  }
2131

    
2132
  /**
2133
   * Allow access to the children through the iterator.
2134
   *
2135
   * @return RulesRecursiveElementIterator
2136
   */
2137
  public function getIterator() {
2138
    return new RulesRecursiveElementIterator($this->children);
2139
  }
2140

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

    
2155
  public function dependencies() {
2156
    $modules = array_flip(parent::dependencies());
2157
    foreach ($this->children as $child) {
2158
      $modules += array_flip($child->dependencies());
2159
    }
2160
    return array_keys($modules);
2161
  }
2162

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

    
2179
  public function availableVariables() {
2180
    if (!isset($this->availableVariables)) {
2181
      if ($this->isRoot()) {
2182
        $this->availableVariables = RulesState::defaultVariables();
2183
        if (isset($this->info['variables'])) {
2184
          $this->availableVariables += $this->info['variables'];
2185
        }
2186
      }
2187
      else {
2188
        $this->availableVariables = $this->parent->stateVariables($this);
2189
      }
2190
    }
2191
    return $this->availableVariables;
2192
  }
2193

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

    
2222
  protected function variableInfoAssertions() {
2223
    $assertions = array();
2224
    foreach ($this->children as $child) {
2225
      if ($add = $child->variableInfoAssertions()) {
2226
        $assertions = rules_update_array($assertions, $add);
2227
      }
2228
    }
2229
    return $assertions;
2230
  }
2231

    
2232
  protected function setUpVariables() {
2233
    return isset($this->info['variables']) ? parent::parameterInfo(TRUE) + $this->info['variables'] : $this->parameterInfo(TRUE);
2234
  }
2235

    
2236
  /**
2237
   * Condition containers just return a boolean while action containers return
2238
   * the configured provided variables as an array of variables.
2239
   */
2240
  public function executeByArgs($args = array()) {
2241
    $replacements = array('%label' => $this->label(), '@plugin' => $this->itemName);
2242
    rules_log('Executing @plugin %label.', $replacements, RulesLog::INFO, $this, TRUE);
2243
    $this->processSettings();
2244
    $state = $this->setUpState($args);
2245

    
2246
    // Handle recursion prevention.
2247
    if ($state->isBlocked($this)) {
2248
      return rules_log('Not evaluating @plugin %label to prevent recursion.', array('%label' => $this->label(), '@plugin' => $this->plugin()), RulesLog::INFO);
2249
    }
2250
    // Block the config to prevent any future recursion.
2251
    $state->block($this);
2252

    
2253
    module_invoke_all('rules_config_execute', $this);
2254
    $result = $this->evaluate($state);
2255
    $return = $this->returnVariables($state, $result);
2256

    
2257
    $state->unblock($this);
2258
    $state->cleanUp();
2259
    rules_log('Finished executing of @plugin %label.', $replacements, RulesLog::INFO, $this, FALSE);
2260
    return $return;
2261
  }
2262

    
2263
  public function access() {
2264
    foreach ($this->children as $key => $child) {
2265
      if (!$child->access()) {
2266
        return FALSE;
2267
      }
2268
    }
2269
    return TRUE;
2270
  }
2271

    
2272
  public function destroy() {
2273
    foreach ($this->children as $key => $child) {
2274
      $child->destroy();
2275
    }
2276
    parent::destroy();
2277
  }
2278

    
2279
  /**
2280
   * By default we do a deep clone.
2281
   */
2282
  public function __clone() {
2283
    parent::__clone();
2284
    foreach ($this->children as $key => $child) {
2285
      $this->children[$key] = clone $child;
2286
      $this->children[$key]->parent = $this;
2287
    }
2288
  }
2289

    
2290
  /**
2291
   * Override delete to keep the children alive, if possible.
2292
   */
2293
  public function delete($keep_children = TRUE) {
2294
    if (isset($this->parent) && $keep_children) {
2295
      foreach ($this->children as $child) {
2296
        $child->setParent($this->parent);
2297
      }
2298
    }
2299
    parent::delete();
2300
  }
2301

    
2302
  public function __sleep() {
2303
    return parent::__sleep() + array('children' => 'children', 'info' => 'info');
2304
  }
2305

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

    
2321
    // Fix up the weights afterwards to be unique integers.
2322
    foreach (array_values($this->children) as $i => $child) {
2323
      $child->weight = $i;
2324
    }
2325

    
2326
    if ($deep) {
2327
      foreach (new ParentIterator($this->getIterator()) as $child) {
2328
        $child->sortChildren(TRUE);
2329
      }
2330
    }
2331
    $this->resetInternalCache();
2332
  }
2333

    
2334
  protected function exportChildren($key = NULL) {
2335
    $key = isset($key) ? $key : strtoupper($this->plugin());
2336
    $export[$key] = array();
2337
    foreach ($this->children as $child) {
2338
      $export[$key][] = $child->export();
2339
    }
2340
    return $export;
2341
  }
2342

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

    
2355
  protected function exportToArray() {
2356
    $export = array();
2357
    if (!empty($this->info['variables'])) {
2358
      $export['USES VARIABLES'] = $this->info['variables'];
2359
    }
2360
    if ($this->exportFlat()) {
2361
      $export += $this->exportSettings() + $this->exportChildren();
2362
    }
2363
    else {
2364
      $export[strtoupper($this->plugin())] = $this->exportSettings() + $this->exportChildren();
2365
    }
2366
    return $export;
2367
  }
2368

    
2369
  public function import(array $export) {
2370
    if (!empty($export['USES VARIABLES'])) {
2371
      $this->info['variables'] = $export['USES VARIABLES'];
2372
    }
2373
    // Care for exports having the export array nested in a sub-array.
2374
    if (!$this->exportFlat()) {
2375
      $export = reset($export);
2376
    }
2377
    $this->importSettings($export);
2378
    $this->importChildren($export);
2379
  }
2380

    
2381
  protected function importChildren($export, $key = NULL) {
2382
    $key = isset($key) ? $key : strtoupper($this->plugin());
2383
    foreach ($export[$key] as $child_export) {
2384
      $plugin = _rules_import_get_plugin(rules_array_key($child_export), $this instanceof RulesActionInterface ? 'action' : 'condition');
2385
      $child = rules_plugin_factory($plugin);
2386
      $child->setParent($this);
2387
      $child->import($child_export);
2388
    }
2389
  }
2390

    
2391
  public function resetInternalCache() {
2392
    $this->availableVariables = NULL;
2393
    foreach ($this->children as $child) {
2394
      $child->resetInternalCache();
2395
    }
2396
  }
2397

    
2398
  /**
2399
   * Overrides optimize().
2400
   */
2401
  public function optimize() {
2402
    parent::optimize();
2403
    // Now let the children optimize itself.
2404
    foreach ($this as $element) {
2405
      $element->optimize();
2406
    }
2407
  }
2408
}
2409

    
2410
/**
2411
 * Base class for all action containers.
2412
 */
2413
abstract class RulesActionContainer extends RulesContainerPlugin implements RulesActionInterface {
2414

    
2415
  public function __construct($variables = array(), $providesVars = array()) {
2416
    parent::__construct($variables);
2417
    // The provided vars of a component are the names of variables, which should
2418
    // be provided to the caller. See rule().
2419
    if ($providesVars) {
2420
      $this->info['provides'] = $providesVars;
2421
    }
2422
  }
2423

    
2424
  /**
2425
   * Add an action. Pass either an instance of the RulesActionInterface
2426
   * or the arguments as needed by rules_action().
2427
   *
2428
   * @return RulesActionContainer
2429
   *   Returns $this to support chained usage.
2430
   */
2431
  public function action($name, $settings = array()) {
2432
    $action = (is_object($name) && $name instanceof RulesActionInterface) ? $name : rules_action($name, $settings);
2433
    $action->setParent($this);
2434
    return $this;
2435
  }
2436

    
2437
  /**
2438
   * Evaluate, whereas by default new vars are visible in the parent's scope.
2439
   */
2440
  public function evaluate(RulesState $state) {
2441
    foreach ($this->children as $action) {
2442
      $action->evaluate($state);
2443
    }
2444
  }
2445

    
2446
  public function pluginProvidesVariables() {
2447
    return array();
2448
  }
2449

    
2450
  public function providesVariables() {
2451
    $provides = parent::providesVariables();
2452
    if (isset($this->info['provides']) && $vars = $this->componentVariables()) {
2453
      // Determine the full variable info for the provided variables. Note that
2454
      // we only support providing variables list in the component vars.
2455
      $provides += array_intersect_key($vars, array_flip($this->info['provides']));
2456
    }
2457
    return $provides;
2458
  }
2459

    
2460
  /**
2461
   * Returns an array of variable names, which are provided by passing through
2462
   * the provided variables of the children.
2463
   */
2464
  public function &componentProvidesVariables() {
2465
    $this->info += array('provides' => array());
2466
    return $this->info['provides'];
2467
  }
2468

    
2469
  protected function exportToArray() {
2470
    $export = parent::exportToArray();
2471
    if (!empty($this->info['provides'])) {
2472
      $export['PROVIDES VARIABLES'] = $this->info['provides'];
2473
    }
2474
    return $export;
2475
  }
2476

    
2477
  public function import(array $export) {
2478
    parent::import($export);
2479
    if (!empty($export['PROVIDES VARIABLES'])) {
2480
      $this->info['provides'] = $export['PROVIDES VARIABLES'];
2481
    }
2482
  }
2483
}
2484

    
2485
/**
2486
 * Base class for all condition containers.
2487
 */
2488
abstract class RulesConditionContainer extends RulesContainerPlugin implements RulesConditionInterface {
2489

    
2490
  protected $negate = FALSE;
2491

    
2492
  /**
2493
   * Add a condition. Pass either an instance of the RulesConditionInterface
2494
   * or the arguments as needed by rules_condition().
2495
   *
2496
   * @return RulesConditionContainer
2497
   *   Returns $this to support chained usage.
2498
   */
2499
  public function condition($name, $settings = array()) {
2500
    $condition = (is_object($name) && $name instanceof RulesConditionInterface) ? $name : rules_condition($name, $settings);
2501
    $condition->setParent($this);
2502
    return $this;
2503
  }
2504

    
2505
  /**
2506
   * Negate this condition.
2507
   *
2508
   * @return RulesConditionContainer
2509
   */
2510
  public function negate($negate = TRUE) {
2511
    $this->negate = (bool) $negate;
2512
    return $this;
2513
  }
2514

    
2515
  public function isNegated() {
2516
    return $this->negate;
2517
  }
2518

    
2519
  public function __sleep() {
2520
    return parent::__sleep() + array('negate' => 'negate');
2521
  }
2522

    
2523
  /**
2524
   * Just return the condition container's result.
2525
   */
2526
  protected function returnVariables(RulesState $state, $result = NULL) {
2527
    return $result;
2528
  }
2529

    
2530
  protected function exportChildren($key = NULL) {
2531
    $key = isset($key) ? $key : strtoupper($this->plugin());
2532
    return parent::exportChildren($this->negate ? 'NOT ' . $key : $key);
2533
  }
2534

    
2535
  protected function importChildren($export, $key = NULL) {
2536
    $key = isset($key) ? $key : strtoupper($this->plugin());
2537
    // Care for negated elements.
2538
    if (!isset($export[$key]) && isset($export['NOT ' . $key])) {
2539
      $this->negate = TRUE;
2540
      $key = 'NOT ' . $key;
2541
    }
2542
    parent::importChildren($export, $key);
2543
  }
2544

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

    
2567
/**
2568
 * The rules default logging class.
2569
 */
2570
class RulesLog {
2571

    
2572
  const INFO  = 1;
2573
  const WARN  = 2;
2574
  const ERROR = 3;
2575

    
2576
  static protected $logger;
2577

    
2578
  /**
2579
   * @return RulesLog
2580
   *   Returns the rules logger instance.
2581
   */
2582
  static function logger() {
2583
    if (!isset(self::$logger)) {
2584
      $class = __CLASS__;
2585
      self::$logger = new $class(variable_get('rules_log_level', self::INFO));
2586
    }
2587
    return self::$logger;
2588
  }
2589

    
2590
  protected $log = array();
2591
  protected $logLevel, $line = 0;
2592

    
2593
  /**
2594
   * This is a singleton.
2595
   */
2596
  protected function __construct($logLevel = self::WARN) {
2597
    $this->logLevel = $logLevel;
2598
  }
2599

    
2600
  public function __clone() {
2601
    throw new Exception("Cannot clone the logger.");
2602
  }
2603

    
2604
  /**
2605
   * Logs a log message.
2606
   *
2607
   * @see rules_log()
2608
   */
2609
  public function log($msg, $args = array(), $logLevel = self::INFO, $scope = NULL, $path = NULL) {
2610
    if ($logLevel >= $this->logLevel) {
2611
      $this->log[] = array($msg, $args, $logLevel, microtime(TRUE), $scope, $path);
2612
    }
2613
  }
2614

    
2615
  /**
2616
   * Checks the log and throws an exception if there were any problems.
2617
   */
2618
  function checkLog($logLevel = self::WARN) {
2619
    foreach ($this->log as $entry) {
2620
      if ($entry[2] >= $logLevel) {
2621
        throw new Exception($this->render());
2622
      }
2623
    }
2624
  }
2625

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

    
2641
  /**
2642
   * Gets an array of logged messages.
2643
   */
2644
  public function get() {
2645
    return $this->log;
2646
  }
2647

    
2648
  /**
2649
   * Renders the whole log.
2650
   */
2651
  public function render() {
2652
    $line = 0;
2653
    $output = array();
2654
    while (isset($this->log[$line])) {
2655
      $vars['head'] = t($this->log[$line][0], $this->log[$line][1]);
2656
      $vars['log'] = $this->renderHelper($line);
2657
      $output[] = theme('rules_debug_element', $vars);
2658
      $line++;
2659
    }
2660
    return implode('', $output);
2661
  }
2662

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

    
2692
        if (isset($this->log[$line][4]) && !$this->log[$line][4]) {
2693
          // This was the last log entry of this set.
2694
          return theme('item_list', array('items' => $output));
2695
        }
2696
      }
2697
      $line++;
2698
    }
2699
    return theme('item_list', array('items' => $output));
2700
  }
2701

    
2702
  /**
2703
   * Clears the logged messages.
2704
   */
2705
  public function clear() {
2706
    $this->log = array();
2707
  }
2708
}
2709

    
2710
/**
2711
 * A common exception for Rules.
2712
 *
2713
 * This class can be used to catch all exceptions thrown by Rules.
2714
 */
2715
abstract class RulesException extends Exception {}
2716

    
2717
/**
2718
 * An exception that is thrown during evaluation.
2719
 *
2720
 * Messages are prepared to be logged to the watchdog, thus not yet translated.
2721
 *
2722
 * @see watchdog()
2723
 */
2724
class RulesEvaluationException extends RulesException {
2725

    
2726
  public $msg, $args, $severity, $element, $keys = array();
2727

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

    
2761
/**
2762
 * An exception that is thrown for Rules configurations that fail the integrity check.
2763
 *
2764
 * @see RulesPlugin::integrityCheck()
2765
 */
2766
class RulesIntegrityException extends RulesException {
2767

    
2768
  public $msg, $element, $keys = array();
2769

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

    
2786
/**
2787
 * An exception that is thrown for missing module dependencies.
2788
 */
2789
class RulesDependencyException extends RulesIntegrityException {}
2790

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