rules.state.inc 27 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770
<?php

/**
 * @file Contains the state and data related stuff.
 */

/**
 * The rules evaluation state.
 *
 * A rule element may clone the state, so any added variables are only visible
 * for elements in the current PHP-variable-scope.
 */
class RulesState {

  /**
   * Globally keeps the ids of rules blocked due to recursion prevention.
   */
  static protected $blocked = array();

  /**
   * The known variables.
   */
  public $variables = array();

  /**
   * Holds info about the variables.
   */
  protected $info = array();

  /**
   * Keeps wrappers to be saved later on.
   */
  protected $save;

  /**
   * Holds the arguments while an element is executed. May be used by the
   * element to easily access the wrapped arguments.
   */
  public $currentArguments;

  /**
   * Variable for saving currently blocked configs for serialization.
   */
  protected $currentlyBlocked;


  public function __construct() {
    // Use an object in order to ensure any cloned states reference the same
    // save information.
    $this->save = new ArrayObject();
    $this->addVariable('site', FALSE, self::defaultVariables('site'));
  }

  /**
   * Adds the given variable to the given execution state.
   */
  public function addVariable($name, $data, $info) {
    $this->info[$name] = $info + array(
      'skip save' => FALSE,
      'type' => 'unknown',
      'handler' => FALSE,
    );
    if (empty($this->info[$name]['handler'])) {
      $this->variables[$name] = rules_wrap_data($data, $this->info[$name]);
    }
  }

  /**
   * Runs post-evaluation tasks, such as saving variables.
   */
  public function cleanUp() {
    // Make changes permanent.
    foreach ($this->save->getArrayCopy() as $selector => $wrapper) {
      $this->saveNow($selector);
    }
    unset($this->currentArguments);
  }

  /**
   * Block a rules configuration from execution.
   */
  public function block($rules_config) {
    if (empty($rules_config->recursion) && $rules_config->id) {
      self::$blocked[$rules_config->id] = TRUE;
    }
  }

  /**
   * Unblock a rules configuration from execution.
   */
  public function unblock($rules_config) {
    if (empty($rules_config->recursion) && $rules_config->id) {
      unset(self::$blocked[$rules_config->id]);
    }
  }

  /**
   * Returns whether a rules configuration should be blocked from execution.
   */
  public function isBlocked($rule_config) {
    return !empty($rule_config->id) && isset(self::$blocked[$rule_config->id]);
  }

  /**
   * Get the info about the state variables or a single variable.
   */
  public function varInfo($name = NULL) {
    if (isset($name)) {
      return isset($this->info[$name]) ? $this->info[$name] : FALSE;
    }
    return $this->info;
  }

  /**
   * Returns whether the given wrapper is savable.
   */
  public function isSavable($wrapper) {
    return ($wrapper instanceof EntityDrupalWrapper && entity_type_supports($wrapper->type(), 'save')) || $wrapper instanceof RulesDataWrapperSavableInterface;
  }

  /**
   * Returns whether the variable with the given name is an entity.
   */
  public function isEntity($name) {
    $entity_info = entity_get_info();
    return isset($this->info[$name]['type']) && isset($entity_info[$this->info[$name]['type']]);
  }

  /**
   * Gets a variable.
   *
   * If necessary, the specified handler is invoked to fetch the variable.
   *
   * @param $name
   *   The name of the variable to return.
   *
   * @return
   *   The variable or a EntityMetadataWrapper containing the variable.
   *
   * @throws RulesEvaluationException
   *   Throws a RulesEvaluationException in case we have info about the
   *   requested variable, but it is not defined.
   */
  public function &get($name) {
    if (!array_key_exists($name, $this->variables)) {
      // If there is handler to load the variable, do it now.
      if (!empty($this->info[$name]['handler'])) {
        $data = call_user_func($this->info[$name]['handler'], rules_unwrap_data($this->variables), $name, $this->info[$name]);
        $this->variables[$name] = rules_wrap_data($data, $this->info[$name]);
        $this->info[$name]['handler'] = FALSE;
        if (!isset($data)) {
          throw new RulesEvaluationException('Unable to load variable %name, aborting.', array('%name' => $name), NULL, RulesLog::INFO);
        }
      }
      else {
        throw new RulesEvaluationException('Unable to get variable %name, it is not defined.', array('%name' => $name), NULL, RulesLog::ERROR);
      }
    }
    return $this->variables[$name];
  }

  /**
   * Apply permanent changes provided the wrapper's data type is savable.
   *
   * @param $selector
   *   The data selector of the wrapper to save or just a variable name.
   * @param $immediate
   *   Pass FALSE to postpone saving to later on. Else it's immediately saved.
   */
  public function saveChanges($selector, $wrapper, $immediate = FALSE) {
    $info = $wrapper->info();
    if (empty($info['skip save']) && $this->isSavable($wrapper)) {
      $this->save($selector, $wrapper, $immediate);
    }
    // No entity, so try saving the parent.
    elseif (empty($info['skip save']) && isset($info['parent']) && !($wrapper instanceof EntityDrupalWrapper)) {
      // Cut of the last part of the selector.
      $selector = implode(':', explode(':', $selector, -1));
      $this->saveChanges($selector, $info['parent'], $immediate);
    }
    return $this;
  }

  /**
   * Remembers to save the wrapper on cleanup or does it now.
   */
  protected function save($selector, EntityMetadataWrapper $wrapper, $immediate) {
    // Convert variable names and selectors to both use underscores.
    $selector = strtr($selector, '-', '_');
    if (isset($this->save[$selector])) {
      if ($this->save[$selector][0]->getIdentifier() == $wrapper->getIdentifier()) {
        // The entity is already remembered. So do a combined save.
        $this->save[$selector][1] += self::$blocked;
      }
      else {
        // The wrapper is already in there, but wraps another entity. So first
        // save the old one, then care about the new one.
        $this->saveNow($selector);
      }
    }
    if (!isset($this->save[$selector])) {
      // In case of immediate saving don't clone the wrapper, so saving a new
      // entity immediately makes the identifier available afterwards.
      $this->save[$selector] = array($immediate ? $wrapper : clone $wrapper, self::$blocked);
    }
    if ($immediate) {
      $this->saveNow($selector);
    }
  }

  /**
   * Saves the wrapper for the given selector.
   */
  protected function saveNow($selector) {
    // Add the set of blocked elements for the recursion prevention.
    $previously_blocked = self::$blocked;
    self::$blocked += $this->save[$selector][1];

    // Actually save!
    $wrapper = $this->save[$selector][0];
    $entity = $wrapper->value();
    // When operating in hook_entity_insert() $entity->is_new might be still
    // set. In that case remove the flag to avoid causing another insert instead
    // of an update.
    if (!empty($entity->is_new) && $wrapper->getIdentifier()) {
      $entity->is_new = FALSE;
    }
    rules_log('Saved %selector of type %type.', array('%selector' => $selector, '%type' => $wrapper->type()));
    $wrapper->save();

    // Restore the state's set of blocked elements.
    self::$blocked = $previously_blocked;
    unset($this->save[$selector]);
  }

  /**
   * Merges the info about to be saved variables form the given state into the
   * existing state. Therefor we can aggregate saves from invoked components.
   * Merged in saves are removed from the given state, but not mergable saves
   * remain there.
   *
   * @param $state
   *   The state for which to merge the to be saved variables in.
   * @param $component
   *   The component which has been invoked, thus needs to be blocked for the
   *   merged in saves.
   * @param $settings
   *   The settings of the element that invoked the component. Contains
   *   information about variable/selector mappings between the states.
   */
  public function mergeSaveVariables(RulesState $state, RulesPlugin $component, $settings) {
    // For any saves that we take over, also block the component.
    $this->block($component);

    foreach ($state->save->getArrayCopy() as $selector => $data) {
      $parts = explode(':', $selector, 2);
      // Adapt the selector to fit for the parent state and move the wrapper.
      if (isset($settings[$parts[0] . ':select'])) {
        $parts[0] = $settings[$parts[0] . ':select'];
        $this->save(implode(':', $parts), $data[0], FALSE);
        unset($state->save[$selector]);
      }
    }
    $this->unblock($component);
  }

  /**
   * Returns an entity metadata wrapper as specified in the selector.
   *
   * @param $selector
   *   The selector string, e.g. "node:author:mail".
   * @param $langcode
   *   (optional) The language code used to get the argument value if the
   *   argument value should be translated. Defaults to LANGUAGE_NONE.
   *
   * @return EntityMetadataWrapper
   *   The wrapper for the given selector.
   *
   * @throws RulesEvaluationException
   *   Throws a RulesEvaluationException in case the selector cannot be applied.
   */
  public function applyDataSelector($selector, $langcode = LANGUAGE_NONE) {
    $parts = explode(':', str_replace('-', '_', $selector), 2);
    $wrapper = $this->get($parts[0]);
    if (count($parts) == 1) {
      return $wrapper;
    }
    elseif (!$wrapper instanceof EntityMetadataWrapper) {
      throw new RulesEvaluationException('Unable to apply data selector %selector. The specified variable is not wrapped correctly.', array('%selector' => $selector));
    }
    try {
      foreach (explode(':', $parts[1]) as $name) {
        if ($wrapper instanceof EntityListWrapper || $wrapper instanceof EntityStructureWrapper) {
          // Make sure we are usign the right language. Wrappers might be cached
          // and have previous langcodes set, so always set the right language.
          if ($wrapper instanceof EntityStructureWrapper) {
            $wrapper->language($langcode);
          }
          $wrapper = $wrapper->get($name);
        }
        else {
          throw new RulesEvaluationException('Unable to apply data selector %selector. The specified variable is not a list or a structure: %wrapper.', array('%selector' => $selector, '%wrapper' => $wrapper));
        }
      }
    }
    catch (EntityMetadataWrapperException $e) {
      // In case of an exception, re-throw it.
      throw new RulesEvaluationException('Unable to apply data selector %selector: %error', array('%selector' => $selector, '%error' => $e->getMessage()));
    }
    return $wrapper;
  }

  /**
   * Magic method. Only serialize variables and their info.
   * Additionally we remember currently blocked configs, so we can restore them
   * upon deserialization using restoreBlocks().
   */
  public function __sleep () {
    $this->currentlyBlocked = self::$blocked;
    return array('info', 'variables', 'currentlyBlocked');
  }

  public function __wakeup() {
    $this->save = new ArrayObject();
  }

  /**
   * Restore the before serialization blocked configurations.
   *
   * Warning: This overwrites any possible currently blocked configs. Thus
   * do not invoke this method, if there might be evaluations active.
   */
  public function restoreBlocks() {
    self::$blocked = $this->currentlyBlocked;
  }

  /**
   * Defines always available variables.
   */
  public static function defaultVariables($key = NULL) {
    // Add a variable for accessing site-wide data properties.
    $vars['site'] = array(
      'type' => 'site',
      'label' => t('Site information'),
      'description' => t("Site-wide settings and other global information."),
      // Add the property info via a callback making use of the cached info.
      'property info alter' => array('RulesData', 'addSiteMetadata'),
      'property info' => array(),
      'optional' => TRUE,
    );
    return isset($key) ? $vars[$key] : $vars;
  }
}

/**
 * A class holding static methods related to data.
 */
class RulesData  {

  /**
   * Returns whether the type match. They match if type1 is compatible to type2.
   *
   * @param $var_info
   *   The name of the type to check for whether it is compatible to type2.
   * @param $param_info
   *   The type expression to check for.
   * @param $ancestors
   *   Whether sub-type relationships for checking type compatibility should be
   *   taken into account. Defaults to TRUE.
   *
   * @return
   *   Whether the types match.
   */
  public static function typesMatch($var_info, $param_info, $ancestors = TRUE) {
    $var_type = $var_info['type'];
    $param_type = $param_info['type'];

    if ($param_type == '*' || $param_type == 'unknown') {
      return TRUE;
    }

    if ($var_type == $param_type) {
      // Make sure the bundle matches, if specified by the parameter.
      return !isset($param_info['bundles']) || isset($var_info['bundle']) && in_array($var_info['bundle'], $param_info['bundles']);
    }

    // Parameters may specify multiple types using an array.
    $valid_types = is_array($param_type) ? $param_type : array($param_type);
    if (in_array($var_type, $valid_types)) {
      return TRUE;
    }

    // Check for sub-type relationships.
    if ($ancestors && !isset($param_info['bundles'])) {
      $cache = &rules_get_cache();
      self::typeCalcAncestors($cache, $var_type);
      // If one of the types is an ancestor return TRUE.
      return (bool)array_intersect_key($cache['data_info'][$var_type]['ancestors'], array_flip($valid_types));
    }
    return FALSE;
  }

  protected static function typeCalcAncestors(&$cache, $type) {
    if (!isset($cache['data_info'][$type]['ancestors'])) {
      $cache['data_info'][$type]['ancestors'] = array();
      if (isset($cache['data_info'][$type]['parent']) && $parent = $cache['data_info'][$type]['parent']) {
        $cache['data_info'][$type]['ancestors'][$parent] = TRUE;
        self::typeCalcAncestors($cache, $parent);
        // Add all parent ancestors to our own ancestors.
        $cache['data_info'][$type]['ancestors'] += $cache['data_info'][$parent]['ancestors'];
      }
      // For special lists like list<node> add in "list" as valid parent.
      if (entity_property_list_extract_type($type)) {
        $cache['data_info'][$type]['ancestors']['list'] = TRUE;
      }
    }
  }

  /**
   * Returns matching data variables or properties for the given info and the to
   * be configured parameter.
   *
   * @param $source
   *   Either an array of info about available variables or a entity metadata
   *   wrapper.
   * @param $param_info
   *   The information array about the to be configured parameter.
   * @param $prefix
   *   An optional prefix for the data selectors.
   * @param $recursions
   *   The number of recursions used to go down the tree. Defaults to 2.
   * @param $suggestions
   *   Whether possibilities to recurse are suggested as soon as the deepest
   *   level of recursions is reached. Defaults to TRUE.
   *
   * @return
   *  An array of info about matching variables or properties that match, keyed
   *  with the data selector.
   */
  public static function matchingDataSelector($source, $param_info, $prefix = '', $recursions = 2, $suggestions = TRUE) {
    // If an array of info is given, get entity metadata wrappers first.
    $data = NULL;
    if (is_array($source)) {
      foreach ($source as $name => $info) {
        $source[$name] = rules_wrap_data($data, $info, TRUE);
      }
    }

    $matches = array();
    foreach ($source as $name => $wrapper) {
      $info = $wrapper->info();
      $name = str_replace('_', '-', $name);

      if (self::typesMatch($info, $param_info)) {
        $matches[$prefix . $name] = $info;
        if (!is_array($source) && $source instanceof EntityListWrapper) {
          // Add some more possible list items.
          for ($i = 1; $i < 4; $i++) {
            $matches[$prefix . $i] = $info;
          }
        }
      }
      // Recurse later on to get an improved ordering of the results.
      if ($wrapper instanceof EntityStructureWrapper || $wrapper instanceof EntityListWrapper) {
        $recurse[$prefix . $name] = $wrapper;
        if ($recursions > 0) {
          $matches += self::matchingDataSelector($wrapper, $param_info, $prefix . $name . ':', $recursions - 1, $suggestions);
        }
        elseif ($suggestions) {
          // We may not recurse any more, but indicate the possibility to recurse.
          $matches[$prefix . $name . ':'] = $wrapper->info();
          if (!is_array($source) && $source instanceof EntityListWrapper) {
            // Add some more possible list items.
            for ($i = 1; $i < 4; $i++) {
              $matches[$prefix . $i . ':'] = $wrapper->info();
            }
          }
        }
      }
    }
    return $matches;
  }

  /**
   * Adds asserted metadata to the variable info. In case there are already
   * assertions for a variable, the assertions are merged such that both apply.
   *
   * @see RulesData::applyMetadataAssertions()
   */
  public static function addMetadataAssertions($var_info, $assertions) {
    foreach ($assertions as $selector => $assertion) {
      // Convert the selector back to underscores, such it matches the varname.
      $selector = str_replace('-', '_', $selector);

      $parts = explode(':', $selector);
      if (isset($var_info[$parts[0]])) {
        // Apply the selector to determine the right target array. We build an
        // array like
        // $var_info['rules assertion']['property1']['property2']['#info'] = ..
        $target = &$var_info[$parts[0]]['rules assertion'];
        foreach (array_slice($parts, 1) as $part) {
          $target = &$target[$part];
        }

        // In case the assertion is directly for a variable, we have to modify
        // the variable info directly. In case the asserted property is nested
        // the info-has to be altered by RulesData::applyMetadataAssertions()
        // before the child-wrapper is created.
        if (count($parts) == 1) {
          // Support asserting a type in case of generic entity references only.
          if (isset($assertion['type']) && $var_info[$parts[0]]['type'] == 'entity') {
            if (entity_get_info($assertion['type'])) {
              $var_info[$parts[0]]['type'] = $assertion['type'];
            }
            unset($assertion['type']);
          }
          // Add any single bundle directly to the variable info, so the
          // variable fits as argument for parameters requiring the bundle.
          if (isset($assertion['bundle']) && count($bundles = (array) $assertion['bundle']) == 1) {
            $var_info[$parts[0]]['bundle'] = reset($bundles);
          }
        }

        // Add the assertions, but merge them with any previously added
        // assertions if necessary.
        $target['#info'] = isset($target['#info']) ? rules_update_array($target['#info'], $assertion) : $assertion;

        // Add in a callback that the entity metadata wrapper pick up for
        // altering the property info, such that we can add in the assertions.
        $var_info[$parts[0]] += array('property info alter' => array('RulesData', 'applyMetadataAssertions'));

        // In case there is a VARNAME_unchanged variable as it is used in update
        // hooks, assume the assertions are valid for the unchanged variable
        // too.
        if (isset($var_info[$parts[0] . '_unchanged'])) {
          $name = $parts[0] . '_unchanged';
          $var_info[$name]['rules assertion'] = $var_info[$parts[0]]['rules assertion'];
          $var_info[$name]['property info alter'] = array('RulesData', 'applyMetadataAssertions');

          if (isset($var_info[$parts[0]]['bundle']) && !isset($var_info[$name]['bundle'])) {
            $var_info[$name]['bundle'] = $var_info[$parts[0]]['bundle'];
          }
        }
      }
    }
    return $var_info;
  }

  /**
   * Property info alter callback for the entity metadata wrapper for applying
   * the rules metadata assertions.
   *
   * @see RulesData::addMetadataAssertions()
   */
  public static function applyMetadataAssertions(EntityMetadataWrapper $wrapper, $property_info) {
    $info = $wrapper->info();

    if (!empty($info['rules assertion'])) {
      $assertion = $info['rules assertion'];

      // In case there are list-wrappers pass through the assertions of the item
      // but make sure we only apply the assertions for the list items for
      // which the conditions are executed.
      if (isset($info['parent']) && $info['parent'] instanceof EntityListWrapper) {
        $assertion = isset($assertion[$info['name']]) ? $assertion[$info['name']] : array();
      }

      // Support specifying multiple bundles, whereas the added properties are
      // the intersection of the bundle properties.
      if (isset($assertion['#info']['bundle'])) {
        $bundles = (array) $assertion['#info']['bundle'];
        foreach ($bundles as $bundle) {
          $properties[] = isset($property_info['bundles'][$bundle]['properties']) ? $property_info['bundles'][$bundle]['properties'] : array();
        }
        // Add the intersection.
        $property_info['properties'] += count($properties) > 1 ? call_user_func_array('array_intersect_key', $properties) : reset($properties);
      }
      // Support adding directly asserted property info.
      if (isset($assertion['#info']['property info'])) {
        $property_info['properties'] += $assertion['#info']['property info'];
      }

      // Pass through any rules assertion of properties to their info, so any
      // derived wrappers apply it.
      foreach (element_children($assertion) as $key) {
        $property_info['properties'][$key]['rules assertion'] = $assertion[$key];
        $property_info['properties'][$key]['property info alter'] = array('RulesData', 'applyMetadataAssertions');

        // Apply any 'type' and 'bundle' assertion directly to the propertyinfo.
        if (isset($assertion[$key]['#info']['type'])) {
          $type = $assertion[$key]['#info']['type'];
          // Support asserting a type in case of generic entity references only.
          if ($property_info['properties'][$key]['type'] == 'entity' && entity_get_info($type)) {
            $property_info['properties'][$key]['type'] = $type;
          }
        }
        if (isset($assertion[$key]['#info']['bundle'])) {
          $bundle = (array) $assertion[$key]['#info']['bundle'];
          // Add any single bundle directly to the variable info, so the
          // property fits as argument for parameters requiring the bundle.
          if (count($bundle) == 1) {
            $property_info['properties'][$key]['bundle'] = reset($bundle);
          }
        }
      }
    }
    return $property_info;
  }

  /**
   * Property info alter callback for the entity metadata wrapper to inject
   * metadata for the 'site' variable. In contrast to doing this via
   * hook_rules_data_info() this callback makes use of the already existing
   * property info cache for site information of entity metadata.
   *
   * @see RulesPlugin::availableVariables()
   */
  public static function addSiteMetadata(EntityMetadataWrapper $wrapper, $property_info) {
    $site_info = entity_get_property_info('site');
    $property_info['properties'] += $site_info['properties'];
    // Also invoke the usual callback for altering metadata, in case actions
    // have specified further metadata.
    return RulesData::applyMetadataAssertions($wrapper, $property_info);
  }
}

/**
 * A wrapper class similar to the EntityDrupalWrapper, but for non-entities.
 *
 * This class is intended to serve as base for a custom wrapper classes of
 * identifiable data types, which are non-entities. By extending this class only
 * the extractIdentifier() and load() methods have to be defined.
 * In order to make the data type savable implement the
 * RulesDataWrapperSavableInterface.
 *
 * That way it is possible for non-entity data types to be work with Rules, i.e.
 * one can implement a 'ui class' with a direct input form returning the
 * identifier of the data. However, instead of that it is suggested to implement
 * an entity type, such that the same is achieved via general API functions like
 * entity_load().
 */
abstract class RulesIdentifiableDataWrapper extends EntityStructureWrapper {

  /**
   * Contains the id.
   */
  protected $id = FALSE;

  /**
   * Construct a new wrapper object.
   *
   * @param $type
   *   The type of the passed data.
   * @param $data
   *   Optional. The data to wrap or its identifier.
   * @param $info
   *   Optional. Used internally to pass info about properties down the tree.
   */
  public function __construct($type, $data = NULL, $info = array()) {
    parent::__construct($type, $data, $info);
    $this->setData($data);
  }

  /**
   * Sets the data internally accepting both the data id and object.
   */
  protected function setData($data) {
    if (isset($data) && $data !== FALSE && !is_object($data)) {
      $this->id = $data;
      $this->data = FALSE;
    }
    elseif (is_object($data)) {
      // We got the data object passed.
      $this->data = $data;
      $id = $this->extractIdentifier($data);
      $this->id = isset($id) ? $id : FALSE;
    }
  }

  /**
   * Returns the identifier of the wrapped data.
   */
  public function getIdentifier() {
    return $this->dataAvailable() && $this->value() ? $this->id : NULL;
  }

  /**
   * Overridden.
   */
  public function value(array $options = array()) {
    $this->setData(parent::value());
    if (!$this->data && !empty($this->id)) {
      // Lazy load the data if necessary.
      $this->data = $this->load($this->id);
      if (!$this->data) {
        throw new EntityMetadataWrapperException('Unable to load the ' . check_plain($this->type) . ' with the id ' . check_plain($this->id) . '.');
      }
    }
    return $this->data;
  }

  /**
   * Overridden to support setting the data by either the object or the id.
   */
  public function set($value) {
    if (!$this->validate($value)) {
      throw new EntityMetadataWrapperException('Invalid data value given. Be sure it matches the required data type and format.');
    }
    // As custom wrapper classes can only appear for Rules variables, but not
    // as properties we don't have to care about updating the parent.
    $this->clear();
    $this->setData($value);
    return $this;
  }

  /**
   * Overridden.
   */
  public function clear() {
    $this->id = NULL;
    parent::clear();
  }

  /**
   * Prepare for serializiation.
   */
  public function __sleep() {
    $vars = parent::__sleep();
    // Don't serialize the loaded data, except for the case the data is not
    // saved yet.
    if (!empty($this->id)) {
      unset($vars['data']);
    }
    return $vars;
  }

  public function __wakeup() {
    if ($this->id !== FALSE) {
      // Make sure data is set, so the data will be loaded when needed.
      $this->data = FALSE;
    }
  }

  /**
   * Extract the identifier of the given data object.
   *
   * @return
   *   The extracted identifier.
   */
  abstract protected function extractIdentifier($data);

  /**
   * Load a data object given an identifier.
   *
   * @return
   *   The loaded data object, or FALSE if loading failed.
   */
  abstract protected function load($id);
}

/**
 * Interface that allows custom wrapper classes to declare that they are savable.
 */
interface RulesDataWrapperSavableInterface {

  /**
   * Save the currently wrapped data.
   */
  public function save();
}