entity properties

From: fago <nuppla@zites.net>


---

 includes/common.inc              |   25 +++
 includes/properties.inc          |  284 ++++++++++++++++++++++++++++++++++++++
 includes/token.inc               |  124 +++++++----------
 modules/node/node.info           |    2 
 modules/node/node.properties.inc |  129 +++++++++++++++++
 modules/node/node.tokens.inc     |  205 ---------------------------
 modules/system/system.module     |   55 +++++++
 modules/system/system.test       |   70 +++++++++
 modules/user/user.info           |    2 
 modules/user/user.properties.inc |   69 +++++++++
 modules/user/user.tokens.inc     |  129 -----------------
 11 files changed, 683 insertions(+), 411 deletions(-)
 create mode 100644 includes/properties.inc
 create mode 100644 modules/node/node.properties.inc
 delete mode 100644 modules/node/node.tokens.inc
 create mode 100644 modules/user/user.properties.inc
 delete mode 100644 modules/user/user.tokens.inc


diff --git includes/common.inc includes/common.inc
index 3bbd9f3..ec327f5 100644
--- includes/common.inc
+++ includes/common.inc
@@ -5130,3 +5130,28 @@ function entity_get_controller($entity_type) {
   }
   return $controllers[$entity_type];
 }
+
+/**
+ * Returns a DrupalPropertyEntityWrapper or DrupalPropertyFormatWrapper
+ * dependent whether the passed data is an entity.
+ *
+ * @param $type
+ *   The type of the passed data.
+ * @param $data
+ *   The data to wrap.
+ * @param $options
+ *   (optional) A keyed array of options. The supported options vary by class.
+ * @param $entity
+ *   (optional) If a non-entity property is given, the entity to which this
+ *   property belongs.
+ * @param $entity_type
+ *   (optional) If a non-entity property is given, the type of the entity to
+ *   which this property belongs.
+ * @return
+ *   An instance of DrupalPropertyWrapperInterface.
+ */
+function drupal_get_property_wrapper($type, $data, array $options = array(), $entity = NULL, $entity_type = NULL) {
+  $entity_info = entity_get_info();
+  $class = isset($entity_info[$type]) ? 'DrupalPropertyEntityWrapper' : 'DrupalPropertyFormatWrapper';
+  return new $class($type, $data, $options, $entity, $entity_type);
+}
diff --git includes/properties.inc includes/properties.inc
new file mode 100644
index 0000000..16f5823
--- /dev/null
+++ includes/properties.inc
@@ -0,0 +1,284 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Provides the drupal entity wrapper class.
+ */
+
+/**
+ * A common interface for all property wrappers. Each wrapper can be used
+ * to derive some values depending on the implementation. If possible another
+ * instance of DrupalPropertyWrapperInterface should be returned to allow
+ * chained usage like
+ * @code
+ *   echo $wrapper->node->author->name;
+ * @endcode
+ */
+interface DrupalPropertyWrapperInterface extends IteratorAggregate {
+  
+  /**
+   * Constructor.
+   *
+   * @see drupal_get_property_wrapper().
+   */
+  public function __construct($type, $data, array $options = array());
+  
+  /**
+   * Get the data wrapped by this object.
+   */
+  public function get();
+  
+  /**
+   * Magic method: Get a derived value.
+   */
+  public function __get($name);
+  
+  /**
+   * Magic method: Check whether a derived value of this name is supported.
+   */
+  public function __isset($name);
+
+}
+
+/**
+ * Provides a wrapper for entities which eases dealing with entity properties.
+ */
+class DrupalPropertyEntityWrapper implements DrupalPropertyWrapperInterface {
+  
+  protected $entityType;
+  protected $entity;
+  protected $info;
+  protected $options;
+  protected $cache = array();
+  
+  /**
+   * Construct a new DrupalPropertyEntityWrapper object.
+   *
+   * @param $entityType
+   *   The type of the passed entity.
+   * @param $entity
+   *   The entity with which properties we deal.
+   * @param $options
+   *   (optional) A keyed array of options. Supported are:
+   *   - language: A language object to be used when getting locale-sensitive
+   *     properties.
+   *   - sanitize: A boolean flag indicating that textual properties should be
+   *     sanitized for display to a web browser. Defaults to FALSE.
+   */
+  public function __construct($entityType, $entity, array $options = array()) {
+    $this->entityType = $entityType;
+    $this->entity = $entity;
+    $this->info = entity_get_info($entityType) + array('properties' => array());
+    $this->options = $options + array('sanitize' => FALSE, 'language' => NULL);
+  }
+  
+  /**
+   * Gets the info about the given property.
+   *
+   * @param $name
+   *   The name of the property.
+   * @throws DrupalPropertyWrapperException
+   *   If there is no such property.
+   * @return
+   *   An array of info about the property.
+   */
+  public function getPropertyInfo($name) {
+    if (!isset($this->info['properties'][$name])) {
+      throw new DrupalPropertyWrapperException('Unknown entity property '. check_plain($name). '.');
+    }
+    $info = $this->info['properties'][$name] + array('type' => 'text');
+    $defaults = ($info['type'] == 'text' && empty($info['getter callback'])) ? array('sanitize' => 'check_plain') : array();
+    return $info + $defaults;
+  }
+
+  /**
+   * Magic method: Get a property.
+   *
+   * @return
+   *   An instance of DrupalPropertyWrapperInterface.
+   */
+  public function __get($name) {
+    // Look it up in the cache if possible.
+    if (!array_key_exists($name, $this->cache)) {
+      $info = $this->getPropertyInfo($name);
+      $this->cache[$name] = NULL;
+
+      if (!empty($info['getter callback']) && drupal_function_exists($info['getter callback'])) {
+        $this->cache[$name] = $info['getter callback']($this->entity, $this->options, $name, $this->entityType);
+      }
+      elseif (is_array($this->entity) && isset($this->entity[$name])) {
+        $this->cache[$name] = $this->entity[$name];
+      }
+      elseif (is_object($this->entity) && isset($this->entity->$name)) {
+        $this->cache[$name] = $this->entity->$name;
+      }
+      // Return another wrapper to support chained usage.
+      if (isset($this->cache[$name]) && !empty($this->options['sanitize']) && !empty($info['sanitize']) && function_exists($info['sanitize'])) {
+        $this->cache[$name] = $info['sanitize']($this->cache[$name]);
+      }
+      $this->cache[$name] = drupal_get_property_wrapper($info['type'], $this->cache[$name], $this->options, $this->entity, $this->entityType);
+    }
+    return $this->cache[$name];
+  }
+  
+  /**
+   * Magic method: Set a property.
+   */
+  public function __set($name, $value) {
+    $info = $this->getPropertyInfo($name);
+    if (!empty($info['setter callback']) && drupal_function_exists($info['setter callback'])) {
+      unset($this->cache[$name]);
+      return $info['setter callback']($this->entity, $name, $value, $this->entityType);
+    }
+    throw new DrupalPropertyWrapperException('Entity property '. check_plain($name). " doesn't support writing.");
+  }
+  
+  /**
+   * Magic method: isset() can be used to check if a property is known.
+   */
+  public function __isset($name) {
+    return isset($this->info['properties'][$name]);
+  }
+  
+  /**
+   * Get the entity wrapped by this object.
+   */
+  public function get() {
+    return $this->entity;
+  }
+  
+  public function getIterator() {
+    return new ArrayIterator(array_keys($this->info['properties']));
+  }
+}
+
+
+/**
+ * Class that eases applying token formats for returned properties.
+ */
+class DrupalPropertyFormatWrapper implements DrupalPropertyWrapperInterface {
+  
+  protected $type;
+  protected $data;
+  protected $info;
+  protected $options;
+  protected $entityType;
+  protected $entity;
+  
+  /**
+   * Construct a new DrupalPropertyFormatWrapper object.
+   *
+   * @param $type
+   *   The type of the passed data.
+   * @param $data
+   *   The data to format.
+   * @param $options
+   *   (optional) A keyed array of options. Supported are:
+   *   - language: A language object to be used when generating
+   *     locale-sensitive formats.
+   *   - formats: An array of further formats for this property
+   *     as defined in hook_entity_info().
+   * @param $entity
+   *   (optional) The entity to which this property belongs.
+   * @param $entityType
+   *   (optional) The type of the entity to which this property belongs.
+   */
+  public function __construct($type, $data, array $options = array(), $entity = NULL, $entityType = NULL) {
+    $this->type = $type;
+    $this->data = $data;
+    $this->options = $options + array('language' => NULL, 'formats' => array());
+    $this->entityType = $entityType;
+    $this->entity = $entity;
+  }
+  
+  /**
+   * We use this to init $this->info onyl when needed.
+   */
+  protected function initInfo() {
+    if (!isset($this->info)) {
+      $this->info = token_get_format_info($this->type) + array('formats' => array());
+      $this->info['formats'] = $this->options['formats'] + $this->info['formats'];
+    }
+  }
+  
+  
+  /**
+   * Gets the info about the given format.
+   *
+   * @param $name
+   *   The name of the format.
+   * @throws DrupalPropertyWrapperException
+   *   If there is no such format.
+   * @return
+   *   An array of info about the format.
+   */
+  public function getFormatInfo($name) {
+    $this->initInfo();
+    if (!isset($this->info['formats'][$name])) {
+      throw new DrupalPropertyWrapperException('Unknown format '. check_plain($name). '.');
+    }
+    return $this->info['formats'][$name];
+  }
+  
+  /**
+   * Magic method: Format the data.
+   */
+  public function __get($name) {
+    $info = $this->getFormatInfo($name);
+    if (isset($this->data) && !empty($info['callback']) && drupal_function_exists($info['callback'])) {
+      return $info['callback']($this->data, $this->options, $name, $this->entity, $this->entityType);
+    }
+  }
+
+  /**
+   * Magic method: isset() can be used to check if a format is known.
+   */
+  public function __isset($name) {
+    $this->initInfo();
+    return isset($this->info['formats'][$name]);
+  }
+  
+  /**
+   * Get the data wrapped by this object.
+   */
+  public function get() {
+    return $this->data;
+  }
+  
+  public function getIterator() {
+    $this->initInfo();
+    return new ArrayIterator(array_keys($this->info['formats']));
+  }
+  
+  /**
+   * For converting to a string use the default format, if any.
+   */
+  public function __toString() {
+    $this->initInfo();
+    if (isset($this->info['default format'])) {
+      return $this->{$this->info['default format']};
+    }
+    return (string)$this->get();
+  }
+}
+
+/**
+ * Provide a separate Exception so it can be caught separately.
+ */
+class DrupalPropertyWrapperException extends Exception {
+  
+}
+
+/**
+ * Sets the property to the given value. May be used as 'setter callback'.
+ */
+function drupal_property_verbatim_set(&$entity, $name, $value) {
+  if (is_array($entity)) {
+    $entity[$name] = $value;
+  }
+  elseif (is_object($entity)) {
+    $entity->$name = $value;
+  }
+}
+
diff --git includes/token.inc includes/token.inc
index c37ca34..998021a 100644
--- includes/token.inc
+++ includes/token.inc
@@ -71,14 +71,15 @@
  *     display to a web browser. Defaults to TRUE. Developers who set this option
  *     to FALSE assume responsibility for running filter_xss(), check_plain() or
  *     other appropriate scrubbing functions before displaying data to users.
+ * @param $types
+ *   (optional) An array mapping the keys of $data to data types. If there is
+ *   no mapping for a key, the data's key is used as type by default.
  * @return
  *   Text with tokens replaced.
  */
-function token_replace($text, array $data = array(), array $options = array()) {
-  $replacements = array();
-  foreach (token_scan($text) as $type => $tokens) {
-    $replacements += token_generate($type, $tokens, $data, $options);
-  }
+function token_replace($text, array $data = array(), array $options = array(), array $types = array()) {
+  $token_list = token_scan($text);
+  $replacements = token_generate($token_list, $data, $options, $types);
 
   // Optionally alter the list of replacement values.
   if (!empty($options['callback']) && drupal_function_exists($options['callback'])) {
@@ -122,11 +123,8 @@ function token_scan($text) {
 /**
  * Generate replacement values for a list of tokens.
  *
- * @param $type
- *   The type of token being replaced. 'node', 'user', and 'date' are common.
- * @param $tokens
- *   An array of tokens to be replaced, keyed by the literal text of the token
- *   as it appeared in the source text.
+ * @param $raw_tokens
+ *   A keyed array of tokens, and their original raw form in the source text.
  * @param $data
  *   (optional) An array of keyed objects. For simple replacement scenarios
  *   'node', 'user', and others are common keys, with an accompanying node or
@@ -145,94 +143,72 @@ function token_scan($text) {
  *     display to a web browser. Developers who set this option to FALSE assume
  *     responsibility for running filter_xss(), check_plain() or other
  *     appropriate scrubbing functions before displaying data to users.
+ * @param $types
+ *   (optional) An array mapping the keys of $data to data types. If there is
+ *   no mapping for a key, the data's key is used as type by default.
  * @return
  *   An associative array of replacement values, keyed by the original 'raw'
  *   tokens that were found in the source text. For example:
  *   $results['[node:title]'] = 'My new node';
  */
-function token_generate($type, array $tokens, array $data = array(), array $options = array()) {
+function token_generate(array $raw_tokens, array $data = array(), array $options = array(), array $types = array()) {
   $results = array();
   $options += array('sanitize' => TRUE);
+  // Add in the current date and the global user by default.
+  $data += array('date' => REQUEST_TIME, 'currentUser' => $GLOBALS['user']);
+  $types += array('currentUser' => 'user');
 
-  foreach (module_implements('tokens') as $module) {
-    $function = $module . '_tokens';
-    if (drupal_function_exists($function)) {
-      $result = $function($type, $tokens, $data, $options);
-      foreach ($result as $original => $replacement) {
-        $results[$original] = $replacement;
+  foreach ($raw_tokens as $key => $tokens) {
+    $type = isset($types[$key]) ? $types[$key] : $key;
+    if (isset($data[$key]) && ($wrapper = drupal_get_property_wrapper($type, $data[$key], $options))) {
+      foreach ($tokens as $token => $original) {
+        try {
+          $results[$original] = _token_get_replacement($wrapper, $token);
+        }
+        catch (DrupalPropertyWrapperException $e) {
+          // A token has not been found, so ignore it.
+        }
       }
     }
   }
-
   return $results;
 }
 
 /**
- * Given a list of tokens, return those that begin with a specific prefix.
- *
- * Used to extract a group of 'chained' tokens (such as [node:author:name]) from
- * the full list of tokens found in text. For example:
- * @code
- *   $data = array(
- *     'author:name' => '[node:author:name]',
- *     'title'       => '[node:title]',
- *     'created'     => '[node:author:name]',
- *   );
- *   $results = token_find_with_prefix($data, 'author');
- *   $results == array('name' => '[node:author:name]');
- * @endcode
- *
- * @param $tokens
- *   A keyed array of tokens, and their original raw form in the source text.
- * @param $prefix
- *   A textual string to be matched at the beginning of the token.
- * @param $delimiter
- *   An optional string containing the character that separates the prefix from
- *   the rest of the token. Defaults to ':'.
- * @return
- *   An associative array of discovered tokens, with the prefix and delimiter
- *   stripped from the key.
+ * Applies chained tokens by getting properties of the given wrapper.
  */
-function token_find_with_prefix(array $tokens, $prefix, $delimiter = ':') {
-  $results = array();
-  foreach ($tokens as $token => $raw) {
-    $parts = split($delimiter, $token, 2);
-    if (count($parts) == 2 && $parts[0] == $prefix) {
-      $results[$parts[1]] = $raw;
-    }
+function _token_get_replacement(DrupalPropertyWrapperInterface $wrapper, $token) {
+  foreach (explode(':', $token) as $i => $name) {
+    $wrapper = $wrapper->$name;
   }
-  return $results;
+  // If no format was given apply the default by converting it to string.
+  return (string)$wrapper;
 }
 
 /**
- * Returns metadata describing supported tokens.
+ * Returns metadata describing token formats.
  *
- * The metadata array contains token type, name, and description data as well as
- * an optional pointer indicating that the token chains to another set of tokens.
- * For example:
- * @code
- *   $data['types']['node'] = array(
- *     'name' => t('Nodes'),
- *     'description' => t('Tokens related to node objects.'),
- *   );
- *   $data['tokens']['node']['title'] = array(
- *     'name' => t('Title'),
- *     'description' => t('The title of the current node.'),
- *   );
- *   $data['tokens']['node']['author'] = array(
- *     'name' => t('Author'),
- *     'description' => t('The author of the current node.'),
- *     'type' => 'user',
- *   );
- * @endcode
- * @return
- *   An associative array of token information, grouped by token type.
+ * @param $type
+ *   The type, e.g. date, for which the info shall be returned, or NULL
+ *   to return an array with info about all types.
+ *
+ * @see hook_token_format_info()
+ * @see hook_token_format_info_alter()
  */
-function token_info() {
+function token_get_format_info($type = NULL) {
   $data = &drupal_static(__FUNCTION__);
   if (!isset($data)) {
-    $data = module_invoke_all('token_info');
-    drupal_alter('token_info', $data);
+    if ($cache = cache_get('token_format_info')) {
+      $data = $cache->data;
+    }
+    else {
+      $data = module_invoke_all('token_format_info');
+      drupal_alter('token_format_info', $data);
+      cache_set('token_format_info', $data);
+    }
+  }
+  if (!empty($type)) {
+    return isset($data[$type]) ? $data[$type] : array();
   }
   return $data;
 }
diff --git modules/node/node.info modules/node/node.info
index 6a690d2..abfec87 100644
--- modules/node/node.info
+++ modules/node/node.info
@@ -10,5 +10,5 @@ files[] = node.admin.inc
 files[] = node.pages.inc
 files[] = node.install
 files[] = node.test
-files[] = node.tokens.inc
+files[] = node.properties.inc
 required = TRUE
diff --git modules/node/node.properties.inc modules/node/node.properties.inc
new file mode 100644
index 0000000..463c918
--- /dev/null
+++ modules/node/node.properties.inc
@@ -0,0 +1,129 @@
+<?php
+// $Id: node.tokens.inc,v 1.16 2009/07/12 16:27:54 eaton Exp $
+
+/**
+ * @file
+ * Provides properties for node-related data.
+ */
+
+/**
+ * Implement hook_entity_info_alter().
+ */
+function node_entity_info_alter(&$entity_info) {
+  $node = &$entity_info['node']['properties'];
+  
+  $node['nid'] = array(
+    'name' => t("Node ID"),
+    'type' => 'integer',
+    'description' => t("The unique ID of the node."),
+  );
+  $node['vid'] = array(
+    'name' => t("Revision ID"),
+    'type' => 'integer',
+    'description' => t("The unique ID of the node's latest revision."),
+  );
+  $node['tnid'] = array(
+    'name' => t("Translation set ID"),
+    'type' => 'integer',
+    'description' => t("The unique ID of the original-language version of this node, if one exists."),
+  );
+  $node['uid'] = array(
+    'name' => t("User ID"),
+    'type' => 'integer',
+    'description' => t("The unique ID of the author of the node."),
+    'setter callback' => 'drupal_property_verbatim_set',
+  );
+  $node['type'] = array(
+    'name' => t("Content type"),
+    'description' => t("The type of the node."),
+  );
+  $node['typeName'] = array(
+    'name' => t("Content type name"),
+    'description' => t("The human-readable name of the node type."),
+    'getter callback' => 'node_get_properties',
+  );
+  $node['title'] = array(
+    'name' => t("Title"),
+    'description' => t("The title of the node."),
+    'setter callback' => 'drupal_property_verbatim_set',
+  );
+  $node['body'] = array(
+    'name' => t("Body"),
+    'description' => t("The main body text of the node."),
+    'getter callback' => 'field_property_get',
+  );
+  $node['summary'] = array(
+    'name' => t("Summary"),
+    'description' => t("The summary of the node's main body text."),
+    'getter callback' => 'field_property_get',
+  );
+  $node['language'] = array(
+    'name' => t("Language"),
+    'description' => t("The language the node is written in."),
+    'setter callback' => 'drupal_property_verbatim_set',
+  );
+  $node['url'] = array(
+    'name' => t("URL"),
+    'description' => t("The URL of the node."),
+    'getter callback' => 'node_get_properties',
+  );
+  $node['editUrl'] = array(
+    'name' => t("Edit URL"),
+    'description' => t("The URL of the node's edit page."),
+    'getter callback' => 'node_get_properties',
+  );
+  $node['created'] = array(
+    'name' => t("Date created"),
+    'type' => 'date',
+    'description' => t("The date the node was posted."),
+    'setter callback' => 'drupal_property_verbatim_set',
+  );
+  $node['changed'] = array(
+    'name' => t("Date changed"),
+    'type' => 'date',
+    'description' => t("The date the node was most recently updated."),
+  );
+  $node['authorName'] = array(
+    'name' => t("Author name"),
+    'description' => t("The node author's name."),
+    'getter callback' => 'node_get_properties',
+  );
+  $node['author'] = array(
+    'name' => t("Author"),
+    'type' => 'user',
+    'description' => t("The author of the node."),
+    'getter callback' => 'node_get_properties',
+  );
+}
+
+function field_property_get($object, array $options, $name, $obj_type) {
+  return $options['sanitize'] ? $object->$name[0]['safe'] : $object->$name[0]['value'];
+}
+
+/**
+ * Callback for getting node properties.
+ * @see node_entity_info_alter().
+ */
+function node_get_properties($node, array $options, $name, $entity_type) {
+
+  switch ($name) {
+    case 'typeName':
+      $type_name = node_type_get_name($node->type);
+      return $options['sanitize'] ? check_plain($type_name) : $type_name;
+
+    case 'url':
+      return url('node/' . $node->nid, $options + array('absolute' => TRUE));
+
+    case 'editUrl':
+      return url('node/' . $node->nid . '/edit', $options + array('absolute' => TRUE));
+
+    case 'authorName':
+      $name = ($node->uid == 0) ? variable_get('anonymous', t('Anonymous')) : $node->name;
+      return $options['sanitize'] ? filter_xss($name) : $name;
+      
+    case 'author':
+      return user_load($node->uid);
+  }
+}
+
+
diff --git modules/node/node.tokens.inc modules/node/node.tokens.inc
deleted file mode 100644
index d6b0ea6..0000000
--- modules/node/node.tokens.inc
+++ /dev/null
@@ -1,205 +0,0 @@
-<?php
-// $Id: node.tokens.inc,v 1.1 2009-08-19 20:19:36 dries Exp $
-
-/**
- * @file
- * Builds placeholder replacement tokens for node-related data.
- */
-
-
-
-/**
- * Implement hook_token_info().
- */
-function node_token_info() {
-  $type = array(
-    'name' => t('Nodes'),
-    'description' => t('Tokens related to individual nodes.'),
-    'needs-data' => 'node',
-  );
-
-  // Core tokens for nodes.
-  $node['nid'] = array(
-    'name' => t("Node ID"),
-    'description' => t("The unique ID of the node."),
-  );
-  $node['vid'] = array(
-    'name' => t("Revision ID"),
-    'description' => t("The unique ID of the node's latest revision."),
-  );
-  $node['tnid'] = array(
-    'name' => t("Translation set ID"),
-    'description' => t("The unique ID of the original-language version of this node, if one exists."),
-  );
-  $node['uid'] = array(
-    'name' => t("User ID"),
-    'description' => t("The unique ID of the user who posted the node."),
-  );
-  $node['type'] = array(
-    'name' => t("Content type"),
-    'description' => t("The type of the node."),
-  );
-  $node['type-name'] = array(
-    'name' => t("Content type name"),
-    'description' => t("The human-readable name of the node type."),
-  );
-  $node['title'] = array(
-    'name' => t("Title"),
-    'description' => t("The title of the node."),
-  );
-  $node['body'] = array(
-    'name' => t("Body"),
-    'description' => t("The main body text of the node."),
-  );
-  $node['summary'] = array(
-    'name' => t("Summary"),
-    'description' => t("The summary of the node's main body text."),
-  );
-  $node['language'] = array(
-    'name' => t("Language"),
-    'description' => t("The language the node is written in."),
-  );
-  $node['url'] = array(
-    'name' => t("URL"),
-    'description' => t("The URL of the node."),
-  );
-  $node['edit-url'] = array(
-    'name' => t("Edit URL"),
-    'description' => t("The URL of the node's edit page."),
-  );
-
-  // Chained tokens for nodes.
-  $node['created'] = array(
-    'name' => t("Date created"),
-    'description' => t("The date the node was posted."),
-    'type' => 'date',
-  );
-  $node['changed'] = array(
-    'name' => t("Date changed"),
-    'description' => t("The date the node was most recently updated."),
-    'type' => 'date',
-  );
-  $node['author'] = array(
-    'name' => t("Author"),
-    'description' => t("The author of the node."),
-    'type' => 'user',
-  );
-
-  return array(
-    'types' => array('node' => $type),
-    'tokens' => array('node' => $node),
-  );
-}
-
-/**
- * Implement hook_tokens().
- */
-function node_tokens($type, $tokens, array $data = array(), array $options = array()) {
-  $url_options = array('absolute' => TRUE);
-  if (isset($options['language'])) {
-    $url_options['language'] = $language;
-    $language_code = $language->language;
-  }
-  else {
-    $language_code = NULL;
-  }
-  $sanitize = !empty($options['sanitize']);
-
-  $replacements = array();
-
-  if ($type == 'node' && !empty($data['node'])) {
-    $node = $data['node'];
-
-    foreach ($tokens as $name => $original) {
-      switch ($name) {
-        // Simple key values on the node.
-        case 'nid':
-          $replacements[$original] = $node->nid;
-          break;
-
-        case 'vid':
-          $replacements[$original] = $node->vid;
-          break;
-
-        case 'tnid':
-          $replacements[$original] = $node->tnid;
-          break;
-
-        case 'uid':
-          $replacements[$original] = $node->uid;
-          break;
-
-        case 'name':
-          $replacements[$original] = $sanitize ? check_plain($node->name) : $node->name;
-          break;
-
-        case 'title':
-          $replacements[$original] = $sanitize ? check_plain($node->title) : $node->title;
-          break;
-
-        case 'body':
-          if (!empty($node->body)) {
-            $replacements[$original] = $sanitize ? $node->body[0]['safe'] : $node->body[0]['value'];
-          }
-          break;
-
-        case 'summary':
-          if (!empty($node->body)) {
-            $replacements[$original] = $sanitize ? $node->body[0]['safe_summary'] : $node->body[0]['summary'];
-          }
-          break;
-
-        case 'type':
-          $replacements[$original] = $sanitize ? check_plain($node->type) : $node->type;
-          break;
-
-        case 'type-name':
-          $type_name = node_get_types('name', $node->type);
-          $replacements[$original] = $sanitize ? check_plain($type_name) : $type_name;
-          break;
-
-        case 'language':
-          $replacements[$original] = $sanitize ? check_plain($node->language) : $node->language;
-          break;
-
-        case 'url':
-          $replacements[$original] = url('node/' . $node->nid, array('absolute' => TRUE));
-          break;
-
-        case 'edit-url':
-          $replacements[$original] = url('node/' . $node->nid . '/edit', array('absolute' => TRUE));
-          break;
-
-        // Default values for the chained tokens handled below.
-        case 'author':
-          $name = ($node->uid == 0) ? variable_get('anonymous', t('Anonymous')) : $node->name;
-          $replacements[$original] = $sanitize ? filter_xss($name) : $name;
-          break;
-
-        case 'created':
-          $replacements[$original] = format_date($node->created, 'medium', '', NULL, $language_code);
-          break;
-
-        case 'changed':
-          $replacements[$original] = format_date($node->changed, 'medium', '', NULL, $language_code);
-          break;
-      }
-    dsm('node');
-    }
-
-    if ($author_tokens = token_find_with_prefix($tokens, 'author')) {
-      $author = user_load($node->uid);
-      $replacements += token_generate('user', $author_tokens, array('user' => $author), $options);
-    }
-
-    if ($created_tokens = token_find_with_prefix($tokens, 'created')) {
-      $replacements += token_generate('date', $created_tokens, array('date' => $node->created), $options);
-    }
-
-    if ($changed_tokens = token_find_with_prefix($tokens, 'changed')) {
-      $replacements += token_generate('date', $changed_tokens, array('date' => $node->changed), $options);
-    }
-  }
-
-  return $replacements;
-}
diff --git modules/system/system.module modules/system/system.module
index 559db49..18a063c 100644
--- modules/system/system.module
+++ modules/system/system.module
@@ -2924,6 +2924,61 @@ function system_image_toolkits() {
 }
 
 /**
+ * Implementation of hook_token_format_info().
+ */
+function system_token_format_info() {
+  // Date related formats.
+  $date['small'] = array(
+    'name' => t("Small format"),
+    'description' => t("A date in 'small' format. (%date)", array('%date' => format_date(REQUEST_TIME, 'small'))),
+    'callback' => 'system_format_date',
+  );
+  $date['medium'] = array(
+    'name' => t("Medium format"),
+    'description' => t("A date in 'medium' format. (%date)", array('%date' => format_date(REQUEST_TIME, 'medium'))),
+    'callback' => 'system_format_date',
+  );
+  $date['large'] = array(
+    'name' => t("Large format"),
+    'description' => t("A date in 'large' format. (%date)", array('%date' => format_date(REQUEST_TIME, 'large'))),
+    'callback' => 'system_format_date',
+  );
+  $date['since'] = array(
+    'name' => t("Time-since"),
+    'description' => t("A data in 'time-since' format. (%date)", array('%date' => format_interval(REQUEST_TIME - 360, 2))),
+    'callback' => 'system_format_date',
+  );
+  $date['raw'] = array(
+    'name' => t("Raw timestamp"),
+    'description' => t("A date in UNIX timestamp format (%date)", array('%date' => REQUEST_TIME)),
+    'callback' => 'system_format_date',
+  );
+  return array('date' => array('default format' => 'medium', 'formats' => $date));
+}
+
+/**
+ * Callback for formating date tokens.
+ * @see system_token_format_info().
+ */
+function system_format_date($date, array $options, $name) {
+  $langcode = isset($options['language']) ? $options['language']->language : NULL;
+
+  switch ($name) {
+    case 'raw':
+      return filter_xss($date);
+
+    case 'small':
+    case 'medium':
+    case 'large':
+      return format_date($date, $name, '', NULL, $langcode);
+
+    case 'since':
+      return format_interval((REQUEST_TIME - $date), 2, $langcode);
+  }
+}
+
+
+/**
  * Attempts to get a file using drupal_http_request and to store it locally.
  *
  * @param $url
diff --git modules/system/system.test modules/system/system.test
index 51cc2db..3df0c29 100644
--- modules/system/system.test
+++ modules/system/system.test
@@ -967,6 +967,72 @@ class SystemThemeFunctionalTest extends DrupalWebTestCase {
   }
 }
 
+/**
+ * Test entity propert wrapper.
+ */
+class DrupalPropertyWrapperTestCase extends DrupalWebTestCase {
+  public static function getInfo() {
+    return array(
+      'name' => 'Property wrappers',
+      'description' => 'Test using the drupal property wrappers.',
+      'group' => 'System',
+    );
+  }
+
+  /**
+   * Creates a user and a node, then tests getting the properties.
+   */
+  function testEntityPropertyWrapper() {
+    $account = $this->drupalCreateUser();
+    $node = $this->drupalCreateNode(array('uid' => $account->uid, 'title' => '<b>Is it bold?<b>'));
+    // For testing sanitizing give the user a malicious user name
+    $account = user_save($account, array('name' => '<b>BadName</b>'));
+
+    // Fetch the object so every properties are set as usual.
+    $node = node_load($node->nid);
+    
+    // First test without sanitizing.
+    $wrapper = drupal_get_property_wrapper('node', $node);
+    
+    $this->assertEqual($node->title, $wrapper->title, 'Getting property.');
+    $this->assertEqual($node->name, $wrapper->authorName, 'Getting property with getter callback.');
+    
+    // Test sanitized output.
+    $wrapper = drupal_get_property_wrapper('node', $node, array('sanitize' => TRUE));
+    
+    $this->assertEqual(check_plain($node->title), $wrapper->title, 'Getting sanitized property.');
+    $this->assertEqual(filter_xss($node->name), $wrapper->authorName, 'Getting sanitized property with getter callback.');
+    
+    // Test getting an not existing property
+    try {
+      echo $wrapper->dummy;
+      $this->fail('Getting an not existing property.');
+    }
+    catch (DrupalPropertyWrapperException $e) {
+      $this->pass('Getting an not existing property.');
+    }
+    
+    // Test setting.
+    $wrapper->title = 'test';
+    $this->assertEqual('test', $wrapper->title, 'Setting a property.');
+    try {
+      $wrapper->type = 'dummy';
+      $this->fail('Setting an unsupported property.');
+    }
+    catch (DrupalPropertyWrapperException $e) {
+      $this->pass('Setting an unsupported property.');
+    }
+    
+    // Test chaining
+    $this->assertEqual(check_plain($account->mail), $wrapper->author->mail, 'Testing chained usage.');
+    $this->assertEqual(filter_xss($account->name), $wrapper->author->name, 'Testing chained usage with callback and sanitizing.');
+    
+    // Test iterator
+    $type_info = entity_get_info('node');
+    $this->assertEqual(iterator_to_array($wrapper->getIterator()), array_keys($type_info['properties']), 'Iterator is working.');
+
+  }
+}
 
 /**
  * Test the basic queue functionality.
@@ -1084,7 +1150,8 @@ class TokenReplaceTestCase extends DrupalWebTestCase {
     $source  = '[node:title]';         // Title of the node we passed in
     $source .= '[node:author:name]';   // Node author's name
     $source .= '[node:created:since]'; // Time since the node was created
-    $source .= '[current-user:name]';  // Current user's name
+    $source .= '[node:changed]';       // Last update time using the default format.
+    $source .= '[currentUser:name]';   // Current user's name
     $source .= '[user:name]';          // No user passed in, should be untouched
     $source .= '[date:small]';         // Small date format of REQUEST_TIME
     $source .= '[bogus:token]';        // Nonexistent token, should be untouched
@@ -1092,6 +1159,7 @@ class TokenReplaceTestCase extends DrupalWebTestCase {
     $target  = check_plain($node->title);
     $target .= check_plain($account->name);
     $target .= format_interval(REQUEST_TIME - $node->created, 2);
+    $target .= format_date($node->changed, 'medium');
     $target .= check_plain($user->name);
     $target .= '[user:name]';
     $target .= format_date(REQUEST_TIME, 'small');
diff --git modules/user/user.info modules/user/user.info
index 54e288e..f9702a4 100644
--- modules/user/user.info
+++ modules/user/user.info
@@ -9,5 +9,5 @@ files[] = user.admin.inc
 files[] = user.pages.inc
 files[] = user.install
 files[] = user.test
-files[] = user.tokens.inc
+files[] = user.properties.inc
 required = TRUE
diff --git modules/user/user.properties.inc modules/user/user.properties.inc
new file mode 100644
index 0000000..0a9bac2
--- /dev/null
+++ modules/user/user.properties.inc
@@ -0,0 +1,69 @@
+<?php
+// $Id: user.tokens.inc,v 1.16 2009/07/12 16:27:54 eaton Exp $
+
+/**
+ * @file
+ * Provides properties for node-related data.
+ */
+
+/**
+ * Implement hook_entity_info_alter().
+ */
+function user_entity_info_alter(&$entity_info) {
+  $user = &$entity_info['user']['properties'];
+
+  $user['uid'] = array(
+    'name' => t('User ID'),
+    'type' => 'integer',
+    'description' => t("The unique ID of the user account."),
+  );
+  $user['name'] = array(
+    'name' => t("Name"),
+    'description' => t("The login name of the user account."),
+    'getter callback' => 'user_get_properties',
+    'setter callback' => 'drupal_property_verbatim_set',
+  );
+  $user['mail'] = array(
+    'name' => t("Email"),
+    'description' => t("The email address of the user account."),
+    'setter callback' => 'drupal_property_verbatim_set',
+  );
+  $user['url'] = array(
+    'name' => t("URL"),
+    'description' => t("The URL of the account profile page."),
+    'getter callback' => 'user_get_properties',
+  );
+  $user['editUrl'] = array(
+    'name' => t("Edit URL"),
+    'description' => t("The url of the account edit page."),
+    'getter callback' => 'user_get_properties',
+  );
+  $user['login'] = array(
+    'name' => t("Last login"),
+    'description' => t("The date the user last logged in to the site."),
+    'type' => 'date',
+  );
+  $user['created'] = array(
+    'name' => t("Created"),
+    'description' => t("The date the user account was created."),
+    'type' => 'date',
+  );
+}
+
+/**
+ * Callback for getting user properties.
+ */
+function user_get_properties($account, array $options, $name, $entity_type) {
+
+  switch ($name) {
+    case 'name':
+      $name = ($account->uid == 0) ? variable_get('anonymous', t('Anonymous')) : $account->name;
+      return $options['sanitize'] ? filter_xss($name) : $name;
+
+    case 'url':
+      return url("user/$account->uid", $options + array('absolute' => TRUE));
+
+    case 'editUrl':
+      return url("user/$account->uid/edit", $options + array('absolute' => TRUE));
+  }
+}
diff --git modules/user/user.tokens.inc modules/user/user.tokens.inc
deleted file mode 100644
index e55a442..0000000
--- modules/user/user.tokens.inc
+++ /dev/null
@@ -1,129 +0,0 @@
-<?php
-// $Id: user.tokens.inc,v 1.1 2009-08-19 20:19:37 dries Exp $
-
-/**
- * @file
- * Builds placeholder replacement tokens for user-related data.
- */
-
-/**
- * Implement hook_token_info().
- */
-function user_token_info() {
-  $types['user'] = array(
-    'name' => t('Users'),
-    'description' => t('Tokens related to individual user accounts.'),
-    'needs-data' => 'user',
-  );
-  $types['current-user'] = array(
-    'name' => t('Current user'),
-    'description' => t('Tokens related to the currently logged in user.'),
-    'type' => 'user',
-  );
-
-  $user['uid'] = array(
-    'name' => t('User ID'),
-    'description' => t("The unique ID of the user account."),
-  );
-  $user['name'] = array(
-    'name' => t("Name"),
-    'description' => t("The login name of the user account."),
-  );
-  $user['mail'] = array(
-    'name' => t("Email"),
-    'description' => t("The email address of the user account."),
-  );
-  $user['url'] = array(
-    'name' => t("URL"),
-    'description' => t("The URL of the account profile page."),
-  );
-  $user['edit-url'] = array(
-    'name' => t("Edit URL"),
-    'description' => t("The url of the account edit page."),
-  );
-  $user['last-login'] = array(
-    'name' => t("Last login"),
-    'description' => t("The date the user last logged in to the site."),
-    'type' => 'date',
-  );
-  $user['created'] = array(
-    'name' => t("Created"),
-    'description' => t("The date the user account was created."),
-    'type' => 'date',
-  );
-
-  return array(
-    'types' => array('user' => $types),
-    'tokens' => array('user' => $user),
-  );
-}
-
-/**
- * Implement hook_tokens().
- */
-function user_tokens($type, $tokens, array $data = array(), array $options = array()) {
-  global $user;
-  $url_options = array('absolute' => TRUE);
-  if (isset($options['language'])) {
-    $url_options['language'] = $language;
-    $language_code = $language->language;
-  }
-  else {
-    $language_code = NULL;
-  }
-  $sanitize = !empty($options['sanitize']);
-
-  $replacements = array();
-
-  if ($type == 'user' && !empty($data['user'])) {
-    $account = $data['user'];
-    foreach ($tokens as $name => $original) {
-      switch ($name) {
-        // Basic user account information.
-        case 'uid':
-          $replacements[$original] = $account->uid;
-          break;
-
-        case 'name':
-          $name = ($account->uid == 0) ? variable_get('anonymous', t('Anonymous')) : $account->name;
-          $replacements[$original] = $sanitize ? filter_xss($name) : $name;
-          break;
-
-        case 'mail':
-          $replacements[$original] = $sanitize ? check_plain($account->mail) : $account->mail;
-          break;
-
-        case 'url':
-          $replacements[$original] = url("user/$account->uid", $url_options);
-          break;
-
-        case 'edit-url':
-          $replacements[$original] = url("user/$account->uid/edit", $url_options);
-          break;
-
-        // These tokens are default variations on the chained tokens handled below.
-        case 'last-login':
-          $replacements[$original] = format_date($account->login, 'medium', '', NULL, $language_code);
-          break;
-
-        case 'created':
-          $replacements[$original] = format_date($account->created, 'medium', '', NULL, $language_code);
-          break;
-      }
-    }
-
-    if ($login_tokens = token_find_with_prefix($tokens, 'last-login')) {
-      $replacements += token_generate('date', $login_tokens, array('date' => $account->login), $options);
-    }
-
-    if ($registered_tokens = token_find_with_prefix($tokens, 'created')) {
-      $replacements += token_generate('date', $registered_tokens, array('date' => $account->created), $options);
-    }
-  }
-  if ($type == 'current-user') {
-    global $user;
-    $replacements += token_generate('user', $tokens, array('user' => $user), $options);
-  }
-
-  return $replacements;
-}
