diff --git includes/common.inc includes/common.inc
index 2df4bba..a675215 100644
--- includes/common.inc
+++ includes/common.inc
@@ -5533,6 +5533,34 @@ function drupal_check_incompatibility($v, $current_version) {
 }
 
 /**
+ * Returns some metadata about basic data types.
+ *
+ * @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_datatype_info()
+ * @see hook_datatype_info_alter()
+ */
+function drupal_get_datatype_info($type = NULL) {
+  $data = &drupal_static(__FUNCTION__);
+  if (!isset($data)) {
+    if ($cache = cache_get('datatype_info')) {
+      $data = $cache->data;
+    }
+    else {
+      $data = module_invoke_all('datatype_info');
+      drupal_alter('datatype_info', $data);
+      cache_set('datatype_info', $data);
+    }
+  }
+  if (!empty($type)) {
+    return isset($data[$type]) ? $data[$type] : array();
+  }
+  return $data;
+}
+
+/**
  * Get the entity info array of an entity type.
  *
  * @see hook_entity_info()
@@ -5551,6 +5579,7 @@ function entity_get_info($entity_type = NULL) {
       $entity_info = $cache->data;
     }
     else {
+      module_load_all_includes('inc', 'entity');
       $entity_info = module_invoke_all('entity_info');
       // Merge in default values.
       foreach ($entity_info as $name => $data) {
@@ -5649,3 +5678,42 @@ function xmlrpc($url) {
   return call_user_func_array('_xmlrpc', $args);
 }
 
+/**
+ * Returns a property wrapper for the given data.
+ *
+ * Dependend on the data type either a DrupalEntityPropertyWrapper or a
+ * DrupalPropertyValueWrapper is returned.
+ *
+ * @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 $context
+ *   (optional) An array of contextual information which format callbacks can
+ *   make use of.
+ * @return
+ *   An instance of the DrupalPropertyWrapperInterface.
+ */
+function drupal_get_property_wrapper($type, $data, array $options = array(), $context = array()) {
+  if (($entity_info = entity_get_info()) && isset($entity_info[$type])) {
+    return new DrupalEntityPropertyWrapper($type, $data, $options, $context);
+  }
+  else {
+    return new DrupalPropertyValueWrapper($type, $data, $options, $context);
+  }
+}
+
+/**
+ * Extracts the contained type for a list type string like list<date>.
+ *
+ * @return
+ *   The contained type or FALSE, if the given type string is no list.
+ */
+function drupal_list_extract_type($type) {
+  if (strpos($type, 'list<') === 0 && $type[strlen($type)-1] == '>') {
+    return substr($type, 5, -1);
+  }
+  return FALSE;
+}
diff --git includes/entity.inc includes/entity.inc
index 66b1eb4..323e302 100644
--- includes/entity.inc
+++ includes/entity.inc
@@ -240,6 +240,10 @@ class DrupalDefaultEntityController implements DrupalEntityControllerInterface {
         field_attach_load($this->entityType, $queried_entities);
       }
     }
+    // Initialize the entity tags array.
+    foreach ($queried_entities as $entity) {
+      $entity->entityTags = array();
+    }
 
     // Call hook_TYPE_load(). The first argument for hook_TYPE_load() are
     // always the queried entities, followed by additional arguments set in
diff --git includes/module.inc includes/module.inc
index ac32df9..a6e2c62 100644
--- includes/module.inc
+++ includes/module.inc
@@ -175,11 +175,17 @@ function module_load_include($type, $module, $name = NULL) {
 /**
  * Load an include file for each of the modules that have been enabled in
  * the system table.
+ *
+ * @param $type
+ *   The include file's type (file extension).
+ * @param $name
+ *   Optionally, specify the base file name to include "$module.$name.$type".
+ *   If not set, "$module.$type" is used.
  */
 function module_load_all_includes($type, $name = NULL) {
-  $modules = module_list();
-  foreach ($modules as $module) {
-    module_load_include($type, $module, $name);
+  $name = isset($name) ? '.' . $name : '';
+  foreach (module_list() as $module) {
+    module_load_include($type, $module, $module . $name);
   }
 }
 
diff --git includes/properties.inc includes/properties.inc
new file mode 100644
index 0000000..eede6d6
--- /dev/null
+++ includes/properties.inc
@@ -0,0 +1,398 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Provides the drupal property wrapper classes.
+ */
+
+/**
+ * A common interface for all property wrappers. Each wrapper can be used
+ * to derive or set values depending on the concrete wrappers, so there are
+ * wrappers to get/set entity properties, to format the property values and to
+ * deal with multiple value properties. If possible, a wrapper always returns
+ * another wrapper to allow chained usage like:
+ * @code
+ *   echo $wrappedNode->author->login->since;
+ * @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.
+ *
+ * The wrapper eases applying getter and setter callbacks of entity properties
+ * specified in hook_entity_info() while respecting the specified options. Any
+ * possible bundles or entity tags of the wrapped entity are detected, so any
+ * associated properties can be used.
+ */
+class DrupalEntityPropertyWrapper implements DrupalPropertyWrapperInterface {
+
+  protected $entityType;
+  protected $entity;
+  protected $info;
+  protected $options;
+  protected $cache = array();
+
+  /**
+   * Construct a new DrupalEntityPropertyWrapper 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, passed through to any returned
+   *   wrappers. Used by this wrapper 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.
+   *   - absolute: Whether generated URLs should be absolute. Defaults to TRUE.
+   */
+  public function __construct($entityType, &$entity, array $options = array()) {
+    $this->entityType = $entityType;
+    $this->entity = $entity;
+    $this->info = entity_get_info($entityType) + array('properties' => array(), 'default tags' => array(), 'name property' => 'name');
+    $this->options = $options + array('sanitize' => FALSE, 'language' => NULL, 'absolute' => TRUE);
+  
+    // Add in properties from the bundle or tags.
+    if (!empty($this->info['fieldable']) && is_object($entity)) {
+      list($id, $vid, $bundle) = field_extract_ids($entityType, $entity);
+      $bundle_info = (array)$this->info['bundles'][$bundle] + array('properties' => array(), 'default tags' => array());
+      $this->info['properties'] += $bundle_info['properties'];
+      $this->info['default tags'] = array_merge($this->info['default tags'], $bundle_info['default tags']);
+    }
+    foreach ($this->getTags() as $tag) {
+      if (isset($this->info['tags'][$tag]['properties'])) {
+        $this->info['properties'] += $this->info['tags'][$tag]['properties'];
+      }
+    }
+  }
+
+  /**
+   * Returns all tags assigned to the wrapped entity.
+   *
+   * @return
+   *   An array of tags.
+   */
+  public function getTags() {
+    if (is_object($this->entity) && isset($this->entity->entityTags)) {
+      return $this->entity->entityTags;
+    }
+    elseif (is_array($this->entity) && isset($this->entity['entity tags'])) {
+      return $this->entity['entity tags'];
+    }
+    // In case the entity doesn't have tags set, fallback to the defaults.
+    return $this->info['default tags'];
+  }
+
+  /**
+   * 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',
+      'bundle' => NULL,
+      'tags' => array(),
+      'formats' => array(),
+    );
+    $info += array('sanitize' => (empty($info['getter callback']) && $info['type'] == 'text' ? 'check_plain' : NULL));
+    $info += (array)drupal_get_datatype_info($info['type']);
+    if ($contained_type = drupal_list_extract_type($info['type'])) {
+      $info['item']['type'] = $contained_type;
+      $info['item'] += (array)drupal_get_datatype_info($contained_type);
+    }
+    return $info;
+  }
+
+  /**
+   * Gets the info about the wrapped entity.
+   */
+  public function getInfo() {
+    return $this->info;
+  }
+
+  /**
+   * 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);
+
+      if (!empty($info['getter callback']) && 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;
+      }
+      else {
+        throw new DrupalPropertyWrapperException('Entity property ' . check_plain($name) . " isn't set.");
+      }
+      // Sanitize values.
+      if (isset($this->cache[$name]) && !empty($this->options['sanitize']) && !empty($info['sanitize']) && function_exists($info['sanitize'])) {
+        $this->cache[$name] = $info['sanitize']($this->cache[$name]);
+      }
+      // Return another wrapper to support chained usage.
+      $options = array_intersect_key($info, drupal_map_assoc(array('default format', 'formats', 'item'))) + $this->options;
+      $context = array('entityWrapper' => $this, 'entity' => $this->entity, 'entity type' => $this->entityType, 'property' => $name);
+      $this->cache[$name] = drupal_get_property_wrapper($info['type'], $this->cache[$name], $options, $context);
+    }
+    return $this->cache[$name];
+  }
+
+  /**
+   * Magic method: Set a property.
+   */
+  public function __set($name, $value) {
+    $info = $this->getPropertyInfo($name);
+    if (!empty($info['setter callback']) && 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']));
+  }
+ 
+  public function __toString() {
+    if (isset($this->{$this->info['name property']})) {
+      // Return the default format of the name property, if specified.
+      return (string)$this->{$this->info['name property']};
+    }
+    return $this->entityType;
+  }
+}
+
+/**
+ * Wraps an actual property to ease formating properties and dealing with lists
+ * of values.
+ *
+ * This wrapper can be used to easily apply formats, which have to be specified
+ * in hook_datatype_info() or specificly for the wrapped property in
+ * hook_entity_info(). If the wrapped data is a list of data, its numerical
+ * indexes may be used to retrieve wrappers for the list items. For that this
+ * wrapper may be also used like a usual numerically indexed array.
+ *
+ * If the wrapper gets converted to string, the default format is applied.
+ */
+class DrupalPropertyValueWrapper implements DrupalPropertyWrapperInterface, ArrayAccess {
+
+  protected $type;
+  protected $isList = FALSE;
+  protected $data;
+  protected $options;
+  protected $context;
+  protected $cache = array();
+
+  /**
+   * Construct a new DrupalPropertyValueWrapper object.
+   *
+   * @param $type
+   *   The type of the passed data.
+   * @param $data
+   *   The data to format. If the data type is a list, the data has to be a
+   *   numerically indexed array.
+   * @param $options
+   *   (optional) A keyed array of options, passed through to any returned
+   *   wrappers. Used by this wrapper 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().
+   *   - default format: Customize the default format.
+   *   - absolute: Whether generated URLs should be absolute. Defaults to TRUE.
+   *   - item: An array of options to be used for contained list items.
+   * @param $context
+   *   (optional) An array of contextual information which format callbacks
+   *   can make use of. Contains 'entityWrapper', 'entity', 'entity_type' and
+   *   'property' when constructed by the DrupalEntityPropertyWrapper.
+   */
+  public function __construct($type, &$data, array $options = array(), $context = array()) {
+    $this->type = $type;
+    $this->data = $data;
+    $this->options = $options + array('formats' => array(), 'item' => array(), 'absolute' => TRUE, 'language' => NULL);
+    $this->applyDataTypeDefaults($this->options, $type);
+    // Add in list specific formats, if we are dealing with a list.
+    if ($this->isList = drupal_list_extract_type($type)) {
+      $this->applyDataTypeDefaults($this->options, 'list');
+    }
+    $this->context = array('wrapper' => $this) + $context;
+  }
+  
+  protected function applyDataTypeDefaults(&$info, $type) {
+    $type_info = drupal_get_datatype_info($type) + array('formats' => array());
+    $info += $type_info;
+    $info['formats'] += $type_info['formats'];
+  }
+
+  /**
+   * Magic method: Get a list item (numeric name) or apply a format (else).
+   */
+  public function __get($name) {
+    // Look it up in the cache if possible.
+    if (!array_key_exists($name, $this->cache)) {
+      if (is_numeric($name) && $this->isList) {
+        if (!isset($this->data[$name])) {
+          throw new DrupalPropertyWrapperException('Array entry ' . check_plain($name) . " isn't set.");
+        }
+        // Return another wrapper for the list item.
+        $options = $this->options['item'] + array_intersect_key($this->options, drupal_map_assoc(array('absolute', 'language', 'sanitize')));
+        $this->context['delta'] = $name;
+        $this->cache[$name] = drupal_get_property_wrapper($this->isList, $this->data[$name], $options, $this->context);
+      }
+      else {
+        if (!isset($this->options['formats'][$name])) {
+          throw new DrupalPropertyWrapperException('Unknown format ' . check_plain($name) . '.');
+        }
+        $this->cache[$name] = token_format($this->data, $this->type, $name, $this->options, $this->context);
+      }
+    }
+    return $this->cache[$name];
+  }
+
+  /**
+   * Magic method: Check if a format is known or a list item exists.
+   */
+  public function __isset($name) {
+    return (is_numeric($name) && $this->isList && isset($this->data[$name])) || isset($this->options['formats'][$name]);
+  }
+
+  /**
+   * Get the data wrapped by this object.
+   */
+  public function get() {
+    return $this->data;
+  }
+
+  /**
+   * If we wrap a list, we return an iterator over the data list.
+   */
+  public function getIterator() {
+    return $this->isList ? new ArrayIterator(array_keys($this->data)) : array();
+  }
+
+  /**
+   * For converting to a string use the default format, if any.
+   */
+  public function __toString() {
+    return (string)token_format($this->data, $this->type, NULL, $this->options, $this->context);
+  }
+
+  /**
+   * Implement the ArrayAccess interface.
+   */
+  public function offsetGet($offset) {
+    return $this->__get((int)$offset);
+  }
+
+  public function offsetExists($offset) {
+    return $this->isList && isset($this->data[$offset]);
+  }
+
+  public function offsetSet($offset, $value) {
+    if ($this->isList && is_numeric($offset)) {
+      $this->data[$offset] = $value;
+      $this->updateParentWrapper();
+    }
+  }
+
+  public function offsetUnset($offset) {
+    if ($this->isList && is_numeric($offset)) {
+      unset($this->data[$offset]);
+      $this->updateParentWrapper();
+    }
+  }
+
+  /**
+   * Also update the value in the parent wrapped entity, if there is one.
+   */
+  protected function updateParentWrapper() {
+    if (!empty($this->context['entityWrapper'])) {
+      $this->context['entityWrapper']->{$this->context['property']} = $this->data;
+    }
+  }
+}
+
+/**
+ * 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;
+  }
+}
+
+/**
+ * Getter callback for getting an array. Makes sure it's numerically indexed.
+ */
+function drupal_property_get_list($entity, $options, $name) {
+  return isset($entity->$name) ? array_values($entity->$name) : array();
+}
diff --git includes/token.inc includes/token.inc
index 6f832b7..8524a2e 100644
--- includes/token.inc
+++ includes/token.inc
@@ -42,8 +42,8 @@
  * and 'mail' is a placeholder available for any 'user'.
  *
  * @see token_replace()
- * @see hook_tokens()
- * @see hook_token_info()
+ * @see hook_token_format_info()
+ * @see hook_entity_info()
  */
 
 /**
@@ -54,7 +54,7 @@
  * @param $data
  *   (optional) An array of keyed objects. For simple replacement scenarios
  *   'node', 'user', and others are common keys, with an accompanying node or
- *   user object being the value. Some token types, like 'site', do not require
+ *   user object being the value. Some token types, like 'system', do not require
  *   any explicit information from $data and can be replaced even if it is empty.
  * @param $options
  *   (optional) A keyed array of settings and flags to control the token
@@ -66,19 +66,21 @@
  *     tokens in a text-only email might provide a callback to strip HTML
  *     entities from token values before they are inserted into the final text.
  *   - clear: A boolean flag indicating that tokens should be removed from the
- *     final text if no replacement value can be generated.
+ *     final text if no replacement value can be generated. Defaults to TRUE.
  *   - sanitize: A boolean flag indicating that tokens should be sanitized for
  *     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 assigning types to the objects of $data, keyed liked
+ *   $data. If there is no assigned type for a data object, the key specified in
+ *   $data is used as type.
  * @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']) && function_exists($options['callback'])) {
@@ -122,15 +124,12 @@ 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
- *   user object being the value. Some token types, like 'site', do not require
+ *   user object being the value. Some token types, like 'system', do not require
  *   any explicit information from $data and can be replaced even if it is empty.
  * @param $options
  *   (optional) A keyed array of settings and flags to control the token
@@ -145,107 +144,89 @@ 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.
+ *   - clear: A boolean flag indicating that tokens should be removed from the
+ *     final text if no replacement value can be generated. Defaults to TRUE.
+ * @param $types
+ *   (optional) An array assigning types to the objects of $data, keyed liked
+ *   $data. If there is no assigned type for a data object, the key specified in
+ *   $data is used as type.
  * @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);
-  _token_initialize();
+  $options += array('sanitize' => TRUE, 'clear' => TRUE);
+  // Add in the current date and the global user by default.
+  $data += array('date' => REQUEST_TIME, 'current-user' => $GLOBALS['user']);
+  $types += array('current-user' => 'user');
 
-  $result = module_invoke_all('tokens', $type, $tokens, $data, $options);
-  foreach ($result as $original => $replacement) {
-    $results[$original] = $replacement;
-  }
-
-  return $results;
-}
+  foreach ($raw_tokens as $key => $tokens) {
+    $types += array($key => $key);
 
-/**
- * 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.
- */
-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;
+    if (isset($data[$key]) && ($wrapper = drupal_get_property_wrapper($types[$key], $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.
+          if ($options['clear']) {
+            $results[$original] = '';
+          }
+        }
+      }
     }
   }
   return $results;
 }
 
 /**
- * Returns metadata describing supported tokens.
- *
- * 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.
+ * Applies chained tokens by getting properties of the given wrapper.
  */
-function token_info() {
-  $data = &drupal_static(__FUNCTION__);
-  if (!isset($data)) {
-    _token_initialize();
-    $data = module_invoke_all('token_info');
-    drupal_alter('token_info', $data);
+function _token_get_replacement(DrupalPropertyWrapperInterface $wrapper, $token) {
+  foreach (explode(':', $token) as $i => $name) {
+    $wrapper = $wrapper->$name;
   }
-  return $data;
+  // If no format was given apply the default just by converting it to string.
+  return (string)$wrapper;
 }
 
+
 /**
- * Load modulename.tokens.inc for all enabled modules.
+ * Formats a token by using the formats defined in drupal_get_datatype_info().
+ *
+ * @param $value
+ *   The token value to format.
+ * @param $type
+ *   The type of the passed token.
+ * @param $format
+ *   (optional) The format to apply. If unset, the default format will be used.
+ *   If there is no default format either, the passed value is returned.
+ * @param $options
+ *   (optional) A keyed array of options. Supported are:
+ *   - language: A language object to be used when generating locale-sensitive
+ *     formats.
+ *   - absolute: Whether generated URLs should be absolute. Defaults to TRUE.
+ *   - formats: An array of additional formats to use.
+ *   - default format: May be used to override the default format.
+ * @param $context
+ *   (optional) An array of contextual information which format callbacks
+ *   can make use of. Used internally.
  */
-function _token_initialize() {
-  $initialized = &drupal_static(__FUNCTION__);
-  if (!$initialized) {
-    foreach (module_list() as $module) {
-      $filename = DRUPAL_ROOT . '/' . drupal_get_path('module', $module) . "/$module.tokens.inc";
-      if (file_exists($filename)) {
-        include_once $filename;
-      }
-    }
-    $initialized = TRUE;
+function token_format($value, $type, $format = NULL, array $options = array(), array $context = array()) {
+  $info = drupal_get_datatype_info($type) + array('formats' => array(), 'default format' => NULL);
+  $options += array('language' => NULL, 'formats' => array(), 'absolute' => TRUE);
+  $formats = $options['formats'] + $info['formats'];
+  if (!isset($format)) {
+    $format = isset($options['default format']) ? $options['default format'] : $info['default format'];
+  }
+  // Now apply the format.
+  if (isset($formats[$format]['callback']) && function_exists($function = $formats[$format]['callback'])) {
+    return $function($value, $format, $options, $context);
+  }
+  elseif (!isset($format)) {
+    return $value;
   }
 }
diff --git modules/book/book.module modules/book/book.module
index f4cd8a6..adda78f 100644
--- modules/book/book.module
+++ modules/book/book.module
@@ -723,6 +723,7 @@ function book_menu_name($bid) {
 function book_node_load($nodes, $types) {
   $result = db_query("SELECT * FROM {book} b INNER JOIN {menu_links} ml ON b.mlid = ml.mlid WHERE b.nid IN (:nids)", array(':nids' =>  array_keys($nodes)), array('fetch' => PDO::FETCH_ASSOC));
   foreach ($result as $record) {
+    $nodes[$record['nid']]->entityTags[] = 'book';
     $nodes[$record['nid']]->book = $record;
     $nodes[$record['nid']]->book['href'] = $record['link_path'];
     $nodes[$record['nid']]->book['title'] = $record['link_title'];
@@ -1245,3 +1246,46 @@ function book_menu_subtree_data($link) {
 
   return $tree[$cid];
 }
+
+/**
+ * Implement hook_entity_info_alter().
+ */
+function book_entity_info_alter(&$entity_info) {
+  $entity_info['node']['tags']['book'] = array(
+    'label' => t('Book page'),
+  );
+  $entity_info['node']['default tags'][] = 'book';
+
+  $properties = &$entity_info['node']['tags']['book']['properties'];
+  $properties['book-id'] = array(
+    'label' => t("Book ID"),
+    'type' => 'integer',
+    'description' => t("The unique ID of this page's book."),
+    'getter callback' => 'book_get_properties',
+  );
+  $properties['book'] = array(
+    'label' => t("Book"),
+    'type' => 'node',
+    'description' => t("The book to which this book page belongs."),
+    'getter callback' => 'book_get_properties',
+  );
+
+}
+
+/**
+ * Callback for getting book node properties.
+ * @see book_entity_info_alter()
+ */
+function book_get_properties($node, array $options, $name, $entity_type) {
+  if (!isset($node->book['bid'])) {
+    throw new DrupalPropertyWrapperException('This node is no book page.');
+  }
+
+  switch ($name) {
+    case 'book-id':
+      return $node->book['bid'];
+
+    case 'book':
+      return node_load($node->book['bid']);
+  }
+}
diff --git modules/comment/comment.entity.inc modules/comment/comment.entity.inc
new file mode 100644
index 0000000..afd8812
--- /dev/null
+++ modules/comment/comment.entity.inc
@@ -0,0 +1,148 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Provides info about the comment entity.
+ */
+
+
+/**
+ * Implement hook_entity_info() {
+ */
+function comment_entity_info() {
+  $return =  array(
+    'comment' => array(
+      'label' => t('Comment'),
+      'base table' => 'comment',
+      'fieldable' => TRUE,
+      'controller class' => 'CommentController',
+      'object keys' => array(
+        'id' => 'cid',
+        'bundle' => 'node_type',
+      ),
+      'bundle keys' => array(
+        'bundle' => 'type',
+      ),
+      'bundles' => array(),
+      'static cache' => FALSE,
+      'name property' => 'title',
+    ),
+  );
+
+  foreach (node_type_get_names() as $type => $name) {
+    $return['comment']['bundles']['comment_node_' . $type] = array(
+      'label' => $name,
+    );
+  }
+
+  // Add meta-data about the basic comment properties.
+  $properties = &$return['comment']['properties'];
+
+  $properties['cid'] = array(
+    'label' => t("Comment ID"),
+    'type' => 'integer',
+    'description' => t("The unique ID of the comment."),
+  );
+  $properties['pid'] = array(
+    'label' => t("Parent ID"),
+    'type' => 'integer',
+    'description' => t("The unique ID of the comment's parent, if comment threading is active."),
+  );
+  $properties['nid'] = array(
+    'label' => t("Node ID"),
+    'type' => 'integer',
+    'description' => t("The unique ID of the node the comment was posted to."),
+  );
+  $properties['uid'] = array(
+    'label' => t("User ID"),
+    'type' => 'integer',
+    'description' => t("The unique ID of the user who posted the comment."),
+  );
+  $properties['hostname'] = array(
+    'label' => t("IP Address"),
+    'description' => t("The IP address of the computer the comment was posted from."),
+  );
+  $properties['name'] = array(
+    'label' => t("Name"),
+    'description' => t("The name left by the comment author."),
+    'getter callback' => 'comment_get_properties',
+    'setter callback' => 'drupal_property_verbatim_set',
+  );
+  $properties['mail'] = array(
+    'label' => t("Email address"),
+    'description' => t("The email address left by the comment author."),
+    'getter callback' => 'comment_get_properties',
+    'setter callback' => 'drupal_property_verbatim_set',
+  );
+  $properties['homepage'] = array(
+    'label' => t("Home page"),
+    'description' => t("The home page URL left by the comment author."),
+    'sanitize' => 'filter_xss_bad_protocol',
+    'setter callback' => 'drupal_property_verbatim_set',
+  );
+  $properties['title'] = array(
+    'label' => t("Title"),
+    'description' => t("The title of the comment."),
+    'getter callback' => 'comment_get_properties',
+  );
+  $properties['body'] = array(
+    'label' => t("Content"),
+    'description' => t("The formatted content of the comment itself."),
+    'getter callback' => 'comment_get_properties',
+    'setter callback' => 'drupal_property_verbatim_set',
+  );
+  $properties['url'] = array(
+    'label' => t("URL"),
+    'description' => t("The URL of the comment."),
+    'getter callback' => 'comment_get_properties',
+  );
+  $properties['edit-url'] = array(
+    'label' => t("Edit URL"),
+    'description' => t("The URL of the comment's edit page."),
+    'getter callback' => 'comment_get_properties',
+  );
+  $properties['created'] = array(
+    'label' => t("Date created"),
+    'description' => t("The date the comment was posted."),
+    'type' => 'date',
+    'setter callback' => 'drupal_property_verbatim_set',
+  );
+  $properties['parent'] = array(
+    'label' => t("Parent"),
+    'description' => t("The comment's parent, if comment threading is active."),
+    'type' => 'comment',
+    'getter callback' => 'comment_get_properties',
+  );
+  $properties['node'] = array(
+    'label' => t("Node"),
+    'description' => t("The node the comment was posted to."),
+    'type' => 'node',
+    'getter callback' => 'comment_get_properties',
+  );
+  $properties['author'] = array(
+    'label' => t("Author"),
+    'description' => t("The author of the comment, if they were logged in."),
+    'type' => 'user',
+    'getter callback' => 'comment_get_properties',
+  );
+  return $return;
+}
+
+/**
+ * Implement hook_entity_info_alter().
+ */
+function comment_entity_info_alter(&$entity_info) {
+  $properties = &$entity_info['node']['properties'];
+
+  $properties['comment-count'] = array(
+    'label' => t("Comment count"),
+    'description' => t("The number of comments posted on a node."),
+    'getter callback' => 'comment_get_node_properties',
+  );
+  $properties['comment-count-new'] = array(
+    'label' => t("New comment count"),
+    'description' => t("The number of comments posted on a node since the reader last viewed it."),
+    'getter callback' => 'comment_get_node_properties',
+  );
+}
diff --git modules/comment/comment.info modules/comment/comment.info
index 278980a..874bc74 100644
--- modules/comment/comment.info
+++ modules/comment/comment.info
@@ -10,4 +10,4 @@ files[] = comment.admin.inc
 files[] = comment.pages.inc
 files[] = comment.install
 files[] = comment.test
-files[] = comment.tokens.inc
+files[] = comment.entity.inc
diff --git modules/comment/comment.module modules/comment/comment.module
index 0f94475..50fd9ff 100644
--- modules/comment/comment.module
+++ modules/comment/comment.module
@@ -96,37 +96,6 @@ function comment_help($path, $arg) {
 }
 
 /**
- * Implement hook_entity_info() {
- */
-function comment_entity_info() {
-  $return =  array(
-    'comment' => array(
-      'label' => t('Comment'),
-      'base table' => 'comment',
-      'fieldable' => TRUE,
-      'controller class' => 'CommentController',
-      'object keys' => array(
-        'id' => 'cid',
-        'bundle' => 'node_type',
-      ),
-      'bundle keys' => array(
-        'bundle' => 'type',
-      ),
-      'bundles' => array(),
-      'static cache' => FALSE,
-    ),
-  );
-
-  foreach (node_type_get_names() as $type => $name) {
-    $return['comment']['bundles']['comment_node_' . $type] = array(
-      'label' => $name,
-    );
-  }
-
-  return $return;
-}
-
-/**
  * Implement hook_theme().
  */
 function comment_theme() {
@@ -2430,3 +2399,62 @@ function comment_filter_format_delete($format, $fallback) {
     ->execute();
 }
 
+/**
+ * Callback for getting comment properties.
+ * @see comment_entity_info()
+ */
+function comment_get_properties($comment, array $options, $name) {
+  switch ($name) {
+    case 'name':
+      $name = ($comment->uid == 0) ? variable_get('anonymous', t('Anonymous')) : $comment->name;
+      return $options['sanitize'] ? filter_xss($name) : $name;
+
+    case 'mail':
+      if ($comment->uid != 0) {
+        $account = user_load($comment->uid);
+        $mail = $account->mail;
+      }
+      else {
+        $mail = $comment->mail;
+      }
+      return $options['sanitize'] ? check_plain($mail) : $mail;
+
+    case 'title':
+      return $options['sanitize'] ? filter_xss($comment->subject) : $comment->subject;
+
+    case 'body':
+      return $options['sanitize'] ? check_markup($comment->comment, $comment->format) : $comment->comment;
+
+    case 'url':
+      return url('comment/' . $comment->cid, array('fragment' => 'comment-' . $comment->cid) + $options);
+
+    case 'edit-url':
+      return url('comment/edit/' . $comment->cid, $options);
+    
+    case 'node':
+      return node_load($comment->nid);
+      
+    case 'parent':
+      if ($parent = comment_load($comment->pid)) {
+        return $parent;
+      }
+      throw new DrupalPropertyWrapperException('This comment has no parent comment.');
+
+    case 'author':
+      return user_load($comment->uid);
+  }
+}
+
+/**
+ * Callback for getting node properties.
+ * @see comment_entity_info_alter()
+ */
+function comment_get_node_properties($node, array $options, $name) {
+  switch ($name) {
+    case 'comment-count':
+      return $node->comment_count;
+
+    case 'comment-count-new':
+      return comment_num_new($node->nid);
+  }
+}
diff --git modules/comment/comment.tokens.inc modules/comment/comment.tokens.inc
deleted file mode 100644
index 08a2466..0000000
--- modules/comment/comment.tokens.inc
+++ /dev/null
@@ -1,248 +0,0 @@
-<?php
-// $Id: comment.tokens.inc,v 1.2 2009/09/30 18:37:30 dries Exp $
-
-/**
- * @file
- * Builds placeholder replacement tokens for comment-related data.
- */
-
-/**
- * Implement hook_token_info().
- */
-function comment_token_info() {
-  $type = array(
-    'name' => t('Comments'),
-    'description' => t('Tokens for comments posted on the site.'),
-    'needs-data' => 'comment',
-  );
-
-  // Comment-related tokens for nodes
-  $node['comment-count'] = array(
-    'name' => t("Comment count"),
-    'description' => t("The number of comments posted on a node."),
-  );
-  $node['comment-count-new'] = array(
-    'name' => t("New comment count"),
-    'description' => t("The number of comments posted on a node since the reader last viewed it."),
-  );
-
-  // Core comment tokens
-  $comment['cid'] = array(
-    'name' => t("Comment ID"),
-    'description' => t("The unique ID of the comment."),
-  );
-  $comment['pid'] = array(
-    'name' => t("Parent ID"),
-    'description' => t("The unique ID of the comment's parent, if comment threading is active."),
-  );
-  $comment['nid'] = array(
-    'name' => t("Node ID"),
-    'description' => t("The unique ID of the node the comment was posted to."),
-  );
-  $comment['uid'] = array(
-    'name' => t("User ID"),
-    'description' => t("The unique ID of the user who posted the comment."),
-  );
-  $comment['hostname'] = array(
-    'name' => t("IP Address"),
-    'description' => t("The IP address of the computer the comment was posted from."),
-  );
-  $comment['name'] = array(
-    'name' => t("Name"),
-    'description' => t("The name left by the comment author."),
-  );
-  $comment['mail'] = array(
-    'name' => t("Email address"),
-    'description' => t("The email address left by the comment author."),
-  );
-  $comment['homepage'] = array(
-    'name' => t("Home page"),
-    'description' => t("The home page URL left by the comment author."),
-  );
-  $comment['title'] = array(
-    'name' => t("Title"),
-    'description' => t("The title of the comment."),
-  );
-  $comment['body'] = array(
-    'name' => t("Content"),
-    'description' => t("The formatted content of the comment itself."),
-  );
-  $comment['url'] = array(
-    'name' => t("URL"),
-    'description' => t("The URL of the comment."),
-  );
-  $comment['edit-url'] = array(
-    'name' => t("Edit URL"),
-    'description' => t("The URL of the comment's edit page."),
-  );
-
-  // Chained tokens for comments
-  $comment['created'] = array(
-    'name' => t("Date created"),
-    'description' => t("The date the comment was posted."),
-    'type' => 'date',
-  );
-  $comment['parent'] = array(
-    'name' => t("Parent"),
-    'description' => t("The comment's parent, if comment threading is active."),
-    'type' => 'comment',
-  );
-  $comment['node'] = array(
-    'name' => t("Node"),
-    'description' => t("The node the comment was posted to."),
-    'type' => 'node',
-  );
-  $comment['author'] = array(
-    'name' => t("Author"),
-    'description' => t("The author of the comment, if they were logged in."),
-    'type' => 'user',
-  );
-
-  return array(
-    'types' => array('comment' => $type),
-    'tokens' => array(
-      'node' => $node,
-      'comment' => $comment,
-    ),
-  );
-}
-
-/**
- * Implement hook_tokens().
- */
-function comment_tokens($type, $tokens, array $data = array(), array $options = array()) {
-  $url_options = array('absolute' => TRUE);
-  if (isset($options['language'])) {
-    $url_options['language'] = $options['language'];
-    $language_code = $options['language']->language;
-  }
-  else {
-    $language_code = NULL;
-  }
-  $sanitize = !empty($options['sanitize']);
-
-  $replacements = array();
-
-  if ($type == 'comment' && !empty($data['comment'])) {
-    $comment = $data['comment'];
-
-    foreach ($tokens as $name => $original) {
-      switch ($name) {
-        // Simple key values on the comment.
-        case 'cid':
-          $replacements[$original] = $comment->cid;
-          break;
-
-        case 'nid':
-          $replacements[$original] = $comment->nid;
-          break;
-
-        case 'uid':
-          $replacements[$original] = $comment->uid;
-          break;
-
-        case 'pid':
-          $replacements[$original] = $comment->pid;
-          break;
-
-        // Poster identity information for comments
-        case 'hostname':
-          $replacements[$original] = $sanitize ? check_plain($comment->hostname) : $comment->hostname;
-          break;
-
-        case 'name':
-          $name = ($comment->uid == 0) ? variable_get('anonymous', t('Anonymous')) : $comment->name;
-          $replacements[$original] = $sanitize ? filter_xss($name) : $name;
-          break;
-
-        case 'mail':
-          if ($comment->uid != 0) {
-            $account = user_load($comment->uid);
-            $mail = $account->mail;
-          }
-          else {
-            $mail = $comment->mail;
-          }
-          $replacements[$original] = $sanitize ? check_plain($mail) : $mail;
-          break;
-
-        case 'homepage':
-          $replacements[$original] = $sanitize ? filter_xss_bad_protocol($comment->homepage) : $comment->homepage;
-          break;
-
-        case 'title':
-          $replacements[$original] = $sanitize ? filter_xss($comment->subject) : $comment->subject;
-          break;
-
-        case 'body':
-          $replacements[$original] = $sanitize ? check_markup($comment->comment, $comment->format) : $replacements[$original] = $comment->comment;
-          break;
-
-        // Comment related URLs.
-        case 'url':
-          $replacements[$original] = url('comment/' . $comment->cid, array('absolute' => TRUE, 'fragment' => 'comment-' . $comment->cid));
-          break;
-
-        case 'edit-url':
-          $replacements[$original] = url('comment/edit/' . $comment->cid, array('absolute' => TRUE));
-          break;
-
-        // Default values for the chained tokens handled below.
-        case 'author':
-          $replacements[$original] = $sanitize ? filter_xss($comment->name) : $comment->name;
-          break;
-
-        case 'parent':
-          if (!empty($comment->pid)) {
-            $parent = comment_load($comment->pid);
-            $replacements[$original] = $sanitize ? filter_xss($parent->subject) : $parent->subject;
-          }
-          break;
-
-        case 'created':
-          $replacements[$original] = format_date($comment->timestamp, 'medium', '', NULL, $language_code);
-          break;
-
-        case 'node':
-          $node = node_load($comment->nid);
-          $replacements[$original] = $sanitize ? filter_xss($node->title) : $node->title;
-          break;
-      }
-    }
-
-    // Chained token relationships.
-    if ($node_tokens = token_find_with_prefix($tokens, 'node')) {
-      $node = node_load($comment->nid);
-      $replacements += token_generate('node', $node_tokens, array('node' => $node), $options);
-    }
-
-    if ($date_tokens = token_find_with_prefix($tokens, 'created')) {
-      $replacements += token_generate('date', $date_tokens, array('date' => $comment->timestamp), $options);
-    }
-
-    if (($parent_tokens = token_find_with_prefix($tokens, 'parent')) && $parent = comment_load($comment->pid)) {
-      $replacements += token_generate('comment', $parent_tokens, array('comment' => $parent), $options);
-    }
-
-    if (($author_tokens = token_find_with_prefix($tokens, 'author')) && $account = user_load($comment->uid)) {
-      $replacements += token_generate('user', $author_tokens, array('user' => $account), $options);
-    }
-  }
-  elseif ($type == 'node' & !empty($data['node'])) {
-    $node = $data['node'];
-
-    foreach ($tokens as $name => $original) {
-      switch($name) {
-        case 'comment-count':
-          $replacements[$original] = $node->comment_count;
-          break;
-
-        case 'comment-count-new':
-          $replacements[$original] = comment_num_new($node->nid);
-          break;
-      }
-    }
-  }
-
-  return $replacements;
-}
diff --git modules/field/field.api.php modules/field/field.api.php
index 572f940..0d33e17 100644
--- modules/field/field.api.php
+++ modules/field/field.api.php
@@ -132,6 +132,14 @@ function hook_field_extra_fields($bundle) {
  *     instance definition. This formatter must be available whenever the field
  *     type is available (i.e. provided by the field type module, or by a module
  *     the field type module depends on).
+ *   - property_type: The type of a field item. Used to generate property info
+ *     defauls for any instance of this field, see hook_entity_info().
+ *   - property_callbacks: An array of callbacks generating property info for
+ *     instances of this field. For an example see
+ *     field_default_property_callback(), which is always invoked first. This
+ *     callback adds in some defaults which work fine for fields using the
+ *     'value' key and the 'safe' key for sanitized data. If it's not like that,
+ *     another callback should be added to override the defaults.
  */
 function hook_field_info() {
   return array(
diff --git modules/field/field.default.inc modules/field/field.default.inc
index 77a61d6..ded22ef 100644
--- modules/field/field.default.inc
+++ modules/field/field.default.inc
@@ -66,7 +66,7 @@ function field_default_view($obj_type, $object, $field, $instance, $langcode, $i
 
   if ($display['type'] !== 'hidden') {
     $theme = 'field_formatter_' . $display['type'];
-    $single = (field_behaviors_formatter('multiple values', $display) == FIELD_BEHAVIOR_DEFAULT);
+    $single = (field_behaviors_formatter('multiple values', $display['type']) == FIELD_BEHAVIOR_DEFAULT);
 
     $label_display = $display['label'];
     if ($build_mode == 'search_index') {
diff --git modules/field/field.info.inc modules/field/field.info.inc
index e9616f2..64156ad 100644
--- modules/field/field.info.inc
+++ modules/field/field.info.inc
@@ -27,6 +27,8 @@ function field_info_cache_clear() {
   _field_info_collate_types(TRUE);
   drupal_static_reset('field_build_modes');
   _field_info_collate_fields(TRUE);
+  drupal_static_reset('entity_get_info');
+  cache_clear_all('entity_info', 'cache');
 }
 
 /**
@@ -140,6 +142,7 @@ function _field_info_collate_types($reset = FALSE) {
       drupal_alter('field_storage_info', $info['storage types']);
 
       // Populate information about 'fieldable' entities.
+      module_load_all_includes('inc', 'entity');
       foreach (module_implements('entity_info') as $module) {
         $entities = (array) module_invoke($module, 'entity_info');
         foreach ($entities as $name => $entity_info) {
@@ -166,6 +169,12 @@ function _field_info_collate_types($reset = FALSE) {
         }
       }
       drupal_alter('entity_info', $info['fieldable types']);
+      // Remove all not fieldable types added by drupal alter.
+      foreach ($info['fieldable types'] as $name => $entity_info) {
+        if (empty($entity_info['fieldable'])) {
+          unset($info['fieldable types'][$name]);
+        }
+      }
 
       cache_set('field_info_types', $info, 'cache_field');
     }
@@ -344,15 +353,15 @@ function field_behaviors_widget($op, $instance) {
  *  @param $op
  *    The name of the operation.
  *    Currently supported: 'multiple values'
- *  @param $display
- *    The $instance['display'][$build_mode] array.
+ *  @param $formatter_type
+ *    The formatter type.
  *  @return
  *    FIELD_BEHAVIOR_NONE    - do nothing for this operation.
  *    FIELD_BEHAVIOR_CUSTOM  - use the formatter's callback function.
  *    FIELD_BEHAVIOR_DEFAULT - use field module default behavior.
  */
-function field_behaviors_formatter($op, $display) {
-  $info = field_info_formatter_types($display['type']);
+function field_behaviors_formatter($op, $formatter_type) {
+  $info = field_info_formatter_types($formatter_type);
   return isset($info['behaviors'][$op]) ? $info['behaviors'][$op] : FIELD_BEHAVIOR_DEFAULT;
 }
 
@@ -514,7 +523,7 @@ function field_info_bundle_entity($bundle) {
  */
 function field_info_fields() {
   $info = _field_info_collate_fields();
-  return $info['fields'];
+  return isset($info['fields']) ? $info['fields'] : array();
 }
 
 /**
@@ -652,5 +661,161 @@ function field_info_storage_settings($type) {
 }
 
 /**
+ * Implement hook_entity_info_alter().
+ *
+ * Add metadata about all properties provided by fields.
+ */
+function field_entity_info_alter(&$entity_info) {
+  // Loop over all field instance and add them as property.
+  foreach (field_info_fields() as $field_name => $field) {
+    $field += array('bundles' => array());
+    $field_type = field_info_field_types($field['type']) + array('property_callbacks' => array());
+    // Add in our default callback as the first one.
+    array_unshift($field_type['property_callbacks'], 'field_default_property_callback');
+
+    foreach ($field['bundles'] as $bundle) {
+      $instance = field_info_instance($field_name, $bundle);
+      if (empty($instance['deleted']) && $entity_type = field_info_bundle_entity($bundle)) {
+        foreach ($field_type['property_callbacks'] as $callback) {
+          $callback($entity_info, $entity_type, $field, $instance, $field_type);
+        }
+      }
+    }
+  }
+}
+
+/**
+ * Callback to add in property info defaults per field instance.
+ * @see field_entity_info_alter().
+ */
+function field_default_property_callback(&$entity_info, $entity_type, $field, $instance, $field_type) {
+  if (!empty($field_type['property_type'])) {
+    $is_list = ($field['cardinality'] > 1);
+    if ($is_list) {
+      $field_type['property_type'] = 'list<' . $field_type['property_type'] . '>';
+    }
+    // Add in instance specific property info, if given and apply defaults.
+    $property = &$entity_info[$entity_type]['bundles'][$instance['bundle']]['properties'][$field['field_name']];
+    $instance += array('property info' => array());
+    $property = $instance['property info'] + array(
+      'label' => $instance['label'],
+      'type' => $field_type['property_type'],
+      'description' => $instance['description'],
+      'getter callback' => $is_list ? 'field_property_get_multiple' : 'field_property_get',
+      'setter callback' => $is_list ? 'field_property_set_multiple' : 'field_property_set',
+    );
+
+    // Add in compatible formatters.
+    foreach (field_info_formatter_types() as $name => $formatter) {
+      if (in_array($field['type'], $formatter['field types'])) {
+        $format = array(
+          'label' => $formatter['label'],
+          'callback' => 'field_format_property',
+        );
+        // Care for multiple values formatters vs single values formatters.
+        $single = (field_behaviors_formatter('multiple values', $name) == FIELD_BEHAVIOR_DEFAULT);
+        if (!$is_list || !$single) {
+          $property['formats'][$name] = $format;
+        }
+        elseif ($is_list && $single) {
+          $property['item']['formats'][$name] = $format;
+        }
+      }
+    }
+    if (isset($property['item']['formats'][$field_type['default_formatter']])) {
+      $property['item']['default format'] = $field_type['default_formatter'];
+    }
+    elseif (isset($property['formats'][$field_type['default_formatter']])) {
+      $property['default format'] = $field_type['default_formatter'];
+    }
+  }
+}
+
+/**
+ * Callback for applying custom property formats.
+ */
+function field_format_property($value, $formatter_type, array $options, array $context) {
+  $field_name = $context['property'];
+  $langcode = _field_property_get_langcode($context['entity'], $options, $field_name);
+  $single = (field_behaviors_formatter('multiple values', $formatter_type) == FIELD_BEHAVIOR_DEFAULT);
+  $delta = isset($context['delta']) ? $context['delta'] : 0;
+  $item = $single ? $context['entity']->{$field_name}[$langcode][$delta] : $context['entity']->$field_name;
+  $field = field_info_field($field_name);
+  return field_format($context['entity type'], $context['entity'], $field, $langcode, $item, $formatter_type);
+}
+
+/**
+ * Callback for getting field property values.
+ */
+function field_property_get($object, array $options, $name, $obj_type) {
+  $langcode = _field_property_get_langcode($object, $options, $name);
+  $key = $options['sanitize'] ? _field_get_sanitized_key($object, $obj_type, $name, $langcode) : 'value';
+  return $object->{$name}[$langcode][0][$key];
+}
+
+/**
+ * Callback for getting multiple field property values.
+ */
+function field_property_get_multiple($object, array $options, $name, $obj_type) {
+  $langcode = _field_property_get_langcode($object, $options, $name);
+  $values = array();
+  if (isset($object->{$name}[$langcode])) {
+    $key = $options['sanitize'] ? _field_get_sanitized_key($object, $obj_type, $name, $langcode) : 'value';
+    foreach ($object->{$name}[$langcode] as $delta => $data) {
+      $values[$delta] = $data[$key];
+    }
+  }
+  return $values;
+}
+
+function _field_property_get_langcode($object, $options, $name) {
+  $langcode = FIELD_LANGUAGE_NONE;
+  if (isset($options['language']) && isset($object->{$name}[$options['language']->language])) {
+    $langcode = $options['language']->language;
+  }
+  return $langcode;
+}
+
+/**
+ * Returns the key used to get a sanitized value. If the field implements
+ * hook_field_sanitize() the key 'safe' is returned, else it's assumed that
+ * the key 'value' is safe.
+ */
+function _field_get_sanitized_key($object, $obj_type, $field_name, $langcode) {
+  if (isset($object->{$field_name}[$langcode][0]['safe'])) {
+    return 'safe';
+  }
+  $field = field_info_field($field_name);
+  list($id, $vid, $bundle) = field_extract_ids($obj_type, $object);
+  $instance = field_info_instance($field_name, $bundle);
+
+  if (function_exists($function = $field['module'] . '_field_sanitize')) {
+    $function($obj_type, $object, $field, $instance, $langcode, $object->{$field_name}[$langcode]);
+    return 'safe';
+  }
+  return 'value';
+}
+
+/**
+ * Callback for setting field property values.
+ */
+function field_property_set(&$object, $name, $value, $obj_type) {
+  $langcode = array_shift(array_keys($object->$name));
+  $object->{$name}[$langcode][0]['value'] = $value;
+  unset($object->{$name}[$langcode][0]['safe']);
+}
+
+/**
+ * Callback for setting multiple field property values.
+ */
+function field_property_set_multiple(&$object, $name, $values, $obj_type) {
+  $langcode = array_shift(array_keys($object->$name));
+  $object->{$name}[$langcode] = array();
+  foreach ($values as $key => $value) {
+    $object->{$name}[$langcode][$key]['value'] = $value;
+  }
+}
+
+/**
  * @} End of "defgroup field_info"
  */
diff --git modules/field/field.module modules/field/field.module
index c3b9ca6..8142c7b 100644
--- modules/field/field.module
+++ modules/field/field.module
@@ -481,11 +481,6 @@ function _field_filter_xss_display_allowed_tags() {
 /**
  * Format a field item for display.
  *
- * TODO D7 : do we still need field_format ?
- * - backwards compatibility of templates - check what fallbacks we can propose...
- * - was used by Views integration in CCK in D6 - do we need now?
- * At least needs a little rehaul/update...
- *
  * Used to display a field's values outside the context of the $node, as
  * when fields are displayed in Views, or to display a field in a template
  * using a different formatter than the one set up on the Display Fields tab
@@ -493,6 +488,8 @@ function _field_filter_xss_display_allowed_tags() {
  *
  * @param $field
  *   Either a field array or the name of the field.
+ * @param $langcode
+ *   The language code of the formatted field items.
  * @param $item
  *   The field item(s) to be formatted (such as $node->field_foo[0],
  *   or $node->field_foo if the formatter handles multiple values itself)
@@ -507,7 +504,7 @@ function _field_filter_xss_display_allowed_tags() {
  *   It will have been passed through the necessary check_plain() or check_markup()
  *   functions as necessary.
  */
-function field_format($obj_type, $object, $field, $item, $formatter_type = NULL, $formatter_settings = array()) {
+function field_format($obj_type, $object, $field, $langcode, $item, $formatter_type = NULL, $formatter_settings = array()) {
   if (!is_array($field)) {
     $field = field_info_field($field);
   }
@@ -526,7 +523,7 @@ function field_format($obj_type, $object, $field, $item, $formatter_type = NULL,
     $display['settings'] += field_info_formatter_settings($display['type']);
 
     if ($display['type'] !== 'hidden') {
-      $theme = $formatter['module'] . '_formatter_' . $display['type'];
+      $theme = 'field_formatter_' . $display['type'];
 
       $element = array(
         '#theme' => $theme,
@@ -539,14 +536,14 @@ function field_format($obj_type, $object, $field, $item, $formatter_type = NULL,
         '#delta' => isset($item['#delta']) ? $item['#delta'] : NULL,
       );
 
-      if (field_behaviors_formatter('multiple values', $display) == FIELD_BEHAVIOR_DEFAULT) {
+      if (field_behaviors_formatter('multiple values', $display['type']) == FIELD_BEHAVIOR_DEFAULT) {
         // Single value formatter.
 
         // hook_field('sanitize') expects an array of items, so we build one.
         $items = array($item);
         $function = $field['module'] . '_field_sanitize';
         if (function_exists($function)) {
-          $function($obj_type, $object, $field, $instance, $items);
+          $function($obj_type, $object, $field, $instance, $langcode, $items);
         }
 
         $element['#item'] = $items[0];
@@ -556,7 +553,7 @@ function field_format($obj_type, $object, $field, $item, $formatter_type = NULL,
         $items = $item;
         $function = $field['module'] . '_field_sanitize';
         if (function_exists($function)) {
-          $function($obj_type, $object, $field, $instance, $items);
+          $function($obj_type, $object, $field, $instance, $langcode, $items);
         }
 
         foreach ($items as $delta => $item) {
diff --git modules/field/modules/list/list.module modules/field/modules/list/list.module
index 06ecebe..a85f733 100644
--- modules/field/modules/list/list.module
+++ modules/field/modules/list/list.module
@@ -17,6 +17,7 @@ function list_field_info() {
       'settings' => array('allowed_values' => '', 'allowed_values_function' => ''),
       'default_widget' => 'options_select',
       'default_formatter' => 'list_default',
+      'property_type' => 'integer',
     ),
     'list_boolean' => array(
       'label' => t('Boolean'),
@@ -24,6 +25,7 @@ function list_field_info() {
       'settings' => array('allowed_values' => '', 'allowed_values_function' => ''),
       'default_widget' => 'options_select',
       'default_formatter' => 'list_default',
+      'property_type' => 'boolean',
     ),
     'list_number' => array(
       'label' => t('List (numeric)'),
@@ -31,6 +33,7 @@ function list_field_info() {
       'settings' => array('allowed_values' => '', 'allowed_values_function' => ''),
       'default_widget' => 'options_select',
       'default_formatter' => 'list_default',
+      'property_type' => 'decimal',
     ),
     'list_text' => array(
       'label' => t('List (text)'),
@@ -38,6 +41,7 @@ function list_field_info() {
       'settings' => array('allowed_values' => '', 'allowed_values_function' => ''),
       'default_widget' => 'options_select',
       'default_formatter' => 'list_default',
+      'property_type' => 'text',
     ),
   );
 }
diff --git modules/field/modules/number/number.module modules/field/modules/number/number.module
index 6205ab7..00edfb3 100644
--- modules/field/modules/number/number.module
+++ modules/field/modules/number/number.module
@@ -35,6 +35,7 @@ function number_field_info() {
       'instance_settings' => array('min' => '', 'max' => '', 'prefix' => '', 'suffix' => ''),
       'default_widget' => 'number',
       'default_formatter' => 'number_default',
+      'property_type' => 'integer',
     ),
     'number_decimal' => array(
       'label' => t('Decimal'),
@@ -43,6 +44,7 @@ function number_field_info() {
       'instance_settings' => array('min' => '', 'max' => '', 'prefix' => '', 'suffix' => ''),
       'default_widget' => 'number',
       'default_formatter' => 'number_decimal',
+      'property_type' => 'decimal',
     ),
     'number_float' => array(
       'label' => t('Float'),
@@ -50,6 +52,7 @@ function number_field_info() {
       'instance_settings' => array('min' => '', 'max' => '', 'prefix' => '', 'suffix' => ''),
       'default_widget' => 'number',
       'default_formatter' => 'number_decimal',
+      'property_type' => 'decimal',
     ),
   );
 }
diff --git modules/field/modules/text/text.module modules/field/modules/text/text.module
index 7596577..6b08b87 100644
--- modules/field/modules/text/text.module
+++ modules/field/modules/text/text.module
@@ -40,6 +40,7 @@ function text_field_info() {
       'instance_settings' => array('text_processing' => 0),
       'default_widget' => 'text_textfield',
       'default_formatter' => 'text_default',
+      'property_type' => 'text',
     ),
     'text_long' => array(
       'label' => t('Long text'),
@@ -48,6 +49,7 @@ function text_field_info() {
       'instance_settings' => array('text_processing' => 0),
       'default_widget' => 'text_textarea',
       'default_formatter' => 'text_default',
+      'property_type' => 'text',
     ),
     'text_with_summary' => array(
       'label' => t('Long text and summary'),
@@ -56,6 +58,8 @@ function text_field_info() {
       'instance_settings' => array('text_processing' => 1, 'display_summary' => 0),
       'default_widget' => 'text_textarea_with_summary',
       'default_formatter' => 'text_summary_or_trimmed',
+      'property_type' => 'text',
+      'property_callbacks' => array(),
     ),
   );
 }
diff --git modules/node/node.entity.inc modules/node/node.entity.inc
new file mode 100644
index 0000000..7cdaf48
--- /dev/null
+++ modules/node/node.entity.inc
@@ -0,0 +1,121 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Provides info about the node entity.
+ */
+
+
+/**
+ * Implement hook_entity_info().
+ */
+function node_entity_info() {
+  $return = array(
+    'node' => array(
+      'label' => t('Node'),
+      'controller class' => 'NodeController',
+      'base table' => 'node',
+      'revision table' => 'node_revision',
+      'fieldable' => TRUE,
+      'object keys' => array(
+        'id' => 'nid',
+        'revision' => 'vid',
+        'bundle' => 'type',
+      ),
+      // Node.module handles its own caching.
+      // 'cacheable' => FALSE,
+      'bundles' => array(),
+      'name property' => 'title',
+    ),
+  );
+  // Bundles must provide a human readable name so we can create help and error
+  // messages, and the path to attach Field admin pages to.
+  foreach (node_type_get_names() as $type => $name) {
+    $return['node']['bundles'][$type] = array(
+      'label' => $name,
+      'admin' => array(
+        'path' => 'admin/structure/types/manage/' . str_replace('_', '-', $type),
+        'access arguments' => array('administer content types'),
+      ),
+    );
+  }
+  // Add meta-data about the basic node properties.
+  $properties = &$return['node']['properties'];
+  
+  $properties['nid'] = array(
+    'label' => t("Node ID"),
+    'type' => 'integer',
+    'description' => t("The unique ID of the node."),
+  );
+  $properties['vid'] = array(
+    'label' => t("Revision ID"),
+    'type' => 'integer',
+    'description' => t("The unique ID of the node's latest revision."),
+  );
+  $properties['tnid'] = array(
+    'label' => t("Translation set ID"),
+    'type' => 'integer',
+    'description' => t("The unique ID of the original-language version of this node, if one exists."),
+  );
+  $properties['uid'] = array(
+    'label' => t("User ID"),
+    'type' => 'integer',
+    'description' => t("The unique ID of the author of the node."),
+    'setter callback' => 'drupal_property_verbatim_set',
+  );
+  $properties['type'] = array(
+    'label' => t("Content type"),
+    'description' => t("The type of the node."),
+  );
+  $properties['type-name'] = array(
+    'label' => t("Content type name"),
+    'description' => t("The human-readable name of the node type."),
+    'getter callback' => 'node_get_properties',
+  );
+  $properties['title'] = array(
+    'label' => t("Title"),
+    'description' => t("The title of the node."),
+    'setter callback' => 'drupal_property_verbatim_set',
+  );
+  $properties['language'] = array(
+    'label' => t("Language"),
+    'description' => t("The language the node is written in."),
+    'setter callback' => 'drupal_property_verbatim_set',
+  );
+  $properties['url'] = array(
+    'label' => t("URL"),
+    'description' => t("The URL of the node."),
+    'getter callback' => 'node_get_properties',
+  );
+  $properties['edit-url'] = array(
+    'label' => t("Edit URL"),
+    'description' => t("The URL of the node's edit page."),
+    'getter callback' => 'node_get_properties',
+  );
+  $properties['created'] = array(
+    'label' => t("Date created"),
+    'type' => 'date',
+    'description' => t("The date the node was posted."),
+    'setter callback' => 'drupal_property_verbatim_set',
+  );
+  $properties['changed'] = array(
+    'label' => t("Date changed"),
+    'type' => 'date',
+    'description' => t("The date the node was most recently updated."),
+  );
+  $properties['author-name'] = array(
+    'label' => t("Author name"),
+    'description' => t("The node author's name."),
+    'getter callback' => 'node_get_properties',
+  );
+  $properties['author'] = array(
+    'label' => t("Author"),
+    'type' => 'user',
+    'description' => t("The author of the node."),
+    'getter callback' => 'node_get_properties',
+  );
+
+  return $return;
+}
+
diff --git modules/node/node.info modules/node/node.info
index 6a690d2..0550c19 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.entity.inc
 required = TRUE
diff --git modules/node/node.module modules/node/node.module
index 8c352d9..1d9e217 100644
--- modules/node/node.module
+++ modules/node/node.module
@@ -174,42 +174,6 @@ function node_cron() {
 }
 
 /**
- * Implement hook_entity_info().
- */
-function node_entity_info() {
-  $return = array(
-    'node' => array(
-      'label' => t('Node'),
-      'controller class' => 'NodeController',
-      'base table' => 'node',
-      'revision table' => 'node_revision',
-      'fieldable' => TRUE,
-      'object keys' => array(
-        'id' => 'nid',
-        'revision' => 'vid',
-        'bundle' => 'type',
-      ),
-      // Node.module handles its own caching.
-      // 'cacheable' => FALSE,
-      'bundles' => array(),
-    ),
-  );
-  // Bundles must provide a human readable name so we can create help and error
-  // messages, and the path to attach Field admin pages to.
-  foreach (node_type_get_names() as $type => $name) {
-    $return['node']['bundles'][$type] = array(
-      'label' => $name,
-      'admin' => array(
-        'path' => 'admin/structure/types/manage/' . str_replace('_', '-', $type),
-        'access arguments' => array('administer content types'),
-      ),
-    );
-  }
-  return $return;
-}
-
-
-/**
  * Implement hook_field_build_modes().
  */
 function node_field_build_modes($obj_type) {
@@ -3062,6 +3026,32 @@ function node_requirements($phase) {
 }
 
 /**
+ * Callback for getting node properties.
+ * @see node_entity_info()
+ */
+function node_get_properties($node, array $options, $name, $entity_type) {
+
+  switch ($name) {
+    case 'type-name':
+      $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);
+
+    case 'edit-url':
+      return url('node/' . $node->nid . '/edit', $options);
+
+    case 'author-name':
+      $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);
+  }
+}
+
+/**
  * Controller class for nodes.
  *
  * This extends the DrupalDefaultEntityController class, adding required
diff --git modules/node/node.tokens.inc modules/node/node.tokens.inc
deleted file mode 100644
index a952e9e..0000000
--- modules/node/node.tokens.inc
+++ /dev/null
@@ -1,204 +0,0 @@
-<?php
-// $Id: node.tokens.inc,v 1.3 2009/09/30 18:37:30 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'] = $options['language'];
-    $language_code = $options['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;
-      }
-    }
-
-    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/poll/poll.info modules/poll/poll.info
index 844f46d..a25e1f6 100644
--- modules/poll/poll.info
+++ modules/poll/poll.info
@@ -7,5 +7,4 @@ core = 7.x
 files[] = poll.module
 files[] = poll.pages.inc
 files[] = poll.install
-files[] = poll.test
-files[] = poll.tokens.inc
+files[] = poll.test
\ No newline at end of file
diff --git modules/poll/poll.module modules/poll/poll.module
index 3bb5fc0..c3998e8 100644
--- modules/poll/poll.module
+++ modules/poll/poll.module
@@ -893,3 +893,71 @@ function poll_user_cancel($edit, $account, $method) {
   }
 }
 
+/**
+ * Implement hook_entity_info_alter().
+ */
+function poll_entity_info_alter(&$entity_info) {
+  $properties = &$entity_info['node']['bundles']['poll']['properties'];
+
+  $properties['poll-votes'] = array(
+    'label' => t("Poll votes"),
+    'description' => t("The number of votes that have been cast on a poll node."),
+    'type' => 'integer',
+  );
+  $properties['poll-winner'] = array(
+    'label' => t("Poll winner"),
+    'description' => t("The winning poll answer."),
+  );
+  $properties['poll-winner-votes'] = array(
+    'label' => t("Poll winner votes"),
+    'description' => t("The number of votes received by the winning poll answer."),
+    'type' => 'integer',
+  );
+  $properties['poll-winner-percent'] = array(
+    'label' => t("Poll winner percent"),
+    'description' => t("The percentage of votes received by the winning poll answer."),
+  );
+  $properties['poll-duration'] = array(
+    'label' => t("Poll duration"),
+    'description' => t("The length of time the poll node is set to run."),
+  );
+}
+
+/**
+ * Callback for getting poll properties.
+ * @see poll_entity_info_alter()
+ */
+function poll_node_get_properties($node, array $options, $name) {
+  $total_votes = 0;
+  $highest_votes = 0;
+  foreach ($node->choice as $choice) {
+    if ($choice['chvotes'] > $highest_votes) {
+      $winner = $choice;
+      $highest_votes = $choice['chvotes'];
+    }
+    $total_votes = $total_votes + $choice['chvotes'];
+  }
+
+  switch ($name) {
+    case 'poll-votes':
+      return $total_votes;
+
+    case 'poll-winner-votes':
+      return isset($winner) ? $winner['chvotes'] : 0;
+
+    case 'poll-winner':
+      if (isset($winner)) {
+        return $options['sanitize'] ? filter_xss($winner['chtext']) : $winner['chtext'];
+      }
+      throw new DrupalPropertyWrapperException('There is no poll winner yet.');
+
+    case 'poll-winner-percent':
+      if (isset($winner)) {
+        return number_format(($winner['chvotes'] / $total_votes) * 100, 0);
+      }
+      throw new DrupalPropertyWrapperException('There is no poll winner yet.');
+
+    case 'poll-duration':
+      return format_interval($node->runtime, 1, isset($options['language']) ? $options['language']->language : NULL);
+  }
+}
diff --git modules/poll/poll.tokens.inc modules/poll/poll.tokens.inc
deleted file mode 100644
index dc1fb01..0000000
--- modules/poll/poll.tokens.inc
+++ /dev/null
@@ -1,91 +0,0 @@
-<?php
-// $Id: poll.tokens.inc,v 1.1 2009/08/19 20:19:36 dries Exp $
-
-/**
- * @file
- * Builds placeholder replacement tokens for values specific to Poll nodes.
- */
-
-/**
- * Implement hook_token_info().
- */
-function poll_token_info() {
-  $node['poll-votes'] = array(
-    'name' => t("Poll votes"),
-    'description' => t("The number of votes that have been cast on a poll node."),
-  );
-  $node['poll-winner'] = array(
-    'name' => t("Poll winner"),
-    'description' => t("The winning poll answer."),
-  );
-  $node['poll-winner-votes'] = array(
-    'name' => t("Poll winner votes"),
-    'description' => t("The number of votes received by the winning poll answer."),
-  );
-  $node['poll-winner-percent'] = array(
-    'name' => t("Poll winner percent"),
-    'description' => t("The percentage of votes received by the winning poll answer."),
-  );
-  $node['poll-duration'] = array(
-    'name' => t("Poll duration"),
-    'description' => t("The length of time the poll node is set to run."),
-  );
-
-  return array(
-    'tokens' => array('node' => $node),
-  );
-}
-
-/**
- * Implement hook_tokens().
- */
-function poll_tokens($type, $tokens, array $data = array(), array $options = array()) {
-  $url_options = array('absolute' => TRUE);
-  $sanitize = !empty($options['sanitize']);
-
-  if ($type == 'node' && !empty($data['node']) && $data['node']->type == 'poll') {
-    $node = $data['node'];
-
-    $total_votes = 0;
-    $highest_votes = 0;
-    foreach ($node->choice as $choice) {
-      if ($choice['chvotes'] > $highest_votes) {
-        $winner = $choice;
-        $highest_votes = $choice['chvotes'];
-      }
-      $total_votes = $total_votes + $choice['chvotes'];
-    }
-    foreach ($tokens as $name => $original) {
-      switch ($name) {
-        case 'poll-votes':
-          $replacements[$original] = $total_votes;
-          break;
-
-        case 'poll-winner':
-          if (isset($winner)) {
-            $replacements[$original] = $sanitize ? filter_xss($winner['chtext']) : $winner['chtext'];
-          }
-          break;
-
-        case 'poll-winner-votes':
-          if (isset($winner)) {
-            $replacements[$original] = $winner['chvotes'];
-          }
-          break;
-
-        case 'poll-winner-percent':
-          if (isset($winner)) {
-            $percent = ($winner['chvotes'] / $total_votes) * 100;
-            $replacements[$original] = number_format($percent, 0);
-          }
-          break;
-
-        case 'poll-duration':
-          $replacements[$original] = format_interval($node->runtime, 1, $language_code);
-          break;
-      }
-    }
-  }
-
-  return $replacements;
-}
diff --git modules/simpletest/tests/field_test.module modules/simpletest/tests/field_test.module
index 91e5013..fe1d8f0 100644
--- modules/simpletest/tests/field_test.module
+++ modules/simpletest/tests/field_test.module
@@ -64,7 +64,7 @@ function field_test_entity_info() {
   $bundles = variable_get('field_test_bundles', array('test_bundle' => array('label' => 'Test Bundle')));
   return array(
     'test_entity' => array(
-      'name' => t('Test Entity'),
+      'label' => t('Test Entity'),
       'object keys' => array(
         'id' => 'ftid',
         'revision' => 'ftvid',
@@ -76,7 +76,7 @@ function field_test_entity_info() {
     ),
     // This entity type doesn't get form handling for now...
     'test_cacheable_entity' => array(
-      'name' => t('Test Entity, cacheable'),
+      'label' => t('Test Entity, cacheable'),
       'object keys' => array(
         'id' => 'ftid',
         'revision' => 'ftvid',
@@ -358,6 +358,8 @@ function field_test_field_info() {
       ),
       'default_widget' => 'test_field_widget',
       'default_formatter' => 'field_test_default',
+      // For testing the property wrapper we map this to dates.
+      'property_type' => 'date',
     ),
   );
 }
@@ -379,7 +381,7 @@ function field_test_field_schema($field) {
     'columns' => array(
       'value' => array(
         'type' => 'int',
-        'size' => 'tiny',
+        'size' => 'big',
         'not null' => FALSE,
       ),
     ),
diff --git modules/statistics/statistics.info modules/statistics/statistics.info
index 59a1410..f7bc9f4 100644
--- modules/statistics/statistics.info
+++ modules/statistics/statistics.info
@@ -8,5 +8,4 @@ files[] = statistics.module
 files[] = statistics.admin.inc
 files[] = statistics.pages.inc
 files[] = statistics.install
-files[] = statistics.test
-files[] = statistics.tokens.inc
+files[] = statistics.test
\ No newline at end of file
diff --git modules/statistics/statistics.module modules/statistics/statistics.module
index fac5d6a..32f72e1 100644
--- modules/statistics/statistics.module
+++ modules/statistics/statistics.module
@@ -411,3 +411,48 @@ function statistics_ranking() {
 function statistics_update_index() {
   variable_set('node_cron_views_scale', 1.0 / max(1, db_query('SELECT MAX(totalcount) FROM {node_counter}')->fetchField()));
 }
+
+/**
+ * Implement hook_entity_info_alter().
+ */
+function statistics_entity_info_alter(&$entity_info) {
+  $properties = &$entity_info['node']['properties'];
+
+  $properties['views'] = array(
+    'label' => t("Number of views"),
+    'description' => t("The number of visitors who have read the node."),
+    'type' => 'integer',
+    'getter callback' => 'statistics_node_get_properties',
+  );
+  $properties['day-views'] = array(
+    'label' => t("Views today"),
+    'description' => t("The number of visitors who have read the node today."),
+    'type' => 'integer',
+    'getter callback' => 'statistics_node_get_properties',
+  );
+  $properties['last-view'] = array(
+    'label' => t("Last view"),
+    'description' => t("The date on which a visitor last read the node."),
+    'type' => 'date',
+    'getter callback' => 'statistics_node_get_properties',
+  );
+}
+
+/**
+ * Callback for getting statistics properties.
+ * @see statistics_entity_info_alter()
+ */
+function statistics_node_get_properties($node, array $options, $name) {
+  $statistics = statistics_get($node->nid);
+
+  switch ($name) {
+    case 'views':
+      return $statistics['totalviews'];
+
+    case 'day-views':
+      return $statistics['dayviews'];
+
+    case 'last-view':
+      return $statistics['timestamp'];
+  }
+}
diff --git modules/statistics/statistics.tokens.inc modules/statistics/statistics.tokens.inc
deleted file mode 100644
index 7f1cbe7..0000000
--- modules/statistics/statistics.tokens.inc
+++ /dev/null
@@ -1,63 +0,0 @@
-<?php
-// $Id: statistics.tokens.inc,v 1.1 2009/08/19 20:19:36 dries Exp $
-
-/**
- * @file
- * Builds placeholder replacement tokens for node visitor statistics.
- */
-
-/**
- * Implement hook_token_info().
- */
-function statistics_token_info() {
-  $node['views'] = array(
-    'name' => t("Number of views"),
-    'description' => t("The number of visitors who have read the node."),
-  );
-  $node['day-views'] = array(
-    'name' => t("Views today"),
-    'description' => t("The number of visitors who have read the node today."),
-  );
-  $node['last-view'] = array(
-    'name' => t("Last view"),
-    'description' => t("The date on which a visitor last read the node."),
-    'type' => 'date',
-  );
-
-  return array(
-    'tokens' => array('node' => $node),
-  );
-}
-
-/**
- * Implement hook_tokens().
- */
-function statistics_tokens($type, $tokens, array $data = array(), array $options = array()) {
-  $url_options = array('absolute' => TRUE);
-
-  if ($type == 'node' & !empty($data['node'])) {
-    $node = $data['node'];
-
-    foreach ($tokens as $name => $original) {
-      if ($name == 'views') {
-        $statistics = statistics_get($node->nid);
-        $replacements[$original] = $statistics['totalviews'];
-      }
-      elseif ($name == 'views-today') {
-        $statistics = statistics_get($node->nid);
-        $replacements[$original] = $statistics['dayviews'];
-      }
-      elseif ($name == 'last-view') {
-        $statistics = statistics_get($node->nid);
-        $replacements[$original] = format_date($statistics['timestamp']);
-      }
-    }
-
-    if ($created_tokens = token_find_with_prefix($tokens, 'last-view')) {
-      $statistics = statistics_get($node->nid);
-      $replacements += token_generate('date', $created_tokens, array('date' => $statistics['timestamp']), $options);
-    }
-  }
-
-  return $replacements;
-}
diff --git modules/system/system.api.php modules/system/system.api.php
index df63d14..71eca8d 100644
--- modules/system/system.api.php
+++ modules/system/system.api.php
@@ -17,13 +17,25 @@
  * Inform the system about one or more entity types (i.e., object types that
  * can be loaded via entity_load() and, optionally, to which fields can be
  * attached).
+ * Optionally one can add metadata about the properties of an entity, which is
+ * used by the DrupalEntityPropertyWrapper, see drupal_get_property_wrapper().
+ * Additional properties can be grouped by "entity tags". In turn entities
+ * having those properties should be tagged accordingly, thus the properties
+ * are automatically picked up by the property wrapper. See book_node_load() for
+ * an example of how to tag an entity.
+ * hook_datatype_info() may be used to specify per data type defaults for
+ * property info, for an example see system_datatype_info(). Apart from that
+ * modules are supposed to build upon this hook to add further entity or entity
+ * property related information.
  *
  * @see entity_load()
  * @see hook_entity_info_alter()
+ * @see drupal_get_property_wrapper()
+ * @see hook_datatype_info()
  *
  * @return
  *   An array whose keys are entity type names and whose values identify
- *   properties of those types that the  system needs to know about:
+ *   info about those types that the system needs to know about:
  *
  *   name: The human-readable name of the type.
  *   controller class: The name of the class that is used to load the objects.
@@ -65,6 +77,7 @@
  *     Keys are bundles machine names, as found in the objects' 'bundle'
  *     property (defined in the 'object keys' entry above).
  *     - label: The human-readable name of the bundle.
+ *     - description: A human-readable description of the bundle.
  *     - admin: An array of information that allow Field UI pages (currently
  *       implemented in a contributed module) to attach themselves to the
  *       existing administration pages for the bundle.
@@ -78,6 +91,42 @@
  *       - access callback: As in hook_menu(). 'user_access' will be assumed if
  *         no value is provided.
  *       - access arguments: As in hook_menu().
+ *     - 'default tags': (optional) An array of entity tags that are usually
+ *       attached to that bundle.
+ *     - properties: An array describing the properties specific to this bundle
+ *       supporting the same keys as usual entity properties below.
+ *   - properties: An array describing the properties of an entity keyed by
+ *     property name defaulting to the info specified in hook_datatype_info().
+ *     - label: The human-readable name of the property.
+ *     - description: Optionally, a human-readable description of the property.
+ *     - type: The type of the property, common types are entity types, 'text',
+ *       'integer', 'decimal' and 'date'. Multiple valued properties should use
+ *       the type 'list<anotherType>' and pass an numerical indexed array as
+ *       data. Defaults to 'text'.
+ *     - 'getter callback': A callback for retrieving the value of a property.
+ *       For an example have a look at system_get_properties().
+ *     - 'setter callback': A callback for setting the value of a property, for
+ *       an example have a look at drupal_property_verbatim_set(),
+ *     - sanitize: An optional function for sanitizing textual values. For
+ *       'text' properties without a 'getter callback' this defaults to
+ *       'check_plain'. Else this is unset as default, as usually the getter
+ *       callback cares about sanitizing too.
+ *     - formats: (optional) Specifies further formats specific to that
+ *       property, see hook_datatype_info() for details.
+ *     - 'default format': Allows overriding the usual default format per
+ *       property.
+ *     - bundle: If this property references another entity, this can be used
+ *       to specify the bundle of the referenced entity beforehand.
+ *     - tags: If this property references another entity, this can be used to
+ *       specify an array of tags of the referenced entity beforehand.
+ *   - 'default tags': (optional) An array of entity tags that are usually
+ *     attached to this entity.
+ *   - tags: An array describing all entity tags for this entity type. Keys are
+ *     the actual tag names.
+ *     - label: The human-readable name of the entity tag.
+ *     - description: A human-readable description of the entity tag.
+ *   - 'name property': Specifies the property used to get the name of an
+ *     entity. If there is a property 'name', it's used by default.
  */
 function hook_entity_info() {
   $return = array(
@@ -91,11 +140,32 @@ function hook_entity_info() {
       'bundle key' => 'type',
       // Node.module handles its own caching.
       // 'cacheable' => FALSE,
-      // Bundles must provide human readable name so
-      // we can create help and error messages about them.
-      'bundles' => node_type_get_names(),
     ),
   );
+  // Bundles must provide a human readable name so we can create help and error
+  // messages, and the path to attach Field admin pages to.
+  foreach (node_type_get_names() as $type => $name) {
+    $return['node']['bundles'][$type] = array(
+      'label' => $name,
+      'admin' => array(
+        'path' => 'admin/structure/node-type/' . str_replace('_', '-', $type),
+        'access arguments' => array('administer content types'),
+      ),
+    );
+  }
+  // Add meta-data about the basic node properties.
+  $properties = &$return['node']['properties'];
+  
+  $properties['nid'] = array(
+    'label' => t("Node ID"),
+    'type' => 'integer',
+    'description' => t("The unique ID of the node."),
+  );
+  $properties['type-name'] = array(
+    'label' => t("Content type name"),
+    'description' => t("The human-readable name of the node type."),
+    'getter callback' => 'node_get_properties',
+  );
   return $return;
 }
 
@@ -118,6 +188,57 @@ function hook_entity_info_alter(&$entity_info) {
 }
 
 /**
+ * Provides info about basic data types like ways to format it.
+ *
+ * The formats specified here can be applied using token_format(). Also any
+ * values specified here serve as defaults for properties of this datatype
+ * specified in hook_entity_info().
+ *
+ * @see hook_entity_info()
+ * @see hook_entity_info_alter()
+ * @see hook_datatype_info_alter()
+ * @see drupal_get_datatype_info()
+ * @see token_format()
+ *
+ * @return
+ *   An array describing the data type keyed by type name. Usual keys are:
+ *   - formats: An array of formats for the data type keyed by format name. Each
+ *     entry has to be an array with the possible keys:
+ *     - label: The human-readable label of the format.
+ *     - description: The human-readable descriptoin of the format.
+ *     - callback: A callback that applies the format to the value. For an
+ *       example have a look at system_format_date().
+ *   - 'default format': The name of the format which should be used be used by
+ *     default.
+ *
+ *  Further any module introducing new attributes for the property info may use
+ *  this hook to provide sane defaults.
+ */
+function hook_datatype_info() {
+  // Specify date related formats.
+  $date['medium'] = array(
+    'label' => t("Medium format"),
+    'description' => t("A date in 'medium' format. (%date)", array('%date' => format_date(REQUEST_TIME, 'medium'))),
+    'callback' => 'system_format_date',
+  );
+  // ..
+  return array('date' => array('default format' => 'medium', 'formats' => $date));
+}
+
+/**
+ * Allows altering info about basic data types.
+ *
+ * @see hook_datatype_info()
+ *
+ * @param $type_info
+ *   An array describing the data type keyed by type name, as specified in
+ *   hook_datatype_info().
+ */
+function hook_datatype_info_alter(&$type_info) {
+  $type_info['date']['default format'] = 'long';
+}
+
+/**
  * Perform periodic actions.
  *
  * This hook will only be called if cron.php is run (e.g. by crontab).
diff --git modules/system/system.entity.inc modules/system/system.entity.inc
new file mode 100644
index 0000000..1879935
--- /dev/null
+++ modules/system/system.entity.inc
@@ -0,0 +1,113 @@
+<?php
+// $Id: system.tokens.inc,v 1.2 2009/08/25 10:27:15 dries Exp $
+
+/**
+ * @file
+ * Provides info about system-wide entities.
+ */
+
+/**
+ * Implement hook_entity_info().
+ */
+function system_entity_info() {
+  // Create a entity for dealing with drupal variables as properties.
+  $types['system'] = array(
+    'label' => t("System information"),
+  );
+  
+  $properties = &$types['system']['properties'];
+  
+  $properties['name'] = array(
+    'label' => t("Name"),
+    'description' => t("The name of the site."),
+    'getter callback' => 'system_get_properties',
+    'sanitize' => 'check_plain',
+  );
+  $properties['slogan'] = array(
+    'label' => t("Slogan"),
+    'description' => t("The slogan of the site."),
+    'getter callback' => 'system_get_properties',
+    'sanitize' => 'check_plain',
+  );
+  $properties['mission'] = array(
+    'label' => t("Mission"),
+    'description' => t("The optional 'mission' of the site."),
+    'getter callback' => 'system_get_properties',
+    'sanitize' => 'filter_xss',
+  );
+  $properties['mail'] = array(
+    'label' => t("Email"),
+    'description' => t("The administrative email address for the site."),
+    'getter callback' => 'system_get_properties',
+  );
+  $properties['url'] = array(
+    'label' => t("URL"),
+    'description' => t("The URL of the site's front page."),
+    'getter callback' => 'system_get_properties',
+  );
+  $properties['login-url'] = array(
+    'label' => t("Login page"),
+    'description' => t("The URL of the site's login page."),
+    'getter callback' => 'system_get_properties',
+  );
+
+  // Describe files.
+  $types['file'] = array(
+    'label' => t('File'),
+    'base table' => 'file',
+    'object keys' => array(
+      'id' => 'fid',
+    ),
+    'static cache' => FALSE,
+  );
+  
+  $properties = &$types['file']['properties'];
+
+  $properties['fid'] = array(
+    'label' => t("File ID"),
+    'description' => t("The unique ID of the uploaded file."),
+  );
+  $properties['uid'] = array(
+    'label' => t("User ID"),
+    'description' => t("The unique ID of the user who owns the file."),
+  );
+  $properties['name'] = array(
+    'label' => t("File name"),
+    'description' => t("The name of the file on disk."),
+    'getter callback' => 'system_get_file_properties',
+  );
+  $properties['path'] = array(
+    'label' => t("Path"),
+    'description' => t("The location of the file on disk."),
+    'getter callback' => 'system_get_file_properties',
+  );
+  $properties['mime'] = array(
+    'label' => t("MIME type"),
+    'description' => t("The MIME type of the file."),
+    'getter callback' => 'system_get_file_properties',
+  );
+  $properties['size'] = array(
+    'label' => t("File size"),
+    'description' => t("The size of the file, in kilobytes."),
+    'getter callback' => 'system_get_file_properties',
+    'type' => 'integer',
+  );
+  $properties['url'] = array(
+    'label' => t("URL"),
+    'description' => t("The web-accessible URL for the file."),
+    'getter callback' => 'system_get_file_properties',
+  );
+  $properties['timestamp'] = array(
+    'label' => t("Timestamp"),
+    'description' => t("The date the file was most recently changed."),
+    'type' => 'date',
+  );
+  $properties['owner'] = array(
+    'label' => t("Owner"),
+    'description' => t("The user who originally uploaded the file."),
+    'type' => 'user',
+    'getter callback' => 'system_get_file_properties',
+  );
+  
+  return $types;
+}
diff --git modules/system/system.info modules/system/system.info
index 7183f87..ede880f 100644
--- modules/system/system.info
+++ modules/system/system.info
@@ -11,6 +11,6 @@ files[] = image.gd.inc
 files[] = system.install
 files[] = system.test
 files[] = system.tar.inc
-files[] = system.tokens.inc
+files[] = system.entity.inc
 files[] = mail.sending.inc
 required = TRUE
diff --git modules/system/system.module modules/system/system.module
index ddd74ca..fff6d7c 100644
--- modules/system/system.module
+++ modules/system/system.module
@@ -263,22 +263,6 @@ function system_rdf_namespaces() {
 }
 
 /**
- * Implement hook_entity_info().
- */
-function system_entity_info() {
-  return array(
-    'file' => array(
-      'label' => t('File'),
-      'base table' => 'file',
-      'object keys' => array(
-        'id' => 'fid',
-      ),
-      'static cache' => FALSE,
-    ),
-  );
-}
-
-/**
  * Implement hook_element_info().
  */
 function system_element_info() {
@@ -2747,6 +2731,145 @@ function system_image_toolkits() {
 }
 
 /**
+ * Implement hook_datatype_info().
+ */
+function system_datatype_info() {
+  $types['text'] = array(
+    // By default don't apply any format to text values.
+    'default format' => NULL,
+  );
+  $types['list'] = array(
+    'default format' => 'concatenated',
+    'formats' => array(
+      'concatenated' => array(
+        'label' => t("Concatenated using commas"),
+        'description' => t("Concatenates the list items using commas as separator. (%list)", array('%list' => system_format_list(array('Item 1', 'Item 2')))),
+        'callback' => 'system_format_list',
+      ),
+    ),
+  );
+  $types['date'] = array(
+    'default format' => 'medium',
+    'formats' => array(),
+  );
+
+  // Specify date related formats.
+  $formats = &$types['date']['formats'];
+  
+  $formats['short'] = array(
+    'label' => t("Short format"),
+    'description' => t("A date in 'short' format. (%date)", array('%date' => format_date(REQUEST_TIME, 'short'))),
+    'callback' => 'system_format_date',
+  );
+  $formats['medium'] = array(
+    'label' => t("Medium format"),
+    'description' => t("A date in 'medium' format. (%date)", array('%date' => format_date(REQUEST_TIME, 'medium'))),
+    'callback' => 'system_format_date',
+  );
+  $formats['long'] = array(
+    'label' => t("Long format"),
+    'description' => t("A date in 'long' format. (%date)", array('%date' => format_date(REQUEST_TIME, 'long'))),
+    'callback' => 'system_format_date',
+  );
+  $formats['since'] = array(
+    'label' => t("Time-since"),
+    'description' => t("A data in 'time-since' format. (%date)", array('%date' => format_interval(REQUEST_TIME - 360, 2))),
+    'callback' => 'system_format_date',
+  );
+  $formats['raw'] = array(
+    'label' => t("Raw timestamp"),
+    'description' => t("A date in UNIX timestamp format (%date)", array('%date' => REQUEST_TIME)),
+    'callback' => 'system_format_date',
+  );
+  return $types;
+}
+
+/**
+ * Callback for formating date tokens.
+ * @see system_datatype_info()
+ */
+function system_format_date($date, $format, $options) {
+  $langcode = isset($options['language']) ? $options['language']->language : NULL;
+
+  switch ($format) {
+    case 'raw':
+      return filter_xss($date);
+
+    case 'short':
+    case 'medium':
+    case 'long':
+      return format_date($date, $format, '', NULL, $langcode);
+
+    case 'since':
+      return format_interval((REQUEST_TIME - $date), 2, $langcode);
+  }
+}
+
+/**
+ * Callback for formating a list.
+ * @see system_datatype_info()
+ */
+function system_format_list(array $list, $format = NULL, $options = array(), $context = array()) {
+  if (empty($context['wrapper'])) {
+    return implode(', ', $list);
+  }
+  // Use the wrapper instead of the values, so that a possible default format
+  // of the list items is applied.
+  $output = array();
+  foreach ($context['wrapper'] as $key) {
+    $output[] = (string)$context['wrapper'][$key];
+  }
+  return implode(', ', $output);
+}
+
+/**
+ * Callback for getting system properties.
+ * @see system_entity_info()
+ */
+function system_get_properties($data = FALSE, array $options, $name) {
+  switch ($name) {
+    case 'name':
+      return variable_get('site_name', 'Drupal');
+
+    default:
+      return variable_get('site_' . $name, '');
+
+    case 'url':
+      return url('<front>', $options);
+
+    case 'login-url':
+      return url('user', $options);
+  }
+}
+
+/**
+ * Callback for getting file properties.
+ * @see system_entity_info()
+ */
+function system_get_file_properties($data, array $options, $name) {
+  switch ($name) {
+    case 'name':
+      return $options['sanitize'] ? check_plain($file->filename) : $file->filename;
+
+    case 'path':
+      return $options['sanitize'] ? filter_xss($file->filepath) : $file->filepath;
+
+    case 'mime':
+      return $options['sanitize'] ? filter_xss($file->filemime) : $file->filemime;
+
+    case 'size':
+      return $file->filesize;
+
+    case 'url':
+      return url(file_create_url($file->filepath), $options);
+
+    case 'owner':
+      return user_load($file->uid);
+  }
+}
+
+
+/**
  * 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 1afa83d..d78b45e 100644
--- modules/system/system.test
+++ modules/system/system.test
@@ -1073,6 +1073,210 @@ class SystemThemeFunctionalTest extends DrupalWebTestCase {
   }
 }
 
+/**
+ * Test entity property wrapper.
+ */
+class DrupalPropertyWrapperTestCase extends DrupalWebTestCase {
+  public static function getInfo() {
+    return array(
+      'name' => 'Property wrappers',
+      'description' => 'Test using the drupal property wrappers.',
+      'group' => 'System',
+    );
+  }
+  
+  function setUp() {
+    parent::setUp('book', 'field_test');
+    // Clear the node load cache.
+    node_load_multiple(array(), array(), TRUE);
+
+    // Add a field with multiple values for testing list properties.
+    $this->field_name = drupal_strtolower($this->randomName() . '_field_name');
+    $this->obj_type = 'node';
+    $this->field = array(
+      'field_name' => $this->field_name,
+      'type' => 'test_field',
+      'cardinality' => 4,
+      'translatable' => TRUE,
+      'settings' => array(
+        'test_hook_in' => FALSE,
+      ),
+    );
+    field_create_field($this->field);
+
+    $this->instance = array(
+      'field_name' => $this->field_name,
+      'bundle' => 'article',
+      'label' => $this->randomName() . '_label',
+      'description' => $this->randomName() . '_description',
+      'weight' => mt_rand(0, 127),
+      'settings' => array(
+        'test_instance_setting' => $this->randomName(),
+      ),
+      'widget' => array(
+        'type' => 'test_field_widget',
+        'label' => 'Test Field',
+        'settings' => array(
+          'test_widget_setting' => $this->randomName(),
+        ),
+      ),
+    );
+    field_create_instance($this->instance);
+  }
+
+  /**
+   * Creates a user and a node, then tests getting the properties.
+   */
+  function testEntityPropertyWrapper() {
+    $account = $this->drupalCreateUser();
+    // For testing sanitizing give the user a malicious user name
+    $account = user_save($account, array('name' => '<b>BadName</b>'));
+    $node = $this->drupalCreateNode(array('uid' => $account->uid, 'name' => $account->name, 'title' => '<b>Is it bold?<b>'));
+    
+    // First test without sanitizing.
+    $wrapper = drupal_get_property_wrapper('node', $node);
+    
+    $this->assertEqual($node->title, $wrapper->title, 'Getting property.');
+    $this->assertEqual($node->name, $wrapper->{'author-name'}, '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->{'author-name'}, '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->assertFalse(array_diff(array_keys($type_info['properties']), iterator_to_array($wrapper->getIterator())), 'Iterator is working.');
+
+  }
+  
+  /**
+   * Tests applying formats with the wrapper.
+   */
+  function testPropertyFormatWrapper() {
+    $date = REQUEST_TIME;
+    $wrapper = drupal_get_property_wrapper('date', $date);
+    
+    $this->assertEqual(format_date($date), (string)$wrapper, 'Apply the default format.');
+    $this->assertEqual(format_date($date, 'long'), $wrapper->long, 'Apply a certain format.');
+
+    // Test applying a not existing format.
+    try {
+      echo $wrapper->dummy;
+      $this->fail('Testing a not existing format.');
+    }
+    catch (DrupalPropertyWrapperException $e) {
+      $this->pass('Testing a not existing format.');
+    }
+  }
+  
+  /**
+   * Test basic field API support.
+   */
+  function testFieldPropertyWrappers() {
+    // Test the body field and so if auto-adding bundle properties works.
+    $body = array();
+    $body[FIELD_LANGUAGE_NONE][0] = array('value' => '<b>The body.</b>', 'summary' => 'The summary.');
+    $node = $this->drupalCreateNode(array('body' => $body));
+    $wrapper = drupal_get_property_wrapper('node', $node, array('sanitize' => FALSE));
+    
+    $this->assertEqual('<b>The body.</b>', $wrapper->body->get(), 'Getting body property.');
+    $this->assertEqual("<p>The summary.</p>\n", $wrapper->body, "Default field formatter applied.");
+    $this->assertEqual("The body.\n", $wrapper->body->text_plain, "Specified field formatter applied.");
+
+    $wrapper->body = "<b>The second body.</b>";
+    $this->assertEqual("The second body.\n", $wrapper->body->text_plain, "Setting a field value and reading it again.");
+  }
+  
+  /**
+   * Test supporting multi-valued fields.
+   */
+  function testListPropertyWrappers() {
+    $name = $this->field_name;
+    $values = array();
+    $values[FIELD_LANGUAGE_NONE][0] = array('value' => REQUEST_TIME);
+    $values[FIELD_LANGUAGE_NONE][1] = array('value' => strtotime('2009-09-05'));
+    $values[FIELD_LANGUAGE_NONE][2] = array('value' => strtotime('2009-08-05'));
+    
+    $node = $this->drupalCreateNode(array('type' => 'article', $name => $values, 'title' => '<b>Bold</b>'));
+    $wrapper = drupal_get_property_wrapper('node', $node, array('sanitize' => FALSE));
+    
+    $this->assertEqual(REQUEST_TIME, $wrapper->{$name}[0]->get(), 'Getting array entry.');
+    $this->assertEqual(strtotime('2009-09-05'), $wrapper->{$name}->{1}->get(), 'Getting array entry.');
+    $this->assertEqual(3, count($wrapper->{$name}->get()), 'Getting the whole array.');
+    $this->assertEqual('dummy test string|' . REQUEST_TIME, $wrapper->{$name}[0], "Default field formatter applied.");
+    $this->assertEqual(format_date(strtotime('2009-09-05'), 'short'), $wrapper->{$name}[1]->short, "Specified field formatter applied.");
+    // Test formating the whole field.
+    foreach (array(REQUEST_TIME, strtotime('2009-09-05'), strtotime('2009-08-05')) as $value) {
+      $output[] = 'dummy test string|' . $value;
+    }
+    $this->assertEqual(implode(', ', $output), (string)$wrapper->{$name}, "Multiple value default format applied.");
+    
+    // Test iterator
+    $this->assertEqual(iterator_to_array($wrapper->{$name}->getIterator()), array(0, 1, 2), 'Iterator is working.');
+    
+    // Make sure changing the array changes the actual entity property.
+    $wrapper->{$name}[0] = strtotime('2009-10-05');
+    unset($wrapper->{$name}[1], $wrapper->{$name}[2]);
+    $this->assertEqual($wrapper->{$name}->get(), array(strtotime('2009-10-05')), 'Setting multiple property values.');
+
+    // Test setting an arbitrary list item.
+    $wrapper = drupal_get_property_wrapper('list<date>', array(0 => REQUEST_TIME));
+    $wrapper[1] = strtotime('2009-09-05');
+    $this->assertEqual($wrapper->get(), array(REQUEST_TIME, strtotime('2009-09-05')), 'Setting a list item.');
+    $formatted = format_date(REQUEST_TIME) . ', ' . format_date(strtotime('2009-09-05'));
+    $this->assertEqual($formatted, (string)$wrapper, 'Formatting a list of values.');
+
+    // Test converting to string.
+    $wrapper = drupal_get_property_wrapper('list<node>', array($node, $node), array('sanitize' => TRUE));
+    $this->assertEqual((string)$wrapper, check_plain($node->title) . ', ' . check_plain($node->title), 'Test converting list to string.');
+  }
+  
+  /**
+   * Test entity tags by using the 'book' default tag of the book module.
+   */
+  function testTaggedEntities() {
+    $node = $this->drupalCreateNode(array('title' => "Book 1", 'type' => 'book'));
+    $node2 = $this->drupalCreateNode(array('title' => "Book page", 'type' => 'book', 'book' => array('bid' => $node->nid)));
+    $node3 = $this->drupalCreateNode(array('title' => "Book page", 'type' => 'book', 'book' => array('bid' => $node2->nid)));
+
+    // Test whether the tag is detected.
+    $wrapper = drupal_get_property_wrapper('node', $node2);
+    $this->assertEqual("Book 1", $wrapper->book->title, "Entity is tagged.");
+
+    // Make sure the returned book has the book properties too.
+    $wrapper = drupal_get_property_wrapper('node', $node3);
+    $this->assertEqual("Book 1", $wrapper->book->book->title, "Retrieved entity is tagged.");
+  }
+
+}
+
+
 
 /**
  * Test the basic queue functionality.
@@ -1176,7 +1380,13 @@ class TokenReplaceTestCase extends DrupalWebTestCase {
       'group' => 'System',
     );
   }
-
+  
+  function setUp() {
+    parent::setUp('book');
+    // Clear the node load cache.
+    node_load_multiple(array(), array(), TRUE);
+  }
+  
   /**
    * Creates a user and a node, then tests the tokens generated from them.
    */
@@ -1190,6 +1400,7 @@ 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 .= '[node:changed]';       // Last update time using the default format.
     $source .= '[current-user:name]';  // Current user's name
     $source .= '[user:name]';          // No user passed in, should be untouched
     $source .= '[date:short]';         // Short date format of REQUEST_TIME
@@ -1198,24 +1409,59 @@ class TokenReplaceTestCase extends DrupalWebTestCase {
     $target  = check_plain($node->title);
     $target .= check_plain($account->name);
     $target .= format_interval(REQUEST_TIME - $node->created, 2, $language->language);
+    $target .= format_date($node->changed, 'medium');
     $target .= check_plain($user->name);
     $target .= '[user:name]';
     $target .= format_date(REQUEST_TIME, 'short', '', NULL, $language->language);
     $target .= '[bogus:token]';
 
-    $result = token_replace($source, array('node' => $node), array('language' => $language));
+    $result = token_replace($source, array('node' => $node), array('language' => $language, 'clear' => FALSE));
+    $this->assertFalse(strcmp($target, $result), t('Basic placeholder tokens replaced.'));
 
     // Check that the results of token_generate are sanitized properly. This does NOT
     // test the cleanliness of every token -- just that the $sanitize flag is being
     // passed properly through the call stack and being handled correctly by a 'known'
     // token, [node:title].
-    $this->assertFalse(strcmp($target, $result), t('Basic placeholder tokens replaced.'));
-    
-    $raw_tokens = array('title' => '[node:title]');
-    $generated = token_generate('node', $raw_tokens, array('node' => $node));
+    $raw_tokens = array('node' => array('title' => '[node:title]'));
+    $generated = token_generate($raw_tokens, array('node' => $node));
     $this->assertFalse(strcmp($generated['[node:title]'], check_plain($node->title)), t('Token sanitized.'));
 
-    $generated = token_generate('node', $raw_tokens, array('node' => $node), array('sanitize' => FALSE));
+    $generated = token_generate($raw_tokens, array('node' => $node), array('sanitize' => FALSE));
     $this->assertFalse(strcmp($generated['[node:title]'], $node->title), t('Unsanitized token generated properly.'));
   }
+
+  
+  /**
+   * Tests using bundle/tag specific tokens and specifying custom data types.
+   */
+  function testContextualTokens() {
+    // Create the initial objects.
+    $body[FIELD_LANGUAGE_NONE][0] = array('value' => '<b>The body.</b>');
+    $node = $this->drupalCreateNode(array('title' => "Book 1", 'type' => 'book', 'body' => $body));
+    $node2 = $this->drupalCreateNode(array('title' => "Book page", 'type' => 'book', 'book' => array('bid' => $node->nid)));
+ 
+    // Test body, which is bundle specific.
+    $source  = '[node:body]';
+    // Test book tokens, which is specific to nodes tagged as "book".
+    $source .= '[bookpage:book:title]';
+
+    $target  = "<p>The body.</p>\n";
+    $target .= "Book 1";
+
+    $result = token_replace($source, array('node' => $node, 'bookpage' => $node2), array(), array('bookpage' => 'node'));
+    $this->assertFalse(strcmp($target, $result), t('Contextual placeholder tokens replaced.'));
+  }
+  
+  /**
+   * Test multiple value tokens by using tags.
+   */
+  function testMultipleValueTokens() {
+    // Use the auto-created tags vocabulary.
+    $edit['taxonomy']['tags'][1] = '<b>tag1, tag2</b>';
+    $node = $this->drupalCreateNode($edit + array('type' => 'article'));
+    // Load from db to get the usual taxonomy structure.
+    $node = node_load($node->nid);
+    $result = token_replace('Tags: [node:taxonomy] - [node:taxonomy:0]', array('node' => $node));
+    $this->assertEqual(check_plain('Tags: <b>tag1, tag2</b> - <b>tag1'), $result, 'Multiple value tokens replaced.');
+  }
 }
diff --git modules/system/system.tokens.inc modules/system/system.tokens.inc
deleted file mode 100644
index 186e74f..0000000
--- modules/system/system.tokens.inc
+++ /dev/null
@@ -1,308 +0,0 @@
-<?php
-// $Id: system.tokens.inc,v 1.2 2009/08/25 10:27:15 dries Exp $
-
-/**
- * @file
- * Builds placeholder replacement tokens system-wide data.
- *
- * This file handles tokens for the global 'site' token type, as well as
- * 'date' and 'file' tokens.
- */
-
-/**
- * Implement hook_token_info().
- */
-function system_token_info() {
-  $types['site'] = array(
-    'name' => t("Site information"),
-    'description' => t("Tokens for site-wide settings and other global information."),
-  );
-  $types['date'] = array(
-    'name' => t("Dates"),
-    'description' => t("Tokens related to times and dates."),
-  );
-  $types['file'] = array(
-    'name' => t("Files"),
-    'description' => t("Tokens related to uploaded files."),
-    'needs-data' => 'file',
-  );
-
-  // Site-wide global tokens.
-  $site['name'] = array(
-    'name' => t("Name"),
-    'description' => t("The name of the site."),
-  );
-  $site['slogan'] = array(
-    'name' => t("Slogan"),
-    'description' => t("The slogan of the site."),
-  );
-  $site['mission'] = array(
-    'name' => t("Mission"),
-    'description' => t("The optional 'mission' of the site."),
-  );
-  $site['mail'] = array(
-    'name' => t("Email"),
-    'description' => t("The administrative email address for the site."),
-  );
-  $site['url'] = array(
-    'name' => t("URL"),
-    'description' => t("The URL of the site's front page."),
-  );
-  $site['login-url'] = array(
-    'name' => t("Login page"),
-    'description' => t("The URL of the site's login page."),
-  );
-
-  // Date related tokens.
-  $date['short'] = array(
-    'name' => t("Short format"),
-    'description' => t("A date in 'short' format. (%date)", array('%date' => format_date(REQUEST_TIME, 'short'))),
-  );
-  $date['medium'] = array(
-    'name' => t("Medium format"),
-    'description' => t("A date in 'medium' format. (%date)", array('%date' => format_date(REQUEST_TIME, 'medium'))),
-  );
-  $date['long'] = array(
-    'name' => t("Long format"),
-    'description' => t("A date in 'long' format. (%date)", array('%date' => format_date(REQUEST_TIME, 'long'))),
-  );
-  $date['custom'] = array(
-    'name' => t("Custom format"),
-    'description' => t("A date in a custom format. See !php-date for details.", array('!php-date' => l(t('the PHP documentation'), 'http://php.net/manual/en/function.date.php'))),
-  );
-  $date['since'] = array(
-    'name' => t("Time-since"),
-    'description' => t("A data in 'time-since' format. (%date)", array('%date' => format_interval(REQUEST_TIME - 360, 2))),
-  );
-  $date['raw'] = array(
-    'name' => t("Raw timestamp"),
-    'description' => t("A date in UNIX timestamp format (%date)", array('%date' => REQUEST_TIME)),
-  );
-
-
-  // File related tokens.
-  $file['fid'] = array(
-    'name' => t("File ID"),
-    'description' => t("The unique ID of the uploaded file."),
-  );
-  $file['uid'] = array(
-    'name' => t("User ID"),
-    'description' => t("The unique ID of the user who owns the file."),
-  );
-  $file['nid'] = array(
-    'name' => t("Node ID"),
-    'description' => t("The unique ID of the node the file is attached to."),
-  );
-  $file['name'] = array(
-    'name' => t("File name"),
-    'description' => t("The name of the file on disk."),
-  );
-  $file['description'] = array(
-    'name' => t("Description"),
-    'description' => t("An optional human-readable description of the file."),
-  );
-  $file['path'] = array(
-    'name' => t("Path"),
-    'description' => t("The location of the file on disk."),
-  );
-  $file['mime'] = array(
-    'name' => t("MIME type"),
-    'description' => t("The MIME type of the file."),
-  );
-  $file['size'] = array(
-    'name' => t("File size"),
-    'description' => t("The size of the file, in kilobytes."),
-  );
-  $file['path'] = array(
-    'name' => t("URL"),
-    'description' => t("The web-accessible URL for the file."),
-  );
-  $file['timestamp'] = array(
-    'name' => t("Timestamp"),
-    'description' => t("The date the file was most recently changed."),
-    'type' => 'date',
-  );
-  $file['node'] = array(
-    'name' => t("Node"),
-    'description' => t("The node the file is attached to."),
-    'type' => 'date',
-  );
-  $file['owner'] = array(
-    'name' => t("Owner"),
-    'description' => t("The user who originally uploaded the file."),
-    'type' => 'user',
-  );
-
-  return array(
-    'types' => $types,
-    'tokens' => array(
-      'site' => $site,
-      'date' => $date,
-      'file' => $file,
-    ),
-  );
-}
-
-/**
- * Implement hook_tokens().
- */
-function system_tokens($type, $tokens, array $data = array(), array $options = array()) {
-  $url_options = array('absolute' => TRUE);
-  if (isset($language)) {
-    $url_options['language'] = $language;
-  }
-  $sanitize = !empty($options['sanitize']);
-
-  $replacements = array();
-
-  if ($type == 'site') {
-    foreach ($tokens as $name => $original) {
-      switch ($name) {
-        case 'name':
-          $site_name = variable_get('site_name', 'Drupal');
-          $replacements[$original] = $sanitize ? check_plain($site_name) : $site_name;
-          break;
-
-        case 'slogan':
-          $slogan = variable_get('site_slogan', '');
-          $replacements[$original] = $sanitize ? check_plain($slogan) : $slogan;
-          break;
-
-        case 'mission':
-          $mission = variable_get('site_mission', '');
-          $replacements[$original] = $sanitize ? filter_xss($mission) : $mission;
-          break;
-
-        case 'mail':
-          $replacements[$original] = variable_get('site_mail', '');
-          break;
-
-        case 'url':
-          $replacements[$original] = url('<front>', $url_options);
-          break;
-
-        case 'login-url':
-          $replacements[$original] = url('user', $url_options);
-          break;
-      }
-    }
-  }
-
-  elseif ($type == 'date') {
-    if (empty($data['date'])) {
-      $date = REQUEST_TIME;
-    }
-    else {
-      $date = $data['date'];
-    }
-    $langcode = (isset($language) ? $language->language : NULL);
-
-    foreach ($tokens as $name => $original) {
-      switch ($name) {
-        case 'raw':
-          $replacements[$original] = filter_xss($date);
-          break;
-
-        case 'short':
-          $replacements[$original] = format_date($date, 'short', '', NULL, $langcode);
-          break;
-
-        case 'medium':
-          $replacements[$original] = format_date($date, 'medium', '', NULL, $langcode);
-          break;
-
-        case 'long':
-          $replacements[$original] = format_date($date, 'long', '', NULL, $langcode);
-          break;
-
-        case 'since':
-          $replacements[$original] = format_interval((REQUEST_TIME - $date), 2, $langcode);
-          break;
-      }
-    }
-
-    if ($created_tokens = token_find_with_prefix($tokens, 'custom')) {
-      foreach ($created_tokens as $name => $original) {
-        $replacements[$original] = format_date($date, 'custom', $name, NULL, $langcode);
-      }
-    }
-  }
-
-  elseif ($type == 'file' && !empty($data['file'])) {
-    $file = $data['file'];
-
-    foreach ($tokens as $name => $original) {
-      switch ($name) {
-        // Basic keys and values.
-        case 'fid':
-          $replacements[$original] = $file->fid;
-          break;
-
-        case 'uid':
-          $replacements[$original] = $file->uid;
-          break;
-
-        case 'nid':
-          $replacements[$original] = $file->nid;
-          break;
-
-        // Essential file data
-        case 'name':
-          $replacements[$original] = $sanitize ? check_plain($file->filename) : $file->filename;
-          break;
-
-        case 'description':
-          $replacements[$original] = $sanitize ? filter_xss($file->description) : $file->description;
-          break;
-
-        case 'path':
-          $replacements[$original] = $sanitize ? filter_xss($file->filepath) : $file->filepath;
-          break;
-
-        case 'mime':
-          $replacements[$original] = $sanitize ? filter_xss($file->filemime) : $file->filemime;
-          break;
-
-        case 'size':
-          $replacements[$original] = format_size($file->filesize);
-          break;
-
-        case 'url':
-          $replacements[$original] = url(file_create_url($file->filepath), $url_options);
-          break;
-
-        // These tokens are default variations on the chained tokens handled below.
-        case 'node':
-          if ($nid = $file->nid) {
-            $node = node_load($file->nid);
-            $replacements[$original] = $sanitize ? filter_xss($node->title) : $node->title;
-          }
-          break;
-
-        case 'timestamp':
-          $replacements[$original] = format_date($file->timestamp, 'medium', '', NULL, (isset($language) ? $language->language : NULL));
-          break;
-
-        case 'owner':
-          $account = user_load($file->uid);
-          $replacements[$original] = $sanitize ? filter_xss($user->name) : $user->name;
-          break;
-      }
-    }
-
-    if ($node_tokens = token_find_with_prefix($tokens, 'node')) {
-      $node = node_load($file->nid);
-      $replacements += token_generate('node', $node_tokens, array('node' => $node), $language, $sanitize);
-    }
-
-    if ($date_tokens = token_find_with_prefix($tokens, 'timestamp')) {
-      $replacements += token_generate('date', $date_tokens, array('date' => $file->timestamp), $language, $sanitize);
-    }
-
-    if (($owner_tokens = token_find_with_prefix($tokens, 'owner')) && $account = user_load($file->uid)) {
-      $replacements += token_generate('user', $owner_tokens, array('user' => $account), $language, $sanitize);
-    }
-  }
-
-  return $replacements;
-}
diff --git modules/taxonomy/taxonomy.entity.inc modules/taxonomy/taxonomy.entity.inc
new file mode 100644
index 0000000..fa07547
--- /dev/null
+++ modules/taxonomy/taxonomy.entity.inc
@@ -0,0 +1,146 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Provides info about the taxonomy entity.
+ */
+
+/**
+ * Implement hook_entity_info().
+ */
+function taxonomy_entity_info() {
+  $return = array(
+    'taxonomy_term' => array(
+      'label' => t('Taxonomy term'),
+      'controller class' => 'TaxonomyTermController',
+      'base table' => 'taxonomy_term_data',
+      'fieldable' => TRUE,
+      'object keys' => array(
+        'id' => 'tid',
+        'bundle' => 'vocabulary_machine_name',
+      ),
+      'bundle keys' => array(
+        'bundle' => 'machine_name',
+      ),
+      'bundles' => array(),
+    ),
+  );
+  foreach (taxonomy_vocabulary_get_names() as $machine_name => $vocabulary) {
+    $return['taxonomy_term']['bundles'][$machine_name] = array(
+      'label' => $vocabulary->name,
+      'admin' => array(
+        'path' => 'admin/structure/taxonomy/%taxonomy_vocabulary',
+        'real path' => 'admin/structure/taxonomy/' . $vocabulary->vid,
+        'bundle argument' => 3,
+        'access arguments' => array('administer taxonomy'),
+      ),
+    );
+  }
+  // Add meta-data about the basic taxonomy properties.
+  $properties = &$return['taxonomy_term']['properties'];
+  
+  $properties['tid'] = array(
+    'label' => t("Term ID"),
+    'description' => t("The unique ID of the taxonomy term."),
+    'type' => 'integer',
+  );
+  $properties['vid'] = array(
+    'label' => t("Vocabulary ID"),
+    'description' => t("The unique ID of the vocabulary the term belongs to."),
+    'setter callback' => 'drupal_property_verbatim_set',
+    'type' => 'integer',
+  );
+  $properties['name'] = array(
+    'label' => t("Name"),
+    'description' => t("The name of the taxonomy term."),
+    'setter callback' => 'drupal_property_verbatim_set',
+  );
+  $properties['description'] = array(
+    'label' => t("Description"),
+    'description' => t("The optional description of the taxonomy term."),
+    'sanitize' => 'filter_xss',
+    'setter callback' => 'drupal_property_verbatim_set',
+  );
+  $properties['node-count'] = array(
+    'label' => t("Node count"),
+    'description' => t("The number of nodes tagged with the taxonomy term."),
+    'getter callback' => 'taxonomy_term_get_properties',
+  );
+  $properties['url'] = array(
+    'label' => t("URL"),
+    'description' => t("The URL of the taxonomy term."),
+    'getter callback' => 'taxonomy_term_get_properties',
+  );
+  $properties['vocabulary'] = array(
+    'label' => t("Vocabulary"),
+    'description' => t("The vocabulary the taxonomy term belongs to."),
+    'getter callback' => 'taxonomy_term_get_properties',
+    'type' => 'vocabulary',
+  );
+  $properties['parent'] = array(
+    'label' => t("Parent term"),
+    'description' => t("The parent term of the taxonomy term, if one exists."),
+    'getter callback' => 'taxonomy_term_get_properties',
+    'type' => 'taxonomy_term',
+  );
+
+  $return['taxonomy_vocabulary'] = array(
+    'label' => t('Taxonomy vocabulary'),
+    'controller class' => 'TaxonomyVocabularyController',
+    'base table' => 'taxonomy_vocabulary',
+    'object keys' => array(
+      'id' => 'vid',
+    ),
+    'fieldable' => FALSE,
+  );
+
+  // Add meta-data about the basic vocabulary properties.
+  $properties = &$return['taxonomy_vocabulary']['properties'];
+  
+  // Taxonomy vocabulary related variables.
+  $properties['vid'] = array(
+    'label' => t("Vocabulary ID"),
+    'description' => t("The unique ID of the taxonomy vocabulary."),
+    'type' => 'integer',
+  );
+  $properties['name'] = array(
+    'label' => t("Name"),
+    'description' => t("The name of the taxonomy vocabulary."),
+    'setter callback' => 'drupal_property_verbatim_set',
+  );
+  $properties['machine_name'] = array(
+    'label' => t("Machine name"),
+    'description' => t("The machine name of the taxonomy vocabulary."),
+  );
+  $properties['description'] = array(
+    'label' => t("Description"),
+    'description' => t("The optional description of the taxonomy vocabulary."),
+    'setter callback' => 'drupal_property_verbatim_set',
+    'sanitize' => 'filter_xss',
+  );
+  $properties['node-count'] = array(
+    'label' => t("Node count"),
+    'description' => t("The number of nodes tagged with terms belonging to the taxonomy vocabulary."),
+    'getter callback' => 'taxonomy_vocabulary_get_properties',
+  );
+  $properties['term-count'] = array(
+    'label' => t("Term count"),
+    'description' => t("The number of terms belonging to the taxonomy vocabulary."),
+    'getter callback' => 'taxonomy_vocabulary_get_properties',
+  );
+  return $return;
+}
+
+/**
+ * Implement hook_entity_info_alter().
+ */
+function taxonomy_entity_info_alter(&$entity_info) {
+  $properties = &$entity_info['node']['properties'];
+  $properties['taxonomy'] = array(
+    'label' => t("Taxonomy terms"),
+    'description' => t("The taxonomy terms associated with this node."),
+    'getter callback' => 'drupal_property_get_list',
+    'type' => 'list<taxonomy_term>',
+  );
+}
diff --git modules/taxonomy/taxonomy.info modules/taxonomy/taxonomy.info
index 41c857f..e9b644c 100644
--- modules/taxonomy/taxonomy.info
+++ modules/taxonomy/taxonomy.info
@@ -9,4 +9,4 @@ files[] = taxonomy.admin.inc
 files[] = taxonomy.pages.inc
 files[] = taxonomy.install
 files[] = taxonomy.test
-files[] = taxonomy.tokens.inc
+files[] = taxonomy.entity.inc
diff --git modules/taxonomy/taxonomy.module modules/taxonomy/taxonomy.module
index ab15c7c..3547007 100644
--- modules/taxonomy/taxonomy.module
+++ modules/taxonomy/taxonomy.module
@@ -19,50 +19,6 @@ function taxonomy_permission() {
 }
 
 /**
- * Implement hook_entity_info().
- */
-function taxonomy_entity_info() {
-  $return = array(
-    'taxonomy_term' => array(
-      'label' => t('Taxonomy term'),
-      'controller class' => 'TaxonomyTermController',
-      'base table' => 'taxonomy_term_data',
-      'fieldable' => TRUE,
-      'object keys' => array(
-        'id' => 'tid',
-        'bundle' => 'vocabulary_machine_name',
-      ),
-      'bundle keys' => array(
-        'bundle' => 'machine_name',
-      ),
-      'bundles' => array(),
-    ),
-  );
-  foreach (taxonomy_vocabulary_get_names() as $machine_name => $vocabulary) {
-    $return['taxonomy_term']['bundles'][$machine_name] = array(
-      'label' => $vocabulary->name,
-      'admin' => array(
-        'path' => 'admin/structure/taxonomy/%taxonomy_vocabulary',
-        'real path' => 'admin/structure/taxonomy/' . $vocabulary->vid,
-        'bundle argument' => 3,
-        'access arguments' => array('administer taxonomy'),
-      ),
-    );
-  }
-  $return['taxonomy_vocabulary'] = array(
-    'label' => t('Taxonomy vocabulary'),
-    'controller class' => 'TaxonomyVocabularyController',
-    'base table' => 'taxonomy_vocabulary',
-    'object keys' => array(
-      'id' => 'vid',
-    ),
-    'fieldable' => FALSE,
-  );
-
-  return $return;
-}
-
-/**
  * Implement hook_field_build_modes();
  *
  * @TODO: build mode for display as a field (when attached to nodes etc.).
@@ -432,6 +388,23 @@ function taxonomy_check_vocabulary_hierarchy($vocabulary, $changed_term) {
 }
 
 /**
+ * Callback for getting vocabulary properties.
+ * @see taxonomy_entity_info()
+ */
+function taxonomy_vocabulary_get_properties($vocabulary, array $options, $name) {
+
+  switch ($name) {
+    case 'node-count':
+      $sql = "SELECT COUNT (1) FROM {taxonomy_term_node} tn LEFT JOIN {taxonomy_term_data} td ON tn.tid = td.tid WHERE td.vid = :vid";
+      return db_query($sql, array(':vid' => $vocabulary->vid))->fetchField();
+
+    case 'term-count':
+      $sql = "SELECT COUNT (1) FROM {taxonomy_term_data} td WHERE td.vid = :vid";
+      return db_query($sql, array(':vid' => $vocabulary->vid))->fetchField();
+  }
+}
+
+/**
  * Save a term object to the database.
  *
  * @param $term
@@ -575,6 +548,28 @@ function taxonomy_term_delete($tid) {
 }
 
 /**
+ * Callback for getting term properties.
+ * @see taxonomy_entity_info()
+ */
+function taxonomy_term_get_properties($term, array $options, $name) {
+
+  switch ($name) {
+    case 'node-count':
+      $sql = "SELECT COUNT (1) FROM {taxonomy_term_node} tn WHERE tn.tid = :tid";
+      return db_query($sql, array(':tid' => $term->tid))->fetchField();
+
+    case 'url':
+      return url(taxonomy_term_path($term), $options);
+
+    case 'vocabulary':
+      return taxonomy_vocabulary_load($term->vid);
+
+    case 'parent':
+      return array_pop(taxonomy_get_parents($term->tid));
+  }
+}
+
+/**
  * Clear all static cache variables for terms..
  */
 function taxonomy_terms_static_reset() {
diff --git modules/taxonomy/taxonomy.test modules/taxonomy/taxonomy.test
index d96506b..2289b05 100644
--- modules/taxonomy/taxonomy.test
+++ modules/taxonomy/taxonomy.test
@@ -276,27 +276,27 @@ class TaxonomyVocabularyUnitTest extends TaxonomyWebTestCase {
     // Fetch all of the vocabularies using taxonomy_get_vocabularies().
     // Confirm that the vocabularies are ordered by weight.
     $vocabularies = taxonomy_get_vocabularies();
-    $this->assertEqual(array_shift($vocabularies), $vocabulary1, t('Vocabulary was found in the vocabularies array.'));
-    $this->assertEqual(array_shift($vocabularies), $vocabulary2, t('Vocabulary was found in the vocabularies array.'));
-    $this->assertEqual(array_shift($vocabularies), $vocabulary3, t('Vocabulary was found in the vocabularies array.'));
+    $this->assertEqual(array_shift($vocabularies)->vid, $vocabulary1->vid, t('Vocabulary was found in the vocabularies array.'));
+    $this->assertEqual(array_shift($vocabularies)->vid, $vocabulary2->vid, t('Vocabulary was found in the vocabularies array.'));
+    $this->assertEqual(array_shift($vocabularies)->vid, $vocabulary3->vid, t('Vocabulary was found in the vocabularies array.'));
 
     // Fetch the vocabularies with taxonomy_vocabulary_load_multiple(), specifying IDs.
     // Ensure they are returned in the same order as the original array.
     $vocabularies = taxonomy_vocabulary_load_multiple(array($vocabulary3->vid, $vocabulary2->vid, $vocabulary1->vid));
-    $this->assertEqual(array_shift($vocabularies), $vocabulary3, t('Vocabulary loaded successfully by ID.'));
-    $this->assertEqual(array_shift($vocabularies), $vocabulary2, t('Vocabulary loaded successfully by ID.'));
-    $this->assertEqual(array_shift($vocabularies), $vocabulary1, t('Vocabulary loaded successfully by ID.'));
+    $this->assertEqual(array_shift($vocabularies)->vid, $vocabulary3->vid, t('Vocabulary loaded successfully by ID.'));
+    $this->assertEqual(array_shift($vocabularies)->vid, $vocabulary2->vid, t('Vocabulary loaded successfully by ID.'));
+    $this->assertEqual(array_shift($vocabularies)->vid, $vocabulary1->vid, t('Vocabulary loaded successfully by ID.'));
 
     // Fetch vocabulary 1 by name.
-    $this->assertTrue(current(taxonomy_vocabulary_load_multiple(array(), array('name' => $vocabulary1->name))) == $vocabulary1, t('Vocabulary loaded successfully by name.'));
+    $this->assertTrue(current(taxonomy_vocabulary_load_multiple(array(), array('name' => $vocabulary1->name)))->vid == $vocabulary1->vid, t('Vocabulary loaded successfully by name.'));
 
     // Fetch vocabulary 1 by name and ID.
-    $this->assertTrue(current(taxonomy_vocabulary_load_multiple(array($vocabulary1->vid), array('name' => $vocabulary1->name))) == $vocabulary1, t('Vocabulary loaded successfully by name and ID.'));
+    $this->assertTrue(current(taxonomy_vocabulary_load_multiple(array($vocabulary1->vid), array('name' => $vocabulary1->name)))->vid == $vocabulary1->vid, t('Vocabulary loaded successfully by name and ID.'));
 
     // Fetch vocabulary 1 with specified node type.
     entity_get_controller('taxonomy_vocabulary')->resetCache();
     $vocabulary_node_type = current(taxonomy_vocabulary_load_multiple(array($vocabulary1->vid), array('type' => 'article')));
-    $this->assertEqual($vocabulary_node_type, $vocabulary1, t('Vocabulary with specified node type loaded successfully.'));
+    $this->assertEqual($vocabulary_node_type->vid, $vocabulary1->vid, t('Vocabulary with specified node type loaded successfully.'));
   }
 }
 
diff --git modules/upload/upload.info modules/upload/upload.info
index 0f3d6a5..ed1f562 100644
--- modules/upload/upload.info
+++ modules/upload/upload.info
@@ -7,5 +7,4 @@ core = 7.x
 files[] = upload.module
 files[] = upload.admin.inc
 files[] = upload.install
-files[] = upload.test
-files[] = upload.tokens.inc
+files[] = upload.test
\ No newline at end of file
diff --git modules/upload/upload.module modules/upload/upload.module
index 852af0c..49f15b9 100644
--- modules/upload/upload.module
+++ modules/upload/upload.module
@@ -270,6 +270,7 @@ function upload_file_load($files) {
   // Add the upload specific data into the file object.
   $result = db_query('SELECT * FROM {upload} u WHERE u.fid IN (:fids)', array(':fids' => array_keys($files)))->fetchAll(PDO::FETCH_ASSOC);
   foreach ($result as $record) {
+    $files[$record['fid']]->entityTags[] = 'attachment';
     foreach ($record as $key => $value) {
       $files[$record['fid']]->$key = $value;
     }
@@ -634,6 +635,50 @@ function theme_upload_form_new($form) {
 }
 
 /**
+ * Implement hook_entity_info_alter().
+ */
+function upload_entity_info_alter(&$entity_info) {
+  $entity_info['file']['tags']['attachment'] = array(
+    'label' => t('Upload attachment'),
+    'properties' => array(),
+  );
+  $properties = &$entity_info['file']['tags']['attachment']['properties'];
+  
+  $properties['nid'] = array(
+    'label' => t("Node ID"),
+    'description' => t("The unique ID of the node the file is attached to."),
+  );
+  $properties['node'] = array(
+    'label' => t("Node"),
+    'description' => t("The node the file is attached to."),
+    'type' => 'node',
+    'getter callback' => 'upload_file_get_node',
+  );
+  $properties['description'] = array(
+    'label' => t("Description"),
+    'description' => t("An optional human-readable description of the file."),
+  );
+
+  $entity_info['node']['properties']['files'] = array(
+    'label' => t('File attachment'),
+    'description' => t('Files attached to a node, if any.'),
+    'type' => 'list<file>',
+    'tags' => array('attachment'),
+    'getter callback' => 'drupal_property_get_list',
+  );
+}
+
+/**
+ * Gets the node associated to an uploaded file.
+ *
+ * @return
+ *   The associated node object.
+ */
+function upload_file_get_node($file) {
+  return node_load($file->nid);
+}
+
+/**
  * Menu-callback for JavaScript-based uploads.
  */
 function upload_js() {
diff --git modules/upload/upload.tokens.inc modules/upload/upload.tokens.inc
deleted file mode 100644
index bb7488f..0000000
--- modules/upload/upload.tokens.inc
+++ /dev/null
@@ -1,45 +0,0 @@
-<?php
-// $Id: upload.tokens.inc,v 1.1 2009/08/19 20:19:37 dries Exp $
-
-/**
- * @file
- * Builds placeholder replacement tokens for uploaded files attached to nodes.
- */
-
-/**
- * Implement hook_token_info().
- */
-function upload_token_info() {
-  $results['tokens']['node'] = array(
-  'upload' => array(
-    'name' => t('File attachment'),
-    'description' => t('The first file attached to a node, if one exists.'),
-    'type' => 'file',
-    )
-  );
-  return $results;
-}
-
-/**
- * Implement hook_tokens().
- */
-function upload_tokens($type, $tokens, array $data = array(), array $options = array()) {
-  $replacements = array();
-
-  if ($type == 'node' && !empty($data['node'])) {
-    $node = $data['node'];
-
-    foreach ($tokens as $name => $original) {
-      if ($name == 'upload') {
-        $upload = array_shift($node->files);
-        $replacements[$original] = file_create_url($upload->filepath);
-      }
-    }
-
-    if (($upload_tokens = token_find_with_prefix($tokens, 'upload')) && !empty($node->files) && $upload = array_shift($node->files)) {
-      $replacements += token_generate('file', $upload_tokens, array('file' => $upload), $options);
-    }
-  }
-
-  return $replacements;
-}
diff --git modules/user/user.entity.inc modules/user/user.entity.inc
new file mode 100644
index 0000000..f4d10e7
--- /dev/null
+++ modules/user/user.entity.inc
@@ -0,0 +1,74 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Provides info about the user entity.
+ */
+
+/**
+ * Implement hook_entity_info().
+ */
+function user_entity_info() {
+  $return = array(
+    'user' => array(
+      'label' => t('User'),
+      'controller class' => 'UserController',
+      'base table' => 'users',
+      'fieldable' => TRUE,
+      'object keys' => array(
+        'id' => 'uid',
+      ),
+      'bundles' => array(
+        'user' => array(
+          'label' => t('User'),
+          'admin' => array(
+            'path' => 'admin/config/people/accounts',
+            'access arguments' => array('administer users'),
+          ),
+        ),
+      ),
+    ),
+  );
+  // Add meta-data about the user properties.
+  $properties = &$return['user']['properties'];
+
+  $properties['uid'] = array(
+    'label' => t("User ID"),
+    'type' => 'integer',
+    'description' => t("The unique ID of the user account."),
+  );
+  $properties['name'] = array(
+    'label' => t("Name"),
+    'description' => t("The login name of the user account."),
+    'getter callback' => 'user_get_properties',
+    'setter callback' => 'drupal_property_verbatim_set',
+  );
+  $properties['mail'] = array(
+    'label' => t("Email"),
+    'description' => t("The email address of the user account."),
+    'setter callback' => 'drupal_property_verbatim_set',
+  );
+  $properties['url'] = array(
+    'label' => t("URL"),
+    'description' => t("The URL of the account profile page."),
+    'getter callback' => 'user_get_properties',
+  );
+  $properties['edit-url'] = array(
+    'label' => t("Edit URL"),
+    'description' => t("The url of the account edit page."),
+    'getter callback' => 'user_get_properties',
+  );
+  $properties['login'] = array(
+    'label' => t("Last login"),
+    'description' => t("The date the user last logged in to the site."),
+    'type' => 'date',
+  );
+  $properties['created'] = array(
+    'label' => t("Created"),
+    'description' => t("The date the user account was created."),
+    'type' => 'date',
+  );
+  return $return;
+}
+
diff --git modules/user/user.info modules/user/user.info
index 54e288e..a828f42 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.entity.inc
 required = TRUE
diff --git modules/user/user.module modules/user/user.module
index 37c54c8..3f89d0e 100644
--- modules/user/user.module
+++ modules/user/user.module
@@ -84,30 +84,21 @@ function user_theme() {
 }
 
 /**
- * Implement hook_entity_info().
- */
-function user_entity_info() {
-  $return = array(
-    'user' => array(
-      'label' => t('User'),
-      'controller class' => 'UserController',
-      'base table' => 'users',
-      'fieldable' => TRUE,
-      'object keys' => array(
-        'id' => 'uid',
-      ),
-      'bundles' => array(
-        'user' => array(
-          'label' => t('User'),
-          'admin' => array(
-            'path' => 'admin/config/people/accounts',
-            'access arguments' => array('administer users'),
-          ),
-        ),
-      ),
-    ),
-  );
-  return $return;
+ * Callback for getting user properties.
+ * @see user_entity_info()
+ */
+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 'edit-url':
+      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 debf833..0000000
--- modules/user/user.tokens.inc
+++ /dev/null
@@ -1,130 +0,0 @@
-<?php
-// $Id: user.tokens.inc,v 1.2 2009/09/30 18:37:30 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'] = $options['language'];
-    $language_code = $options['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;
-}
