Issue #2405469 by yched, googletorp, amateescu, larowlan, Berdir: FileFormatterBase should extend EntityReferenceFormatterBase

8.0.x
Alex Pott 2015-02-27 11:47:51 +00:00
parent 9c0bfb2d3b
commit ef0cb6e091
14 changed files with 282 additions and 143 deletions

View File

@ -7,7 +7,8 @@
namespace Drupal\Core\Field\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\EntityReferenceFieldItemListInterface;
use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
use Drupal\Core\Field\FormatterBase;
use Drupal\Core\TypedData\TranslatableInterface;
@ -17,15 +18,26 @@ use Drupal\Core\TypedData\TranslatableInterface;
abstract class EntityReferenceFormatterBase extends FormatterBase {
/**
* Returns the accessible and translated entities for view.
* Returns the referenced entities for display.
*
* @param \Drupal\Core\Field\FieldItemListInterface $items
* The method takes care of:
* - checking entity access,
* - placing the entities in the language expected for display.
* It is thus strongly recommended that formatters use it in their
* implementation of viewElements($items) rather than dealing with $items
* directly.
*
* For each entity, the EntityReferenceItem by which the entity is referenced
* is available in $entity->_referringItem. This is useful for field types
* that store additional values next to the reference itself.
*
* @param \Drupal\Core\Field\EntityReferenceFieldItemListInterface $items
* The item list.
*
* @return \Drupal\Core\Entity\EntityInterface[]
* The entities to view.
* The array of referenced entities to display, keyed by delta.
*/
protected function getEntitiesToView(FieldItemListInterface $items) {
protected function getEntitiesToView(EntityReferenceFieldItemListInterface $items) {
$entities = array();
$parent_entity_langcode = $items->getEntity()->language()->getId();
@ -39,8 +51,10 @@ abstract class EntityReferenceFormatterBase extends FormatterBase {
$entity = $entity->getTranslation($parent_entity_langcode);
}
// Check entity access.
if ($entity->access('view')) {
// Check entity access if needed.
if (!$this->needsAccessCheck($item) || $entity->access('view')) {
// Add the referring item, in case the formatter needs it.
$entity->_referringItem = $items[$delta];
$entities[$delta] = $entity;
}
}
@ -56,10 +70,9 @@ abstract class EntityReferenceFormatterBase extends FormatterBase {
* viewed.
*/
public function prepareView(array $entities_items) {
// Load the existing (non-autocreate) entities. For performance, we want to
// use a single "multiple entity load" to load all the entities for the
// multiple "entity reference item lists" that are being displayed. We thus
// cannot use
// Collect entity IDs to load. For performance, we want to use a single
// "multiple entity load" to load all the entities for the multiple
// "entity reference item lists" being displayed. We thus cannot use
// \Drupal\Core\Field\EntityReferenceFieldItemList::referencedEntities().
$ids = array();
foreach ($entities_items as $items) {
@ -69,7 +82,7 @@ abstract class EntityReferenceFormatterBase extends FormatterBase {
// contains a valid entity ready for display. All items are initialized
// at FALSE.
$item->_loaded = FALSE;
if ($item->target_id !== NULL) {
if ($this->needsEntityLoad($item)) {
$ids[] = $item->target_id;
}
}
@ -94,4 +107,30 @@ abstract class EntityReferenceFormatterBase extends FormatterBase {
}
}
/**
* Returns whether the entity referenced by an item needs to be loaded.
*
* @param \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem $item
* The item to check.
*
* @return bool
* TRUE if the entity needs to be loaded.
*/
protected function needsEntityLoad(EntityReferenceItem $item) {
return !$item->hasNewEntity();
}
/**
* Returns whether entity access should be checked.
*
* @param \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem $item
* The item to check.
*
* @return bool
* TRUE if entity access should be checked.
*/
protected function needsAccessCheck(EntityReferenceItem $item) {
return TRUE;
}
}

View File

@ -0,0 +1,30 @@
<?php
/**
* @file
* Contains \Drupal\file\FileAccessFormatterControlHandlerInterface.
*/
namespace Drupal\file;
use Drupal\Core\Entity\EntityAccessControlHandlerInterface;
/**
* Defines an interface for file access handlers that need to run on file formatters.
*
* \Drupal\Core\Field\Plugin\Field\FieldFormatter\EntityReferenceFormatterBase,
* which file and image formatters extend, checks 'view' access on the
* referenced files before displaying them. That check would be useless and
* costly with Core's default access control implementation for files
* (\Drupal\file\FileAccessControlHandler grants access based on whether
* there are existing entities with granted access that reference the file). But
* it might be needed if a different access control handler with different logic
* is swapped in.
*
* \Drupal\file\Plugin\Field\FieldFormatter\FileFormatterBase thus adjusts that
* behavior, and only checks access if the access control handler in use for
* files opts in by implementing this interface.
*
* @see \Drupal\file\Plugin\Field\FieldFormatter\FileFormatterBase::needsAccessCheck()
*/
interface FileAccessFormatterControlHandlerInterface extends EntityAccessControlHandlerInterface { }

View File

@ -7,40 +7,29 @@
namespace Drupal\file\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\FormatterBase;
use Drupal\Core\Field\Plugin\Field\FieldFormatter\EntityReferenceFormatterBase;
use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
/**
* Base class for file formatters.
*/
abstract class FileFormatterBase extends FormatterBase {
abstract class FileFormatterBase extends EntityReferenceFormatterBase {
/**
* {@inheritdoc}
*/
public function prepareView(array $entities_items) {
// Remove files specified to not be displayed.
$fids = array();
foreach ($entities_items as $items) {
foreach ($items as $item) {
if ($item->isDisplayed() && !empty($item->target_id)) {
// Load the files from the files table.
$fids[] = $item->target_id;
}
}
}
if ($fids) {
$files = file_load_multiple($fids);
foreach ($entities_items as $items) {
/** @var \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem $item */
foreach ($items as $item) {
// If the file does not exist, mark the entire item as empty.
if (!empty($item->target_id) && !$item->hasNewEntity()) {
$item->entity = isset($files[$item->target_id]) ? $files[$item->target_id] : NULL;
}
}
}
}
protected function needsEntityLoad(EntityReferenceItem $item) {
return parent::needsEntityLoad($item) && $item->isDisplayed();
}
/**
* {@inheritdoc}
*/
protected function needsAccessCheck(EntityReferenceItem $item) {
// Only check access if the current file access control handler explicitly
// opts in by implementing FileAccessFormatterControlHandlerInterface.
$access_handler_class = $item->entity->getEntityType()->getHandlerClass('access');
return is_subclass_of($access_handler_class, '\Drupal\file\FileAccessFormatterControlHandlerInterface');
}
}

View File

@ -28,21 +28,20 @@ class GenericFileFormatter extends FileFormatterBase {
public function viewElements(FieldItemListInterface $items) {
$elements = array();
foreach ($items as $delta => $item) {
if ($item->isDisplayed() && $item->entity) {
$elements[$delta] = array(
'#theme' => 'file_link',
'#file' => $item->entity,
'#description' => $item->description,
);
// Pass field item attributes to the theme function.
if (isset($item->_attributes)) {
$elements[$delta] += array('#attributes' => array());
$elements[$delta]['#attributes'] += $item->_attributes;
// Unset field item attributes since they have been included in the
// formatter output and should not be rendered in the field template.
unset($item->_attributes);
}
foreach ($this->getEntitiesToView($items) as $delta => $file) {
$item = $file->_referringItem;
$elements[$delta] = array(
'#theme' => 'file_link',
'#file' => $file,
'#description' => $item->description,
);
// Pass field item attributes to the theme function.
if (isset($item->_attributes)) {
$elements[$delta] += array('#attributes' => array());
$elements[$delta]['#attributes'] += $item->_attributes;
// Unset field item attributes since they have been included in the
// formatter output and should not be rendered in the field template.
unset($item->_attributes);
}
}
if (!empty($elements)) {

View File

@ -29,21 +29,16 @@ class RSSEnclosureFormatter extends FileFormatterBase {
$entity = $items->getEntity();
// Add the first file as an enclosure to the RSS item. RSS allows only one
// enclosure per item. See: http://en.wikipedia.org/wiki/RSS_enclosure
foreach ($items as $item) {
if ($item->isDisplayed() && $item->entity) {
$file = $item->entity;
$entity->rss_elements[] = array(
'key' => 'enclosure',
'attributes' => array(
'url' => file_create_url($file->getFileUri()),
'length' => $file->getSize(),
'type' => $file->getMimeType(),
),
);
break;
}
foreach ($this->getEntitiesToView($items) as $delta => $file) {
$entity->rss_elements[] = array(
'key' => 'enclosure',
'attributes' => array(
'url' => file_create_url($file->getFileUri()),
'length' => $file->getSize(),
'type' => $file->getMimeType(),
),
);
}
}
}

View File

@ -28,22 +28,19 @@ class TableFormatter extends FileFormatterBase {
public function viewElements(FieldItemListInterface $items) {
$elements = array();
if (!$items->isEmpty()) {
if ($files = $this->getEntitiesToView($items)) {
$header = array(t('Attachment'), t('Size'));
$rows = array();
foreach ($items as $delta => $item) {
if ($item->isDisplayed() && $item->entity) {
$rows[] = array(
array(
'data' => array(
'#theme' => 'file_link',
'#file' => $item->entity,
),
foreach ($files as $delta => $file) {
$rows[] = array(
array(
'data' => array(
'#theme' => 'file_link',
'#file' => $file,
),
array('data' => format_size($item->entity->getSize())),
);
}
),
array('data' => format_size($file->getSize())),
);
}
$elements[0] = array();

View File

@ -28,10 +28,8 @@ class UrlPlainFormatter extends FileFormatterBase {
public function viewElements(FieldItemListInterface $items) {
$elements = array();
foreach ($items as $delta => $item) {
if ($item->isDisplayed() && $item->entity) {
$elements[$delta] = array('#markup' => empty($item->entity) ? '' : file_create_url($item->entity->getFileUri()));
}
foreach ($this->getEntitiesToView($items) as $delta => $file) {
$elements[$delta] = array('#markup' => file_create_url($file->getFileUri()));
}
return $elements;

View File

@ -0,0 +1,38 @@
<?php
/**
* @file
* Contains \Drupal\file\Tests\FileFieldFormatterAccessTest.
*/
namespace Drupal\file\Tests;
/**
* Tests file formatter access.
* @group file
*/
class FileFieldFormatterAccessTest extends FileFieldTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['node', 'file', 'field_ui', 'file_test'];
/**
* Tests the custom access handler is invoked.
*/
public function testFileAccessHandler() {
$type_name = 'article';
$field_name = strtolower($this->randomMachineName());
$this->createFileField($field_name, 'node', $type_name);
\Drupal::state()->set('file_test_alternate_access_handler', TRUE);
\Drupal::entityManager()->clearCachedDefinitions();
$test_file = $this->getTestFile('text');
$nid = $this->uploadNodeFile($test_file, $field_name, $type_name);
$this->drupalGet('node/' . $nid);
$this->assertTrue(\Drupal::state()->get('file_access_formatter_check', FALSE));
}
}

View File

@ -333,3 +333,14 @@ function file_test_file_scan_callback($filepath = NULL) {
function file_test_file_scan_callback_reset() {
drupal_static_reset('file_test_file_scan_callback');
}
/**
* Implements hook_entity_info_alter().
*/
function file_test_entity_type_alter(&$entity_types) {
if (\Drupal::state()->get('file_test_alternate_access_handler', FALSE)) {
/** @var $entity_types \Drupal\Core\Entity\EntityTypeInterface[] */
$entity_types['file']
->setAccessClass('Drupal\file_test\FileTestAccessControlHandler');
}
}

View File

@ -0,0 +1,27 @@
<?php
/**
* @file
* Contains \Drupal\file_test\FileTestAccessControlHandler.
*/
namespace Drupal\file_test;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\file\FileAccessFormatterControlHandlerInterface;
use Drupal\file\FileAccessControlHandler;
/**
* Defines a class for an alternate file access control handler.
*/
class FileTestAccessControlHandler extends FileAccessControlHandler implements FileAccessFormatterControlHandlerInterface {
/**
* {@inheritdoc}
*/
protected function checkAccess(EntityInterface $entity, $operation, $langcode, AccountInterface $account) {
\Drupal::state()->set('file_access_formatter_check', TRUE);
return parent::checkAccess($entity, $operation, $langcode, $account);
}
}

View File

@ -167,8 +167,14 @@ class ImageFormatter extends ImageFormatterBase implements ContainerFactoryPlugi
*/
public function viewElements(FieldItemListInterface $items) {
$elements = array();
$url = NULL;
$files = $this->getEntitiesToView($items);
// Early opt-out if the field is empty.
if (empty($files)) {
return $elements;
}
$url = NULL;
$image_link_setting = $this->getSetting('image_link');
// Check if the formatter involves a link.
if ($image_link_setting == 'content') {
@ -190,29 +196,28 @@ class ImageFormatter extends ImageFormatterBase implements ContainerFactoryPlugi
$cache_tags = $image_style->getCacheTags();
}
foreach ($items as $delta => $item) {
if ($item->entity) {
if (isset($link_file)) {
$image_uri = $item->entity->getFileUri();
$url = Url::fromUri(file_create_url($image_uri));
}
// Extract field item attributes for the theme function, and unset them
// from the $item so that the field template does not re-render them.
$item_attributes = $item->_attributes;
unset($item->_attributes);
$elements[$delta] = array(
'#theme' => 'image_formatter',
'#item' => $item,
'#item_attributes' => $item_attributes,
'#image_style' => $image_style_setting,
'#url' => $url,
'#cache' => array(
'tags' => $cache_tags,
),
);
foreach ($files as $delta => $file) {
if (isset($link_file)) {
$image_uri = $file->getFileUri();
$url = Url::fromUri(file_create_url($image_uri));
}
// Extract field item attributes for the theme function, and unset them
// from the $item so that the field template does not re-render them.
$item = $file->_referringItem;
$item_attributes = $item->_attributes;
unset($item->_attributes);
$elements[$delta] = array(
'#theme' => 'image_formatter',
'#item' => $item,
'#item_attributes' => $item_attributes,
'#image_style' => $image_style_setting,
'#url' => $url,
'#cache' => array(
'tags' => $cache_tags,
),
);
}
return $elements;

View File

@ -7,6 +7,7 @@
namespace Drupal\image\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\EntityReferenceFieldItemListInterface;
use Drupal\field\FieldConfigInterface;
use Drupal\file\Plugin\Field\FieldFormatter\FileFormatterBase;
@ -18,33 +19,36 @@ abstract class ImageFormatterBase extends FileFormatterBase {
/**
* {@inheritdoc}
*/
public function prepareView(array $entities_items) {
parent::prepareView($entities_items);
protected function getEntitiesToView(EntityReferenceFieldItemListInterface $items) {
// Add the default image if needed.
if ($items->isEmpty()) {
$default_image = $this->getFieldSetting('default_image');
// If we are dealing with a configurable field, look in both
// instance-level and field-level settings.
if (empty($default_image['uuid']) && $this->fieldDefinition instanceof FieldConfigInterface) {
$default_image = $this->fieldDefinition->getFieldStorageDefinition()->getSetting('default_image');
}
// If there are no files specified at all, use the default.
foreach ($entities_items as $items) {
if ($items->isEmpty()) {
// Add the default image if one is found.
$default_image = $this->getFieldSetting('default_image');
// If we are dealing with a configurable field, look in both
// instance-level and field-level settings.
if (empty($default_image['uuid']) && $this->fieldDefinition instanceof FieldConfigInterface) {
$default_image = $this->fieldDefinition->getFieldStorageDefinition()->getSetting('default_image');
}
if (!empty($default_image['uuid']) && ($file = \Drupal::entityManager()->loadEntityByUuid('file', $default_image['uuid']))) {
$items->setValue(array(array(
'is_default' => TRUE,
'alt' => $default_image['alt'],
'title' => $default_image['title'],
'width' => $default_image['width'],
'height' => $default_image['height'],
'entity' => $file,
'target_id' => $file->id(),
)));
}
if (!empty($default_image['uuid']) && $file = \Drupal::entityManager()->loadEntityByUuid('file', $default_image['uuid'])) {
// Clone the FieldItemList into a runtime-only object for the formatter,
// so that the fallback image can be rendered without affecting the
// field values in the entity being rendered.
$items = clone $items;
$items->setValue(array(
'target_id' => $file->id(),
'alt' => $default_image['alt'],
'title' => $default_image['title'],
'width' => $default_image['width'],
'height' => $default_image['height'],
'entity' => $file,
'_loaded' => TRUE,
'_is_default' => TRUE,
));
$file->_referringItem = $items[0];
}
}
return parent::getEntitiesToView($items);
}
}

View File

@ -154,7 +154,7 @@ class ImageFieldDefaultImagesTest extends ImageFieldTestBase {
$article = $this->drupalCreateNode(array('type' => 'article'));
$article_built = $this->drupalBuildEntityView($article);
$this->assertEqual(
$article_built[$field_name]['#items'][0]->target_id,
$article_built[$field_name][0]['#item']->target_id,
$default_images['field']->id(),
format_string(
'A new article node without an image has the expected default image file ID of @fid.',
@ -166,7 +166,7 @@ class ImageFieldDefaultImagesTest extends ImageFieldTestBase {
$page = $this->drupalCreateNode(array('type' => 'page'));
$page_built = $this->drupalBuildEntityView($page);
$this->assertEqual(
$page_built[$field_name]['#items'][0]->target_id,
$page_built[$field_name][0]['#item']->target_id,
$default_images['field2']->id(),
format_string(
'A new page node without an image has the expected default image file ID of @fid.',
@ -196,7 +196,7 @@ class ImageFieldDefaultImagesTest extends ImageFieldTestBase {
$article_built = $this->drupalBuildEntityView($article = $node_storage->load($article->id()));
$page_built = $this->drupalBuildEntityView($page = $node_storage->load($page->id()));
$this->assertEqual(
$article_built[$field_name]['#items'][0]->target_id,
$article_built[$field_name][0]['#item']->target_id,
$default_images['field']->id(),
format_string(
'An existing article node without an image has the expected default image file ID of @fid.',
@ -204,7 +204,7 @@ class ImageFieldDefaultImagesTest extends ImageFieldTestBase {
)
);
$this->assertEqual(
$page_built[$field_name]['#items'][0]->target_id,
$page_built[$field_name][0]['#item']->target_id,
$default_images['field2']->id(),
format_string(
'An existing page node without an image has the expected default image file ID of @fid.',
@ -235,7 +235,7 @@ class ImageFieldDefaultImagesTest extends ImageFieldTestBase {
// Confirm the article uses the new default.
$this->assertEqual(
$article_built[$field_name]['#items'][0]->target_id,
$article_built[$field_name][0]['#item']->target_id,
$default_images['field_new']->id(),
format_string(
'An existing article node without an image has the expected default image file ID of @fid.',
@ -244,7 +244,7 @@ class ImageFieldDefaultImagesTest extends ImageFieldTestBase {
);
// Confirm the page remains unchanged.
$this->assertEqual(
$page_built[$field_name]['#items'][0]->target_id,
$page_built[$field_name][0]['#item']->target_id,
$default_images['field2']->id(),
format_string(
'An existing page node without an image has the expected default image file ID of @fid.',
@ -275,7 +275,7 @@ class ImageFieldDefaultImagesTest extends ImageFieldTestBase {
$page_built = $this->drupalBuildEntityView($page = $node_storage->load($page->id()));
// Confirm the article uses the new field (not field) default.
$this->assertEqual(
$article_built[$field_name]['#items'][0]->target_id,
$article_built[$field_name][0]['#item']->target_id,
$default_images['field_new']->id(),
format_string(
'An existing article node without an image has the expected default image file ID of @fid.',
@ -284,7 +284,7 @@ class ImageFieldDefaultImagesTest extends ImageFieldTestBase {
);
// Confirm the page remains unchanged.
$this->assertEqual(
$page_built[$field_name]['#items'][0]->target_id,
$page_built[$field_name][0]['#item']->target_id,
$default_images['field2']->id(),
format_string(
'An existing page node without an image has the expected default image file ID of @fid.',

View File

@ -175,6 +175,13 @@ class ResponsiveImageFormatter extends ImageFormatterBase implements ContainerFa
*/
public function viewElements(FieldItemListInterface $items) {
$elements = array();
$files = $this->getEntitiesToView($items);
// Early opt-out if the field is empty.
if (empty($files)) {
return $elements;
}
$url = NULL;
// Check if the formatter involves a link.
if ($this->getSetting('image_link') == 'content') {
@ -220,10 +227,10 @@ class ResponsiveImageFormatter extends ImageFormatterBase implements ContainerFa
$cache_tags = Cache::mergeTags($cache_tags, $image_style->getCacheTags());
}
foreach ($items as $delta => $item) {
foreach ($files as $delta => $file) {
// Link the <picture> element to the original file.
if (isset($link_file)) {
$url = Url::fromUri(file_create_url($item->entity->getFileUri()));
$url = Url::fromUri(file_create_url($file->getFileUri()));
}
$elements[$delta] = array(
'#theme' => 'responsive_image_formatter',
@ -232,7 +239,7 @@ class ResponsiveImageFormatter extends ImageFormatterBase implements ContainerFa
'core/picturefill',
),
),
'#item' => $item,
'#item' => $file->_referringItem,
'#image_style' => $fallback_image_style,
'#responsive_image_style_id' => $responsive_image_style ? $responsive_image_style->id() : '',
'#url' => $url,