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 @ a5ba142b

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 is the configuration name and the value the actual export.
117
    $name = key($export);
118
    $export = current($export);
119
    if (!isset($export['PLUGIN'])) {
120
      $error_msg = t('Export misses plugin information.');
121
      return FALSE;
122
    }
123
    // Create an empty configuration, re-set basic keys and import.
124
    $config = rules_plugin_factory($export['PLUGIN']);
125
    $config->name = $name;
126
    foreach (array('label', 'active', 'weight', 'tags', 'access_exposed', 'owner') as $key) {
127
      if (isset($export[strtoupper($key)])) {
128
        $config->$key = $export[strtoupper($key)];
129
      }
130
    }
131
    if (!empty($export['REQUIRES'])) {
132
      foreach ($export['REQUIRES'] as $module) {
133
        if (!module_exists($module)) {
134
          $error_msg = t('Missing the required module %module.', array('%module' => $module));
135
          return FALSE;
136
        }
137
      }
138
      $config->dependencies = $export['REQUIRES'];
139
    }
140
    $config->import($export);
141
    return $config;
142
  }
143

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

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

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

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

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

    
176
    return $return;
177
  }
178

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

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

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

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

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

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

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

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

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

    
306
    return $return;
307
  }
308
}
309

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

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

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

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

    
331

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
458

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1135
      return $return;
1136
    }
1137
  }
1138

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1656

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1866
interface RulesTriggerableInterface {
1867

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

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

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

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

    
1907
}
1908

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

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

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

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

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

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

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

    
1966

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

    
1976
}
1977

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
2111
  protected $children = array();
2112

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
2491
  protected $negate = FALSE;
2492

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

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

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

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

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

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

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

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

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

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

    
2577
  static protected $logger;
2578

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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