From e4b6c4d5e490b4af6f7ebf63ef99ceddb35697c3 Mon Sep 17 00:00:00 2001
From: Alex Pott <alex.a.pott@googlemail.com>
Date: Wed, 4 Nov 2015 13:35:04 +0000
Subject: [PATCH] Issue #2594565 by klausi, marthinal, dawehner, Berdir, yched:
 File extension + max_filesize should be validated using a Constraint on file
 fields

---
 .../src/Plugin/Field/FieldType/FileItem.php   |   2 +-
 .../Plugin/Field/FieldWidget/FileWidget.php   |  14 ++-
 .../Constraint/FileValidationConstraint.php   |  22 ++++
 .../FileValidationConstraintValidator.php     |  34 +++++
 .../file/src/Tests/FileFieldValidateTest.php  |  31 +++++
 .../src/Kernel/FileItemValidationTest.php     | 117 ++++++++++++++++++
 .../src/Plugin/Field/FieldType/ImageItem.php  |   2 +-
 7 files changed, 219 insertions(+), 3 deletions(-)
 create mode 100644 core/modules/file/src/Plugin/Validation/Constraint/FileValidationConstraint.php
 create mode 100644 core/modules/file/src/Plugin/Validation/Constraint/FileValidationConstraintValidator.php
 create mode 100644 core/modules/file/tests/src/Kernel/FileItemValidationTest.php

diff --git a/core/modules/file/src/Plugin/Field/FieldType/FileItem.php b/core/modules/file/src/Plugin/Field/FieldType/FileItem.php
index 1633147b2b4e..65f4f39d7115 100644
--- a/core/modules/file/src/Plugin/Field/FieldType/FileItem.php
+++ b/core/modules/file/src/Plugin/Field/FieldType/FileItem.php
@@ -28,7 +28,7 @@
  *   default_widget = "file_generic",
  *   default_formatter = "file_default",
  *   list_class = "\Drupal\file\Plugin\Field\FieldType\FileFieldItemList",
- *   constraints = {"ValidReference" = {}, "ReferenceAccess" = {}}
+ *   constraints = {"ValidReference" = {}, "ReferenceAccess" = {}, "FileValidation" = {}}
  * )
  */
 class FileItem extends EntityReferenceItem {
diff --git a/core/modules/file/src/Plugin/Field/FieldWidget/FileWidget.php b/core/modules/file/src/Plugin/Field/FieldWidget/FileWidget.php
index bd511c8cf267..6634a5f20170 100644
--- a/core/modules/file/src/Plugin/Field/FieldWidget/FileWidget.php
+++ b/core/modules/file/src/Plugin/Field/FieldWidget/FileWidget.php
@@ -18,8 +18,9 @@
 use Drupal\Core\Render\Element;
 use Drupal\Core\Render\ElementInfoManagerInterface;
 use Drupal\file\Element\ManagedFile;
-use Symfony\Component\DependencyInjection\ContainerInterface;
 use Drupal\file\Entity\File;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\Validator\ConstraintViolationListInterface;
 
 /**
  * Plugin implementation of the 'file_generic' widget.
@@ -575,4 +576,15 @@ public static function submit($form, FormStateInterface $form_state) {
     static::setWidgetState($parents, $field_name, $form_state, $field_state);
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function flagErrors(FieldItemListInterface $items, ConstraintViolationListInterface $violations, array $form, FormStateInterface $form_state) {
+    // Never flag validation errors for the remove button.
+    $clicked_button = end($form_state->getTriggeringElement()['#parents']);
+    if ($clicked_button !== 'remove_button') {
+      parent::flagErrors($items, $violations, $form, $form_state);
+    }
+  }
+
 }
diff --git a/core/modules/file/src/Plugin/Validation/Constraint/FileValidationConstraint.php b/core/modules/file/src/Plugin/Validation/Constraint/FileValidationConstraint.php
new file mode 100644
index 000000000000..3b14feea017b
--- /dev/null
+++ b/core/modules/file/src/Plugin/Validation/Constraint/FileValidationConstraint.php
@@ -0,0 +1,22 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\file\Plugin\Validation\Constraint\FileValidationConstraint.
+ */
+
+namespace Drupal\file\Plugin\Validation\Constraint;
+
+use Symfony\Component\Validator\Constraint;
+
+/**
+ * Validation File constraint.
+ *
+ * @Constraint(
+ *   id = "FileValidation",
+ *   label = @Translation("File Validation", context = "Validation")
+ * )
+ */
+class FileValidationConstraint extends Constraint {
+
+}
diff --git a/core/modules/file/src/Plugin/Validation/Constraint/FileValidationConstraintValidator.php b/core/modules/file/src/Plugin/Validation/Constraint/FileValidationConstraintValidator.php
new file mode 100644
index 000000000000..d4f48834300f
--- /dev/null
+++ b/core/modules/file/src/Plugin/Validation/Constraint/FileValidationConstraintValidator.php
@@ -0,0 +1,34 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\file\Plugin\Validation\Constraint\FileValidationConstraintValidator.
+ */
+
+namespace Drupal\file\Plugin\Validation\Constraint;
+
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\ConstraintValidator;
+
+/**
+ * Checks that a file referenced in a file field is valid.
+ */
+class FileValidationConstraintValidator extends ConstraintValidator {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validate($value, Constraint $constraint) {
+    // Get the file to execute validators.
+    $file = $value->get('entity')->getTarget()->getValue();
+    // Get the validators.
+    $validators = $value->getUploadValidators();
+    // Checks that a file meets the criteria specified by the validators.
+    if ($errors = file_validate($file, $validators)) {
+      foreach ($errors as $error) {
+        $this->context->addViolation($error);
+      }
+    }
+  }
+
+}
diff --git a/core/modules/file/src/Tests/FileFieldValidateTest.php b/core/modules/file/src/Tests/FileFieldValidateTest.php
index 291c43297795..12fe9255261f 100644
--- a/core/modules/file/src/Tests/FileFieldValidateTest.php
+++ b/core/modules/file/src/Tests/FileFieldValidateTest.php
@@ -159,4 +159,35 @@ function testFileExtension() {
     $this->assertFileEntryExists($node_file, 'File entry exists after uploading a file with extension checking.');
   }
 
+  /**
+   * Checks that a file can always be removed if it does not pass validation.
+   */
+  public function testFileRemoval() {
+    $node_storage = $this->container->get('entity.manager')->getStorage('node');
+    $type_name = 'article';
+    $field_name = 'file_test';
+    $this->createFileField($field_name, 'node', $type_name);
+
+    $test_file = $this->getTestFile('image');
+
+    // Disable extension checking.
+    $this->updateFileField($field_name, $type_name, array('file_extensions' => ''));
+
+    // Check that the file can be uploaded with no extension checking.
+    $nid = $this->uploadNodeFile($test_file, $field_name, $type_name);
+    $node_storage->resetCache(array($nid));
+    $node = $node_storage->load($nid);
+    $node_file = File::load($node->{$field_name}->target_id);
+    $this->assertFileExists($node_file, 'File exists after uploading a file with no extension checking.');
+    $this->assertFileEntryExists($node_file, 'File entry exists after uploading a file with no extension checking.');
+
+    // Enable extension checking for text files.
+    $this->updateFileField($field_name, $type_name, array('file_extensions' => 'txt'));
+
+    // Check that the file can still be removed.
+    $this->removeNodeFile($nid);
+    $this->assertNoText('Only files with the following extensions are allowed: txt.');
+    $this->assertText('Article ' . $node->getTitle() . ' has been updated.');
+  }
+
 }
diff --git a/core/modules/file/tests/src/Kernel/FileItemValidationTest.php b/core/modules/file/tests/src/Kernel/FileItemValidationTest.php
new file mode 100644
index 000000000000..cc83b23e399b
--- /dev/null
+++ b/core/modules/file/tests/src/Kernel/FileItemValidationTest.php
@@ -0,0 +1,117 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\file\Kernel\FileItemValidationTest.
+ */
+
+namespace Drupal\Tests\file\Kernel;
+
+use Drupal\entity_test\Entity\EntityTest;
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\file\Entity\File;
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\user\Entity\User;
+use org\bovigo\vfs\vfsStream;
+
+/**
+ * Tests that files referenced in file and image fields are always validated.
+ *
+ * @group file
+ */
+class FileItemValidationTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['file', 'image', 'entity_test', 'field', 'user', 'system'];
+
+  /**
+   * A user.
+   *
+   * @var \Drupal\user\UserInterface
+   */
+  protected $user;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->installEntitySchema('user');
+    $this->installEntitySchema('file');
+    $this->installSchema('file', 'file_usage');
+    $this->installSchema('system', 'sequences');
+
+    $this->user = User::create([
+      'name' => 'username',
+    ]);
+    $this->user->save();
+  }
+
+  /**
+   * @covers \Drupal\file\Plugin\Validation\Constraint\FileValidationConstraint
+   * @covers \Drupal\file\Plugin\Validation\Constraint\FileValidationConstraintValidator
+   * @dataProvider getFileTypes
+   */
+  public function testFileValidationConstraint($file_type) {
+    $field_storage = FieldStorageConfig::create([
+      'field_name' => 'field_test_file',
+      'entity_type' => 'entity_test',
+      'type' => $file_type,
+    ]);
+    $field_storage->save();
+
+    $field = FieldConfig::create([
+      'field_name' => 'field_test_file',
+      'entity_type' => 'entity_test',
+      'bundle' => 'entity_test',
+      'settings' => [
+        'max_filesize' => '2k',
+        'file_extensions' => 'jpg|png',
+      ],
+    ]);
+    $field->save();
+
+    vfsStream::setup('drupal_root');
+    vfsStream::create([
+      'sites' => [
+        'default' => [
+          'files' => [
+            'test.txt' => str_repeat('a', 3000),
+          ]
+        ]
+      ]
+    ]);
+
+    // Test for max filesize.
+    $file = File::create([
+      'uri' => 'vfs://drupal_root/sites/default/files/test.txt',
+    ]);
+    $file->save();
+
+    $entity_test = EntityTest::create([
+      'uid' => $this->user->id(),
+      'field_test_file' => [
+        'target_id' => $file->id(),
+      ]
+    ]);
+    $result = $entity_test->validate();
+    $this->assertCount(2, $result);
+
+    $this->assertEquals('field_test_file.0', $result->get(0)->getPropertyPath());
+    $this->assertEquals('The file is <em class="placeholder">2.93 KB</em> exceeding the maximum file size of <em class="placeholder">2 KB</em>.', (string) $result->get(0)->getMessage());
+    $this->assertEquals('field_test_file.0', $result->get(1)->getPropertyPath());
+    $this->assertEquals('Only files with the following extensions are allowed: <em class="placeholder">jpg|png</em>.', (string) $result->get(1)->getMessage());
+  }
+
+  /**
+   * Provides a list of file types to test.
+   */
+  public function getFileTypes() {
+    return [['file'], ['image']];
+  }
+
+}
diff --git a/core/modules/image/src/Plugin/Field/FieldType/ImageItem.php b/core/modules/image/src/Plugin/Field/FieldType/ImageItem.php
index 442929fe7fee..ecaaf1afbe05 100644
--- a/core/modules/image/src/Plugin/Field/FieldType/ImageItem.php
+++ b/core/modules/image/src/Plugin/Field/FieldType/ImageItem.php
@@ -43,7 +43,7 @@
  *     },
  *   },
  *   list_class = "\Drupal\file\Plugin\Field\FieldType\FileFieldItemList",
- *   constraints = {"ValidReference" = {}, "ReferenceAccess" = {}}
+ *   constraints = {"ValidReference" = {}, "ReferenceAccess" = {}, "FileValidation" = {}}
  * )
  */
 class ImageItem extends FileItem {
-- 
GitLab