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. + * + * @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 @@ +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 @@ + 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 @@ - 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 @@ + 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 @@ - 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 @@ - 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 @@ - 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' 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 @@ + 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('', $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' => 'BadName')); + $node = $this->drupalCreateNode(array('uid' => $account->uid, 'name' => $account->name, 'title' => 'Is it bold?')); + + // 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' => 'The body.', 'summary' => 'The summary.'); + $node = $this->drupalCreateNode(array('body' => $body)); + $wrapper = drupal_get_property_wrapper('node', $node, array('sanitize' => FALSE)); + + $this->assertEqual('The body.', $wrapper->body->get(), 'Getting body property.'); + $this->assertEqual("

The summary.

\n", $wrapper->body, "Default field formatter applied."); + $this->assertEqual("The body.\n", $wrapper->body->text_plain, "Specified field formatter applied."); + + $wrapper->body = "The second body."; + $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' => 'Bold')); + $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', 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', 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' => 'The body.'); + $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 = "

The body.

\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] = 'tag1, tag2'; + $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: tag1, tag2 - 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 @@ - 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('', $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 @@ + 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', + ); +} 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', + '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 @@ - 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 @@ + 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 @@ - 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; -}