Projet

Général

Profil

Paste
Télécharger (93,2 ko) Statistiques
| Branche: | Révision:

root / drupal7 / sites / all / modules / rules / includes / rules.core.inc @ 950416da

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 Remove for a future release.
11
require_once dirname(__FILE__) . '/faces.inc';
12

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

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

    
29
  /**
30
   * Overridden.
31
   *
32
   * @see DrupalDefaultEntityController::attachLoad()
33
   */
34
  protected function attachLoad(&$queried_entities, $revision_id = FALSE) {
35
    // Retrieve stdClass records and store them as rules objects 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
   *
72
   * @see EntityAPIController::applyConditions()
73
   * @see rules_query_rules_config_load_multiple_alter()
74
   */
75
  protected function applyConditions($entities, $conditions = array()) {
76
    if (isset($conditions['event']) || isset($conditions['plugin'])) {
77
      foreach ($entities as $key => $entity) {
78
        if (isset($conditions['event']) && (!($entity instanceof RulesTriggerableInterface) || !in_array($conditions['event'], $entity->events()))) {
79
          unset($entities[$key]);
80
        }
81
        if (isset($conditions['plugin']) && !is_array($conditions['plugin'])) {
82
          $conditions['plugin'] = array($conditions['plugin']);
83
        }
84
        if (isset($conditions['plugin']) && !in_array($entity->plugin(), $conditions['plugin'])) {
85
          unset($entities[$key]);
86
        }
87
      }
88
      unset($conditions['event'], $conditions['plugin']);
89
    }
90
    if (!empty($conditions['tags'])) {
91
      foreach ($entities as $key => $entity) {
92
        foreach ($conditions['tags'] as $tag) {
93
          if (in_array($tag, $entity->tags)) {
94
            continue 2;
95
          }
96
        }
97
        unset($entities[$key]);
98
      }
99
      unset($conditions['tags']);
100
    }
101
    return parent::applyConditions($entities, $conditions);
102
  }
103

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

    
147
  public function save($rules_config, DatabaseTransaction $transaction = NULL) {
148
    $transaction = isset($transaction) ? $transaction : db_transaction();
149

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

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

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

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

    
179
    return $return;
180
  }
181

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

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

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

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

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

    
284
    // Stop event dispatchers when deleting the last rule of an event set.
285
    $processed = array();
286
    foreach ($configs as $config) {
287
      if ($config->getPluginName() != 'reaction rule') {
288
        continue;
289
      }
290

    
291
      foreach ($config->events() as $event_name) {
292
        // Only process each event once.
293
        if (!empty($processed[$event_name])) {
294
          continue;
295
        }
296
        $processed[$event_name] = TRUE;
297

    
298
        // Check if the event handler implements the event dispatcher interface.
299
        $handler = rules_get_event_handler($event_name, $config->getEventSettings($event_name));
300
        if (!$handler instanceof RulesEventDispatcherInterface) {
301
          continue;
302
        }
303

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

    
311
    return $return;
312
  }
313

    
314
}
315

    
316
/**
317
 * Base class for RulesExtendables.
318
 *
319
 * The RulesExtendable uses the rules cache to setup the defined extenders
320
 * and overrides automatically.
321
 * As soon faces is used the faces information is autoloaded using setUp().
322
 */
323
abstract class RulesExtendable extends FacesExtendable {
324

    
325
  /**
326
   * The name of the info definitions associated with info about this class.
327
   *
328
   * This would be defined abstract, if possible. Common rules hooks with class
329
   * info are e.g. plugin_info and data_info.
330
   */
331
  protected $hook;
332

    
333
  /**
334
   * The name of the item this class represents in the info hook.
335
   */
336
  protected $itemName;
337

    
338
  protected $cache;
339
  protected $itemInfo = array();
340

    
341
  public function __construct() {
342
    $this->setUp();
343
  }
344

    
345
  protected function setUp() {
346
    // Keep a reference on the cache, so elements created during cache
347
    // rebuilding end up with a complete cache in the end too.
348
    $this->cache = &rules_get_cache();
349
    if (isset($this->cache[$this->hook][$this->itemName])) {
350
      $this->itemInfo = &$this->cache[$this->hook][$this->itemName];
351
    }
352
    // Set up the Faces Extenders.
353
    if (!empty($this->itemInfo['faces_cache'])) {
354
      list($this->facesMethods, $this->facesIncludes, $this->faces) = $this->itemInfo['faces_cache'];
355
    }
356
  }
357

    
358
  /**
359
   * Forces the object to be setUp, this executes setUp() if not done yet.
360
   */
361
  public function forceSetUp() {
362
    if (!isset($this->cache) || (!empty($this->itemInfo['faces_cache']) && !$this->faces)) {
363
      $this->setUp();
364
    }
365
  }
366

    
367
  /**
368
   * Magic method: Invoke the dynamically implemented methods.
369
   */
370
  public function __call($name, $arguments = array()) {
371
    $this->forceSetUp();
372
    return parent::__call($name, $arguments);
373
  }
374

    
375
  public function facesAs($interface = NULL) {
376
    $this->forceSetUp();
377
    return parent::facesAs($interface);
378
  }
379

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

    
407
  /**
408
   * Returns whether the a RuleExtendable supports the given interface.
409
   *
410
   * @param $itemInfo
411
   *   The info about the item as specified in the hook.
412
   * @param $interface
413
   *   The interface to check for.
414
   *
415
   * @return bool
416
   *   Whether it supports the given interface.
417
   */
418
  public static function itemFacesAs(&$itemInfo, $interface) {
419
    return in_array($interface, class_implements($itemInfo['class'])) || isset($itemInfo['faces_cache'][2][$interface]);
420
  }
421

    
422
}
423

    
424
/**
425
 * Base class for rules plugins.
426
 *
427
 * We cannot inherit from EntityDB at the same time, so we implement our own
428
 * entity related methods. Any CRUD related actions performed on contained
429
 * plugins are applied and the root element representing the configuration is
430
 * saved.
431
 */
432
abstract class RulesPlugin extends RulesExtendable {
433

    
434
  /**
435
   * If this is a configuration saved to the db, the id of it.
436
   */
437
  public $id = NULL;
438
  public $weight = 0;
439
  public $name = NULL;
440

    
441
  /**
442
   * An array of settings for this element.
443
   */
444
  public $settings = array();
445

    
446
  /**
447
   * Info about this element. Usage depends on the plugin.
448
   */
449
  protected $info = array();
450

    
451
  /**
452
   * The parent element, if any.
453
   *
454
   * @var RulesContainerPlugin
455
   */
456
  protected $parent = NULL;
457

    
458
  protected $cache = NULL;
459
  protected $hook = 'plugin_info';
460

    
461
  /**
462
   * Identifies an element inside a configuration.
463
   */
464
  protected $elementId = NULL;
465

    
466
  /**
467
   * Static cache for availableVariables().
468
   */
469
  protected $availableVariables;
470

    
471
  /**
472
   * Sets a new parent element.
473
   */
474
  public function setParent(RulesContainerPlugin $parent) {
475
    if ($this->parent == $parent) {
476
      return;
477
    }
478
    if (isset($this->parent) && ($key = array_search($this, $this->parent->children)) !== FALSE) {
479
      // Remove element from any previous parent.
480
      unset($this->parent->children[$key]);
481
      $this->parent->resetInternalCache();
482
    }
483
    // Make sure the interface matches the type of the container.
484
    if (($parent instanceof RulesActionContainer && $this instanceof RulesActionInterface) ||
485
       ($parent instanceof RulesConditionContainer && $this instanceof RulesConditionInterface)) {
486

    
487
      $this->parent = $parent;
488
      $parent->children[] = $this;
489
      $this->parent->resetInternalCache();
490
    }
491
    else {
492
      throw new RulesEvaluationException('The given container is incompatible with this element.', array(), $this, RulesLog::ERROR);
493
    }
494
  }
495

    
496
  /**
497
   * Gets the root element of the configuration.
498
   */
499
  public function root() {
500
    $element = $this;
501
    while (!$element->isRoot()) {
502
      $element = $element->parent;
503
    }
504
    return $element;
505
  }
506

    
507
  /**
508
   * Returns whether the element is the root of the configuration.
509
   */
510
  public function isRoot() {
511
    return empty($this->parent) || isset($this->name);
512
  }
513

    
514
  /**
515
   * Returns the element's parent.
516
   */
517
  public function parentElement() {
518
    return $this->parent;
519
  }
520

    
521
  /**
522
   * Returns the element id, which identifies the element inside the config.
523
   */
524
  public function elementId() {
525
    if (!isset($this->elementId)) {
526
      $this->elementMap()->index();
527
    }
528
    return $this->elementId;
529
  }
530

    
531
  /**
532
   * Gets the element map helper object, which helps mapping elements to ids.
533
   *
534
   * @return RulesElementMap
535
   */
536
  public function elementMap() {
537
    $config = $this->root();
538
    if (empty($config->map)) {
539
      $config->map = new RulesElementMap($config);
540
    }
541
    return $config->map;
542
  }
543

    
544
  /**
545
   * Iterate over all elements nested below the current element.
546
   *
547
   * This helper can be used to recursively iterate over all elements of a
548
   * configuration. To iterate over the children only, just regularly iterate
549
   * over the object.
550
   *
551
   * @param $mode
552
   *   (optional) The iteration mode used. See
553
   *   RecursiveIteratorIterator::construct(). Defaults to SELF_FIRST.
554
   *
555
   * @return RecursiveIteratorIterator
556
   */
557
  public function elements($mode = RecursiveIteratorIterator::SELF_FIRST) {
558
    return new RecursiveIteratorIterator($this, $mode);
559
  }
560

    
561
  /**
562
   * Do a deep clone.
563
   */
564
  public function __clone() {
565
    // Make sure the element map is cleared.
566
    // @see self::elementMap()
567
    unset($this->map);
568
  }
569

    
570
  /**
571
   * Returns the depth of this element in the configuration.
572
   */
573
  public function depth() {
574
    $element = $this;
575
    $i = 0;
576
    while (!empty($element->parent)) {
577
      $element = $element->parent;
578
      $i++;
579
    }
580
    return $i;
581
  }
582

    
583
  /**
584
   * Execute the configuration.
585
   *
586
   * @param ...
587
   *   Arguments to pass to the configuration.
588
   */
589
  public function execute() {
590
    return $this->executeByArgs(func_get_args());
591
  }
592

    
593
  /**
594
   * Execute the configuration by passing arguments in a single array.
595
   */
596
  abstract public function executeByArgs($args = array());
597

    
598
  /**
599
   * Evaluate the element on a given rules evaluation state.
600
   */
601
  abstract public function evaluate(RulesState $state);
602

    
603
  protected static function compare(RulesPlugin $a, RulesPlugin $b) {
604
    if ($a->weight == $b->weight) {
605
      return 0;
606
    }
607
    return ($a->weight < $b->weight) ? -1 : 1;
608
  }
609

    
610
  /**
611
   * Returns info about parameters needed by the plugin.
612
   *
613
   * Note that not necessarily all parameters are needed when executing the
614
   * plugin, as values for the parameter might have been already configured via
615
   * the element settings.
616
   *
617
   * @see self::parameterInfo()
618
   */
619
  public function pluginParameterInfo() {
620
    return isset($this->info['parameter']) ? $this->info['parameter'] : array();
621
  }
622

    
623
  /**
624
   * Returns info about parameters needed for executing the configured plugin.
625
   *
626
   * @param bool $optional
627
   *   Whether optional parameters should be included.
628
   *
629
   * @see self::pluginParameterInfo()
630
   */
631
  public function parameterInfo($optional = FALSE) {
632
    // We have to filter out parameters that are already configured.
633
    foreach ($this->pluginParameterInfo() as $name => $info) {
634
      if (!isset($this->settings[$name . ':select']) && !isset($this->settings[$name]) && ($optional || (empty($info['optional']) && $info['type'] != 'hidden'))) {
635
        $vars[$name] = $info;
636
      }
637
    }
638
    return isset($vars) ? $vars : array();
639
  }
640

    
641
  /**
642
   * Returns the about variables the plugin provides for later evaluated elements.
643
   *
644
   * Note that this method returns info about the provided variables as defined
645
   * by the plugin. Thus this resembles the original info, which may be
646
   * adapted via configuration.
647
   *
648
   * @see self::providesVariables()
649
   */
650
  public function pluginProvidesVariables() {
651
    return isset($this->info['provides']) ? $this->info['provides'] : array();
652
  }
653

    
654
  /**
655
   * Returns info about all variables provided for later evaluated elements.
656
   *
657
   * @see self::pluginProvidesVariables()
658
   */
659
  public function providesVariables() {
660
    foreach ($this->pluginProvidesVariables() as $name => $info) {
661
      $info['source name'] = $name;
662
      $info['label'] = isset($this->settings[$name . ':label']) ? $this->settings[$name . ':label'] : $info['label'];
663
      if (isset($this->settings[$name . ':var'])) {
664
        $name = $this->settings[$name . ':var'];
665
      }
666
      $provides[$name] = $info;
667
    }
668
    return isset($provides) ? $provides : array();
669
  }
670

    
671
  /**
672
   * Returns the info of the plugin.
673
   */
674
  public function info() {
675
    return $this->info;
676
  }
677

    
678
  /**
679
   * When converted to a string, just use the export format.
680
   */
681
  public function __toString() {
682
    return $this->isRoot() ? $this->export() : entity_var_json_export($this->export());
683
  }
684

    
685
  /**
686
   * Gets variables to return once the configuration has been executed.
687
   */
688
  protected function returnVariables(RulesState $state, $result = NULL) {
689
    $var_info = $this->providesVariables();
690
    foreach ($var_info as $name => $info) {
691
      try {
692
        $vars[$name] = $this->getArgument($name, $info, $state);
693
      }
694
      catch (RulesEvaluationException $e) {
695
        // Ignore not existing variables.
696
        $vars[$name] = NULL;
697
      }
698
      $var_info[$name] += array('allow null' => TRUE);
699
    }
700
    return isset($vars) ? array_values(rules_unwrap_data($vars, $var_info)) : array();
701
  }
702

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

    
736
  /**
737
   * Returns info about all variables that have to be setup in the state.
738
   */
739
  protected function setUpVariables() {
740
    return $this->parameterInfo(TRUE);
741
  }
742

    
743
  /**
744
   * Returns info about variables available to be used as arguments for this element.
745
   *
746
   * As this is called very often, e.g. during integrity checks, we statically
747
   * cache the results.
748
   *
749
   * @see RulesPlugin::resetInternalCache()
750
   */
751
  public function availableVariables() {
752
    if (!isset($this->availableVariables)) {
753
      $this->availableVariables = !$this->isRoot() ? $this->parent->stateVariables($this) : RulesState::defaultVariables();
754
    }
755
    return $this->availableVariables;
756
  }
757

    
758
  /**
759
   * Returns asserted additions to the available variable info.
760
   *
761
   * Any returned info is merged into the variable info, in case the execution
762
   * flow passes the element.
763
   * E.g. this is used to assert the content type of a node if the condition
764
   * is met, such that the per-node type properties are available.
765
   */
766
  protected function variableInfoAssertions() {
767
    return array();
768
  }
769

    
770
  /**
771
   * Gets the name of this plugin instance.
772
   *
773
   * The returned name should identify the code which drives this plugin.
774
   */
775
  public function getPluginName() {
776
    return $this->itemName;
777
  }
778

    
779
  /**
780
   * Calculates an array of required modules.
781
   *
782
   * You can use $this->dependencies to access dependencies for saved
783
   * configurations.
784
   */
785
  public function dependencies() {
786
    $this->processSettings();
787
    $modules = isset($this->itemInfo['module']) && $this->itemInfo['module'] != 'rules' ? array($this->itemInfo['module'] => 1) : array();
788
    foreach ($this->pluginParameterInfo() as $name => $info) {
789
      if (isset($this->settings[$name . ':process']) && $this->settings[$name . ':process'] instanceof RulesDataProcessor) {
790
        $modules += array_flip($this->settings[$name . ':process']->dependencies());
791
      }
792
    }
793
    return array_keys($modules);
794
  }
795

    
796
  /**
797
   * Whether the currently logged in user has access to all configured elements.
798
   *
799
   * Note that this only checks whether the current user has permission to all
800
   * configured elements, but not whether a user has access to configure Rule
801
   * configurations in general. Use rules_config_access() for that.
802
   *
803
   * Use this to determine access permissions for configuring or triggering the
804
   * execution of certain configurations independent of the Rules UI.
805
   *
806
   * @see rules_config_access()
807
   */
808
  public function access() {
809
    $this->processSettings();
810
    foreach ($this->pluginParameterInfo() as $name => $info) {
811
      if (isset($this->settings[$name . ':select']) && $wrapper = $this->applyDataSelector($this->settings[$name . ':select'])) {
812
        if ($wrapper->access('view') === FALSE) {
813
          return FALSE;
814
        }
815
      }
816
      // Incorporate access checks for data processors and input evaluators.
817
      if (isset($this->settings[$name . ':process']) && $this->settings[$name . ':process'] instanceof RulesDataProcessor && !$this->settings[$name . ':process']->editAccess()) {
818
        return FALSE;
819
      }
820
    }
821
    return TRUE;
822
  }
823

    
824
  /**
825
   * Processes the settings e.g. to prepare input evaluators.
826
   *
827
   * Usually settings get processed automatically, however if $this->settings
828
   * has been altered manually after element construction, it needs to be
829
   * invoked explicitly with $force set to TRUE.
830
   */
831
  public function processSettings($force = FALSE) {
832
    // Process if not done yet.
833
    if ($force || !empty($this->settings['#_needs_processing'])) {
834
      $var_info = $this->availableVariables();
835
      foreach ($this->pluginParameterInfo() as $name => $info) {
836
        // Prepare input evaluators.
837
        if (isset($this->settings[$name])) {
838
          $this->settings[$name . ':process'] = $this->settings[$name];
839
          RulesDataInputEvaluator::prepareSetting($this->settings[$name . ':process'], $info, $var_info);
840
        }
841
        // Prepare data processors.
842
        elseif (isset($this->settings[$name . ':select']) && !empty($this->settings[$name . ':process'])) {
843
          RulesDataProcessor::prepareSetting($this->settings[$name . ':process'], $info, $var_info);
844
        }
845
        // Clean up.
846
        if (empty($this->settings[$name . ':process'])) {
847
          unset($this->settings[$name . ':process']);
848
        }
849
      }
850
      unset($this->settings['#_needs_processing']);
851
    }
852
  }
853

    
854
  /**
855
   * Makes sure the plugin is configured right.
856
   *
857
   * "Configured right" means all needed variables are available in the
858
   * element's scope and dependent modules are enabled.
859
   *
860
   * @return $this
861
   *
862
   * @throws RulesIntegrityException
863
   *   In case of a failed integrity check, a RulesIntegrityException exception
864
   *   is thrown.
865
   */
866
  public function integrityCheck() {
867
    // First process the settings if not done yet.
868
    $this->processSettings();
869
    // Check dependencies using the pre-calculated dependencies stored in
870
    // $this->dependencies. Fail back to calculation them on the fly, e.g.
871
    // during creation.
872
    $dependencies = empty($this->dependencies) ? $this->dependencies() : $this->dependencies;
873
    foreach ($dependencies as $module) {
874
      if (!module_exists($module)) {
875
        throw new RulesDependencyException(t('Missing required module %name.', array('%name' => $module)));
876
      }
877
    }
878
    // Check the parameter settings.
879
    $this->checkParameterSettings();
880
    // Check variable names for provided variables to be valid.
881
    foreach ($this->pluginProvidesVariables() as $name => $info) {
882
      if (isset($this->settings[$name . ':var'])) {
883
        $this->checkVarName($this->settings[$name . ':var']);
884
      }
885
    }
886
    return $this;
887
  }
888

    
889
  protected function checkVarName($name) {
890
    if (!preg_match('/^[0-9a-zA-Z_]*$/', $name)) {
891
      throw new RulesIntegrityException(t('%plugin: The variable name %name contains not allowed characters.', array('%plugin' => $this->getPluginName(), '%name' => $name)), $this);
892
    }
893
  }
894

    
895
  /**
896
   * Checks whether parameters are correctly configured.
897
   */
898
  protected function checkParameterSettings() {
899
    foreach ($this->pluginParameterInfo() as $name => $info) {
900
      if (isset($info['restriction']) && $info['restriction'] == 'selector' && isset($this->settings[$name])) {
901
        throw new RulesIntegrityException(t("The parameter %name may only be configured using a selector.", array('%name' => $name)), array($this, 'parameter', $name));
902
      }
903
      elseif (isset($info['restriction']) && $info['restriction'] == 'input' && isset($this->settings[$name . ':select'])) {
904
        throw new RulesIntegrityException(t("The parameter %name may not be configured using a selector.", array('%name' => $name)), array($this, 'parameter', $name));
905
      }
906
      elseif (!empty($this->settings[$name . ':select']) && !$this->applyDataSelector($this->settings[$name . ':select'])) {
907
        throw new RulesIntegrityException(t("Data selector %selector for parameter %name is invalid.", array('%selector' => $this->settings[$name . ':select'], '%name' => $name)), array($this, 'parameter', $name));
908
      }
909
      elseif ($arg_info = $this->getArgumentInfo($name)) {
910
        // If we have enough metadata, check whether the types match.
911
        if (!RulesData::typesMatch($arg_info, $info)) {
912
          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));
913
        }
914
      }
915
      elseif (!$this->isRoot() && !isset($this->settings[$name]) && empty($info['optional']) && $info['type'] != 'hidden') {
916
        throw new RulesIntegrityException(t('Missing configuration for parameter %name.', array('%name' => $name)), array($this, 'parameter', $name));
917
      }
918
      // @todo Make sure used values are allowed.
919
      // (key/value pairs + allowed values).
920
    }
921
  }
922

    
923
  /**
924
   * Returns the argument for the parameter $name described with $info.
925
   *
926
   * Returns the argument as configured in the element settings for the
927
   * parameter $name described with $info.
928
   *
929
   * @param string $name
930
   *   The name of the parameter for which to get the argument.
931
   * @param $info
932
   *   Info about the parameter.
933
   * @param RulesState $state
934
   *   The current evaluation state.
935
   * @param string $langcode
936
   *   (optional) The language code used to get the argument value if the
937
   *   argument value should be translated. By default (NULL) the current
938
   *   interface language will be used.
939
   *
940
   * @return
941
   *   The argument, possibly wrapped.
942
   *
943
   * @throws RulesEvaluationException
944
   *   In case the argument cannot be retrieved an exception is thrown.
945
   */
946
  protected function getArgument($name, $info, RulesState $state, $langcode = NULL) {
947
    // Only apply the langcode if the parameter has been marked translatable.
948
    if (empty($info['translatable'])) {
949
      $langcode = LANGUAGE_NONE;
950
    }
951
    elseif (!isset($langcode)) {
952
      $langcode = $GLOBALS['language']->language;
953
    }
954

    
955
    if (!empty($this->settings[$name . ':select'])) {
956
      $arg = $state->applyDataSelector($this->settings[$name . ':select'], $langcode);
957
    }
958
    elseif (isset($this->settings[$name])) {
959
      $arg = rules_wrap_data($this->settings[$name], $info);
960
      // We don't sanitize directly specified values.
961
      $skip_sanitize = TRUE;
962
    }
963
    elseif ($state->varinfo($name)) {
964
      $arg = $state->get($name);
965
    }
966
    elseif (empty($info['optional']) && $info['type'] != 'hidden') {
967
      throw new RulesEvaluationException('Required parameter %name is missing.', array('%name' => $name), $this, RulesLog::ERROR);
968
    }
969
    else {
970
      $arg = isset($info['default value']) ? $info['default value'] : NULL;
971
      $skip_sanitize = TRUE;
972
      $info['allow null'] = TRUE;
973
    }
974
    // Make sure the given value is set if required (default).
975
    if (!isset($arg) && empty($info['allow null'])) {
976
      throw new RulesEvaluationException('The provided argument for parameter %name is empty.', array('%name' => $name), $this);
977
    }
978

    
979
    // Support passing already sanitized values.
980
    if ($info['type'] == 'text' && !isset($skip_sanitize) && !empty($info['sanitize']) && !($arg instanceof EntityMetadataWrapper)) {
981
      $arg = check_plain((string) $arg);
982
    }
983

    
984
    // Apply any configured data processors.
985
    if (!empty($this->settings[$name . ':process'])) {
986
      // For processing, make sure the data is unwrapped now.
987
      $return = rules_unwrap_data(array($arg), array($info));
988
      // @todo For Drupal 8: Refactor to add the name and language code as
989
      // separate parameter to process().
990
      $info['#name'] = $name;
991
      $info['#langcode'] = $langcode;
992
      return isset($return[0]) ? $this->settings[$name . ':process']->process($return[0], $info, $state, $this) : NULL;
993
    }
994
    return $arg;
995
  }
996

    
997
  /**
998
   * Gets the right arguments for executing the element.
999
   *
1000
   * @throws RulesEvaluationException
1001
   *   If case an argument cannot be retrieved an exception is thrown.
1002
   */
1003
  protected function getExecutionArguments(RulesState $state) {
1004
    $parameters = $this->pluginParameterInfo();
1005
    // If there is language parameter, get its value first so it can be used
1006
    // for getting other translatable values.
1007
    $langcode = NULL;
1008
    if (isset($parameters['language'])) {
1009
      $lang_arg = $this->getArgument('language', $parameters['language'], $state);
1010
      $langcode = $lang_arg instanceof EntityMetadataWrapper ? $lang_arg->value() : $lang_arg;
1011
    }
1012
    // Now get all arguments.
1013
    foreach ($parameters as $name => $info) {
1014
      $args[$name] = $name == 'language' ? $lang_arg : $this->getArgument($name, $info, $state, $langcode);
1015
    }
1016
    // Append the settings and the execution state. Faces will append $this.
1017
    $args['settings'] = $this->settings;
1018
    $args['state'] = $state;
1019
    // Make the wrapped variables for the arguments available in the state.
1020
    $state->currentArguments = $args;
1021
    return rules_unwrap_data($args, $parameters);
1022
  }
1023

    
1024
  /**
1025
   * Applies the given data selector.
1026
   *
1027
   * Applies the given data selector by using the info about available
1028
   * variables. Thus it doesn't require an actual evaluation state.
1029
   *
1030
   * @param string $selector
1031
   *   The selector string, e.g. "node:author:mail".
1032
   *
1033
   * @return EntityMetadataWrapper
1034
   *   An empty wrapper for the given selector or FALSE if the selector couldn't
1035
   *   be applied.
1036
   */
1037
  public function applyDataSelector($selector) {
1038
    $parts = explode(':', str_replace('-', '_', $selector), 2);
1039
    if (($vars = $this->availableVariables()) && isset($vars[$parts[0]]['type'])) {
1040
      $wrapper = rules_wrap_data(NULL, $vars[$parts[0]], TRUE);
1041
      if (count($parts) > 1 && $wrapper instanceof EntityMetadataWrapper) {
1042
        try {
1043
          foreach (explode(':', $parts[1]) as $name) {
1044
            if ($wrapper instanceof EntityListWrapper || $wrapper instanceof EntityStructureWrapper) {
1045
              $wrapper = $wrapper->get($name);
1046
            }
1047
            else {
1048
              return FALSE;
1049
            }
1050
          }
1051
        }
1052
        // Return FALSE if there is no wrappper or we get an exception.
1053
        catch (EntityMetadataWrapperException $e) {
1054
          return FALSE;
1055
        }
1056
      }
1057
    }
1058
    return isset($wrapper) ? $wrapper : FALSE;
1059
  }
1060

    
1061
  /**
1062
   * Returns info about the configured argument.
1063
   *
1064
   * @return
1065
   *   The determined info. If it's not known NULL is returned.
1066
   */
1067
  public function getArgumentInfo($name) {
1068
    $vars = $this->availableVariables();
1069
    if (!empty($this->settings[$name . ':select']) && !empty($vars[$this->settings[$name . ':select']])) {
1070
      return $vars[$this->settings[$name . ':select']];
1071
    }
1072
    elseif (!empty($this->settings[$name . ':select'])) {
1073
      if ($wrapper = $this->applyDataSelector($this->settings[$name . ':select'])) {
1074
        return $wrapper->info();
1075
      }
1076
      return;
1077
    }
1078
    elseif (isset($this->settings[$name . ':type'])) {
1079
      return array('type' => $this->settings[$name . ':type']);
1080
    }
1081
    elseif (!isset($this->settings[$name]) && isset($vars[$name])) {
1082
      return $vars[$name];
1083
    }
1084
  }
1085

    
1086
  /**
1087
   * Saves the configuration to the database.
1088
   *
1089
   * The configuration is saved regardless whether this method is invoked on
1090
   * the rules configuration or a contained rule element.
1091
   */
1092
  public function save($name = NULL, $module = 'rules') {
1093
    if (isset($this->parent)) {
1094
      $this->parent->sortChildren();
1095
      return $this->parent->save($name, $module);
1096
    }
1097
    else {
1098
      // Update the dirty flag before saving.
1099
      // However, this operation depends on a fully built Rules-cache, so skip
1100
      // it when entities in code are imported to the database.
1101
      // @see _rules_rebuild_cache()
1102
      if (empty($this->is_rebuild)) {
1103
        rules_config_update_dirty_flag($this, FALSE);
1104
        // In case the config is not dirty, pre-calculate the dependencies for
1105
        // later checking. Note that this also triggers processing settings if
1106
        // necessary.
1107
        // @see rules_modules_enabled()
1108
        if (empty($this->dirty)) {
1109
          $this->dependencies = $this->dependencies();
1110
        }
1111
      }
1112

    
1113
      $this->plugin = $this->itemName;
1114
      $this->name = isset($name) ? $name : $this->name;
1115
      // Module stores the module via which the rule is configured and is used
1116
      // for generating machine names with the right prefix. However, for
1117
      // default configurations 'module' points to the module providing the
1118
      // default configuration, so the module via which the rules is configured
1119
      // is stored in the "owner" property.
1120
      // @todo For Drupal 8 use "owner" for generating machine names also and
1121
      // module only for the modules providing default configurations.
1122
      $this->module = !isset($this->module) || $module != 'rules' ? $module : $this->module;
1123
      if (!isset($this->owner)) {
1124
        $this->owner = 'rules';
1125
      }
1126
      $this->ensureNameExists();
1127
      $this->data = $this;
1128
      $return = entity_get_controller('rules_config')->save($this);
1129
      unset($this->data);
1130

    
1131
      // Care about clearing necessary caches.
1132
      if (!empty($this->is_rebuild)) {
1133
        rules_clear_cache();
1134
      }
1135
      else {
1136
        $plugin_info = $this->pluginInfo();
1137
        if (!empty($plugin_info['component'])) {
1138
          // When component variables changes rebuild the complete cache so the
1139
          // changes to the provided action/condition take affect.
1140
          if (empty($this->original) || $this->componentVariables() != $this->original->componentVariables()) {
1141
            rules_clear_cache();
1142
          }
1143
          // Clear components cached for evaluation.
1144
          cache_clear_all('comp_', 'cache_rules', TRUE);
1145
        }
1146
        elseif ($this->plugin == 'reaction rule') {
1147
          // Clear event sets cached for evaluation.
1148
          cache_clear_all('event_', 'cache_rules', TRUE);
1149
          // Clear event whitelist for rebuild.
1150
          cache_clear_all('rules_event_whitelist', 'cache_rules', TRUE);
1151
        }
1152
        drupal_static_reset('rules_get_cache');
1153
        drupal_static_reset('rules_config_update_dirty_flag');
1154
      }
1155

    
1156
      return $return;
1157
    }
1158
  }
1159

    
1160
  /**
1161
   * Ensure the configuration has a name. If not, generate one.
1162
   */
1163
  protected function ensureNameExists() {
1164
    if (!isset($this->module)) {
1165
      $this->module = 'rules';
1166
    }
1167
    if (!isset($this->name)) {
1168
      // Find a unique name for this configuration.
1169
      $this->name = $this->module . '_';
1170
      for ($i = 0; $i < 8; $i++) {
1171
        // Alphanumeric name generation.
1172
        $rnd = mt_rand(97, 122);
1173
        $this->name .= chr($rnd);
1174
      }
1175
    }
1176
  }
1177

    
1178
  public function __sleep() {
1179
    // Keep the id always as we need it for the recursion prevention.
1180
    $array = drupal_map_assoc(array('parent', 'id', 'elementId', 'weight', 'settings'));
1181
    // Keep properties related to configurations if they are there.
1182
    $info = entity_get_info('rules_config');
1183
    $fields = array_merge($info['schema_fields_sql']['base table'], array('recursion', 'tags'));
1184
    foreach ($fields as $key) {
1185
      if (isset($this->$key)) {
1186
        $array[$key] = $key;
1187
      }
1188
    }
1189
    return $array;
1190
  }
1191

    
1192
  /**
1193
   * Optimizes a rule configuration in order to speed up evaluation.
1194
   *
1195
   * Additional optimization methods may be inserted by an extender
1196
   * implementing the RulesOptimizationInterface. By default, there is no
1197
   * optimization extender.
1198
   *
1199
   * An optimization method may rearrange the internal structure of a
1200
   * configuration in order to speed up the evaluation. As the configuration may
1201
   * change optimized configurations should not be saved permanently, except
1202
   * when saving it temporary, for later execution only.
1203
   *
1204
   * @see RulesOptimizationInterface
1205
   */
1206
  public function optimize() {
1207
    // Make sure settings are processed before configs are cached.
1208
    $this->processSettings();
1209
    if ($this->facesAs('RulesOptimizationInterface')) {
1210
      $this->__call('optimize');
1211
    }
1212
  }
1213

    
1214
  /**
1215
   * Deletes configuration from database.
1216
   *
1217
   * If invoked on a rules configuration it is deleted from database. If
1218
   * invoked on a contained rule element, it's removed from the configuration.
1219
   */
1220
  public function delete() {
1221
    if (isset($this->parent)) {
1222
      foreach ($this->parent->children as $key => $child) {
1223
        if ($child === $this) {
1224
          unset($this->parent->children[$key]);
1225
          break;
1226
        }
1227
      }
1228
    }
1229
    elseif (isset($this->id)) {
1230
      entity_get_controller('rules_config')->delete(array($this->name));
1231
      rules_clear_cache();
1232
    }
1233
  }
1234

    
1235
  public function internalIdentifier() {
1236
    return isset($this->id) ? $this->id : NULL;
1237
  }
1238

    
1239
  /**
1240
   * Returns the config name.
1241
   */
1242
  public function identifier() {
1243
    return isset($this->name) ? $this->name : NULL;
1244
  }
1245

    
1246
  public function entityInfo() {
1247
    return entity_get_info('rules_config');
1248
  }
1249

    
1250
  public function entityType() {
1251
    return 'rules_config';
1252
  }
1253

    
1254
  /**
1255
   * Checks if the configuration has a certain exportable status.
1256
   *
1257
   * @param $status
1258
   *   A status constant, i.e. one of ENTITY_CUSTOM, ENTITY_IN_CODE,
1259
   *   ENTITY_OVERRIDDEN or ENTITY_FIXED.
1260
   *
1261
   * @return bool
1262
   *   TRUE if the configuration has the status, else FALSE.
1263
   *
1264
   * @see entity_has_status()
1265
   */
1266
  public function hasStatus($status) {
1267
    return $this->isRoot() && isset($this->status) && ($this->status & $status) == $status;
1268
  }
1269

    
1270
  /**
1271
   * Removes circular object references so PHP garbage collector can work.
1272
   */
1273
  public function destroy() {
1274
    parent::destroy();
1275
    $this->parent = NULL;
1276
  }
1277

    
1278
  /**
1279
   * Seamlessly invokes the method implemented via faces.
1280
   *
1281
   * Frees the caller from having to think about references.
1282
   */
1283
  public function form(&$form, &$form_state, array $options = array()) {
1284
    $this->__call('form', array(&$form, &$form_state, $options));
1285
  }
1286

    
1287
  public function form_validate($form, &$form_state) {
1288
    $this->__call('form_validate', array($form, &$form_state));
1289
  }
1290

    
1291
  public function form_submit($form, &$form_state) {
1292
    $this->__call('form_submit', array($form, &$form_state));
1293
  }
1294

    
1295
  /**
1296
   * Returns the label of the element.
1297
   */
1298
  public function label() {
1299
    if (!empty($this->label) && $this->label != t('unlabeled')) {
1300
      return $this->label;
1301
    }
1302
    $info = $this->info();
1303
    return isset($info['label']) ? $info['label'] : (!empty($this->name) ? $this->name : t('unlabeled'));
1304
  }
1305

    
1306
  /**
1307
   * Returns the name of the element's plugin.
1308
   */
1309
  public function plugin() {
1310
    return $this->itemName;
1311
  }
1312

    
1313
  /**
1314
   * Returns info about the element's plugin.
1315
   */
1316
  public function pluginInfo() {
1317
    $this->forceSetUp();
1318
    return $this->itemInfo;
1319
  }
1320

    
1321
  /**
1322
   * Applies the given export.
1323
   */
1324
  public function import(array $export) {
1325
    $this->importSettings($export[strtoupper($this->plugin())]);
1326
  }
1327

    
1328
  protected function importSettings($export) {
1329
    // Import parameter settings.
1330
    $export += array('USING' => array(), 'PROVIDE' => array());
1331
    foreach ($export['USING'] as $name => $param_export) {
1332
      $this->importParameterSetting($name, $param_export);
1333
    }
1334
    foreach ($export['PROVIDE'] as $name => $var_export) {
1335
      // The key of $var_export is the variable name, the value the label.
1336
      $this->settings[$name . ':var'] = rules_array_key($var_export);
1337
      $this->settings[$name . ':label'] = reset($var_export);
1338
    }
1339
  }
1340

    
1341
  protected function importParameterSetting($name, $export) {
1342
    if (is_array($export) && isset($export['select'])) {
1343
      $this->settings[$name . ':select'] = $export['select'];
1344
      if (count($export) > 1) {
1345
        // Add in processor settings.
1346
        unset($export['select']);
1347
        $this->settings[$name . ':process'] = $export;
1348
      }
1349
    }
1350
    // Convert back the [selector] strings being an array with one entry.
1351
    elseif (is_array($export) && count($export) == 1 && isset($export[0])) {
1352
      $this->settings[$name . ':select'] = $export[0];
1353
    }
1354
    elseif (is_array($export) && isset($export['value'])) {
1355
      $this->settings[$name] = $export['value'];
1356
    }
1357
    else {
1358
      $this->settings[$name] = $export;
1359
    }
1360
  }
1361

    
1362
  /**
1363
   * Exports a rule configuration.
1364
   *
1365
   * @param string $prefix
1366
   *   An optional prefix for each line.
1367
   * @param bool $php
1368
   *   Optional. Set to TRUE to format the export using PHP arrays. By default
1369
   *   JSON is used.
1370
   *
1371
   * @return
1372
   *   The exported configuration.
1373
   *
1374
   * @see rules_import()
1375
   */
1376
  public function export($prefix = '', $php = FALSE) {
1377
    $export = $this->exportToArray();
1378
    return $this->isRoot() ? $this->returnExport($export, $prefix, $php) : $export;
1379
  }
1380

    
1381
  protected function exportToArray() {
1382
    $export[strtoupper($this->plugin())] = $this->exportSettings();
1383
    return $export;
1384
  }
1385

    
1386
  protected function exportSettings() {
1387
    $export = array();
1388
    if (!$this->isRoot()) {
1389
      foreach ($this->pluginParameterInfo() as $name => $info) {
1390
        if (($return = $this->exportParameterSetting($name, $info)) !== NULL) {
1391
          $export['USING'][$name] = $return;
1392
        }
1393
      }
1394
      foreach ($this->providesVariables() as $name => $info) {
1395
        if (!empty($info['source name'])) {
1396
          $export['PROVIDE'][$info['source name']][$name] = $info['label'];
1397
        }
1398
      }
1399
    }
1400
    return $export;
1401
  }
1402

    
1403
  protected function exportParameterSetting($name, $info) {
1404
    if (isset($this->settings[$name]) && (empty($info['optional']) || !isset($info['default value']) || $this->settings[$name] != $info['default value'])) {
1405
      // In case of an array-value wrap the value into another array, such that
1406
      // the value cannot be confused with an exported data selector.
1407
      return is_array($this->settings[$name]) ? array('value' => $this->settings[$name]) : $this->settings[$name];
1408
    }
1409
    elseif (isset($this->settings[$name . ':select'])) {
1410
      if (isset($this->settings[$name . ':process']) && $processor = $this->settings[$name . ':process']) {
1411
        $export['select'] = $this->settings[$name . ':select'];
1412
        $export += $processor instanceof RulesDataProcessor ? $processor->getChainSettings() : $processor;
1413
        return $export;
1414
      }
1415
      // If there is no processor use a simple array to abbreviate this usual
1416
      // case. In JSON this turns to a nice [selector] string.
1417
      return array($this->settings[$name . ':select']);
1418
    }
1419
  }
1420

    
1421
  /**
1422
   * Finalizes the configuration export.
1423
   *
1424
   * Adds general attributes regarding the configuration and returns it in the
1425
   * right format for export.
1426
   *
1427
   * @param $export
1428
   * @param string $prefix
1429
   *   An optional prefix for each line.
1430
   * @param bool $php
1431
   *   Optional. Set to TRUE to format the export using PHP arrays. By default
1432
   *   JSON is used.
1433
   */
1434
  protected function returnExport($export, $prefix = '', $php = FALSE) {
1435
    $this->ensureNameExists();
1436
    if (!empty($this->label) && $this->label != t('unlabeled')) {
1437
      $export_cfg[$this->name]['LABEL'] = $this->label;
1438
    }
1439
    $export_cfg[$this->name]['PLUGIN'] = $this->plugin();
1440
    if (!empty($this->weight)) {
1441
      $export_cfg[$this->name]['WEIGHT'] = $this->weight;
1442
    }
1443
    if (isset($this->active) && !$this->active) {
1444
      $export_cfg[$this->name]['ACTIVE'] = FALSE;
1445
    }
1446
    if (!empty($this->owner)) {
1447
      $export_cfg[$this->name]['OWNER'] = $this->owner;
1448
    }
1449
    if (!empty($this->tags)) {
1450
      $export_cfg[$this->name]['TAGS'] = $this->tags;
1451
    }
1452
    if ($modules = $this->dependencies()) {
1453
      $export_cfg[$this->name]['REQUIRES'] = $modules;
1454
    }
1455
    if (!empty($this->access_exposed)) {
1456
      $export_cfg[$this->name]['ACCESS_EXPOSED'] = $this->access_exposed;
1457
    };
1458
    $export_cfg[$this->name] += $export;
1459
    return $php ? entity_var_export($export_cfg, $prefix) : entity_var_json_export($export_cfg, $prefix);
1460
  }
1461

    
1462
  /**
1463
   * Resets any internal static caches.
1464
   *
1465
   * This function does not reset regular caches as retrieved via
1466
   * rules_get_cache(). Usually, it's invoked automatically when a Rules
1467
   * configuration is modified.
1468
   *
1469
   * Static caches are reset for the element and any elements down the tree. To
1470
   * clear static caches of the whole configuration, invoke the function at the
1471
   * root.
1472
   *
1473
   * @see RulesPlugin::availableVariables()
1474
   */
1475
  public function resetInternalCache() {
1476
    $this->availableVariables = NULL;
1477
  }
1478

    
1479
}
1480

    
1481
/**
1482
 * Defines a common base class for so-called "Abstract Plugins" like actions.
1483
 *
1484
 * Modules have to provide the concrete plugin implementation.
1485
 */
1486
abstract class RulesAbstractPlugin extends RulesPlugin {
1487

    
1488
  protected $elementName;
1489
  protected $info = array('parameter' => array(), 'provides' => array());
1490
  protected $infoLoaded = FALSE;
1491

    
1492
  /**
1493
   * @param string $name
1494
   *   The plugin implementation's name.
1495
   * @param $settings
1496
   *   Further information provided about the plugin. Optional.
1497
   * @throws RulesException
1498
   *   If validation of the passed settings fails RulesExceptions are thrown.
1499
   */
1500
  public function __construct($name = NULL, $settings = array()) {
1501
    $this->elementName = $name;
1502
    $this->settings = (array) $settings + array('#_needs_processing' => TRUE);
1503
    $this->setUp();
1504
  }
1505

    
1506
  protected function setUp() {
1507
    parent::setUp();
1508
    if (isset($this->cache[$this->itemName . '_info'][$this->elementName])) {
1509
      $this->info = $this->cache[$this->itemName . '_info'][$this->elementName];
1510
      // Remember that the info has been correctly setup.
1511
      // @see self::forceSetup()
1512
      $this->infoLoaded = TRUE;
1513

    
1514
      // Register the defined class, if any.
1515
      if (isset($this->info['class'])) {
1516
        $this->faces['RulesPluginImplInterface'] = 'RulesPluginImplInterface';
1517
        $face_methods = get_class_methods('RulesPluginImplInterface');
1518
        $class_info = array(1 => $this->info['class']);
1519
        foreach ($face_methods as $method) {
1520
          $this->facesMethods[$method] = $class_info;
1521
        }
1522
      }
1523
      // Add in per-plugin implementation callbacks if any.
1524
      if (!empty($this->info['faces_cache'])) {
1525
        foreach ($this->info['faces_cache'] as $face => $data) {
1526
          list($methods, $file_names) = $data;
1527
          foreach ($methods as $method => $callback) {
1528
            $this->facesMethods[$method] = $callback;
1529
          }
1530
          foreach ((array) $file_names as $method => $name) {
1531
            $this->facesIncludes[$method] = array('module' => $this->info['module'], 'name' => $name);
1532
          }
1533
        }
1534
        // Invoke the info_alter callback, but only if it has been implemented.
1535
        if ($this->facesMethods['info_alter'] != $this->itemInfo['faces_cache'][0]['info_alter']) {
1536
          $this->__call('info_alter', array(&$this->info));
1537
        }
1538
      }
1539
    }
1540
    elseif (!empty($this->itemInfo['faces_cache']) && function_exists($this->elementName)) {
1541
      // We don't have any info, so just add the name as execution callback.
1542
      $this->override(array('execute' => $this->elementName));
1543
    }
1544
  }
1545

    
1546
  public function forceSetUp() {
1547
    if (!isset($this->cache) || (!empty($this->itemInfo['faces_cache']) && !$this->faces)) {
1548
      $this->setUp();
1549
    }
1550
    // In case we have element specific information, which is not loaded yet,
1551
    // do so now. This might happen if the element has been initially loaded
1552
    // with an incomplete cache, i.e. during cache rebuilding.
1553
    elseif (!$this->infoLoaded && isset($this->cache[$this->itemName . '_info'][$this->elementName])) {
1554
      $this->setUp();
1555
    }
1556
  }
1557

    
1558
  /**
1559
   * Returns the label of the element.
1560
   */
1561
  public function label() {
1562
    $info = $this->info();
1563
    return isset($info['label']) ? $info['label'] : t('@plugin "@name"', array('@name' => $this->elementName, '@plugin' => $this->plugin()));
1564
  }
1565

    
1566
  public function access() {
1567
    $info = $this->info();
1568
    $this->loadBasicInclude();
1569
    if (!empty($info['access callback']) && !call_user_func($info['access callback'], $this->itemName, $this->getElementName())) {
1570
      return FALSE;
1571
    }
1572
    return parent::access() && $this->__call('access');
1573
  }
1574

    
1575
  public function integrityCheck() {
1576
    // Do the usual integrity check first so the implementation's validation
1577
    // handler can rely on that already.
1578
    parent::integrityCheck();
1579
    // Make sure the element is known.
1580
    $this->forceSetUp();
1581
    if (!isset($this->cache[$this->itemName . '_info'][$this->elementName])) {
1582
      throw new RulesIntegrityException(t('Unknown @plugin %name.', array('@plugin' => $this->plugin(), '%name' => $this->elementName)));
1583
    }
1584
    $this->validate();
1585
    return $this;
1586
  }
1587

    
1588
  public function processSettings($force = FALSE) {
1589
    // Process if not done yet.
1590
    if ($force || !empty($this->settings['#_needs_processing'])) {
1591
      $this->resetInternalCache();
1592
      // In case the element implements the info alteration callback, (re-)run
1593
      // the alteration so that any settings depending info alterations are
1594
      // applied.
1595
      if ($this->facesMethods && $this->facesMethods['info_alter'] != $this->itemInfo['faces_cache'][0]['info_alter']) {
1596
        $this->__call('info_alter', array(&$this->info));
1597
      }
1598
      // First let the plugin implementation do processing, so data types of the
1599
      // parameters are fixed when we process the settings.
1600
      $this->process();
1601
      parent::processSettings($force);
1602
    }
1603
  }
1604

    
1605
  public function pluginParameterInfo() {
1606
    // Ensure the info alter callback has been executed.
1607
    $this->forceSetup();
1608
    return parent::pluginParameterInfo();
1609
  }
1610

    
1611
  public function pluginProvidesVariables() {
1612
    // Ensure the info alter callback has been executed.
1613
    $this->forceSetup();
1614
    return parent::pluginProvidesVariables();
1615
  }
1616

    
1617
  public function info() {
1618
    // Ensure the info alter callback has been executed.
1619
    $this->forceSetup();
1620
    return $this->info;
1621
  }
1622

    
1623
  protected function variableInfoAssertions() {
1624
    // Get the implementation's assertions and map them to the variable names.
1625
    if ($assertions = $this->__call('assertions')) {
1626
      foreach ($assertions as $param_name => $data) {
1627
        $name = isset($this->settings[$param_name . ':select']) ? $this->settings[$param_name . ':select'] : $param_name;
1628
        $return[$name] = $data;
1629
      }
1630
      return $return;
1631
    }
1632
  }
1633

    
1634
  public function import(array $export) {
1635
    // The key is the element name and the value the actual export.
1636
    $this->elementName = rules_array_key($export);
1637
    $export = reset($export);
1638

    
1639
    // After setting the element name, setup the element again so the right
1640
    // element info is loaded.
1641
    $this->setUp();
1642

    
1643
    if (!isset($export['USING']) && !isset($export['PROVIDES']) && !empty($export)) {
1644
      // The export has been abbreviated to skip "USING".
1645
      $export = array('USING' => $export);
1646
    }
1647
    $this->importSettings($export);
1648
  }
1649

    
1650
  protected function exportToArray() {
1651
    $export = $this->exportSettings();
1652
    if (!$this->providesVariables()) {
1653
      // Abbreviate the export making "USING" implicit.
1654
      $export = isset($export['USING']) ? $export['USING'] : array();
1655
    }
1656
    return array($this->elementName => $export);
1657
  }
1658

    
1659
  public function dependencies() {
1660
    $modules = array_flip(parent::dependencies());
1661
    $modules += array_flip((array) $this->__call('dependencies'));
1662
    return array_keys($modules + (!empty($this->info['module']) ? array($this->info['module'] => 1) : array()));
1663
  }
1664

    
1665
  public function executeByArgs($args = array()) {
1666
    $replacements = array('%label' => $this->label(), '@plugin' => $this->itemName);
1667
    rules_log('Executing @plugin %label.', $replacements, RulesLog::INFO, $this, TRUE);
1668
    $this->processSettings();
1669
    // If there is no element info, just pass through the passed arguments.
1670
    // That way we support executing actions without any info at all.
1671
    if ($this->info()) {
1672
      $state = $this->setUpState($args);
1673
      module_invoke_all('rules_config_execute', $this);
1674

    
1675
      $result = $this->evaluate($state);
1676
      $return = $this->returnVariables($state, $result);
1677
    }
1678
    else {
1679
      rules_log('Unable to execute @plugin %label.', $replacements, RulesLog::ERROR, $this);
1680
    }
1681
    $state->cleanUp();
1682
    rules_log('Finished executing of @plugin %label.', $replacements, RulesLog::INFO, $this, FALSE);
1683
    return $return;
1684
  }
1685

    
1686
  /**
1687
   * Execute the configured execution callback and log that.
1688
   */
1689
  abstract protected function executeCallback(array $args, RulesState $state = NULL);
1690

    
1691
  public function evaluate(RulesState $state) {
1692
    $this->processSettings();
1693
    try {
1694
      // Get vars as needed for execute and call it.
1695
      return $this->executeCallback($this->getExecutionArguments($state), $state);
1696
    }
1697
    catch (RulesEvaluationException $e) {
1698
      rules_log($e->msg, $e->args, $e->severity);
1699
      rules_log('Unable to evaluate %name.', array('%name' => $this->getPluginName()), RulesLog::WARN, $this);
1700
    }
1701
    // Catch wrapper exceptions that might occur due to failures loading an
1702
    // entity or similar.
1703
    catch (EntityMetadataWrapperException $e) {
1704
      rules_log('Unable to get a data value. Error: !error', array('!error' => $e->getMessage()), RulesLog::WARN);
1705
      rules_log('Unable to evaluate %name.', array('%name' => $this->getPluginName()), RulesLog::WARN, $this);
1706
    }
1707
  }
1708

    
1709
  public function __sleep() {
1710
    return parent::__sleep() + array('elementName' => 'elementName');
1711
  }
1712

    
1713
  public function getPluginName() {
1714
    return $this->itemName . " " . $this->elementName;
1715
  }
1716

    
1717
  /**
1718
   * Gets the name of the configured action or condition.
1719
   */
1720
  public function getElementName() {
1721
    return $this->elementName;
1722
  }
1723

    
1724
  /**
1725
   * Add in the data provided by the info hooks to the cache.
1726
   */
1727
  public function rebuildCache(&$itemInfo, &$cache) {
1728
    parent::rebuildCache($itemInfo, $cache);
1729

    
1730
    // Include all declared files so we can find all implementations.
1731
    self::includeFiles();
1732

    
1733
    // Get the plugin's own info data.
1734
    $cache[$this->itemName . '_info'] = rules_fetch_data($this->itemName . '_info');
1735
    foreach ($cache[$this->itemName . '_info'] as $name => &$info) {
1736
      $info += array(
1737
        'parameter' => isset($info['arguments']) ? $info['arguments'] : array(),
1738
        'provides' => isset($info['new variables']) ? $info['new variables'] : array(),
1739
        'base' => $name,
1740
        'callbacks' => array(),
1741
      );
1742
      unset($info['arguments'], $info['new variables']);
1743

    
1744
      if (function_exists($info['base'])) {
1745
        $info['callbacks'] += array('execute' => $info['base']);
1746
      }
1747

    
1748
      // We do not need to build a faces cache for RulesPluginHandlerInterface,
1749
      // which gets added in automatically as its a parent of
1750
      // RulesPluginImplInterface.
1751
      unset($this->faces['RulesPluginHandlerInterface']);
1752

    
1753
      // Build up the per-plugin implementation faces cache.
1754
      foreach ($this->faces as $interface) {
1755
        $methods = $file_names = array();
1756
        $includes = self::getIncludeFiles($info['module']);
1757

    
1758
        foreach (get_class_methods($interface) as $method) {
1759
          if (isset($info['callbacks'][$method]) && ($function = $info['callbacks'][$method])) {
1760
            $methods[$method][0] = $function;
1761
            $file_names[$method] = $this->getFileName($function, $includes);
1762
          }
1763
          // Note that this skips RulesPluginImplInterface, which is not
1764
          // implemented by plugin handlers.
1765
          elseif (isset($info['class']) && is_subclass_of($info['class'], $interface)) {
1766
            $methods[$method][1] = $info['class'];
1767
          }
1768
          elseif (function_exists($function = $info['base'] . '_' . $method)) {
1769
            $methods[$method][0] = $function;
1770
            $file_names[$method] = $this->getFileName($function, $includes);
1771
          }
1772
        }
1773
        // Cache only the plugin implementation specific callbacks.
1774
        $info['faces_cache'][$interface] = array($methods, array_filter($file_names));
1775
      }
1776
      // Filter out interfaces with no overridden methods.
1777
      $info['faces_cache'] = rules_filter_array($info['faces_cache'], 0, TRUE);
1778
      // We don't need that any more.
1779
      unset($info['callbacks'], $info['base']);
1780
    }
1781
  }
1782

    
1783
  /**
1784
   * Loads this module's .rules.inc file.
1785
   *
1786
   * Makes sure the providing modules' .rules.inc file is included, as diverse
1787
   * callbacks may reside in that file.
1788
   */
1789
  protected function loadBasicInclude() {
1790
    static $included = array();
1791

    
1792
    if (isset($this->info['module']) && !isset($included[$this->info['module']])) {
1793
      $module = $this->info['module'];
1794
      module_load_include('inc', $module, $module . '.rules');
1795
      $included[$module] = TRUE;
1796
    }
1797
  }
1798

    
1799
  /**
1800
   * Makes sure all supported destinations are included.
1801
   */
1802
  public static function includeFiles() {
1803
    static $included;
1804

    
1805
    if (!isset($included)) {
1806
      foreach (module_implements('rules_file_info') as $module) {
1807
        // rules.inc are already included thanks to the rules_hook_info() group.
1808
        foreach (self::getIncludeFiles($module, FALSE) as $name) {
1809
          module_load_include('inc', $module, $name);
1810
        }
1811
      }
1812
      $dirs = array();
1813
      foreach (module_implements('rules_directory') as $module) {
1814
        // Include all files once, so the discovery can find them.
1815
        $result = module_invoke($module, 'rules_directory');
1816
        if (!is_array($result)) {
1817
          $result = array($module => $result);
1818
        }
1819
        $dirs += $result;
1820
      }
1821
      foreach ($dirs as $module => $directory) {
1822
        $module_path = drupal_get_path('module', $module);
1823
        foreach (array('inc', 'php') as $extension) {
1824
          foreach (glob("$module_path/$directory/*.$extension") as $filename) {
1825
            include_once $filename;
1826
          }
1827
        }
1828
      }
1829
      $included = TRUE;
1830
    }
1831
  }
1832

    
1833
  /**
1834
   * Returns all include files for a module.
1835
   *
1836
   * @param string $module
1837
   *   The module name.
1838
   * @param bool $all
1839
   *   If FALSE, the $module.rules.inc file isn't added.
1840
   *
1841
   * @return string[]
1842
   *   An array containing the names of all the include files for a module.
1843
   */
1844
  protected static function getIncludeFiles($module, $all = TRUE) {
1845
    $files = (array) module_invoke($module, 'rules_file_info');
1846
    // Automatically add "$module.rules_forms.inc" and "$module.rules.inc".
1847
    $files[] = $module . '.rules_forms';
1848
    if ($all) {
1849
      $files[] = $module . '.rules';
1850
    }
1851
    return $files;
1852
  }
1853

    
1854
  protected function getFileName($function, $includes) {
1855
    static $filenames;
1856
    if (!isset($filenames) || !array_key_exists($function, $filenames)) {
1857
      $filenames[$function] = NULL;
1858
      $reflector = new ReflectionFunction($function);
1859
      // On windows the path contains backslashes instead of slashes, fix that.
1860
      $file = str_replace('\\', '/', $reflector->getFileName());
1861
      foreach ($includes as $include) {
1862
        $pos = strpos($file, $include . '.inc');
1863
        // Test whether the file ends with the given filename.inc.
1864
        if ($pos !== FALSE && strlen($file) - $pos == strlen($include) + 4) {
1865
          $filenames[$function] = $include;
1866
          return $include;
1867
        }
1868
      }
1869
    }
1870
    return $filenames[$function];
1871
  }
1872

    
1873
}
1874

    
1875
/**
1876
 * Interface for objects that can be used as actions.
1877
 */
1878
interface RulesActionInterface {
1879

    
1880
  /**
1881
   * @return
1882
   *   As specified.
1883
   *
1884
   * @throws RulesEvaluationException
1885
   *   Throws an exception if not all necessary arguments have been provided.
1886
   */
1887
  public function execute();
1888

    
1889
}
1890

    
1891
/**
1892
 * Interface for objects that can be used as conditions.
1893
 */
1894
interface RulesConditionInterface {
1895

    
1896
  /**
1897
   * @return bool
1898
   *
1899
   * @throws RulesEvaluationException
1900
   *   Throws an exception if not all necessary arguments have been provided.
1901
   */
1902
  public function execute();
1903

    
1904
  /**
1905
   * Negate the result.
1906
   */
1907
  public function negate($negate = TRUE);
1908

    
1909
  /**
1910
   * Returns whether the element is configured to negate the result.
1911
   */
1912
  public function isNegated();
1913

    
1914
}
1915

    
1916
/**
1917
 * Interface for objects that are triggerable.
1918
 */
1919
interface RulesTriggerableInterface {
1920

    
1921
  /**
1922
   * Returns the array of (configured) event names associated with this object.
1923
   */
1924
  public function events();
1925

    
1926
  /**
1927
   * Removes an event from the rule configuration.
1928
   *
1929
   * @param string $event_name
1930
   *   The name of the (configured) event to remove.
1931
   *
1932
   * @return RulesTriggerableInterface
1933
   *   The object instance itself, to allow chaining.
1934
   */
1935
  public function removeEvent($event_name);
1936

    
1937
  /**
1938
   * Adds the specified event.
1939
   *
1940
   * @param string $event_name
1941
   *   The base name of the event to add.
1942
   * @param array $settings
1943
   *   (optional) The event settings. If there are no event settings, pass an
1944
   *   empty array (default).
1945
   *
1946
   * @return RulesTriggerableInterface
1947
   */
1948
  public function event($event_name, array $settings = array());
1949

    
1950
  /**
1951
   * Gets the event settings associated with the given (configured) event.
1952
   *
1953
   * @param string $event_name
1954
   *   The (configured) event's name.
1955
   *
1956
   * @return array|null
1957
   *   The array of event settings, or NULL if there are no settings.
1958
   */
1959
  public function getEventSettings($event_name);
1960

    
1961
}
1962

    
1963
/**
1964
 * Provides the base interface for implementing abstract plugins via classes.
1965
 */
1966
interface RulesPluginHandlerInterface {
1967

    
1968
  /**
1969
   * Validates $settings independent from a form submission.
1970
   *
1971
   * @throws RulesIntegrityException
1972
   *   In case of validation errors, RulesIntegrityExceptions are thrown.
1973
   */
1974
  public function validate();
1975

    
1976
  /**
1977
   * Processes settings independent from a form submission.
1978
   *
1979
   * Processing results may be stored and accessed on execution time
1980
   * in $settings.
1981
   */
1982
  public function process();
1983

    
1984
  /**
1985
   * Allows altering of the element's action/condition info.
1986
   *
1987
   * Note that this method is also invoked on evaluation time, thus any costly
1988
   * operations should be avoided.
1989
   *
1990
   * @param $element_info
1991
   *   A reference on the element's info as returned by RulesPlugin::info().
1992
   */
1993
  public function info_alter(&$element_info);
1994

    
1995
  /**
1996
   * Checks whether the user has access to configure this element.
1997
   *
1998
   * Note that this only covers access for already created elements. In order to
1999
   * control access for creating or using elements specify an 'access callback'
2000
   * in the element's info array.
2001
   *
2002
   * @see hook_rules_action_info()
2003
   */
2004
  public function access();
2005

    
2006
  /**
2007
   * Returns an array of required modules.
2008
   */
2009
  public function dependencies();
2010

    
2011
  /**
2012
   * Alters the generated configuration form of the element.
2013
   *
2014
   * Validation and processing of the settings should be untied from the form
2015
   * and implemented in validate() and process() wherever it makes sense.
2016
   * For the remaining cases where form tied validation and processing is needed
2017
   * make use of the form API #element_validate and #value_callback properties.
2018
   */
2019
  public function form_alter(&$form, $form_state, $options);
2020

    
2021
  /**
2022
   * Returns an array of info assertions for the specified parameters.
2023
   *
2024
   * This allows conditions to assert additional metadata, such as info about
2025
   * the fields of a bundle.
2026
   *
2027
   * @see RulesPlugin::variableInfoAssertions()
2028
   */
2029
  public function assertions();
2030

    
2031
}
2032

    
2033
/**
2034
 * Interface for implementing conditions via classes.
2035
 *
2036
 * In addition to the interface an execute() and a static getInfo() method must
2037
 * be implemented. The static getInfo() method has to return the info as
2038
 * returned by hook_rules_condition_info() but including an additional 'name'
2039
 * key, specifying the plugin name.
2040
 * The execute method is the equivalent to the usual execution callback and
2041
 * gets the parameters passed as specified in the info array.
2042
 *
2043
 * See RulesNodeConditionType for an example and rules_discover_plugins()
2044
 * for information about class discovery.
2045
 */
2046
interface RulesConditionHandlerInterface extends RulesPluginHandlerInterface {}
2047

    
2048
/**
2049
 * Interface for implementing actions via classes.
2050
 *
2051
 * In addition to the interface an execute() and a static getInfo() method must
2052
 * be implemented. The static getInfo() method has to return the info as
2053
 * returned by hook_rules_action_info() but including an additional 'name' key,
2054
 * specifying the plugin name.
2055
 * The execute method is the equivalent to the usual execution callback and
2056
 * gets the parameters passed as specified in the info array.
2057
 *
2058
 * See RulesNodeConditionType for an example and rules_discover_plugins()
2059
 * for information about class discovery.
2060
 */
2061
interface RulesActionHandlerInterface extends RulesPluginHandlerInterface {}
2062

    
2063
/**
2064
 * Interface used for implementing an abstract plugin via Faces.
2065
 *
2066
 * Provides the interface used for implementing an abstract plugin by using
2067
 * the Faces extension mechanism.
2068
 */
2069
interface RulesPluginImplInterface extends RulesPluginHandlerInterface {
2070

    
2071
  /**
2072
   * Executes the action or condition making use of the parameters as specified.
2073
   */
2074
  public function execute();
2075

    
2076
}
2077

    
2078
/**
2079
 * Interface for optimizing evaluation.
2080
 *
2081
 * @see RulesContainerPlugin::optimize()
2082
 */
2083
interface RulesOptimizationInterface {
2084

    
2085
  /**
2086
   * Optimizes a rule configuration in order to speed up evaluation.
2087
   */
2088
  public function optimize();
2089

    
2090
}
2091

    
2092
/**
2093
 * Base class for implementing abstract plugins via classes.
2094
 */
2095
abstract class RulesPluginHandlerBase extends FacesExtender implements RulesPluginHandlerInterface {
2096

    
2097
  /**
2098
   * @var RulesAbstractPlugin
2099
   */
2100
  protected $element;
2101

    
2102
  /**
2103
   * Overridden to provide $this->element to make the code more meaningful.
2104
   */
2105
  public function __construct(FacesExtendable $object) {
2106
    $this->object = $object;
2107
    $this->element = $object;
2108
  }
2109

    
2110
  /**
2111
   * Implements RulesPluginImplInterface.
2112
   */
2113
  public function access() {
2114
    return TRUE;
2115
  }
2116

    
2117
  public function validate() {}
2118

    
2119
  public function process() {}
2120

    
2121
  public function info_alter(&$element_info) {}
2122

    
2123
  public function dependencies() {}
2124

    
2125
  public function form_alter(&$form, $form_state, $options) {}
2126

    
2127
  public function assertions() {}
2128

    
2129
}
2130

    
2131
/**
2132
 * Base class for implementing conditions via classes.
2133
 */
2134
abstract class RulesConditionHandlerBase extends RulesPluginHandlerBase implements RulesConditionHandlerInterface {}
2135

    
2136
/**
2137
 * Base class for implementing actions via classes.
2138
 */
2139
abstract class RulesActionHandlerBase extends RulesPluginHandlerBase implements RulesActionHandlerInterface {}
2140

    
2141
/**
2142
 * Provides default implementations of all RulesPluginImplInterface methods.
2143
 *
2144
 * If a plugin implementation does not provide a function for a method, the
2145
 * default method of this class will be invoked.
2146
 *
2147
 * @see RulesPluginImplInterface
2148
 * @see RulesAbstractPlugin
2149
 */
2150
class RulesAbstractPluginDefaults extends RulesPluginHandlerBase implements RulesPluginImplInterface {
2151

    
2152
  public function execute() {
2153
    throw new RulesEvaluationException($this->object->getPluginName() . ": Execution implementation is missing.", array(), $this->object, RulesLog::ERROR);
2154
  }
2155

    
2156
}
2157

    
2158
/**
2159
 * A RecursiveIterator for rule elements.
2160
 */
2161
class RulesRecursiveElementIterator extends ArrayIterator implements RecursiveIterator {
2162

    
2163
  public function getChildren() {
2164
    return $this->current()->getIterator();
2165
  }
2166

    
2167
  public function hasChildren() {
2168
    return $this->current() instanceof IteratorAggregate;
2169
  }
2170

    
2171
}
2172

    
2173
/**
2174
 * Base class for ContainerPlugins like Rules, Logical Operations or Loops.
2175
 */
2176
abstract class RulesContainerPlugin extends RulesPlugin implements IteratorAggregate {
2177

    
2178
  protected $children = array();
2179

    
2180
  public function __construct($variables = array()) {
2181
    $this->setUp();
2182
    if (!empty($variables) && $this->isRoot()) {
2183
      $this->info['variables'] = $variables;
2184
    }
2185
  }
2186

    
2187
  /**
2188
   * Returns the specified variables, in case the plugin is used as component.
2189
   */
2190
  public function &componentVariables() {
2191
    if ($this->isRoot()) {
2192
      $this->info += array('variables' => array());
2193
      return $this->info['variables'];
2194
    }
2195
    // We have to return a reference in any case.
2196
    $return = NULL;
2197
    return $return;
2198
  }
2199

    
2200
  /**
2201
   * Allows access to the children through the iterator.
2202
   *
2203
   * @return RulesRecursiveElementIterator
2204
   */
2205
  public function getIterator() {
2206
    return new RulesRecursiveElementIterator($this->children);
2207
  }
2208

    
2209
  /**
2210
   * @return RulesContainerPlugin
2211
   */
2212
  public function integrityCheck() {
2213
    if (!empty($this->info['variables']) && !$this->isRoot()) {
2214
      throw new RulesIntegrityException(t('%plugin: Specifying state variables is not possible for child elements.', array('%plugin' => $this->getPluginName())), $this);
2215
    }
2216
    parent::integrityCheck();
2217
    foreach ($this->children as $child) {
2218
      $child->integrityCheck();
2219
    }
2220
    return $this;
2221
  }
2222

    
2223
  public function dependencies() {
2224
    $modules = array_flip(parent::dependencies());
2225
    foreach ($this->children as $child) {
2226
      $modules += array_flip($child->dependencies());
2227
    }
2228
    return array_keys($modules);
2229
  }
2230

    
2231
  public function parameterInfo($optional = FALSE) {
2232
    $params = parent::parameterInfo($optional);
2233
    if (isset($this->info['variables'])) {
2234
      foreach ($this->info['variables'] as $name => $var_info) {
2235
        if (empty($var_info['handler']) && (!isset($var_info['parameter']) || $var_info['parameter'])) {
2236
          $params[$name] = $var_info;
2237
          // For lists allow empty variables by default.
2238
          if (entity_property_list_extract_type($var_info['type'])) {
2239
            $params[$name] += array('allow null' => TRUE);
2240
          }
2241
        }
2242
      }
2243
    }
2244
    return $params;
2245
  }
2246

    
2247
  public function availableVariables() {
2248
    if (!isset($this->availableVariables)) {
2249
      if ($this->isRoot()) {
2250
        $this->availableVariables = RulesState::defaultVariables();
2251
        if (isset($this->info['variables'])) {
2252
          $this->availableVariables += $this->info['variables'];
2253
        }
2254
      }
2255
      else {
2256
        $this->availableVariables = $this->parent->stateVariables($this);
2257
      }
2258
    }
2259
    return $this->availableVariables;
2260
  }
2261

    
2262
  /**
2263
   * Returns available state variables for an element.
2264
   *
2265
   * Returns info about variables available in the evaluation state for any
2266
   * children elements or if given for a special child element.
2267
   *
2268
   * @param $element
2269
   *   The element for which the available state variables should be returned.
2270
   *   If NULL is given, the variables available before any children are invoked
2271
   *   are returned. If set to TRUE, the variables available after evaluating
2272
   *   all children will be returned.
2273
   */
2274
  protected function stateVariables($element = NULL) {
2275
    $vars = $this->availableVariables();
2276
    if (isset($element)) {
2277
      // Add in variables provided by siblings executed before the element.
2278
      foreach ($this->children as $child) {
2279
        if ($child === $element) {
2280
          break;
2281
        }
2282
        $vars += $child->providesVariables();
2283
        // Take variable info assertions into account.
2284
        if ($assertions = $child->variableInfoAssertions()) {
2285
          $vars = RulesData::addMetadataAssertions($vars, $assertions);
2286
        }
2287
      }
2288
    }
2289
    return $vars;
2290
  }
2291

    
2292
  protected function variableInfoAssertions() {
2293
    $assertions = array();
2294
    foreach ($this->children as $child) {
2295
      if ($add = $child->variableInfoAssertions()) {
2296
        $assertions = rules_update_array($assertions, $add);
2297
      }
2298
    }
2299
    return $assertions;
2300
  }
2301

    
2302
  protected function setUpVariables() {
2303
    return isset($this->info['variables']) ? parent::parameterInfo(TRUE) + $this->info['variables'] : $this->parameterInfo(TRUE);
2304
  }
2305

    
2306
  /**
2307
   * Executes container with the given arguments.
2308
   *
2309
   * Condition containers just return a boolean while action containers return
2310
   * the configured provided variables as an array of variables.
2311
   */
2312
  public function executeByArgs($args = array()) {
2313
    $replacements = array('%label' => $this->label(), '@plugin' => $this->itemName);
2314
    rules_log('Executing @plugin %label.', $replacements, RulesLog::INFO, $this, TRUE);
2315
    $this->processSettings();
2316
    $state = $this->setUpState($args);
2317

    
2318
    // Handle recursion prevention.
2319
    if ($state->isBlocked($this)) {
2320
      return rules_log('Not evaluating @plugin %label to prevent recursion.', array('%label' => $this->label(), '@plugin' => $this->plugin()), RulesLog::INFO);
2321
    }
2322
    // Block the config to prevent any future recursion.
2323
    $state->block($this);
2324

    
2325
    module_invoke_all('rules_config_execute', $this);
2326
    $result = $this->evaluate($state);
2327
    $return = $this->returnVariables($state, $result);
2328

    
2329
    $state->unblock($this);
2330
    $state->cleanUp();
2331
    rules_log('Finished executing of @plugin %label.', $replacements, RulesLog::INFO, $this, FALSE);
2332
    return $return;
2333
  }
2334

    
2335
  public function access() {
2336
    foreach ($this->children as $key => $child) {
2337
      if (!$child->access()) {
2338
        return FALSE;
2339
      }
2340
    }
2341
    return TRUE;
2342
  }
2343

    
2344
  public function destroy() {
2345
    foreach ($this->children as $key => $child) {
2346
      $child->destroy();
2347
    }
2348
    parent::destroy();
2349
  }
2350

    
2351
  /**
2352
   * By default we do a deep clone.
2353
   */
2354
  public function __clone() {
2355
    parent::__clone();
2356
    foreach ($this->children as $key => $child) {
2357
      $this->children[$key] = clone $child;
2358
      $this->children[$key]->parent = $this;
2359
    }
2360
  }
2361

    
2362
  /**
2363
   * Overrides delete to keep the children alive, if possible.
2364
   */
2365
  public function delete($keep_children = TRUE) {
2366
    if (isset($this->parent) && $keep_children) {
2367
      foreach ($this->children as $child) {
2368
        $child->setParent($this->parent);
2369
      }
2370
    }
2371
    parent::delete();
2372
  }
2373

    
2374
  public function __sleep() {
2375
    return parent::__sleep() + array('children' => 'children', 'info' => 'info');
2376
  }
2377

    
2378
  /**
2379
   * Sorts all child elements by their weight.
2380
   *
2381
   * @param bool $deep
2382
   *   If enabled a deep sort is performed, thus the whole element tree below
2383
   *   this element is sorted.
2384
   */
2385
  public function sortChildren($deep = FALSE) {
2386
    // Make sure the array order is kept in case two children have the same
2387
    // weight by ensuring later children would have higher weights.
2388
    foreach (array_values($this->children) as $i => $child) {
2389
      $child->weight += $i / 1000;
2390
    }
2391
    usort($this->children, array('RulesPlugin', 'compare'));
2392

    
2393
    // Fix up the weights afterwards to be unique integers.
2394
    foreach (array_values($this->children) as $i => $child) {
2395
      $child->weight = $i;
2396
    }
2397

    
2398
    if ($deep) {
2399
      foreach (new ParentIterator($this->getIterator()) as $child) {
2400
        $child->sortChildren(TRUE);
2401
      }
2402
    }
2403
    $this->resetInternalCache();
2404
  }
2405

    
2406
  protected function exportChildren($key = NULL) {
2407
    $key = isset($key) ? $key : strtoupper($this->plugin());
2408
    $export[$key] = array();
2409
    foreach ($this->children as $child) {
2410
      $export[$key][] = $child->export();
2411
    }
2412
    return $export;
2413
  }
2414

    
2415
  /**
2416
   * Determines whether the element should be exported in flat style.
2417
   *
2418
   * Flat style means that the export keys are written directly into the export
2419
   * array, whereas else the export is written into a sub-array.
2420
   */
2421
  protected function exportFlat() {
2422
    // By default we always use flat style for plugins without any parameters
2423
    // or provided variables, as then only children have to be exported. E.g.
2424
    // this applies to the OR and AND plugins.
2425
    return $this->isRoot() || (!$this->pluginParameterInfo() && !$this->providesVariables());
2426
  }
2427

    
2428
  protected function exportToArray() {
2429
    $export = array();
2430
    if (!empty($this->info['variables'])) {
2431
      $export['USES VARIABLES'] = $this->info['variables'];
2432
    }
2433
    if ($this->exportFlat()) {
2434
      $export += $this->exportSettings() + $this->exportChildren();
2435
    }
2436
    else {
2437
      $export[strtoupper($this->plugin())] = $this->exportSettings() + $this->exportChildren();
2438
    }
2439
    return $export;
2440
  }
2441

    
2442
  public function import(array $export) {
2443
    if (!empty($export['USES VARIABLES'])) {
2444
      $this->info['variables'] = $export['USES VARIABLES'];
2445
    }
2446
    // Care for exports having the export array nested in a sub-array.
2447
    if (!$this->exportFlat()) {
2448
      $export = reset($export);
2449
    }
2450
    $this->importSettings($export);
2451
    $this->importChildren($export);
2452
  }
2453

    
2454
  protected function importChildren($export, $key = NULL) {
2455
    $key = isset($key) ? $key : strtoupper($this->plugin());
2456
    foreach ($export[$key] as $child_export) {
2457
      $plugin = _rules_import_get_plugin(rules_array_key($child_export), $this instanceof RulesActionInterface ? 'action' : 'condition');
2458
      $child = rules_plugin_factory($plugin);
2459
      $child->setParent($this);
2460
      $child->import($child_export);
2461
    }
2462
  }
2463

    
2464
  public function resetInternalCache() {
2465
    $this->availableVariables = NULL;
2466
    foreach ($this->children as $child) {
2467
      $child->resetInternalCache();
2468
    }
2469
  }
2470

    
2471
  /**
2472
   * Overrides optimize().
2473
   */
2474
  public function optimize() {
2475
    parent::optimize();
2476
    // Now let the children optimize itself.
2477
    foreach ($this as $element) {
2478
      $element->optimize();
2479
    }
2480
  }
2481

    
2482
}
2483

    
2484
/**
2485
 * Base class for all action containers.
2486
 */
2487
abstract class RulesActionContainer extends RulesContainerPlugin implements RulesActionInterface {
2488

    
2489
  public function __construct($variables = array(), $providesVars = array()) {
2490
    parent::__construct($variables);
2491
    // The provided vars of a component are the names of variables, which should
2492
    // be provided to the caller. See rule().
2493
    if ($providesVars) {
2494
      $this->info['provides'] = $providesVars;
2495
    }
2496
  }
2497

    
2498
  /**
2499
   * Adds an action to the container.
2500
   *
2501
   * Pass in either an instance of the RulesActionInterface or the arguments
2502
   * as needed by rules_action().
2503
   *
2504
   * @return $this
2505
   */
2506
  public function action($name, $settings = array()) {
2507
    $action = (is_object($name) && $name instanceof RulesActionInterface) ? $name : rules_action($name, $settings);
2508
    $action->setParent($this);
2509
    return $this;
2510
  }
2511

    
2512
  /**
2513
   * Evaluate, whereas by default new vars are visible in the parent's scope.
2514
   */
2515
  public function evaluate(RulesState $state) {
2516
    foreach ($this->children as $action) {
2517
      $action->evaluate($state);
2518
    }
2519
  }
2520

    
2521
  public function pluginProvidesVariables() {
2522
    return array();
2523
  }
2524

    
2525
  public function providesVariables() {
2526
    $provides = parent::providesVariables();
2527
    if (isset($this->info['provides']) && $vars = $this->componentVariables()) {
2528
      // Determine the full variable info for the provided variables. Note that
2529
      // we only support providing variables list in the component vars.
2530
      $provides += array_intersect_key($vars, array_flip($this->info['provides']));
2531
    }
2532
    return $provides;
2533
  }
2534

    
2535
  /**
2536
   * Returns an array of provided variable names.
2537
   *
2538
   * Returns an array of variable names, which are provided by passing through
2539
   * the provided variables of the children.
2540
   */
2541
  public function &componentProvidesVariables() {
2542
    $this->info += array('provides' => array());
2543
    return $this->info['provides'];
2544
  }
2545

    
2546
  protected function exportToArray() {
2547
    $export = parent::exportToArray();
2548
    if (!empty($this->info['provides'])) {
2549
      $export['PROVIDES VARIABLES'] = $this->info['provides'];
2550
    }
2551
    return $export;
2552
  }
2553

    
2554
  public function import(array $export) {
2555
    parent::import($export);
2556
    if (!empty($export['PROVIDES VARIABLES'])) {
2557
      $this->info['provides'] = $export['PROVIDES VARIABLES'];
2558
    }
2559
  }
2560

    
2561
}
2562

    
2563
/**
2564
 * Base class for all condition containers.
2565
 */
2566
abstract class RulesConditionContainer extends RulesContainerPlugin implements RulesConditionInterface {
2567

    
2568
  protected $negate = FALSE;
2569

    
2570
  /**
2571
   * Adds a condition to the container.
2572
   *
2573
   * Pass in either an instance of the RulesConditionInterface or the arguments
2574
   * as needed by rules_condition().
2575
   *
2576
   * @return $this
2577
   */
2578
  public function condition($name, $settings = array()) {
2579
    $condition = (is_object($name) && $name instanceof RulesConditionInterface) ? $name : rules_condition($name, $settings);
2580
    $condition->setParent($this);
2581
    return $this;
2582
  }
2583

    
2584
  /**
2585
   * Negate this condition.
2586
   *
2587
   * @return RulesConditionContainer
2588
   */
2589
  public function negate($negate = TRUE) {
2590
    $this->negate = (bool) $negate;
2591
    return $this;
2592
  }
2593

    
2594
  public function isNegated() {
2595
    return $this->negate;
2596
  }
2597

    
2598
  public function __sleep() {
2599
    return parent::__sleep() + array('negate' => 'negate');
2600
  }
2601

    
2602
  /**
2603
   * Just return the condition container's result.
2604
   */
2605
  protected function returnVariables(RulesState $state, $result = NULL) {
2606
    return $result;
2607
  }
2608

    
2609
  protected function exportChildren($key = NULL) {
2610
    $key = isset($key) ? $key : strtoupper($this->plugin());
2611
    return parent::exportChildren($this->negate ? 'NOT ' . $key : $key);
2612
  }
2613

    
2614
  protected function importChildren($export, $key = NULL) {
2615
    $key = isset($key) ? $key : strtoupper($this->plugin());
2616
    // Care for negated elements.
2617
    if (!isset($export[$key]) && isset($export['NOT ' . $key])) {
2618
      $this->negate = TRUE;
2619
      $key = 'NOT ' . $key;
2620
    }
2621
    parent::importChildren($export, $key);
2622
  }
2623

    
2624
  /**
2625
   * Overridden to exclude variable assertions of negated conditions.
2626
   */
2627
  protected function stateVariables($element = NULL) {
2628
    $vars = $this->availableVariables();
2629
    if (isset($element)) {
2630
      // Add in variables provided by siblings executed before the element.
2631
      foreach ($this->children as $child) {
2632
        if ($child === $element) {
2633
          break;
2634
        }
2635
        $vars += $child->providesVariables();
2636
        // Take variable info assertions into account.
2637
        if (!$this->negate && !$child->isNegated() && ($assertions = $child->variableInfoAssertions())) {
2638
          $vars = RulesData::addMetadataAssertions($vars, $assertions);
2639
        }
2640
      }
2641
    }
2642
    return $vars;
2643
  }
2644

    
2645
}
2646

    
2647
/**
2648
 * The rules default logging class.
2649
 */
2650
class RulesLog {
2651

    
2652
  const INFO  = 1;
2653
  const WARN  = 2;
2654
  const ERROR = 3;
2655

    
2656
  static protected $logger;
2657

    
2658
  /**
2659
   * @return RulesLog
2660
   *   Returns the rules logger instance.
2661
   */
2662
  public static function logger() {
2663
    if (!isset(self::$logger)) {
2664
      $class = __CLASS__;
2665
      self::$logger = new $class(variable_get('rules_log_level', self::INFO));
2666
    }
2667
    return self::$logger;
2668
  }
2669

    
2670
  protected $log = array();
2671
  protected $logLevel;
2672
  protected $line = 0;
2673

    
2674
  /**
2675
   * This is a singleton.
2676
   */
2677
  protected function __construct($logLevel = self::WARN) {
2678
    $this->logLevel = $logLevel;
2679
  }
2680

    
2681
  public function __clone() {
2682
    throw new Exception("Cannot clone the logger.");
2683
  }
2684

    
2685
  /**
2686
   * Logs a log message.
2687
   *
2688
   * @see rules_log()
2689
   */
2690
  public function log($msg, $args = array(), $logLevel = self::INFO, $scope = NULL, $path = NULL) {
2691
    if ($logLevel >= $this->logLevel) {
2692
      $this->log[] = array($msg, $args, $logLevel, microtime(TRUE), $scope, $path);
2693
    }
2694
  }
2695

    
2696
  /**
2697
   * Checks the log and throws an exception if there were any problems.
2698
   */
2699
  public function checkLog($logLevel = self::WARN) {
2700
    foreach ($this->log as $entry) {
2701
      if ($entry[2] >= $logLevel) {
2702
        throw new Exception($this->render());
2703
      }
2704
    }
2705
  }
2706

    
2707
  /**
2708
   * Checks the log for error messages.
2709
   *
2710
   * @param int $logLevel
2711
   *   Lowest log level to return. Values lower than $logLevel will not be
2712
   *   returned.
2713
   *
2714
   * @return bool
2715
   *   Whether the an error has been logged.
2716
   */
2717
  public function hasErrors($logLevel = self::WARN) {
2718
    foreach ($this->log as $entry) {
2719
      if ($entry[2] >= $logLevel) {
2720
        return TRUE;
2721
      }
2722
    }
2723
    return FALSE;
2724
  }
2725

    
2726
  /**
2727
   * Gets an array of logged messages.
2728
   */
2729
  public function get() {
2730
    return $this->log;
2731
  }
2732

    
2733
  /**
2734
   * Renders the whole log.
2735
   */
2736
  public function render() {
2737
    $line = 0;
2738
    $output = array();
2739
    while (isset($this->log[$line])) {
2740
      $vars['head'] = t($this->log[$line][0], $this->log[$line][1]);
2741
      $vars['log'] = $this->renderHelper($line);
2742
      $output[] = theme('rules_debug_element', $vars);
2743
      $line++;
2744
    }
2745
    return implode('', $output);
2746
  }
2747

    
2748
  /**
2749
   * Renders the log of one event invocation.
2750
   */
2751
  protected function renderHelper(&$line = 0) {
2752
    $startTime = isset($this->log[$line][3]) ? $this->log[$line][3] : 0;
2753
    $output = array();
2754
    while ($line < count($this->log)) {
2755
      if ($output && !empty($this->log[$line][4])) {
2756
        // The next entry stems from another evaluated set, add in its log
2757
        // messages here.
2758
        $vars['head'] = t($this->log[$line][0], $this->log[$line][1]);
2759
        if (isset($this->log[$line][5])) {
2760
          $vars['link'] = '[' . l(t('edit'), $this->log[$line][5]) . ']';
2761
        }
2762
        $vars['log'] = $this->renderHelper($line);
2763
        $output[] = theme('rules_debug_element', $vars);
2764
      }
2765
      else {
2766
        $formatted_diff = round(($this->log[$line][3] - $startTime) * 1000, 3) . ' ms';
2767
        $msg = $formatted_diff . ' ' . t($this->log[$line][0], $this->log[$line][1]);
2768
        if ($this->log[$line][2] >= RulesLog::WARN) {
2769
          $level = $this->log[$line][2] == RulesLog::WARN ? 'warn' : 'error';
2770
          $msg = '<span class="rules-debug-' . $level . '">' . $msg . '</span>';
2771
        }
2772
        if (isset($this->log[$line][5]) && !isset($this->log[$line][4])) {
2773
          $msg .= ' [' . l(t('edit'), $this->log[$line][5]) . ']';
2774
        }
2775
        $output[] = $msg;
2776

    
2777
        if (isset($this->log[$line][4]) && !$this->log[$line][4]) {
2778
          // This was the last log entry of this set.
2779
          return theme('item_list', array('items' => $output));
2780
        }
2781
      }
2782
      $line++;
2783
    }
2784
    return theme('item_list', array('items' => $output));
2785
  }
2786

    
2787
  /**
2788
   * Clears the logged messages.
2789
   */
2790
  public function clear() {
2791
    $this->log = array();
2792
  }
2793

    
2794
}
2795

    
2796
/**
2797
 * A base exception class for Rules.
2798
 *
2799
 * This class can be used to catch all exceptions thrown by Rules, and it
2800
 * may be subclassed to describe more specific exceptions.
2801
 */
2802
abstract class RulesException extends Exception {}
2803

    
2804
/**
2805
 * An exception that is thrown during evaluation.
2806
 *
2807
 * Messages are prepared to be logged to the watchdog, thus not yet translated.
2808
 *
2809
 * @see watchdog()
2810
 */
2811
class RulesEvaluationException extends RulesException {
2812

    
2813
  public $msg;
2814
  public $args;
2815
  public $severity;
2816
  public $element;
2817
  public $keys = array();
2818

    
2819
  /**
2820
   * Constructor.
2821
   *
2822
   * @param string $msg
2823
   *   The exception message containing placeholder as t().
2824
   * @param array $args
2825
   *   Replacement arguments such as for t().
2826
   * @param $element
2827
   *   The element of a configuration causing the exception or an array
2828
   *   consisting of the element and keys specifying a setting value causing
2829
   *   the exception.
2830
   * @param int $severity
2831
   *   The RulesLog severity. Defaults to RulesLog::WARN.
2832
   */
2833
  public function __construct($msg, array $args = array(), $element = NULL, $severity = RulesLog::WARN) {
2834
    $this->element = is_array($element) ? array_shift($element) : $element;
2835
    $this->keys = is_array($element) ? $element : array();
2836
    $this->msg = $msg;
2837
    $this->args = $args;
2838
    $this->severity = $severity;
2839
    // If an error happened, run the integrity check on the rules configuration
2840
    // and mark it as dirty if it the check fails.
2841
    if ($severity == RulesLog::ERROR && isset($this->element)) {
2842
      $rules_config = $this->element->root();
2843
      rules_config_update_dirty_flag($rules_config);
2844
      // If we discovered a broken configuration, exclude it in future.
2845
      if ($rules_config->dirty) {
2846
        rules_clear_cache();
2847
      }
2848
    }
2849
    // @todo Fix _drupal_decode_exception() to use __toString() and override it.
2850
    $this->message = t($this->msg, $this->args);
2851
  }
2852

    
2853
}
2854

    
2855
/**
2856
 * Indicates the Rules configuration failed the integrity check.
2857
 *
2858
 * @see RulesPlugin::integrityCheck()
2859
 */
2860
class RulesIntegrityException extends RulesException {
2861

    
2862
  public $msg;
2863
  public $element;
2864
  public $keys = array();
2865

    
2866
  /**
2867
   * Constructs a RulesIntegrityException object.
2868
   *
2869
   * @param string $msg
2870
   *   The exception message, already translated.
2871
   * @param $element
2872
   *   The element of a configuration causing the exception or an array
2873
   *   consisting of the element and keys specifying a parameter or provided
2874
   *   variable causing the exception, e.g.
2875
   *   @code array($element, 'parameter', 'node') @endcode
2876
   */
2877
  public function __construct($msg, $element = NULL) {
2878
    $this->element = is_array($element) ? array_shift($element) : $element;
2879
    $this->keys = is_array($element) ? $element : array();
2880
    parent::__construct($msg);
2881
  }
2882

    
2883
}
2884

    
2885
/**
2886
 * An exception that is thrown for missing module dependencies.
2887
 */
2888
class RulesDependencyException extends RulesIntegrityException {}
2889

    
2890
/**
2891
 * Determines the plugin to be used for importing a child element.
2892
 *
2893
 * @param string $key
2894
 *   The key to look for, e.g. 'OR' or 'DO'.
2895
 * @param string $default
2896
 *   The default to return if no special plugin can be found.
2897
 */
2898
function _rules_import_get_plugin($key, $default = 'action') {
2899
  $map = &drupal_static(__FUNCTION__);
2900
  if (!isset($map)) {
2901
    $cache = rules_get_cache();
2902
    foreach ($cache['plugin_info'] as $name => $info) {
2903
      if (!empty($info['embeddable'])) {
2904
        $info += array('import keys' => array(strtoupper($name)));
2905
        foreach ($info['import keys'] as $k) {
2906
          $map[$k] = $name;
2907
        }
2908
      }
2909
    }
2910
  }
2911
  // Cut off any leading NOT from the key.
2912
  if (strpos($key, 'NOT ') === 0) {
2913
    $key = substr($key, 4);
2914
  }
2915
  if (isset($map[$key])) {
2916
    return $map[$key];
2917
  }
2918
  return $default;
2919
}