entity properties From: fago --- includes/common.inc | 22 ++ includes/module.inc | 9 + includes/properties.inc | 285 +++++++++++++++++++++++++++ includes/token.inc | 171 ++++++++-------- modules/field/field.default.inc | 4 modules/field/field.info | 1 modules/field/field.info.inc | 107 ++++++++++ modules/field/field.module | 17 +- modules/field/modules/number/number.module | 3 modules/field/modules/text/text.module | 4 modules/node/node.entity.inc | 127 ++++++++++++ modules/node/node.info | 2 modules/node/node.module | 62 ++---- modules/simpletest/drupal_web_test_case.php | 1 modules/system/system.module | 55 +++++ modules/system/system.test | 123 +++++++++++- modules/user/user.entity.inc | 74 +++++++ modules/user/user.info | 2 modules/user/user.module | 37 +--- 19 files changed, 936 insertions(+), 170 deletions(-) create mode 100644 includes/properties.inc create mode 100644 modules/node/node.entity.inc create mode 100644 modules/user/user.entity.inc diff --git includes/common.inc includes/common.inc index 5cea431..a40ebfe 100644 --- includes/common.inc +++ includes/common.inc @@ -5090,6 +5090,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) { @@ -5188,3 +5189,24 @@ function xmlrpc($url) { return call_user_func_array('_xmlrpc', $args); } +/** + * Returns a DrupalPropertyEntityWrapper or DrupalPropertyFormatWrapper + * dependent whether the passed data is an entity. + * + * @param $type + * The type of the passed data. + * @param $data + * The data to wrap. + * @param $options + * (optional) A keyed array of options. The supported options vary by class. + * @param $context + * (optional) An array of contextual information which format callbacks can + * make use of. + * @return + * An instance of DrupalPropertyWrapperInterface. + */ +function drupal_get_property_wrapper($type, $data, array $options = array(), $context = array()) { + $entity_info = entity_get_info(); + $class = isset($entity_info[$type]) ? 'DrupalPropertyEntityWrapper' : 'DrupalPropertyFormatWrapper'; + return new $class($type, $data, $options, $context); +} diff --git includes/module.inc includes/module.inc index bde7f5d..aff42c0 100644 --- includes/module.inc +++ includes/module.inc @@ -176,11 +176,18 @@ 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(); + $name = isset($name) ? '.'. $name : ''; foreach ($modules as $module) { - module_load_include($type, $module, $name); + module_load_include($type, $module, $module . $name); } } diff --git includes/properties.inc includes/properties.inc new file mode 100644 index 0000000..5feb22c --- /dev/null +++ includes/properties.inc @@ -0,0 +1,285 @@ +node->author->name; + * @endcode + */ +interface DrupalPropertyWrapperInterface extends IteratorAggregate { + + /** + * Constructor. + * + * @see drupal_get_property_wrapper(). + */ + public function __construct($type, $data, array $options = array()); + + /** + * Get the data wrapped by this object. + */ + public function get(); + + /** + * Magic method: Get a derived value. + */ + public function __get($name); + + /** + * Magic method: Check whether a derived value of this name is supported. + */ + public function __isset($name); + +} + +/** + * Provides a wrapper for entities which eases dealing with entity properties. + */ +class DrupalPropertyEntityWrapper implements DrupalPropertyWrapperInterface { + + protected $entityType; + protected $entity; + protected $info; + protected $options; + protected $cache = array(); + + /** + * Construct a new DrupalPropertyEntityWrapper object. + * + * @param $entityType + * The type of the passed entity. + * @param $entity + * The entity with which properties we deal. + * @param $options + * (optional) A keyed array of options. Supported are: + * - language: A language object to be used when getting locale-sensitive + * properties. + * - sanitize: A boolean flag indicating that textual properties should be + * sanitized for display to a web browser. Defaults to FALSE. + * - bundle: The bundle of the passed entity. If specified, bundle specific + * properties are available too. + * - tags: An array of tags assigned to the passed entity. If specified, + * properties specific to the respective entities are available too. + */ + public function __construct($entityType, $entity, array $options = array()) { + $this->entityType = $entityType; + $this->entity = $entity; + $this->info = entity_get_info($entityType) + array('properties' => array()); + $this->options = $options + array('sanitize' => FALSE, 'language' => NULL, 'tags' => array()); + + // Add in properties from the bundle or tags, if specified. + if (isset($this->options['bundle']) && isset($this->info['bundles'][$this->options['bundle']])) { + $bundle_info = $this->info['bundles'][$this->options['bundle']] + array('properties' => array(), 'tags' => array()); + $this->info['properties'] += $bundle_info['properties']; + $this->options['tags'] += $bundle_info['tags']; + } + foreach ($this->options['tags'] as $tag) { + if (isset($this->info['tags'][$tag]['properties'])) { + $this->info['properties'] += $this->info['tags'][$tag]['properties']; + } + } + } + + /** + * 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(), + 'default format' => NULL, + ); + return $info + array('sanitize' => (empty($info['getter callback']) && $info['type'] == 'text' ? 'check_plain' : NULL)); + } + + /** + * Magic method: Get a property. + * + * @return + * An instance of DrupalPropertyWrapperInterface. + */ + public function __get($name) { + // Look it up in the cache if possible. + if (!array_key_exists($name, $this->cache)) { + $info = $this->getPropertyInfo($name); + $this->cache[$name] = NULL; + + if (!empty($info['getter callback']) && 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; + } + // 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('bundle', 'tags', 'default format', 'formats'))) + $this->options; + $context = array('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'])); + } +} + + +/** + * Class that eases applying token formats for returned properties. + */ +class DrupalPropertyFormatWrapper implements DrupalPropertyWrapperInterface { + + protected $type; + protected $data; + protected $info; + protected $options; + protected $context; + + /** + * Construct a new DrupalPropertyFormatWrapper object. + * + * @param $type + * The type of the passed data. + * @param $data + * The data to format. + * @param $options + * (optional) A keyed array of options. Supported are: + * - language: A language object to be used when generating + * locale-sensitive formats. + * - formats: An array of further formats for this property + * as defined in hook_entity_info(). + * - default format: Customize the default format. + * @param $context + * (optional) An array of contextual information which format callbacks + * can make use of. Contains 'entity', 'entity_type' and 'property' when + * constructed by the DrupalPropertyEntityWrapper. + */ + public function __construct($type, $data, array $options = array(), $context = array()) { + $this->type = $type; + $this->data = $data; + $this->options = $options + array('formats' => array()); + $this->context = $context; + } + + /** + * We use this to init $this->info only when needed. + */ + protected function initInfo() { + if (!isset($this->info)) { + $this->info = token_get_format_info($this->type) + array('formats' => array()); + $this->info['formats'] = $this->options['formats'] + $this->info['formats']; + } + } + + /** + * Magic method: Format the data. + */ + public function __get($name) { + $this->initInfo(); + if (!isset($this->info['formats'][$name])) { + throw new DrupalPropertyWrapperException('Unknown format '. check_plain($name). '.'); + } + return token_format($this->data, $this->type, $name, $this->options, $this->context); + } + + /** + * Magic method: isset() can be used to check if a format is known. + */ + public function __isset($name) { + $this->initInfo(); + return isset($this->info['formats'][$name]); + } + + /** + * Get the data wrapped by this object. + */ + public function get() { + return $this->data; + } + + public function getIterator() { + $this->initInfo(); + return new ArrayIterator(array_keys($this->info['formats'])); + } + + /** + * For converting to a string use the default format, if any. + */ + public function __toString() { + return (string)token_format($this->data, $this->type, NULL, $this->options, $this->context); + } +} + +/** + * Provide a separate Exception so it can be caught separately. + */ +class DrupalPropertyWrapperException extends Exception { + +} + +/** + * Sets the property to the given value. May be used as 'setter callback'. + */ +function drupal_property_verbatim_set(&$entity, $name, $value) { + if (is_array($entity)) { + $entity[$name] = $value; + } + elseif (is_object($entity)) { + $entity->$name = $value; + } +} + diff --git includes/token.inc includes/token.inc index 6f832b7..70bb273 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() */ /** @@ -71,14 +71,15 @@ * display to a web browser. Defaults to TRUE. Developers who set this option * to FALSE assume responsibility for running filter_xss(), check_plain() or * other appropriate scrubbing functions before displaying data to users. + * @param $types + * (optional) An array mapping the keys of $data to data types. If there is + * no mapping for a key, the data's key is used as type by default. * @return * Text with tokens replaced. */ -function token_replace($text, array $data = array(), array $options = array()) { - $replacements = array(); - foreach (token_scan($text) as $type => $tokens) { - $replacements += token_generate($type, $tokens, $data, $options); - } +function token_replace($text, array $data = array(), array $options = array(), array $types = array()) { + $token_list = token_scan($text); + $replacements = token_generate($token_list, $data, $options, $types); // Optionally alter the list of replacement values. if (!empty($options['callback']) && function_exists($options['callback'])) { @@ -122,11 +123,8 @@ function token_scan($text) { /** * Generate replacement values for a list of tokens. * - * @param $type - * The type of token being replaced. 'node', 'user', and 'date' are common. - * @param $tokens - * An array of tokens to be replaced, keyed by the literal text of the token - * as it appeared in the source text. + * @param $raw_tokens + * A keyed array of tokens, and their original raw form in the source text. * @param $data * (optional) An array of keyed objects. For simple replacement scenarios * 'node', 'user', and others are common keys, with an accompanying node or @@ -145,107 +143,108 @@ function token_scan($text) { * display to a web browser. Developers who set this option to FALSE assume * responsibility for running filter_xss(), check_plain() or other * appropriate scrubbing functions before displaying data to users. + * @param $types + * (optional) An array mapping the keys of $data to data types. If there is + * no mapping for a key, the data's key is used as type by default. * @return * An associative array of replacement values, keyed by the original 'raw' * tokens that were found in the source text. For example: * $results['[node:title]'] = 'My new node'; */ -function token_generate($type, array $tokens, array $data = array(), array $options = array()) { +function token_generate(array $raw_tokens, array $data = array(), array $options = array(), array $types = array()) { $results = array(); $options += array('sanitize' => TRUE); - _token_initialize(); + // 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; + foreach ($raw_tokens as $key => $tokens) { + $type = isset($types[$key]) ? $types[$key] : $key; + if (isset($data[$key]) && ($wrapper = drupal_get_property_wrapper($type, $data[$key], $options))) { + foreach ($tokens as $token => $original) { + try { + $results[$original] = _token_get_replacement($wrapper, $token); + } + catch (DrupalPropertyWrapperException $e) { + // A token has not been found, so ignore it. + } + } + } } - return $results; } /** - * Given a list of tokens, return those that begin with a specific prefix. - * - * Used to extract a group of 'chained' tokens (such as [node:author:name]) from - * the full list of tokens found in text. For example: - * @code - * $data = array( - * 'author:name' => '[node:author:name]', - * 'title' => '[node:title]', - * 'created' => '[node:author:name]', - * ); - * $results = token_find_with_prefix($data, 'author'); - * $results == array('name' => '[node:author:name]'); - * @endcode - * - * @param $tokens - * A keyed array of tokens, and their original raw form in the source text. - * @param $prefix - * A textual string to be matched at the beginning of the token. - * @param $delimiter - * An optional string containing the character that separates the prefix from - * the rest of the token. Defaults to ':'. - * @return - * An associative array of discovered tokens, with the prefix and delimiter - * stripped from the key. + * Applies chained tokens by getting properties of the given wrapper. */ -function token_find_with_prefix(array $tokens, $prefix, $delimiter = ':') { - $results = array(); - foreach ($tokens as $token => $raw) { - $parts = split($delimiter, $token, 2); - if (count($parts) == 2 && $parts[0] == $prefix) { - $results[$parts[1]] = $raw; - } +function _token_get_replacement(DrupalPropertyWrapperInterface $wrapper, $token) { + foreach (explode(':', $token) as $i => $name) { + $wrapper = $wrapper->$name; } - return $results; + // If no format was given apply the default just by converting it to string. + return (string)$wrapper; } /** - * Returns metadata describing supported tokens. + * Returns metadata describing token formats. * - * The metadata array contains token type, name, and description data as well as - * an optional pointer indicating that the token chains to another set of tokens. - * For example: - * @code - * $data['types']['node'] = array( - * 'name' => t('Nodes'), - * 'description' => t('Tokens related to node objects.'), - * ); - * $data['tokens']['node']['title'] = array( - * 'name' => t('Title'), - * 'description' => t('The title of the current node.'), - * ); - * $data['tokens']['node']['author'] = array( - * 'name' => t('Author'), - * 'description' => t('The author of the current node.'), - * 'type' => 'user', - * ); - * @endcode - * @return - * An associative array of token information, grouped by token type. + * @param $type + * The type, e.g. date, for which the info shall be returned, or NULL + * to return an array with info about all types. + * + * @see hook_token_format_info() + * @see hook_token_format_info_alter() */ -function token_info() { +function token_get_format_info($type = NULL) { $data = &drupal_static(__FUNCTION__); if (!isset($data)) { - _token_initialize(); - $data = module_invoke_all('token_info'); - drupal_alter('token_info', $data); + if ($cache = cache_get('token_format_info')) { + $data = $cache->data; + } + else { + $data = module_invoke_all('token_format_info'); + drupal_alter('token_format_info', $data); + cache_set('token_format_info', $data); + } + } + if (!empty($type)) { + return isset($data[$type]) ? $data[$type] : array(); } return $data; } /** - * Load modulename.tokens.inc for all enabled modules. + * Formats a token by using a token format as defined in an implementation of + * hook_token_format_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. + * @param $options + * (optional) A keyed array of options. Supported are: + * - language: A language object to be used when generating locale-sensitive + * formats. + * - formats: Only used internally to add in further formats. + * - default format: Only used internally to customize 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 = token_get_format_info($type) + array('formats' => array(), 'default format' => NULL); + $options += array('language' => NULL, 'formats' => array()); + $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['language'], $context); + } + elseif (!isset($format)) { + return $value; } } diff --git modules/field/field.default.inc modules/field/field.default.inc index 4b1f377..092f66b 100644 --- modules/field/field.default.inc +++ modules/field/field.default.inc @@ -47,7 +47,7 @@ function field_default_insert($obj_type, $object, $field, $instance, $langcode, // assigning it a default value. This way we ensure that only the intended // languages get a default value. Otherwise we could have default values for // not yet open languages. - if (empty($object) || !property_exists($object, $field['field_name']) || + if (empty($object) || !property_exists($object, $field['field_name']) || (isset($object->{$field['field_name']}[$langcode]) && count($object->{$field['field_name']}[$langcode]) == 0)) { $items = field_get_default_value($obj_type, $object, $field, $instance, $langcode); } @@ -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 modules/field/field.info index 3aa5930..a9858db 100644 --- modules/field/field.info +++ modules/field/field.info @@ -12,6 +12,7 @@ files[] = field.default.inc files[] = field.multilingual.inc files[] = field.attach.inc files[] = field.form.inc +files[] = field.properties.inc files[] = field.test dependencies[] = field_sql_storage required = TRUE diff --git modules/field/field.info.inc modules/field/field.info.inc index c29ead5..90a5900 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'); } /** @@ -328,15 +330,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; } @@ -597,5 +599,102 @@ function field_info_formatter_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_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) { + $entity_type = field_info_bundle_entity($bundle); + $instance = field_info_instance($field_name, $bundle); + if (empty($instance['deleted'])) { + foreach ($field_type['property_callbacks'] as $callback) { + $callback($entity_info, $entity_type, $field, $instance, $field_type); + } + } + } + } +} + +/** + * Callback to add in property info per field instance. + * @see field_entity_info_alter(). + */ +function field_default_property_callback(&$entity_info, $entity_type, $field, $instance, $field_type) { + // Add in compatible formatters. + $formatters = array(); + foreach (field_info_formatter_types() as $name => $formatter) { + if (in_array($field['type'], $formatter['field types'])) { + $formatters[$name] = array( + 'label' => $formatter['label'], + 'callback' => 'field_format_property', + ); + } + } + $entity_info[$entity_type]['bundles'][$instance['bundle']]['properties'][$field['field_name']] = array( + 'label' => $instance['label'], + 'type' => $field_type['property_type'], + 'description' => $instance['description'], + 'getter callback' => 'field_property_get', + 'setter callback' => 'field_property_set', + 'formats' => $formatters, + 'default format' => $field_type['default_formatter'], + ); +} + +/** + * Callback for applying custom property formats. + */ +function field_format_property($value, $formatter_type, $language, array $context) { + $field_name = $context['property']; + $langcode = _field_property_get_langcode($context['entity'], $language, $field_name); + $single = (field_behaviors_formatter('multiple values', $formatter_type) == FIELD_BEHAVIOR_DEFAULT); + $item = $single ? $context['entity']->{$field_name}[$langcode][0] : $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['language'], $name); + return $options['sanitize'] ? $object->{$name}[$langcode][0]['safe'] : $object->{$name}[$langcode][0]['value']; +} + +function _field_property_get_langcode($object, $language, $name) { + $langcode = FIELD_LANGUAGE_NONE; + if (isset($language) && isset($object->{$name}[$language->language])) { + $langcode = $language->language; + } + return $langcode; +} + +/** + * 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']); + + // Refresh the sanitized value too, so subsequent reads work right. + $field = field_info_field($name); + list($id, $vid, $bundle) = field_attach_extract_ids($obj_type, $object); + $instance = field_info_instance($name, $bundle); + + $function = $field['module'] . '_field_sanitize'; + if (function_exists($function)) { + $function($obj_type, $object, $field, $instance, $langcode, $object->{$name}[$langcode]); + } +} + +/** * @} End of "defgroup field_info" */ diff --git modules/field/field.module modules/field/field.module index 66e70e5..d84ceb2 100644 --- modules/field/field.module +++ modules/field/field.module @@ -483,11 +483,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 @@ -495,6 +490,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) @@ -509,7 +506,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); } @@ -528,7 +525,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, @@ -541,14 +538,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]; @@ -558,7 +555,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/number/number.module modules/field/modules/number/number.module index 1cd79a3..ea37d2d 100644 --- modules/field/modules/number/number.module +++ modules/field/modules/number/number.module @@ -29,6 +29,7 @@ function number_field_info() { 'instance_settings' => array('min' => '', 'max' => '', 'prefix' => '', 'suffix' => ''), 'default_widget' => 'number', 'default_formatter' => 'number_integer', + 'property_type' => 'integer', ), 'number_decimal' => array( 'label' => t('Decimal'), @@ -37,6 +38,7 @@ function number_field_info() { 'instance_settings' => array('min' => '', 'max' => '', 'prefix' => '', 'suffix' => ''), 'default_widget' => 'number', 'default_formatter' => 'number_integer', + 'property_type' => 'decimal', ), 'number_float' => array( 'label' => t('Float'), @@ -44,6 +46,7 @@ function number_field_info() { 'instance_settings' => array('min' => '', 'max' => '', 'prefix' => '', 'suffix' => ''), 'default_widget' => 'number', 'default_formatter' => 'number_integer', + 'property_type' => 'decimal', ), ); } diff --git modules/field/modules/text/text.module modules/field/modules/text/text.module index be7b3c7..4218098 100644 --- modules/field/modules/text/text.module +++ modules/field/modules/text/text.module @@ -52,6 +52,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'), @@ -60,6 +61,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'), @@ -68,6 +70,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..3e8de92 --- /dev/null +++ modules/node/node.entity.inc @@ -0,0 +1,127 @@ + 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/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['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; +} + +/** + * Implement hook_entity_info_alter(). + */ +function node_entity_info_alter(&$entity_info) { + +} + 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 19b2d9f..4cbaac4 100644 --- modules/node/node.module +++ modules/node/node.module @@ -147,42 +147,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/node-type/' . str_replace('_', '-', $type), - 'access arguments' => array('administer content types'), - ), - ); - } - return $return; -} - - -/** * Implement hook_field_build_modes(). */ function node_field_build_modes($obj_type) { @@ -3126,6 +3090,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 + array('absolute' => TRUE)); + + case 'edit-url': + return url('node/' . $node->nid . '/edit', $options + array('absolute' => TRUE)); + + 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/simpletest/drupal_web_test_case.php modules/simpletest/drupal_web_test_case.php index 1655d4f..44aa512 100644 --- modules/simpletest/drupal_web_test_case.php +++ modules/simpletest/drupal_web_test_case.php @@ -1091,6 +1091,7 @@ class DrupalWebTestCase extends DrupalTestCase { // Rebuild caches. node_types_rebuild(); + module_implements(NULL, NULL, TRUE); actions_synchronize(); _drupal_flush_css_js(); $this->refreshVariables(); diff --git modules/system/system.module modules/system/system.module index 5233131..6ba508b 100644 --- modules/system/system.module +++ modules/system/system.module @@ -3055,6 +3055,61 @@ function system_image_toolkits() { } /** + * Implementation of hook_token_format_info(). + */ +function system_token_format_info() { + // Date related formats. + $date['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', + ); + $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', + ); + $date['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', + ); + $date['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', + ); + $date['raw'] = array( + 'label' => t("Raw timestamp"), + 'description' => t("A date in UNIX timestamp format (%date)", array('%date' => REQUEST_TIME)), + 'callback' => 'system_format_date', + ); + return array('date' => array('default format' => 'medium', 'formats' => $date)); +} + +/** + * Callback for formating date tokens. + * @see system_token_format_info(). + */ +function system_format_date($date, $format, $language) { + $langcode = isset($language) ? $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); + } +} + + +/** * 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 b7f224b..3d5a2af 100644 --- modules/system/system.test +++ modules/system/system.test @@ -1081,6 +1081,116 @@ class SystemThemeFunctionalTest extends DrupalWebTestCase { } } +/** + * Test entity propert wrapper. + */ +class DrupalPropertyWrapperTestCase extends DrupalWebTestCase { + public static function getInfo() { + return array( + 'name' => 'Property wrappers', + 'description' => 'Test using the drupal property wrappers.', + 'group' => 'System', + ); + } + + /** + * Creates a user and a node, then tests getting the properties. + */ + function testEntityPropertyWrapper() { + $account = $this->drupalCreateUser(); + $node = $this->drupalCreateNode(array('uid' => $account->uid, 'title' => 'Is it bold?')); + // For testing sanitizing give the user a malicious user name + $account = user_save($account, array('name' => 'BadName')); + + // Fetch the object so every properties are set as usual. + $node = node_load($node->nid); + + // First test without sanitizing. + $wrapper = drupal_get_property_wrapper('node', $node); + + $this->assertEqual($node->title, $wrapper->title, 'Getting property.'); + $this->assertEqual($node->name, $wrapper->{'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->assertEqual(iterator_to_array($wrapper->getIterator()), array_keys($type_info['properties']), 'Iterator is working.'); + + } + + /** + * Tests the property format 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 iterator + $info = token_get_format_info('date'); + $this->assertEqual(iterator_to_array($wrapper->getIterator()), array_keys($info['formats']), 'Iterator is working.'); + } + + /** + * Test field API support. + */ + function testFieldPropertyWrappers() { + // Test the body field. + $body = array(); + $body[FIELD_LANGUAGE_NONE][0] = array('value' => 'The body.', 'summary' => 'The summary.'); + $node = $this->drupalCreateNode(array('body' => $body)); + // Fetch the object so every properties are set as usual. + $node = node_load($node->nid, array(), TRUE); + $wrapper = drupal_get_property_wrapper('node', $node, array('bundle' => $node->type, '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 the basic queue functionality. @@ -1198,6 +1308,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 @@ -1206,24 +1317,24 @@ class TokenReplaceTestCase extends DrupalWebTestCase { $target = check_plain($node->title); $target .= check_plain($account->name); $target .= format_interval(REQUEST_TIME - $node->created, 2); + $target .= format_date($node->changed, 'medium'); $target .= check_plain($user->name); $target .= '[user:name]'; $target .= format_date(REQUEST_TIME, 'short'); $target .= '[bogus:token]'; $result = token_replace($source, array('node' => $node)); - + $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.')); } } 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 5f92858..070b64a 100644 --- modules/user/user.module +++ modules/user/user.module @@ -84,30 +84,21 @@ function user_theme() { } /** - * Implement hook_entity_info(). + * Callback for getting user properties. + * @see user_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; +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)); + } } /**