commit e31e50e670ef4c8498255d973129310e0b34f60b 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..8b2dabd --- /dev/null +++ b/entityreference.handler.inc @@ -0,0 +1,263 @@ +field = $field; + } + + /** + * Return a list of referencable entities. + */ + 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; + } + + /** + * Count entities that are referencable by a given field. + */ + public function countReferencableEntities($match = NULL, $match_operator = 'CONTAINS') { + $query = $this->buildEntityFieldQuery($match, $match_operator); + $query + ->count() + ->execute(); + } + + /** + * Validate that entities can be referenced by this field. + * + * @return + * An array of entity ids that are valid. + */ + 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. + */ + public 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; + } + + /** + * Give the handler a change to alter the SelectQuery generated by EntityFieldQuery. + */ + public function entityFieldQueryAlter($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; + } + + /** + * Return the label of a given entity. + */ + 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($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); + } +} + +/** + * Override for the Comment type. + * + * This only exists to workaround core bugs. + */ +class EntityReferenceHandler_comment extends EntityReferenceHandler_base { + public function entityFieldQueryAlter(SelectQueryInterface $query) { + // 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); + + // Alias, 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/entityreference.info b/entityreference.info index bc6104f..9440196 100644 --- a/entityreference.info +++ b/entityreference.info @@ -3,3 +3,4 @@ description = Provides a field that can reference other entities. core = 7.x dependencies[] = entity +files[] = entityreference.handler.inc diff --git a/entityreference.module b/entityreference.module index dc5e004..fa85055 100644 --- a/entityreference.module +++ b/entityreference.module @@ -46,10 +46,42 @@ 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) { + $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); + } +} + +/** * 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)), + ); + } + } } /** @@ -210,52 +242,14 @@ function entityreference_field_widget_settings_form($field, $instance) { * Implements hook_options_list(). */ function entityreference_options_list($field) { - return entityreference_get_referencable_entities($field); -} - -/** - * Check the number of entities referencable by a given field. - */ -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 entityreference_get_handler($field)->getReferencableEntities(); } /** - * Return the labels of referencable entities matching some criteria. + * Implements hook_query_TAG_alter(). */ -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 +260,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 +271,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 +327,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 +341,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 +488,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']));