SaveUploadFormTest.php 21 KB
Newer Older
1 2
<?php

3
namespace Drupal\Tests\file\Functional;
4 5

use Drupal\file\Entity\File;
6
use Drupal\Tests\TestFileCreationTrait;
7 8 9 10 11 12 13 14 15 16

/**
 * Tests the _file_save_upload_from_form() function.
 *
 * @group file
 *
 * @see _file_save_upload_from_form()
 */
class SaveUploadFormTest extends FileManagedTestBase {

17 18 19 20
  use TestFileCreationTrait {
    getTestFiles as drupalGetTestFiles;
  }

21 22 23 24 25
  /**
   * Modules to enable.
   *
   * @var array
   */
26
  public static $modules = ['dblog'];
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68

  /**
   * An image file path for uploading.
   *
   * @var \Drupal\file\FileInterface
   */
  protected $image;

  /**
   * A PHP file path for upload security testing.
   */
  protected $phpfile;

  /**
   * The largest file id when the test starts.
   */
  protected $maxFidBefore;

  /**
   * Extension of the image filename.
   *
   * @var string
   */
  protected $imageExtension;

  /**
   * {@inheritdoc}
   */
  protected function setUp() {
    parent::setUp();
    $account = $this->drupalCreateUser(['access site reports']);
    $this->drupalLogin($account);

    $image_files = $this->drupalGetTestFiles('image');
    $this->image = File::create((array) current($image_files));

    list(, $this->imageExtension) = explode('.', $this->image->getFilename());
    $this->assertTrue(is_file($this->image->getFileUri()), "The image file we're going to upload exists.");

    $this->phpfile = current($this->drupalGetTestFiles('php'));
    $this->assertTrue(is_file($this->phpfile->uri), 'The PHP file we are going to upload exists.');

69
    $this->maxFidBefore = (int) \Drupal::entityQueryAggregate('file')->aggregate('fid', 'max')->execute()[0]['fid_max'];
70

71 72
    /** @var \Drupal\Core\File\FileSystemInterface $file_system */
    $file_system = \Drupal::service('file_system');
73 74 75
    // Upload with replace to guarantee there's something there.
    $edit = [
      'file_test_replace' => FILE_EXISTS_REPLACE,
76
      'files[file_test_upload][]' => $file_system->realpath($this->image->getFileUri()),
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
    ];
    $this->drupalPostForm('file-test/save_upload_from_form_test', $edit, t('Submit'));
    $this->assertResponse(200, 'Received a 200 response for posted test file.');
    $this->assertRaw(t('You WIN!'), 'Found the success message.');

    // Check that the correct hooks were called then clean out the hook
    // counters.
    $this->assertFileHooksCalled(['validate', 'insert']);
    file_test_reset();
  }

  /**
   * Tests the _file_save_upload_from_form() function.
   */
  public function testNormal() {
92
    $max_fid_after = (int) \Drupal::entityQueryAggregate('file')->aggregate('fid', 'max')->execute()[0]['fid_max'];
93 94 95 96 97 98 99 100 101 102 103
    $this->assertTrue($max_fid_after > $this->maxFidBefore, 'A new file was created.');
    $file1 = File::load($max_fid_after);
    $this->assertTrue($file1, 'Loaded the file.');
    // MIME type of the uploaded image may be either image/jpeg or image/png.
    $this->assertEqual(substr($file1->getMimeType(), 0, 5), 'image', 'A MIME type was set.');

    // Reset the hook counters to get rid of the 'load' we just called.
    file_test_reset();

    // Upload a second file.
    $image2 = current($this->drupalGetTestFiles('image'));
104 105 106
    /** @var \Drupal\Core\File\FileSystemInterface $file_system */
    $file_system = \Drupal::service('file_system');
    $edit = ['files[file_test_upload][]' => $file_system->realpath($image2->uri)];
107 108 109
    $this->drupalPostForm('file-test/save_upload_from_form_test', $edit, t('Submit'));
    $this->assertResponse(200, 'Received a 200 response for posted test file.');
    $this->assertRaw(t('You WIN!'));
110
    $max_fid_after = (int) \Drupal::entityQueryAggregate('file')->aggregate('fid', 'max')->execute()[0]['fid_max'];
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126

    // Check that the correct hooks were called.
    $this->assertFileHooksCalled(['validate', 'insert']);

    $file2 = File::load($max_fid_after);
    $this->assertTrue($file2, 'Loaded the file');
    // MIME type of the uploaded image may be either image/jpeg or image/png.
    $this->assertEqual(substr($file2->getMimeType(), 0, 5), 'image', 'A MIME type was set.');

    // Load both files using File::loadMultiple().
    $files = File::loadMultiple([$file1->id(), $file2->id()]);
    $this->assertTrue(isset($files[$file1->id()]), 'File was loaded successfully');
    $this->assertTrue(isset($files[$file2->id()]), 'File was loaded successfully');

    // Upload a third file to a subdirectory.
    $image3 = current($this->drupalGetTestFiles('image'));
127
    $image3_realpath = $file_system->realpath($image3->uri);
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142
    $dir = $this->randomMachineName();
    $edit = [
      'files[file_test_upload][]' => $image3_realpath,
      'file_subdir' => $dir,
    ];
    $this->drupalPostForm('file-test/save_upload_from_form_test', $edit, t('Submit'));
    $this->assertResponse(200, 'Received a 200 response for posted test file.');
    $this->assertRaw(t('You WIN!'));
    $this->assertTrue(is_file('temporary://' . $dir . '/' . trim(drupal_basename($image3_realpath))));
  }

  /**
   * Tests extension handling.
   */
  public function testHandleExtension() {
143 144
    /** @var \Drupal\Core\File\FileSystemInterface $file_system */
    $file_system = \Drupal::service('file_system');
145 146 147 148 149 150 151
    // The file being tested is a .gif which is in the default safe list
    // of extensions to allow when the extension validator isn't used. This is
    // implicitly tested at the testNormal() test. Here we tell
    // _file_save_upload_from_form() to only allow ".foo".
    $extensions = 'foo';
    $edit = [
      'file_test_replace' => FILE_EXISTS_REPLACE,
152
      'files[file_test_upload][]' => $file_system->realpath($this->image->getFileUri()),
153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171
      'extensions' => $extensions,
    ];

    $this->drupalPostForm('file-test/save_upload_from_form_test', $edit, t('Submit'));
    $this->assertResponse(200, 'Received a 200 response for posted test file.');
    $message = t('Only files with the following extensions are allowed:') . ' <em class="placeholder">' . $extensions . '</em>';
    $this->assertRaw($message, 'Cannot upload a disallowed extension');
    $this->assertRaw(t('Epic upload FAIL!'), 'Found the failure message.');

    // Check that the correct hooks were called.
    $this->assertFileHooksCalled(['validate']);

    // Reset the hook counters.
    file_test_reset();

    $extensions = 'foo ' . $this->imageExtension;
    // Now tell _file_save_upload_from_form() to allow the extension of our test image.
    $edit = [
      'file_test_replace' => FILE_EXISTS_REPLACE,
172
      'files[file_test_upload][]' => $file_system->realpath($this->image->getFileUri()),
173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189
      'extensions' => $extensions,
    ];

    $this->drupalPostForm('file-test/save_upload_from_form_test', $edit, t('Submit'));
    $this->assertResponse(200, 'Received a 200 response for posted test file.');
    $this->assertNoRaw(t('Only files with the following extensions are allowed:'), 'Can upload an allowed extension.');
    $this->assertRaw(t('You WIN!'), 'Found the success message.');

    // Check that the correct hooks were called.
    $this->assertFileHooksCalled(['validate', 'load', 'update']);

    // Reset the hook counters.
    file_test_reset();

    // Now tell _file_save_upload_from_form() to allow any extension.
    $edit = [
      'file_test_replace' => FILE_EXISTS_REPLACE,
190
      'files[file_test_upload][]' => $file_system->realpath($this->image->getFileUri()),
191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206
      'allow_all_extensions' => TRUE,
    ];
    $this->drupalPostForm('file-test/save_upload_from_form_test', $edit, t('Submit'));
    $this->assertResponse(200, 'Received a 200 response for posted test file.');
    $this->assertNoRaw(t('Only files with the following extensions are allowed:'), 'Can upload any extension.');
    $this->assertRaw(t('You WIN!'), 'Found the success message.');

    // Check that the correct hooks were called.
    $this->assertFileHooksCalled(['validate', 'load', 'update']);
  }

  /**
   * Tests dangerous file handling.
   */
  public function testHandleDangerousFile() {
    $config = $this->config('system.file');
207 208
    /** @var \Drupal\Core\File\FileSystemInterface $file_system */
    $file_system = \Drupal::service('file_system');
209 210 211 212
    // Allow the .php extension and make sure it gets renamed to .txt for
    // safety. Also check to make sure its MIME type was changed.
    $edit = [
      'file_test_replace' => FILE_EXISTS_REPLACE,
213
      'files[file_test_upload][]' => $file_system->realpath($this->phpfile->uri),
214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250
      'is_image_file' => FALSE,
      'extensions' => 'php',
    ];

    $this->drupalPostForm('file-test/save_upload_from_form_test', $edit, t('Submit'));
    $this->assertResponse(200, 'Received a 200 response for posted test file.');
    $message = t('For security reasons, your upload has been renamed to') . ' <em class="placeholder">' . $this->phpfile->filename . '.txt' . '</em>';
    $this->assertRaw($message, 'Dangerous file was renamed.');
    $this->assertRaw(t('File MIME type is text/plain.'), "Dangerous file's MIME type was changed.");
    $this->assertRaw(t('You WIN!'), 'Found the success message.');

    // Check that the correct hooks were called.
    $this->assertFileHooksCalled(['validate', 'insert']);

    // Ensure dangerous files are not renamed when insecure uploads is TRUE.
    // Turn on insecure uploads.
    $config->set('allow_insecure_uploads', 1)->save();
    // Reset the hook counters.
    file_test_reset();

    $this->drupalPostForm('file-test/save_upload_from_form_test', $edit, t('Submit'));
    $this->assertResponse(200, 'Received a 200 response for posted test file.');
    $this->assertNoRaw(t('For security reasons, your upload has been renamed'), 'Found no security message.');
    $this->assertRaw(t('File name is @filename', ['@filename' => $this->phpfile->filename]), 'Dangerous file was not renamed when insecure uploads is TRUE.');
    $this->assertRaw(t('You WIN!'), 'Found the success message.');

    // Check that the correct hooks were called.
    $this->assertFileHooksCalled(['validate', 'insert']);

    // Turn off insecure uploads.
    $config->set('allow_insecure_uploads', 0)->save();
  }

  /**
   * Tests file munge handling.
   */
  public function testHandleFileMunge() {
251 252
    /** @var \Drupal\Core\File\FileSystemInterface $file_system */
    $file_system = \Drupal::service('file_system');
253 254 255 256 257 258 259 260 261
    // Ensure insecure uploads are disabled for this test.
    $this->config('system.file')->set('allow_insecure_uploads', 0)->save();
    $this->image = file_move($this->image, $this->image->getFileUri() . '.foo.' . $this->imageExtension);

    // Reset the hook counters to get rid of the 'move' we just called.
    file_test_reset();

    $extensions = $this->imageExtension;
    $edit = [
262
      'files[file_test_upload][]' => $file_system->realpath($this->image->getFileUri()),
263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283
      'extensions' => $extensions,
    ];

    $munged_filename = $this->image->getFilename();
    $munged_filename = substr($munged_filename, 0, strrpos($munged_filename, '.'));
    $munged_filename .= '_.' . $this->imageExtension;

    $this->drupalPostForm('file-test/save_upload_from_form_test', $edit, t('Submit'));
    $this->assertResponse(200, 'Received a 200 response for posted test file.');
    $this->assertRaw(t('For security reasons, your upload has been renamed'), 'Found security message.');
    $this->assertRaw(t('File name is @filename', ['@filename' => $munged_filename]), 'File was successfully munged.');
    $this->assertRaw(t('You WIN!'), 'Found the success message.');

    // Check that the correct hooks were called.
    $this->assertFileHooksCalled(['validate', 'insert']);

    // Ensure we don't munge files if we're allowing any extension.
    // Reset the hook counters.
    file_test_reset();

    $edit = [
284
      'files[file_test_upload][]' => $file_system->realpath($this->image->getFileUri()),
285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301
      'allow_all_extensions' => TRUE,
    ];

    $this->drupalPostForm('file-test/save_upload_from_form_test', $edit, t('Submit'));
    $this->assertResponse(200, 'Received a 200 response for posted test file.');
    $this->assertNoRaw(t('For security reasons, your upload has been renamed'), 'Found no security message.');
    $this->assertRaw(t('File name is @filename', ['@filename' => $this->image->getFilename()]), 'File was not munged when allowing any extension.');
    $this->assertRaw(t('You WIN!'), 'Found the success message.');

    // Check that the correct hooks were called.
    $this->assertFileHooksCalled(['validate', 'insert']);
  }

  /**
   * Tests renaming when uploading over a file that already exists.
   */
  public function testExistingRename() {
302 303
    /** @var \Drupal\Core\File\FileSystemInterface $file_system */
    $file_system = \Drupal::service('file_system');
304 305
    $edit = [
      'file_test_replace' => FILE_EXISTS_RENAME,
306
      'files[file_test_upload][]' => $file_system->realpath($this->image->getFileUri()),
307 308 309 310 311 312 313 314 315 316 317 318 319
    ];
    $this->drupalPostForm('file-test/save_upload_from_form_test', $edit, t('Submit'));
    $this->assertResponse(200, 'Received a 200 response for posted test file.');
    $this->assertRaw(t('You WIN!'), 'Found the success message.');

    // Check that the correct hooks were called.
    $this->assertFileHooksCalled(['validate', 'insert']);
  }

  /**
   * Tests replacement when uploading over a file that already exists.
   */
  public function testExistingReplace() {
320 321
    /** @var \Drupal\Core\File\FileSystemInterface $file_system */
    $file_system = \Drupal::service('file_system');
322 323
    $edit = [
      'file_test_replace' => FILE_EXISTS_REPLACE,
324
      'files[file_test_upload][]' => $file_system->realpath($this->image->getFileUri()),
325 326 327 328 329 330 331 332 333 334 335 336 337
    ];
    $this->drupalPostForm('file-test/save_upload_from_form_test', $edit, t('Submit'));
    $this->assertResponse(200, 'Received a 200 response for posted test file.');
    $this->assertRaw(t('You WIN!'), 'Found the success message.');

    // Check that the correct hooks were called.
    $this->assertFileHooksCalled(['validate', 'load', 'update']);
  }

  /**
   * Tests for failure when uploading over a file that already exists.
   */
  public function testExistingError() {
338 339
    /** @var \Drupal\Core\File\FileSystemInterface $file_system */
    $file_system = \Drupal::service('file_system');
340 341
    $edit = [
      'file_test_replace' => FILE_EXISTS_ERROR,
342
      'files[file_test_upload][]' => $file_system->realpath($this->image->getFileUri()),
343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368
    ];
    $this->drupalPostForm('file-test/save_upload_from_form_test', $edit, t('Submit'));
    $this->assertResponse(200, 'Received a 200 response for posted test file.');
    $this->assertRaw(t('Epic upload FAIL!'), 'Found the failure message.');

    // Check that the no hooks were called while failing.
    $this->assertFileHooksCalled([]);
  }

  /**
   * Tests for no failures when not uploading a file.
   */
  public function testNoUpload() {
    $this->drupalPostForm('file-test/save_upload_from_form_test', [], t('Submit'));
    $this->assertNoRaw(t('Epic upload FAIL!'), 'Failure message not found.');
  }

  /**
   * Tests for log entry on failing destination.
   */
  public function testDrupalMovingUploadedFileError() {
    // Create a directory and make it not writable.
    $test_directory = 'test_drupal_move_uploaded_file_fail';
    drupal_mkdir('temporary://' . $test_directory, 0000);
    $this->assertTrue(is_dir('temporary://' . $test_directory));

369 370
    /** @var \Drupal\Core\File\FileSystemInterface $file_system */
    $file_system = \Drupal::service('file_system');
371 372
    $edit = [
      'file_subdir' => $test_directory,
373
      'files[file_test_upload][]' => $file_system->realpath($this->image->getFileUri()),
374 375 376 377 378 379 380 381 382 383 384 385 386
    ];

    \Drupal::state()->set('file_test.disable_error_collection', TRUE);
    $this->drupalPostForm('file-test/save_upload_from_form_test', $edit, t('Submit'));
    $this->assertResponse(200, 'Received a 200 response for posted test file.');
    $this->assertRaw(t('File upload error. Could not move uploaded file.'), 'Found the failure message.');
    $this->assertRaw(t('Epic upload FAIL!'), 'Found the failure message.');

    // Uploading failed. Now check the log.
    $this->drupalGet('admin/reports/dblog');
    $this->assertResponse(200);
    $this->assertRaw(t('Upload error. Could not move uploaded file @file to destination @destination.', [
      '@file' => $this->image->getFilename(),
387
      '@destination' => 'temporary://' . $test_directory . '/' . $this->image->getFilename(),
388 389 390 391 392 393 394 395 396
    ]), 'Found upload error log entry.');
  }

  /**
   * Tests that form validation does not change error messages.
   */
  public function testErrorMessagesAreNotChanged() {
    $error = 'An error message set before _file_save_upload_from_form()';

397 398
    /** @var \Drupal\Core\File\FileSystemInterface $file_system */
    $file_system = \Drupal::service('file_system');
399
    $edit = [
400
      'files[file_test_upload][]' => $file_system->realpath($this->image->getFileUri()),
401 402 403 404 405 406 407 408 409 410 411 412 413 414
      'error_message' => $error,
    ];
    $this->drupalPostForm('file-test/save_upload_from_form_test', $edit, t('Submit'));
    $this->assertResponse(200, 'Received a 200 response for posted test file.');
    $this->assertRaw(t('You WIN!'), 'Found the success message.');

    // Ensure the expected error message is present and the counts before and
    // after calling _file_save_upload_from_form() are correct.
    $this->assertText($error);
    $this->assertRaw('Number of error messages before _file_save_upload_from_form(): 1');
    $this->assertRaw('Number of error messages after _file_save_upload_from_form(): 1');

    // Test that error messages are preserved when an error occurs.
    $edit = [
415
      'files[file_test_upload][]' => $file_system->realpath($this->image->getFileUri()),
416
      'error_message' => $error,
417
      'extensions' => 'foo',
418 419 420 421 422 423 424 425 426
    ];
    $this->drupalPostForm('file-test/save_upload_from_form_test', $edit, t('Submit'));
    $this->assertResponse(200, 'Received a 200 response for posted test file.');
    $this->assertRaw(t('Epic upload FAIL!'), 'Found the failure message.');

    // Ensure the expected error message is present and the counts before and
    // after calling _file_save_upload_from_form() are correct.
    $this->assertText($error);
    $this->assertRaw('Number of error messages before _file_save_upload_from_form(): 1');
427
    $this->assertRaw('Number of error messages after _file_save_upload_from_form(): 1');
428 429 430

    // Test a successful upload with no messages.
    $edit = [
431
      'files[file_test_upload][]' => $file_system->realpath($this->image->getFileUri()),
432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450
    ];
    $this->drupalPostForm('file-test/save_upload_from_form_test', $edit, t('Submit'));
    $this->assertResponse(200, 'Received a 200 response for posted test file.');
    $this->assertRaw(t('You WIN!'), 'Found the success message.');

    // Ensure the error message is not present and the counts before and after
    // calling _file_save_upload_from_form() are correct.
    $this->assertNoText($error);
    $this->assertRaw('Number of error messages before _file_save_upload_from_form(): 0');
    $this->assertRaw('Number of error messages after _file_save_upload_from_form(): 0');
  }

  /**
   * Tests that multiple validation errors are combined in one message.
   */
  public function testCombinedErrorMessages() {
    $textfile = current($this->drupalGetTestFiles('text'));
    $this->assertTrue(is_file($textfile->uri), 'The text file we are going to upload exists.');

451 452
    /** @var \Drupal\Core\File\FileSystemInterface $file_system */
    $file_system = \Drupal::service('file_system');
453 454 455 456 457 458

    // Can't use drupalPostForm() for set nonexistent fields.
    $this->drupalGet('file-test/save_upload_from_form_test');
    $client = $this->getSession()->getDriver()->getClient();
    $submit_xpath = $this->assertSession()->buttonExists('Submit')->getXpath();
    $form = $client->getCrawler()->filterXPath($submit_xpath)->form();
459 460 461 462 463
    $edit = [
      'allow_all_extensions' => FALSE,
      'is_image_file' => TRUE,
      'extensions' => 'jpeg',
    ];
464 465 466 467
    $edit += $form->getPhpValues();
    $files['files']['file_test_upload'][0] = $file_system->realpath($this->phpfile->uri);
    $files['files']['file_test_upload'][1] = $file_system->realpath($textfile->uri);
    $client->request($form->getMethod(), $form->getUri(), $edit, $files);
468 469 470 471 472 473 474 475 476 477 478 479 480 481

    $this->assertResponse(200, 'Received a 200 response for posted test file.');
    $this->assertRaw(t('Epic upload FAIL!'), 'Found the failure message.');

    // Search for combined error message followed by a formatted list of messages.
    $this->assertRaw(t('One or more files could not be uploaded.') . '<div class="item-list">', 'Error message contains combined list of validation errors.');
  }

  /**
   * Tests highlighting of file upload field when it has an error.
   */
  public function testUploadFieldIsHighlighted() {
    $this->assertEqual(0, count($this->cssSelect('input[name="files[file_test_upload][]"].error')), 'Successful file upload has no error.');

482 483
    /** @var \Drupal\Core\File\FileSystemInterface $file_system */
    $file_system = \Drupal::service('file_system');
484
    $edit = [
485
      'files[file_test_upload][]' => $file_system->realpath($this->image->getFileUri()),
486
      'extensions' => 'foo',
487 488 489 490 491 492 493 494
    ];
    $this->drupalPostForm('file-test/save_upload_from_form_test', $edit, t('Submit'));
    $this->assertResponse(200, 'Received a 200 response for posted test file.');
    $this->assertRaw(t('Epic upload FAIL!'), 'Found the failure message.');
    $this->assertEqual(1, count($this->cssSelect('input[name="files[file_test_upload][]"].error')), 'File upload field has error.');
  }

}