commit f25147f7edee8de7018f448928748d6009a01cb9 Author: Damien Tournoud Date: Sun Aug 28 04:08:15 2011 +0200 Issue #1261856: workaround core bugs for node, user, comment, file, and taxonomy terms. diff --git a/entityreference.handler.inc b/entityreference.handler.inc new file mode 100644 index 0000000..404c58b --- /dev/null +++ b/entityreference.handler.inc @@ -0,0 +1,82 @@ +field = $field; + } + + public static function settingsForm($field) { + $form['handler'] = array( + '#markup' => t('The selected handler is broken.'), + ); + return $form; + } + + public function getReferencableEntities($match = NULL, $match_operator = 'CONTAINS', $limit = 0) { + return array(); + } + + public function countReferencableEntities($match = NULL, $match_operator = 'CONTAINS') { + return 0; + } + + public function validateReferencableEntities(array $ids) { + return array(); + } + + public function entityFieldQueryAlter(SelectQueryInterface $query) {} + + public function getLabel($entity) { + return ''; + } +} diff --git a/entityreference.info b/entityreference.info index d3a34b2..7e2b7ed 100644 --- a/entityreference.info +++ b/entityreference.info @@ -2,5 +2,9 @@ name = Entity Reference description = Provides a field that can reference other entities. core = 7.x dependencies[] = entity +dependencies[] = ctools files[] = entityreference.migrate.inc +files[] = entityreference.handler.inc + +files[] = tests/entityreference.handlers.test diff --git a/entityreference.install b/entityreference.install index 4aca182..e7d395f 100644 --- a/entityreference.install +++ b/entityreference.install @@ -27,3 +27,27 @@ function entityreference_field_schema($field) { ); } } + +/** + * Update the field configuration to the new plugin structure. + */ +function entityreference_update_7000() { + // Enable ctools. + if (!module_enable(array('ctools'))) { + throw new Exception('This version of Entity Reference requires ctools, but it could not be enabled.'); + } + + // Get the list of fields of type 'entityreference'. + $fields = array(); + foreach (field_info_fields() as $field_name => $field) { + if ($field_info['type'] == 'entityreference') { + $settings = &$field['settings']; + if (!isset($settings['handler'])) { + $settings['handler'] = 'base'; + $settings['handler_settings']['target_bundles'] = $settings['target_bundles']; + unset($settings['target_bundles']); + field_update_field($field); + } + } + } +} diff --git a/entityreference.module b/entityreference.module index dc5e004..05cbd9b 100644 --- a/entityreference.module +++ b/entityreference.module @@ -1,6 +1,23 @@ t('Entity Reference'), 'description' => t('This field reference another entity.'), 'settings' => array( - // The target entity type. - 'target_type' => '', - // The target entity bundles (optional). - 'target_bundles' => array(), + // The target entity type, pick node if it exists, or the first entity type if node. + 'target_type' => ($entity_info = entity_get_info()) && isset($entity_info['node']) ? 'node' : key($entity_info), + // The handler for this field. + 'handler' => 'base', + // The handler settings. + 'handler_settings' => array(), ), 'instance_settings' => array(), 'default_widget' => 'entityreference_autocomplete', @@ -46,10 +65,45 @@ function entityreference_field_is_empty($item, $field) { } /** + * Get the handler for a given entityreference field. + * + * The handler contains most of the business logic of the field. + */ +function entityreference_get_handler($field) { + $handler = $field['settings']['handler']; + ctools_include('plugins'); + $class = ctools_plugin_load_class('entityreference', 'handler', $handler, 'handler'); + + if (class_exists($class)) { + return call_user_func(array($class, 'getInstance'), $field); + } + else { + return EntityReferenceHandler_broken::getInstance($field); + } +} + +/** * Implements hook_field_validate(). */ function entityreference_field_validate($entity_type, $entity, $field, $instance, $langcode, $items, &$errors) { - // @todo: implement (we need to check if the target entity exists). + $ids = array(); + foreach ($items as $delta => $item) { + if (!entityreference_field_is_empty($item, $field)) { + $ids[$item['target_id']] = $delta; + } + } + + $valid_ids = entityreference_get_handler($field)->validateReferencableEntities(array_keys($ids)); + + $invalid_entities = array_diff_key($ids, array_flip($valid_ids)); + if ($invalid_entities) { + foreach ($invalid_entities as $id => $delta) { + $errors[$field['field_name']][$langcode][$delta][] = array( + 'error' => 'entityreference_invalid_entity', + 'message' => t('The referenced entity (@type: @id) is invalid.', array('@type' => $field['settings']['target_type'], '@id' => $id)), + ); + } + } } /** @@ -69,69 +123,123 @@ function entityreference_field_presave($entity_type, $entity, $field, $instance, function entityreference_field_settings_form($field, $instance, $has_data) { $settings = $field['settings']; - // Build the possible entity type - bundle combinations. + // Select the target entity type. $entity_type_options = array(); foreach (entity_get_info() as $entity_type => $entity_info) { - $entity_type_options[$entity_info['label']] = array(); - $entity_type_options[$entity_info['label']][$entity_type . ':'] = t('@entity_type: all bundles', array('@entity_type' => $entity_info['label'])); - foreach ($entity_info['bundles'] as $bundle_name => $bundle_info) { - $entity_type_options[$entity_info['label']][$entity_type . ':' . $bundle_name] = t('@entity_type: @bundle_name', array('@entity_type' => $entity_info['label'], '@bundle_name' => $bundle_info['label'])); - } - } - - // Restore the value. - $current_types = array(); - if (!empty($settings['target_type'])) { - if (empty($settings['target_bundles'])) { - $settings['target_bundles'] = array(''); - } - foreach ($settings['target_bundles'] as $bundle_name) { - $current_types[] = $settings['target_type'] . ':' . $bundle_name; - } + $entity_type_options[$entity_type] = $entity_info['label']; } $form['target_type'] = array( '#type' => 'select', '#title' => t('Target type'), '#options' => $entity_type_options, - '#default_value' => $current_types, + '#default_value' => $field['settings']['target_type'], '#required' => TRUE, '#description' => t('The entity type that can be referenced thru this field.'), '#disabled' => $has_data, - '#multiple' => TRUE, - '#size' => 10, - '#element_validate' => array('_entityreference_target_type_validate'), + '#size' => 1, + '#ajax' => array( + 'callback' => 'entityreference_settings_ajax', + 'wrapper' => 'entityreference-settings', + ), + '#limit_validation_errors' => array(), + ); + + ctools_include('plugins'); + $handlers = ctools_get_plugins('entityreference', 'handler'); + uasort($handlers, 'ctools_plugin_sort'); + $handlers_options = array(); + foreach ($handlers as $handler => $handler_info) { + $handlers_options[$handler] = check_plain($handler_info['title']); + } + + $form['handler'] = array( + '#type' => 'radios', + '#title' => t('Entity selection mode'), + '#options' => $handlers_options, + '#default_value' => $settings['handler'], + '#required' => TRUE, + '#ajax' => array( + 'callback' => 'entityreference_settings_ajax', + 'wrapper' => 'entityreference-settings', + ), + '#limit_validation_errors' => array(), + ); + $form['handler_submit'] = array( + '#type' => 'submit', + '#value' => t('Change handler'), + '#limit_validation_errors' => array(), + '#attributes' => array( + 'class' => array('js-hide'), + ), + '#submit' => array('entityreference_settings_ajax_submit'), + ); + + $form['handler_settings'] = array( + '#type' => 'container', + '#id' => 'entityreference-settings', + '#process' => array('entityreference_render_settings'), + '#tree' => TRUE, + '#field' => $field, ); return $form; } /** - * Validation callback for the target type selector: split it into target type and target bundle. + * #process callback: generates the handler settings form. + * + * @see entityreference_field_settings_form() */ -function _entityreference_target_type_validate($element, &$form_state, $form) { - $target_type = NULL; - $target_bundles = array(); - - foreach ($element['#value'] as $value) { - list($entity_type, $bundle) = explode(':', $value, 2); - if (isset($target_type) && $entity_type != $target_type) { - form_error($element, t('You must select a single entity type.')); - break; - } - $target_type = $entity_type; - $target_bundles[] = $bundle; +function entityreference_render_settings($element, $form_state) { + $parents = $element['#parents']; + array_pop($parents); + $parents[] = 'handler'; + $handler = drupal_array_get_nested_value($form_state['values'], $parents); + + ctools_include('plugins'); + $class = ctools_plugin_load_class('entityreference', 'handler', $handler, 'handler'); + if (!class_exists($class)) { + $class = 'EntityReferenceHandler_broken'; } - // When the "all bundles" option is selected, remove all the other bundles. - if (in_array('', $target_bundles)) { - $target_bundles = array(); + // Rebuild the field configuration based on the submitted structure. + $field = $element['#field']; + if (isset($form_state['values']['field']['settings'])) { + $field['settings'] = $form_state['values']['field']['settings'] + $field['settings']; } - $parents = $element['#parents']; + $element += call_user_func(array($class, 'settingsForm'), $field); + return $element; +} + +/** + * Ajax callback for the handler settings form. + * + * @see entityreference_field_settings_form() + */ +function entityreference_settings_ajax($form, $form_state) { + $trigger = $form_state['triggering_element']; + $parents = $trigger['#array_parents']; + if ($trigger['#type'] == 'radio') { + // Pop the radio itself. + array_pop($parents); + } + // Pop the container. array_pop($parents); - drupal_array_set_nested_value($form_state['values'], array_merge($parents, array('target_type')), $target_type, TRUE); - drupal_array_set_nested_value($form_state['values'], array_merge($parents, array('target_bundles')), $target_bundles, TRUE); + $parents[] = 'handler_settings'; + + $element = drupal_array_get_nested_value($form, $parents); + return $element; +} + +/** + * Submit handler for the non-JS case. + * + * @see entityreference_field_settings_form() + */ +function entityreference_settings_ajax_submit($form, &$form_state) { + $form_state['rebuild'] = TRUE; } /** @@ -210,52 +318,14 @@ function entityreference_field_widget_settings_form($field, $instance) { * Implements hook_options_list(). */ function entityreference_options_list($field) { - return entityreference_get_referencable_entities($field); + return entityreference_get_handler($field)->getReferencableEntities(); } /** - * Check the number of entities referencable by a given field. + * Implements hook_query_TAG_alter(). */ -function entityreference_get_referencable_count($field) { - $query = new EntityFieldQuery(); - $query->entityCondition('entity_type', $field['settings']['target_type']); - if ($field['settings']['target_bundle']) { - $query->entityCondition('bundle', $field['settings']['target_bundle']); - } - - return $query->count()->execute(); -} - -/** - * Return the labels of referencable entities matching some criteria. - */ -function entityreference_get_referencable_entities($field, $match = NULL, $match_operator = 'CONTAINS', $limit = 0) { - $options = array(); - $entity_type = $field['settings']['target_type']; - - $query = new EntityFieldQuery(); - $query->entityCondition('entity_type', $entity_type); - if ($field['settings']['target_bundles']) { - $query->entityCondition('bundle', $field['settings']['target_bundles'], 'IN'); - } - if (isset($match)) { - $entity_info = entity_get_info($entity_type); - $query->propertyCondition($entity_info['entity keys']['label'], $match, $match_operator); - } - if ($limit > 0) { - $query->range(0, $limit); - } - - $results = $query->execute(); - - if (!empty($results[$entity_type])) { - $entities = entity_load($entity_type, array_keys($results[$entity_type])); - foreach ($entities as $entity_id => $entity) { - $options[$entity_id] = entity_label($entity_type, $entity); - } - } - - return $options; +function entityreference_query_entityreference_alter(QueryAlterableInterface $query) { + entityreference_get_handler($query->getMetadata('field'))->entityFieldQueryAlter($query); } /** @@ -266,6 +336,8 @@ function entityreference_field_widget_form(&$form, &$form_state, $field, $instan $entity_ids = array(); $entity_labels = array(); + $handler = entityreference_get_handler($field); + // Build an array of entities ID. foreach ($items as $item) { $entity_ids[] = $item['target_id']; @@ -275,7 +347,7 @@ function entityreference_field_widget_form(&$form, &$form_state, $field, $instan $entities = entity_load($field['settings']['target_type'], $entity_ids); foreach ($entities as $entity_id => $entity) { - $label = entity_label($field['settings']['target_type'], $entity); + $label = $handler->getLabel($entity); $key = "$label ($entity_id)"; // Labels containing commas or quotes must be wrapped in quotes. if (strpos($key, ',') !== FALSE || strpos($key, '"') !== FALSE) { @@ -331,10 +403,12 @@ function entityreference_autocomplete_callback($field_name, $entity_type, $bundl $instance = field_info_instance($entity_type, $field_name, $bundle_name); $matches = array(); - if (!$field || !$instance || !field_access('edit', $field, $entity_type)) { + if (!$field || !$instance || $field['type'] != 'entityreference' || !field_access('edit', $field, $entity_type)) { return MENU_ACCESS_DENIED; } + $handler = entityreference_get_handler($field); + // The user enters a comma-separated list of tags. We only autocomplete the last tag. $tags_typed = drupal_explode_tags($string); $tag_last = drupal_strtolower(array_pop($tags_typed)); @@ -343,7 +417,7 @@ function entityreference_autocomplete_callback($field_name, $entity_type, $bundl $prefix = count($tags_typed) ? implode(', ', $tags_typed) . ', ' : ''; // Get an array of matching entities. - $entity_labels = entityreference_get_referencable_entities($field, $tag_last, $instance['widget']['settings']['match_operator'], 10); + $entity_labels = $handler->getReferencableEntities($tag_last, $instance['widget']['settings']['match_operator'], 10); // Loop through the products and convert them into autocomplete output. foreach ($entity_labels as $entity_id => $label) { @@ -490,8 +564,10 @@ function entityreference_field_formatter_view($entity_type, $entity, $field, $in switch ($display['type']) { case 'entityreference_label': + $handler = entityreference_get_handler($field); + foreach ($items as $delta => $item) { - $label = entity_label($field['settings']['target_type'], $item['entity']); + $label = $handler->getLabel($item['entity']); if ($display['settings']['link']) { $uri = entity_uri($field['settings']['target_type'], $item['entity']); $result[$delta] = array('#markup' => l($label, $uri['path'], $uri['options'])); @@ -533,72 +609,6 @@ class EntityReferenceRecursiveRenderingException extends Exception {} function entityreference_views_api() { return array( 'api' => 3, + 'path' => dirname(__FILE__), ); } - -/** - * Implements hook_field_views_data(). - */ -function entityreference_field_views_data($field) { - $data = field_views_field_default_views_data($field); - $entity_info = entity_get_info($field['settings']['target_type']); - foreach ($data as $table_name => $table_data) { - if (isset($entity_info['base table'])) { - $entity = $entity_info['label']; - if ($entity == t('Node')) { - $entity = t('Content'); - } - - $field_name = $field['field_name'] . '_target_id'; - $parameters = array('@entity' => $entity, '!field_name' => $field['field_name']); - $data[$table_name][$field_name]['relationship'] = array( - 'handler' => 'views_handler_relationship', - 'base' => $entity_info['base table'], - 'base field' => $entity_info['entity keys']['id'], - 'label' => t('@entity entity referenced from !field_name', $parameters), - 'group' => t('Entity Reference'), - 'title' => t('Referenced Entity'), - 'help' => t('A bridge to the @entity entity that is referenced via !field_name', $parameters), - ); - } - } - - return $data; -} - -/** - * Implements hook_field_views_data_views_data_alter(). - * - * Views integration to provide reverse relationships on entityreference fields. - */ -function entityreference_field_views_data_views_data_alter(&$data, $field) { - foreach ($field['bundles'] as $entity_type => $bundles) { - $target_entity_info = entity_get_info($field['settings']['target_type']); - if (isset($target_entity_info['base table'])) { - $entity_info = entity_get_info($entity_type); - $entity = $entity_info['label']; - if ($entity == t('Node')) { - $entity = t('Content'); - } - $target_entity = $target_entity_info['label']; - if ($target_entity == t('Node')) { - $target_entity = t('Content'); - } - - $pseudo_field_name = 'reverse_' . $field['field_name'] . '_' . $entity_type; - $replacements = array('@entity' => $entity, '@target_entity' => $target_entity, '!field_name' => $field['field_name']); - $data[$target_entity_info['base table']][$pseudo_field_name]['relationship'] = array( - 'handler' => 'views_handler_relationship_entity_reverse', - 'field_name' => $field['field_name'], - 'field table' => _field_sql_storage_tablename($field), - 'field field' => $field['field_name'] . '_target_id', - 'base' => $entity_info['base table'], - 'base field' => $entity_info['entity keys']['id'], - 'label' => t('@entity referencing @target_entity from !field_name', $replacements), - 'group' => t('Entity Reference'), - 'title' => t('Referencing entity'), - 'help' => t('A bridge to the @entity entity that is referencing @target_entity via !field_name', $replacements), - ); - } - } -} diff --git a/entityreference.views.inc b/entityreference.views.inc new file mode 100644 index 0000000..76cbc2b --- /dev/null +++ b/entityreference.views.inc @@ -0,0 +1,73 @@ + $table_data) { + if (isset($entity_info['base table'])) { + $entity = $entity_info['label']; + if ($entity == t('Node')) { + $entity = t('Content'); + } + + $field_name = $field['field_name'] . '_target_id'; + $parameters = array('@entity' => $entity, '!field_name' => $field['field_name']); + $data[$table_name][$field_name]['relationship'] = array( + 'handler' => 'views_handler_relationship', + 'base' => $entity_info['base table'], + 'base field' => $entity_info['entity keys']['id'], + 'label' => t('@entity entity referenced from !field_name', $parameters), + 'group' => t('Entity Reference'), + 'title' => t('Referenced Entity'), + 'help' => t('A bridge to the @entity entity that is referenced via !field_name', $parameters), + ); + } + } + + return $data; +} + +/** + * Implements hook_field_views_data_views_data_alter(). + * + * Views integration to provide reverse relationships on entityreference fields. + */ +function entityreference_field_views_data_views_data_alter(&$data, $field) { + foreach ($field['bundles'] as $entity_type => $bundles) { + $target_entity_info = entity_get_info($field['settings']['target_type']); + if (isset($target_entity_info['base table'])) { + $entity_info = entity_get_info($entity_type); + $entity = $entity_info['label']; + if ($entity == t('Node')) { + $entity = t('Content'); + } + $target_entity = $target_entity_info['label']; + if ($target_entity == t('Node')) { + $target_entity = t('Content'); + } + + $pseudo_field_name = 'reverse_' . $field['field_name'] . '_' . $entity_type; + $replacements = array('@entity' => $entity, '@target_entity' => $target_entity, '!field_name' => $field['field_name']); + $data[$target_entity_info['base table']][$pseudo_field_name]['relationship'] = array( + 'handler' => 'views_handler_relationship_entity_reverse', + 'field_name' => $field['field_name'], + 'field table' => _field_sql_storage_tablename($field), + 'field field' => $field['field_name'] . '_target_id', + 'base' => $entity_info['base table'], + 'base field' => $entity_info['entity keys']['id'], + 'label' => t('@entity referencing @target_entity from !field_name', $replacements), + 'group' => t('Entity Reference'), + 'title' => t('Referencing entity'), + 'help' => t('A bridge to the @entity entity that is referencing @target_entity via !field_name', $replacements), + ); + } + } +} diff --git a/handler/base.inc b/handler/base.inc new file mode 100644 index 0000000..31fc852 --- /dev/null +++ b/handler/base.inc @@ -0,0 +1,349 @@ + t('Simple (with optional filter by bundle)'), + 'handler' => 'EntityReferenceHandler_base', + 'weight' => -100, +); + +/** + * A generic Entity handler. + * + * The generic base implementation has a variety of overrides to workaround + * core's largely deficient entity handling. + */ +class EntityReferenceHandler_base implements EntityReferenceHandler { + + /** + * Implements EntityReferenceHandler::getInstance(). + */ + public static function getInstance($field) { + $entity_type = $field['settings']['target_type']; + if (class_exists($class_name = 'EntityReferenceHandler_' . $entity_type)) { + return new $class_name($field); + } + else { + return new EntityReferenceHandler_base($field); + } + } + + protected function __construct($field) { + $this->field = $field; + } + + /** + * Implements EntityReferenceHandler::settingsForm(). + */ + public static function settingsForm($field) { + $entity_info = entity_get_info($field['settings']['target_type']); + $bundles = array(); + foreach ($entity_info['bundles'] as $bundle_name => $bundle_info) { + $bundles[$bundle_name] = $bundle_info['label']; + } + + $form['target_bundles'] = array( + '#type' => 'select', + '#title' => t('Target bundles'), + '#options' => $bundles, + '#default_value' => isset($field['settings']['handler_settings']['target_bundles']) ? $field['settings']['handler_settings']['target_bundles'] : array(), + '#size' => 6, + '#multiple' => TRUE, + '#description' => t('The bundles of the entity type that can be referenced. Optional, leave empty for all bundles.') + ); + return $form; + } + + /** + * Implements EntityReferenceHandler::getReferencableEntities(). + */ + public function getReferencableEntities($match = NULL, $match_operator = 'CONTAINS', $limit = 0) { + $options = array(); + $entity_type = $this->field['settings']['target_type']; + + $query = $this->buildEntityFieldQuery($match, $match_operator); + if ($limit > 0) { + $query->range(0, $limit); + } + + $results = $query->execute(); + + if (!empty($results[$entity_type])) { + $entities = entity_load($entity_type, array_keys($results[$entity_type])); + foreach ($entities as $entity_id => $entity) { + $options[$entity_id] = $this->getLabel($entity); + } + } + + return $options; + } + + /** + * Implements EntityReferenceHandler::countReferencableEntities(). + */ + public function countReferencableEntities($match = NULL, $match_operator = 'CONTAINS') { + $query = $this->buildEntityFieldQuery($match, $match_operator); + return $query + ->count() + ->execute(); + } + + /** + * Implements EntityReferenceHandler::validateReferencableEntities(). + */ + public function validateReferencableEntities(array $ids) { + if ($ids) { + $entity_type = $this->field['settings']['target_type']; + $query = $this->buildEntityFieldQuery(); + $query->entityCondition('entity_id', $ids, 'IN'); + $result = $query->execute(); + if (!empty($result[$entity_type])) { + return array_keys($result[$entity_type]); + } + } + + return array(); + } + + /** + * Build an EntityFieldQuery to get referencable entities. + */ + protected function buildEntityFieldQuery($match = NULL, $match_operator = 'CONTAINS') { + $query = new EntityFieldQuery(); + $query->entityCondition('entity_type', $this->field['settings']['target_type']); + if ($this->field['settings']['target_bundles']) { + $query->entityCondition('bundle', $this->field['settings']['target_bundles'], 'IN'); + } + if (isset($match)) { + $entity_info = entity_get_info($this->field['settings']['target_type']); + if (isset($entity_info['entity keys']['label'])) { + $query->propertyCondition($entity_info['entity keys']['label'], $match, $match_operator); + } + } + + // Add a generic entity access tag to the query. + $query->addTag($this->field['settings']['target_type'] . '_access'); + $query->addTag('entityreference'); + $query->addMetaData('field', $this->field); + + return $query; + } + + /** + * Implements EntityReferenceHandler::entityFieldQueryAlter(). + */ + public function entityFieldQueryAlter(SelectQueryInterface $query) { + + } + + /** + * Helper method: pass a query to the alteration system again. + */ + protected function reAlterQuery(SelectQueryInterface $query, $tag, $base_table) { + // Save the old tags and metadata. + // For some reason, those are public. + $old_tags = $query->alterTags; + $old_metadata = $query->alterMetaData; + + $query->alterTags = array($tag => TRUE); + $query->alterMetaData['base_table'] = $base_table; + drupal_alter(array('query', 'query_' . $tag), $query); + + // Restore the tags and metadata. + $query->alterTags = $old_tags; + $query->alterMetaData = $old_metadata; + } + + /** + * Implements EntityReferenceHandler::getLabel(). + */ + public function getLabel($entity) { + return entity_label($this->field['settings']['target_type'], $entity); + } +} + +/** + * Override for the Node type. + * + * This only exists to workaround core bugs. + */ +class EntityReferenceHandler_node extends EntityReferenceHandler_base { + public function entityFieldQueryAlter(SelectQueryInterface $query) { + // Adding the 'node_access' tag is sadly insufficient for nodes: core + // requires us to also know about the concept of 'published' and + // 'unpublished'. We need to do that as long as there are no access control + // modules in use on the site. As long as one access control module is there, + // it is supposed to handle this check. + if (!user_access('bypass node access') && !count(module_implements('node_grants'))) { + $tables = $query->getTables(); + $query->condition(key($tables) . '.status', NODE_PUBLISHED); + } + } +} + +/** + * Override for the User type. + * + * This only exists to workaround core bugs. + */ +class EntityReferenceHandler_user extends EntityReferenceHandler_base { + public function buildEntityFieldQuery($match = NULL, $match_operator = 'CONTAINS') { + $query = parent::buildEntityFieldQuery($match, $match_operator); + + // The user entity doesn't have a label column. + if (isset($match)) { + $query->propertyCondition('name', $match, $match_operator); + } + + // Adding the 'user_access' tag is sadly insufficient for users: core + // requires us to also know about the concept of 'blocked' and + // 'active'. + if (!user_access('administer users')) { + $query->propertyCondition('status', 1); + } + return $query; + } + + public function getLabel($entity) { + // entity_label() doesn't work at all for users. + // @see http://drupal.org/1261918 + return format_username($entity); + } + + public function entityFieldQueryAlter(SelectQueryInterface $query) { + if (user_access('administer users')) { + // In addition, if the user is administrator, we need to make sure to + // match the anonymous user, that doesn't actually have a name in the + // database. + $conditions = &$query->conditions(); + foreach ($conditions as $key => $condition) { + if ($condition['field'] == 'users.name') { + // Remove the condition. + unset($conditions[$key]); + + // Re-add the condition and a condition on uid = 0 so that we end up + // with a query in the form: + // WHERE (name LIKE :name) OR (:anonymous_name LIKE :name AND uid = 0) + $or = db_or(); + $or->condition($condition['field'], $condition['value'], $condition['operator']); + // Sadly, the Database layer doesn't allow us to build a condition + // in the form ':placeholder = :placeholder2', because the 'field' + // part of a condition is always escaped. + // As a (cheap) workaround, we separately build a condition with no + // field, and concatenate the field and the condition separately. + $value_part = db_and(); + $value_part->condition('anonymous_name', $condition['value'], $condition['operator']); + $value_part->compile(Database::getConnection(), $query); + $or->condition(db_and() + ->where(str_replace('anonymous_name', ':anonymous_name', (string) $value_part), $value_part->arguments() + array(':anonymous_name' => format_username(user_load(0)))) + ->condition('users.uid', 0) + ); + $query->condition($or); + } + } + } + } +} + +/** + * Override for the Comment type. + * + * This only exists to workaround core bugs. + */ +class EntityReferenceHandler_comment extends EntityReferenceHandler_base { + public function entityFieldQueryAlter(SelectQueryInterface $query) { + // Adding the 'comment_access' tag is sadly insufficient for comments: core + // requires us to also know about the concept of 'published' and + // 'unpublished'. + if (!user_access('administer comments')) { + $tables = $query->getTables(); + $query->condition(key($tables) . '.status', COMMENT_PUBLISHED); + } + + // The Comment module doesn't implement any proper comment access, + // and as a consequence doesn't make sure that comments cannot be viewed + // when the user doesn't have access to the node. + $tables = $query->getTables(); + $base_table = key($tables); + $node_alias = $query->innerJoin('node', 'n', '%alias.nid = ' . $base_table . '.nid'); + // Pass the query to the node access control. + $this->reAlterQuery($query, 'node_access', $node_alias); + + // Alas, the comment entity exposes a bundle, but doesn't have a bundle column + // in the database. We have to alter the query ourself to go fetch the + // bundle. + $conditions = &$query->conditions(); + foreach ($conditions as $id => &$condition) { + if (is_array($condition) && $condition['field'] == 'node_type') { + $condition['field'] = $node_alias . '.type'; + foreach ($condition['value'] as &$value) { + if (substr($value, 0, 13) == 'comment_node_') { + $value = substr($value, 13); + } + } + break; + } + } + + // Passing the query to node_query_node_access_alter() is sadly + // insufficient for nodes. + // @see EntityReferenceHandler_node::entityFieldQueryAlter() + if (!user_access('bypass node access') && !count(module_implements('node_grants'))) { + $query->condition($node_alias . '.status', 1); + } + } +} + +/** + * Override for the File type. + * + * This only exists to workaround core bugs. + */ +class EntityReferenceHandler_file extends EntityReferenceHandler_base { + public function entityFieldQueryAlter(SelectQueryInterface $query) { + // Core forces us to know about 'permanent' vs. 'temporary' files. + $tables = $query->getTables(); + $base_table = key($tables); + $query->condition('status', FILE_STATUS_PERMANENT); + + // Access control to files is a very difficult business. For now, we are not + // going to give it a shot. + // @todo: fix this when core access control is less insane. + return $query; + } + + public function getLabel($entity) { + // The file entity doesn't have a label. More over, the filename is + // sometimes empty, so use the basename in that case. + return $entity->filename !== '' ? $entity->filename : basename($entity->uri); + } +} + +/** + * Override for the Taxonomy term type. + * + * This only exists to workaround core bugs. + */ +class EntityReferenceHandler_taxonomy_term extends EntityReferenceHandler_base { + public function entityFieldQueryAlter(SelectQueryInterface $query) { + // The Taxonomy module doesn't implement any proper taxonomy term access, + // and as a consequence doesn't make sure that taxonomy terms cannot be viewed + // when the user doesn't have access to the vocabulary. + $tables = $query->getTables(); + $base_table = key($tables); + $vocabulary_alias = $query->innerJoin('taxonomy_vocabulary', 'n', '%alias.vid = ' . $base_table . '.vid'); + $query->addMetadata('base_table', $vocabulary_alias); + // Pass the query to the taxonomy access control. + $this->reAlterQuery($query, 'taxonomy_vocabulary_access', $vocabulary_alias); + + // Also, the taxonomy term entity exposes a bundle, but doesn't have a bundle + // column in the database. We have to alter the query ourself to go fetch + // the bundle. + $conditions = &$query->conditions(); + foreach ($conditions as $id => &$condition) { + if (is_array($condition) && $condition['field'] == 'vocabulary_machine_name') { + $condition['field'] = $vocabulary_alias . '.machine_name'; + break; + } + } + } +} diff --git a/tests/entityreference.handlers.test b/tests/entityreference.handlers.test new file mode 100644 index 0000000..ae9266d --- /dev/null +++ b/tests/entityreference.handlers.test @@ -0,0 +1,414 @@ + 'Entity Reference Handlers', + 'description' => 'Tests for the base handlers provided by Entity Reference.', + 'group' => 'Entity Reference', + ); + } + + public function setUp() { + parent::setUp(array('entity', 'entityreference')); + } + + protected function assertReferencable($field, $tests, $handler_name) { + $handler = entityreference_get_handler($field); + + foreach ($tests as $test) { + foreach ($test['arguments'] as $arguments) { + $result = call_user_func_array(array($handler, 'getReferencableEntities'), $arguments); + $this->assertEqual($result, $test['result'], t('Valid result set returned by @handler.', array('@handler' => $handler_name))); + + $result = call_user_func_array(array($handler, 'countReferencableEntities'), $arguments); + $this->assertEqual($result, count($test['result']), t('Valid count returned by @handler.', array('@handler' => $handler_name))); + } + } + } + + /** + * Test the node-specific overrides of the entity handler. + */ + public function testNodeHandler() { + // Build a fake field instance. + $field = array( + 'translatable' => FALSE, + 'entity_types' => array(), + 'settings' => array( + 'handler' => 'base', + 'target_type' => 'node', + 'target_bundles' => array(), + ), + 'field_name' => 'test_field', + 'type' => 'entityreference', + 'cardinality' => '1', + ); + + // Build a set of test data. + $nodes = array( + 'published1' => (object) array( + 'type' => 'article', + 'status' => 1, + 'title' => 'Node published1', + 'uid' => 1, + ), + 'published2' => (object) array( + 'type' => 'article', + 'status' => 1, + 'title' => 'Node published2', + 'uid' => 1, + ), + 'unpublished' => (object) array( + 'type' => 'article', + 'status' => 0, + 'title' => 'Node unpublished', + 'uid' => 1, + ), + ); + + $node_labels = array(); + foreach ($nodes as $node) { + node_save($node); + $node_labels[$node->nid] = $node->title; + } + + // Test as a non-admin. + $normal_user = $this->drupalCreateUser(array('access content')); + $GLOBALS['user'] = $normal_user; + $referencable_tests = array( + array( + 'arguments' => array( + array(NULL, 'CONTAINS'), + ), + 'result' => array( + $nodes['published1']->nid => $nodes['published1']->title, + $nodes['published2']->nid => $nodes['published2']->title, + ), + ), + array( + 'arguments' => array( + array('published1', 'CONTAINS'), + array('Published1', 'CONTAINS'), + ), + 'result' => array( + $nodes['published1']->nid => $nodes['published1']->title, + ), + ), + array( + 'arguments' => array( + array('published2', 'CONTAINS'), + array('Published2', 'CONTAINS'), + ), + 'result' => array( + $nodes['published2']->nid => $nodes['published2']->title, + ), + ), + array( + 'arguments' => array( + array('invalid node', 'CONTAINS'), + ), + 'result' => array(), + ), + array( + 'arguments' => array( + array('Node unpublished', 'CONTAINS'), + ), + 'result' => array(), + ), + ); + $this->assertReferencable($field, $referencable_tests, 'Node handler'); + + // Test as an admin. + $admin_user = $this->drupalCreateUser(array('access content', 'bypass node access')); + $GLOBALS['user'] = $admin_user; + $referencable_tests = array( + array( + 'arguments' => array( + array(NULL, 'CONTAINS'), + ), + 'result' => array( + $nodes['published1']->nid => $nodes['published1']->title, + $nodes['published2']->nid => $nodes['published2']->title, + $nodes['unpublished']->nid => $nodes['unpublished']->title, + ), + ), + array( + 'arguments' => array( + array('Node unpublished', 'CONTAINS'), + ), + 'result' => array( + $nodes['unpublished']->nid => $nodes['unpublished']->title, + ), + ), + ); + $this->assertReferencable($field, $referencable_tests, 'Node handler (admin)'); + } + + /** + * Test the user-specific overrides of the entity handler. + */ + public function testUserHandler() { + // Build a fake field instance. + $field = array( + 'translatable' => FALSE, + 'entity_types' => array(), + 'settings' => array( + 'handler' => 'base', + 'target_type' => 'user', + 'target_bundles' => array(), + ), + 'field_name' => 'test_field', + 'type' => 'entityreference', + 'cardinality' => '1', + ); + + // Build a set of test data. + $users = array( + 'anonymous' => user_load(0), + 'admin' => user_load(1), + 'non_admin' => (object) array( + 'name' => 'non_admin', + 'mail' => 'non_admin@example.com', + 'roles' => array(), + 'pass' => user_password(), + 'status' => 1, + ), + 'blocked' => (object) array( + 'name' => 'blocked', + 'mail' => 'blocked@example.com', + 'roles' => array(), + 'pass' => user_password(), + 'status' => 0, + ), + ); + + // The label of the anonymous user is variable_get('anonymous'). + $users['anonymous']->name = variable_get('anonymous', t('Anonymous')); + + $user_labels = array(); + foreach ($users as $key => $user) { + if (!isset($user->uid)) { + $users[$key] = $user = user_save(drupal_anonymous_user(), (array) $user); + } + $user_labels[$user->uid] = $user->name; + } + + // Test as a non-admin. + $GLOBALS['user'] = $users['non_admin']; + $referencable_tests = array( + array( + 'arguments' => array( + array(NULL, 'CONTAINS'), + ), + 'result' => array( + $users['admin']->uid => $users['admin']->name, + $users['non_admin']->uid => $users['non_admin']->name, + ), + ), + array( + 'arguments' => array( + array('non_admin', 'CONTAINS'), + array('NON_ADMIN', 'CONTAINS'), + ), + 'result' => array( + $users['non_admin']->uid => $users['non_admin']->name, + ), + ), + array( + 'arguments' => array( + array('invalid user', 'CONTAINS'), + ), + 'result' => array(), + ), + array( + 'arguments' => array( + array('blocked', 'CONTAINS'), + ), + 'result' => array(), + ), + ); + $this->assertReferencable($field, $referencable_tests, 'User handler'); + + $GLOBALS['user'] = $users['admin']; + $referencable_tests = array( + array( + 'arguments' => array( + array(NULL, 'CONTAINS'), + ), + 'result' => array( + $users['anonymous']->uid => $users['anonymous']->name, + $users['admin']->uid => $users['admin']->name, + $users['non_admin']->uid => $users['non_admin']->name, + $users['blocked']->uid => $users['blocked']->name, + ), + ), + array( + 'arguments' => array( + array('blocked', 'CONTAINS'), + ), + 'result' => array( + $users['blocked']->uid => $users['blocked']->name, + ), + ), + array( + 'arguments' => array( + array('Anonymous', 'CONTAINS'), + array('anonymous', 'CONTAINS'), + ), + 'result' => array( + $users['anonymous']->uid => $users['anonymous']->name, + ), + ), + ); + $this->assertReferencable($field, $referencable_tests, 'User handler (admin)'); + } + + /** + * Test the comment-specific overrides of the entity handler. + */ + public function testCommentHandler() { + // Build a fake field instance. + $field = array( + 'translatable' => FALSE, + 'entity_types' => array(), + 'settings' => array( + 'handler' => 'base', + 'target_type' => 'comment', + 'target_bundles' => array(), + ), + 'field_name' => 'test_field', + 'type' => 'entityreference', + 'cardinality' => '1', + ); + + // Build a set of test data. + $nodes = array( + 'published' => (object) array( + 'type' => 'article', + 'status' => 1, + 'title' => 'Node published', + 'uid' => 1, + ), + 'unpublished' => (object) array( + 'type' => 'article', + 'status' => 0, + 'title' => 'Node unpublished', + 'uid' => 1, + ), + ); + foreach ($nodes as $node) { + node_save($node); + } + + $comments = array( + 'published_published' => (object) array( + 'nid' => $nodes['published']->nid, + 'uid' => 1, + 'cid' => NULL, + 'pid' => 0, + 'status' => COMMENT_PUBLISHED, + 'subject' => 'Comment Published', + 'hostname' => ip_address(), + 'language' => LANGUAGE_NONE, + ), + 'published_unpublished' => (object) array( + 'nid' => $nodes['published']->nid, + 'uid' => 1, + 'cid' => NULL, + 'pid' => 0, + 'status' => COMMENT_NOT_PUBLISHED, + 'subject' => 'Comment Unpublished', + 'hostname' => ip_address(), + 'language' => LANGUAGE_NONE, + ), + 'unpublished_published' => (object) array( + 'nid' => $nodes['unpublished']->nid, + 'uid' => 1, + 'cid' => NULL, + 'pid' => 0, + 'status' => COMMENT_NOT_PUBLISHED, + 'subject' => 'Comment Published on Unpublished node', + 'hostname' => ip_address(), + 'language' => LANGUAGE_NONE, + ), + ); + + $comment_labels = array(); + foreach ($comments as $comment) { + comment_save($comment); + $comment_labels[$comment->cid] = $comment->subject; + } + + // Test as a non-admin. + $normal_user = $this->drupalCreateUser(array('access content', 'access comments')); + $GLOBALS['user'] = $normal_user; + $referencable_tests = array( + array( + 'arguments' => array( + array(NULL, 'CONTAINS'), + ), + 'result' => array( + $comments['published_published']->cid => $comments['published_published']->subject, + ), + ), + array( + 'arguments' => array( + array('Published', 'CONTAINS'), + ), + 'result' => array( + $comments['published_published']->cid => $comments['published_published']->subject, + ), + ), + array( + 'arguments' => array( + array('invalid comment', 'CONTAINS'), + ), + 'result' => array(), + ), + array( + 'arguments' => array( + array('Comment Unpublished', 'CONTAINS'), + ), + 'result' => array(), + ), + ); + $this->assertReferencable($field, $referencable_tests, 'Comment handler'); + + // Test as a comment admin. + $admin_user = $this->drupalCreateUser(array('access content', 'access comments', 'administer comments')); + $GLOBALS['user'] = $admin_user; + $referencable_tests = array( + array( + 'arguments' => array( + array(NULL, 'CONTAINS'), + ), + 'result' => array( + $comments['published_published']->cid => $comments['published_published']->subject, + $comments['published_unpublished']->cid => $comments['published_unpublished']->subject, + ), + ), + ); + $this->assertReferencable($field, $referencable_tests, 'Comment handler (comment admin)'); + + // Test as a node and comment admin. + $admin_user = $this->drupalCreateUser(array('access content', 'access comments', 'administer comments', 'bypass node access')); + $GLOBALS['user'] = $admin_user; + $referencable_tests = array( + array( + 'arguments' => array( + array(NULL, 'CONTAINS'), + ), + 'result' => array( + $comments['published_published']->cid => $comments['published_published']->subject, + $comments['published_unpublished']->cid => $comments['published_unpublished']->subject, + $comments['unpublished_published']->cid => $comments['unpublished_published']->subject, + ), + ), + ); + $this->assertReferencable($field, $referencable_tests, 'Comment handler (comment + node admin)'); + } +}