diff --git a/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/ValidReferenceConstraint.php b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/ValidReferenceConstraint.php index 2a7d6a9..f3a7b7c 100644 --- a/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/ValidReferenceConstraint.php +++ b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/ValidReferenceConstraint.php @@ -26,6 +26,6 @@ class ValidReferenceConstraint extends Constraint { * * @var string */ - public $message = 'The referenced entity (%type: %id) does not exist.'; + public $message = 'The referenced entity (%type: %id) does not exist or you do not have access to it.'; } diff --git a/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/ValidReferenceConstraintValidator.php b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/ValidReferenceConstraintValidator.php index 75cc152..556886a 100644 --- a/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/ValidReferenceConstraintValidator.php +++ b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/ValidReferenceConstraintValidator.php @@ -19,6 +19,7 @@ class ValidReferenceConstraintValidator extends ConstraintValidator { * {@inheritdoc} */ public function validate($value, Constraint $constraint) { + /* @var \Drupal\Core\Field\FieldItemInterface $value */ if (!isset($value)) { return; } @@ -27,10 +28,36 @@ public function validate($value, Constraint $constraint) { if (empty($id)) { return; } + /* @var \Drupal\Core\Entity\FieldableEntityInterface $referenced_entity */ $referenced_entity = $value->get('entity')->getValue(); if (!$referenced_entity) { $type = $value->getFieldDefinition()->getSetting('target_type'); $this->context->addViolation($constraint->message, array('%type' => $type, '%id' => $id)); } + else { + $entity = $value->getEntity(); + $check_permission = TRUE; + if (!$entity->isNew()) { + $existing_entity = \Drupal::entityManager()->getStorage($entity->getEntityTypeId())->loadUnchanged($entity->id()); + $referenced_entities = $existing_entity->{$value->getFieldDefinition()->getName()}->referencedEntities(); + // Need to check permission if we are not already referencing the entity + foreach ($referenced_entities as $ref) { + if (isset($referenced_entities[$ref->id()])) { + debug($ref->id()); + $check_permission = FALSE; + break; + } + } + } + // We check that the current user had access to view any newly added + // referenced entity. + +// debug('got here'); +// debug($value->getFieldDefinition()->getName()); + if ($check_permission && !$referenced_entity->access('view')) { + $type = $value->getFieldDefinition()->getSetting('target_type'); + $this->context->addViolation($constraint->message, array('%type' => $type, '%id' => $id)); + } + } } } diff --git a/core/modules/file/src/FileAccessControlHandler.php b/core/modules/file/src/FileAccessControlHandler.php index c6183da..2e336af 100644 --- a/core/modules/file/src/FileAccessControlHandler.php +++ b/core/modules/file/src/FileAccessControlHandler.php @@ -23,18 +23,26 @@ class FileAccessControlHandler extends EntityAccessControlHandler { */ protected function checkAccess(EntityInterface $entity, $operation, $langcode, AccountInterface $account) { - if ($operation == 'download') { - foreach ($this->getFileReferences($entity) as $field_name => $entity_map) { - foreach ($entity_map as $referencing_entity_type => $referencing_entities) { - /** @var \Drupal\Core\Entity\EntityInterface $referencing_entity */ - foreach ($referencing_entities as $referencing_entity) { - $entity_and_field_access = $referencing_entity->access('view', $account, TRUE)->andIf($referencing_entity->$field_name->access('view', $account, TRUE)); - if ($entity_and_field_access->isAllowed()) { - return $entity_and_field_access; + if ($operation == 'download' || $operation == 'view') { + $references = $this->getFileReferences($entity); + if ($references) { + foreach ($references as $field_name => $entity_map) { + foreach ($entity_map as $referencing_entity_type => $referencing_entities) { + /** @var \Drupal\Core\Entity\EntityInterface $referencing_entity */ + foreach ($referencing_entities as $referencing_entity) { + $entity_and_field_access = $referencing_entity->access('view', $account, TRUE)->andIf($referencing_entity->$field_name->access('view', $account, TRUE)); + if ($entity_and_field_access->isAllowed()) { + return $entity_and_field_access; + } } } } } + elseif ($entity->getOwnerId() == $account->id()) { + // This case handles new nodes, or detached files. The user who uploaded + // the file can always access if it's not yet used. + return AccessResult::allowed(); + } } // No opinion. diff --git a/core/modules/file/src/Plugin/Field/FieldType/FileItem.php b/core/modules/file/src/Plugin/Field/FieldType/FileItem.php index a5a229a..755d943 100644 --- a/core/modules/file/src/Plugin/Field/FieldType/FileItem.php +++ b/core/modules/file/src/Plugin/Field/FieldType/FileItem.php @@ -24,7 +24,8 @@ * description = @Translation("This field stores the ID of a file as an integer value."), * default_widget = "file_generic", * default_formatter = "file_default", - * list_class = "\Drupal\file\Plugin\Field\FieldType\FileFieldItemList" + * list_class = "\Drupal\file\Plugin\Field\FieldType\FileFieldItemList", + * constraints = {"ValidReference" = {}} * ) */ class FileItem extends EntityReferenceItem { diff --git a/core/modules/file/src/Tests/FilePrivateTest.php b/core/modules/file/src/Tests/FilePrivateTest.php index 4cf054f..8d443cf 100644 --- a/core/modules/file/src/Tests/FilePrivateTest.php +++ b/core/modules/file/src/Tests/FilePrivateTest.php @@ -6,8 +6,11 @@ */ namespace Drupal\file\Tests; + +use Drupal\Component\Utility\String; use Drupal\file\Entity\File; use Drupal\node\Entity\Node; +use \Drupal\Core\Entity\Plugin\Validation\Constraint\ValidReferenceConstraint; /** * Uploads a test to a private node and checks access. @@ -38,15 +41,11 @@ function testPrivateFile() { $field_name = strtolower($this->randomMachineName()); $this->createFileField($field_name, 'node', $type_name, array('uri_scheme' => 'private')); - // Create a field with no view access. See - // field_test_entity_field_access(). - $no_access_field_name = 'field_no_view_access'; - $this->createFileField($no_access_field_name, 'node', $type_name, array('uri_scheme' => 'private')); - $test_file = $this->getTestFile('text'); $nid = $this->uploadNodeFile($test_file, $field_name, $type_name, TRUE, array('private' => TRUE)); - $node = node_load($nid, TRUE); - $node_file = file_load($node->{$field_name}->target_id); + \Drupal::entityManager()->getStorage('node')->resetCache(array($nid)); + $node = Node::load($nid);; + $node_file = File::load($node->{$field_name}->target_id); // Ensure the file can be viewed. $this->drupalGet('node/' . $node->id()); $this->assertRaw($node_file->getFilename(), 'File reference is displayed after attaching it'); @@ -57,6 +56,10 @@ function testPrivateFile() { $this->drupalGet(file_create_url($node_file->getFileUri())); $this->assertResponse(403, 'Confirmed that access is denied for the file without the needed permission.'); + // Create a field with no view access. See + // field_test_entity_field_access(). + $no_access_field_name = 'field_no_view_access'; + $this->createFileField($no_access_field_name, 'node', $type_name, array('uri_scheme' => 'private')); // Test with the field that should deny access through field access. $this->drupalLogin($this->admin_user); $nid = $this->uploadNodeFile($test_file, $no_access_field_name, $type_name, TRUE, array('private' => TRUE)); @@ -67,6 +70,19 @@ function testPrivateFile() { // Ensure the file cannot be downloaded. $this->drupalGet(file_create_url($node_file->getFileUri())); $this->assertResponse(403, 'Confirmed that access is denied for the file without view field access permission.'); + + // Attempt to reuse the file when editing a node. + $edit = array(); + $edit['title[0][value]'] = $this->randomMachineName(); + $this->drupalPostForm('node/add/' . $type_name, $edit, t('Save and publish')); + $new_node = $this->drupalGetNodeByTitle($edit['title[0][value]']); + $edit[$field_name . '[0][fids]'] = $node_file->id(); + $this->drupalPostForm('node/' . $new_node->id() .'/edit', $edit, t('Save and keep published')); + // Make sure the form submit failed - we stayed on the edit form. + $this->assertUrl('node/' . $new_node->id() .'/edit'); + // Check that we got the expected constraint form error. + $constraint = new ValidReferenceConstraint(); + $this->assertRaw(String::format($constraint->message, array('%type' => 'file', '%id' => $node_file->id()))); // Attempt to reuse the existing file when creating a new node, and confirm // that access is still denied. $edit = array(); @@ -74,10 +90,8 @@ function testPrivateFile() { $edit[$field_name . '[0][fids]'] = $node_file->id(); $this->drupalPostForm('node/add/' . $type_name, $edit, t('Save and publish')); $new_node = $this->drupalGetNodeByTitle($edit['title[0][value]']); - $this->assertTrue(!empty($new_node), 'Node was created.'); - $this->assertUrl('node/' . $new_node->id()); - $this->assertTrue($new_node->{$field_name}->isEmpty(), 'File was not attached'); - $this->drupalGet('node/' . $new_node->id()); - $this->assertNoRaw($node_file->getFilename(), 'File without view field access permission does not appear after attempting to attach it to a new node.'); + $this->assertTrue(empty($new_node), 'Node was not created.'); + $this->assertUrl('node/add/' . $type_name); + $this->assertRaw(String::format($constraint->message, array('%type' => 'file', '%id' => $node_file->id()))); } } diff --git a/core/modules/image/src/Plugin/Field/FieldType/ImageItem.php b/core/modules/image/src/Plugin/Field/FieldType/ImageItem.php index 5d90b49..2cca320 100644 --- a/core/modules/image/src/Plugin/Field/FieldType/ImageItem.php +++ b/core/modules/image/src/Plugin/Field/FieldType/ImageItem.php @@ -40,7 +40,8 @@ * "translatable" = TRUE * }, * }, - * list_class = "\Drupal\file\Plugin\Field\FieldType\FileFieldItemList" + * list_class = "\Drupal\file\Plugin\Field\FieldType\FileFieldItemList", + * constraints = {"ValidReference" = {}} * ) */ class ImageItem extends FileItem { diff --git a/core/modules/taxonomy/src/Plugin/Field/FieldType/TaxonomyTermReferenceItem.php b/core/modules/taxonomy/src/Plugin/Field/FieldType/TaxonomyTermReferenceItem.php index a2965c9..83d9780 100644 --- a/core/modules/taxonomy/src/Plugin/Field/FieldType/TaxonomyTermReferenceItem.php +++ b/core/modules/taxonomy/src/Plugin/Field/FieldType/TaxonomyTermReferenceItem.php @@ -23,7 +23,8 @@ * description = @Translation("This field stores a reference to a taxonomy term."), * default_widget = "options_select", * default_formatter = "taxonomy_term_reference_link", - * list_class = "\Drupal\Core\Field\EntityReferenceFieldItemList" + * list_class = "\Drupal\Core\Field\EntityReferenceFieldItemList", + * constraints = {"ValidReference" = {}} * ) */ class TaxonomyTermReferenceItem extends EntityReferenceItem implements OptionsProviderInterface {