FileFieldWidgetTest.php 26.4 KB
Newer Older
1 2 3
<?php

namespace Drupal\file\Tests;
4 5

use Drupal\comment\Entity\Comment;
6
use Drupal\comment\Tests\CommentTestTrait;
7
use Drupal\Component\Utility\Unicode;
8
use Drupal\Core\Url;
9
use Drupal\field\Entity\FieldConfig;
10
use Drupal\field\Entity\FieldStorageConfig;
11
use Drupal\field_ui\Tests\FieldUiTestTrait;
12
use Drupal\user\RoleInterface;
13
use Drupal\file\Entity\File;
14 15
use Drupal\user\Entity\User;
use Drupal\user\UserInterface;
16 17

/**
18 19 20 21
 * Tests the file field widget, single and multi-valued, with and without AJAX,
 * with public and private files.
 *
 * @group file
22 23
 */
class FileFieldWidgetTest extends FileFieldTestBase {
24

25
  use CommentTestTrait;
26 27
  use FieldUiTestTrait;

28 29 30 31 32 33 34 35
  /**
   * {@inheritdoc}
   */
  protected function setUp() {
    parent::setUp();
    $this->drupalPlaceBlock('system_breadcrumb_block');
  }

36 37 38 39 40
  /**
   * Modules to enable.
   *
   * @var array
   */
41
  public static $modules = array('comment', 'block');
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 69 70 71 72
  /**
   * Creates a temporary file, for a specific user.
   *
   * @param string $data
   *   A string containing the contents of the file.
   * @param \Drupal\user\UserInterface $user
   *   The user of the file owner.
   *
   * @return \Drupal\file\FileInterface
   *   A file object, or FALSE on error.
   */
  protected function createTemporaryFile($data, UserInterface $user = NULL) {
    $file = file_save_data($data, NULL, NULL);

    if ($file) {
      if ($user) {
        $file->setOwner($user);
      }
      else {
        $file->setOwner($this->adminUser);
      }
      // Change the file status to be temporary.
      $file->setTemporary();
      // Save the changes.
      $file->save();
    }

    return $file;
  }

73 74 75 76
  /**
   * Tests upload and remove buttons for a single-valued File field.
   */
  function testSingleValuedWidget() {
77
    $node_storage = $this->container->get('entity.manager')->getStorage('node');
78
    $type_name = 'article';
79
    $field_name = strtolower($this->randomMachineName());
80
    $this->createFileField($field_name, 'node', $type_name);
81 82 83 84 85 86

    $test_file = $this->getTestFile('text');

    foreach (array('nojs', 'js') as $type) {
      // Create a new node with the uploaded file and ensure it got uploaded
      // successfully.
87
      // @todo This only tests a 'nojs' submission, because drupalPostAjaxForm()
88 89
      //   does not yet support file uploads.
      $nid = $this->uploadNodeFile($test_file, $field_name, $type_name);
90 91
      $node_storage->resetCache(array($nid));
      $node = $node_storage->load($nid);
92
      $node_file = File::load($node->{$field_name}->target_id);
93
      $this->assertFileExists($node_file, 'New file saved to disk on node creation.');
94 95

      // Ensure the file can be downloaded.
96
      $this->drupalGet(file_create_url($node_file->getFileUri()));
97
      $this->assertResponse(200, 'Confirmed that the generated URL is correct by downloading the shipped file.');
98 99 100

      // Ensure the edit page has a remove button instead of an upload button.
      $this->drupalGet("node/$nid/edit");
101 102
      $this->assertNoFieldByXPath('//input[@type="submit"]', t('Upload'), 'Node with file does not display the "Upload" button.');
      $this->assertFieldByXpath('//input[@type="submit"]', t('Remove'), 'Node with file displays the "Remove" button.');
103 104 105 106

      // "Click" the remove button (emulating either a nojs or js submission).
      switch ($type) {
        case 'nojs':
107
          $this->drupalPostForm(NULL, array(), t('Remove'));
108 109 110
          break;
        case 'js':
          $button = $this->xpath('//input[@type="submit" and @value="' . t('Remove') . '"]');
111
          $this->drupalPostAjaxForm(NULL, array(), array((string) $button[0]['name'] => (string) $button[0]['value']));
112 113 114 115
          break;
      }

      // Ensure the page now has an upload button instead of a remove button.
116 117
      $this->assertNoFieldByXPath('//input[@type="submit"]', t('Remove'), 'After clicking the "Remove" button, it is no longer displayed.');
      $this->assertFieldByXpath('//input[@type="submit"]', t('Upload'), 'After clicking the "Remove" button, the "Upload" button is displayed.');
118
      // Test label has correct 'for' attribute.
119 120
      $input = $this->xpath('//input[@name="files[' . $field_name . '_0]"]');
      $label = $this->xpath('//label[@for="' . (string) $input[0]['id'] . '"]');
121
      $this->assertTrue(isset($label[0]), 'Label for upload found.');
122 123

      // Save the node and ensure it does not have the file.
124
      $this->drupalPostForm(NULL, array(), t('Save and keep published'));
125 126
      $node_storage->resetCache(array($nid));
      $node = $node_storage->load($nid);
127
      $this->assertTrue(empty($node->{$field_name}->target_id), 'File was successfully removed from the node.');
128 129 130 131 132 133 134
    }
  }

  /**
   * Tests upload and remove buttons for multiple multi-valued File fields.
   */
  function testMultiValuedWidget() {
135
    $node_storage = $this->container->get('entity.manager')->getStorage('node');
136
    $type_name = 'article';
137
    // Use explicit names instead of random names for those fields, because of a
138
    // bug in drupalPostForm() with multiple file uploads in one form, where the
139 140 141 142 143 144
    // order of uploads depends on the order in which the upload elements are
    // added to the $form (which, in the current implementation of
    // FileStorage::listAll(), comes down to the alphabetical order on field
    // names).
    $field_name = 'test_file_field_1';
    $field_name2 = 'test_file_field_2';
145 146 147
    $cardinality = 3;
    $this->createFileField($field_name, 'node', $type_name, array('cardinality' => $cardinality));
    $this->createFileField($field_name2, 'node', $type_name, array('cardinality' => $cardinality));
148 149 150 151 152 153 154 155 156

    $test_file = $this->getTestFile('text');

    foreach (array('nojs', 'js') as $type) {
      // Visit the node creation form, and upload 3 files for each field. Since
      // the field has cardinality of 3, ensure the "Upload" button is displayed
      // until after the 3rd file, and after that, isn't displayed. Because
      // SimpleTest triggers the last button with a given name, so upload to the
      // second field first.
157
      // @todo This is only testing a non-Ajax upload, because drupalPostAjaxForm()
158 159 160 161 162
      //   does not yet emulate jQuery's file upload.
      //
      $this->drupalGet("node/add/$type_name");
      foreach (array($field_name2, $field_name) as $each_field_name) {
        for ($delta = 0; $delta < 3; $delta++) {
163
          $edit = array('files[' . $each_field_name . '_' . $delta . '][]' => drupal_realpath($test_file->getFileUri()));
164
          // If the Upload button doesn't exist, drupalPostForm() will automatically
165
          // fail with an assertion message.
166
          $this->drupalPostForm(NULL, $edit, t('Upload'));
167 168
        }
      }
169
      $this->assertNoFieldByXpath('//input[@type="submit"]', t('Upload'), 'After uploading 3 files for each field, the "Upload" button is no longer displayed.');
170 171 172 173 174 175 176 177 178 179 180 181

      $num_expected_remove_buttons = 6;

      foreach (array($field_name, $field_name2) as $current_field_name) {
        // How many uploaded files for the current field are remaining.
        $remaining = 3;
        // Test clicking each "Remove" button. For extra robustness, test them out
        // of sequential order. They are 0-indexed, and get renumbered after each
        // iteration, so array(1, 1, 0) means:
        // - First remove the 2nd file.
        // - Then remove what is then the 2nd file (was originally the 3rd file).
        // - Then remove the first file.
182
        foreach (array(1, 1, 0) as $delta) {
183 184 185
          // Ensure we have the expected number of Remove buttons, and that they
          // are numbered sequentially.
          $buttons = $this->xpath('//input[@type="submit" and @value="Remove"]');
186
          $this->assertTrue(is_array($buttons) && count($buttons) === $num_expected_remove_buttons, format_string('There are %n "Remove" buttons displayed (JSMode=%type).', array('%n' => $num_expected_remove_buttons, '%type' => $type)));
187 188 189 190 191 192 193
          foreach ($buttons as $i => $button) {
            $key = $i >= $remaining ? $i - $remaining : $i;
            $check_field_name = $field_name2;
            if ($current_field_name == $field_name && $i < $remaining) {
              $check_field_name = $field_name;
            }

194
            $this->assertIdentical((string) $button['name'], $check_field_name . '_' . $key . '_remove_button');
195 196 197
          }

          // "Click" the remove button (emulating either a nojs or js submission).
198
          $button_name = $current_field_name . '_' . $delta . '_remove_button';
199 200
          switch ($type) {
            case 'nojs':
201
              // drupalPostForm() takes a $submit parameter that is the value of the
202 203 204 205
              // button whose click we want to emulate. Since we have multiple
              // buttons with the value "Remove", and want to control which one we
              // use, we change the value of the other ones to something else.
              // Since non-clicked buttons aren't included in the submitted POST
206
              // data, and since drupalPostForm() will result in $this being updated
207 208 209 210 211 212
              // with a newly rebuilt form, this doesn't cause problems.
              foreach ($buttons as $button) {
                if ($button['name'] != $button_name) {
                  $button['value'] = 'DUMMY';
                }
              }
213
              $this->drupalPostForm(NULL, array(), t('Remove'));
214 215
              break;
            case 'js':
216
              // drupalPostAjaxForm() lets us target the button precisely, so we don't
217
              // require the workaround used above for nojs.
218
              $this->drupalPostAjaxForm(NULL, array(), array($button_name => t('Remove')));
219 220 221 222 223 224 225
              break;
          }
          $num_expected_remove_buttons--;
          $remaining--;

          // Ensure an "Upload" button for the current field is displayed with the
          // correct name.
226
          $upload_button_name = $current_field_name . '_' . $remaining . '_upload_button';
227
          $buttons = $this->xpath('//input[@type="submit" and @value="Upload" and @name=:name]', array(':name' => $upload_button_name));
228
          $this->assertTrue(is_array($buttons) && count($buttons) == 1, format_string('The upload button is displayed with the correct name (JSMode=%type).', array('%type' => $type)));
229 230 231 232

          // Ensure only at most one button per field is displayed.
          $buttons = $this->xpath('//input[@type="submit" and @value="Upload"]');
          $expected = $current_field_name == $field_name ? 1 : 2;
233
          $this->assertTrue(is_array($buttons) && count($buttons) == $expected, format_string('After removing a file, only one "Upload" button for each possible field is displayed (JSMode=%type).', array('%type' => $type)));
234 235 236 237
        }
      }

      // Ensure the page now has no Remove buttons.
238
      $this->assertNoFieldByXPath('//input[@type="submit"]', t('Remove'), format_string('After removing all files, there is no "Remove" button displayed (JSMode=%type).', array('%type' => $type)));
239 240

      // Save the node and ensure it does not have any files.
241
      $this->drupalPostForm(NULL, array('title[0][value]' => $this->randomMachineName()), t('Save and publish'));
242 243 244
      $matches = array();
      preg_match('/node\/([0-9]+)/', $this->getUrl(), $matches);
      $nid = $matches[1];
245 246
      $node_storage->resetCache(array($nid));
      $node = $node_storage->load($nid);
247
      $this->assertTrue(empty($node->{$field_name}->target_id), 'Node was successfully saved without any files.');
248
    }
249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291

    $upload_files = array($test_file, $test_file);
    // Try to upload multiple files, but fewer than the maximum.
    $nid = $this->uploadNodeFiles($upload_files, $field_name, $type_name);
    $node_storage->resetCache(array($nid));
    $node = $node_storage->load($nid);
    $this->assertEqual(count($node->{$field_name}), count($upload_files), 'Node was successfully saved with mulitple files.');

    // Try to upload more files than allowed on revision.
    $this->uploadNodeFiles($upload_files, $field_name, $nid, 1);
    $args = array(
      '%field' => $field_name,
      '@count' => $cardinality
    );
    $this->assertRaw(t('%field: this field cannot hold more than @count values.', $args));
    $node_storage->resetCache(array($nid));
    $node = $node_storage->load($nid);
    $this->assertEqual(count($node->{$field_name}), count($upload_files), 'More files than allowed could not be saved to node.');

    // Try to upload exactly the allowed number of files on revision.
    $this->uploadNodeFile($test_file, $field_name, $nid, 1);
    $node_storage->resetCache(array($nid));
    $node = $node_storage->load($nid);
    $this->assertEqual(count($node->{$field_name}), $cardinality, 'Node was successfully revised to maximum number of files.');

    // Try to upload exactly the allowed number of files, new node.
    $upload_files[] = $test_file;
    $nid = $this->uploadNodeFiles($upload_files, $field_name, $type_name);
    $node_storage->resetCache(array($nid));
    $node = $node_storage->load($nid);
    $this->assertEqual(count($node->{$field_name}), $cardinality, 'Node was successfully saved with maximum number of files.');

    // Try to upload more files than allowed, new node.
    $upload_files[] = $test_file;
    $this->uploadNodeFiles($upload_files, $field_name, $type_name);

    $args = [
      '%field' => $field_name,
      '@max' => $cardinality,
      '@count' => count($upload_files),
      '%list' => $test_file->getFileName(),
    ];
    $this->assertRaw(t('Field %field can only hold @max values but there were @count uploaded. The following files have been omitted as a result: %list.', $args));
292 293 294 295 296 297
  }

  /**
   * Tests a file field with a "Private files" upload destination setting.
   */
  function testPrivateFileSetting() {
298
    $node_storage = $this->container->get('entity.manager')->getStorage('node');
299
    // Grant the admin user required permissions.
300
    user_role_grant_permissions($this->adminUser->roles[0]->target_id, array('administer node fields'));
301

302
    $type_name = 'article';
303
    $field_name = strtolower($this->randomMachineName());
304
    $this->createFileField($field_name, 'node', $type_name);
305
    $field = FieldConfig::loadByName('node', $type_name, $field_name);
306
    $field_id = $field->id();
307 308 309 310

    $test_file = $this->getTestFile('text');

    // Change the field setting to make its files private, and upload a file.
311
    $edit = array('settings[uri_scheme]' => 'private');
312
    $this->drupalPostForm("admin/structure/types/manage/$type_name/fields/$field_id/storage", $edit, t('Save field settings'));
313
    $nid = $this->uploadNodeFile($test_file, $field_name, $type_name);
314 315
    $node_storage->resetCache(array($nid));
    $node = $node_storage->load($nid);
316
    $node_file = File::load($node->{$field_name}->target_id);
317
    $this->assertFileExists($node_file, 'New file saved to disk on node creation.');
318 319

    // Ensure the private file is available to the user who uploaded it.
320
    $this->drupalGet(file_create_url($node_file->getFileUri()));
321
    $this->assertResponse(200, 'Confirmed that the generated URL is correct by downloading the shipped file.');
322 323 324

    // Ensure we can't change 'uri_scheme' field settings while there are some
    // entities with uploaded files.
325
    $this->drupalGet("admin/structure/types/manage/$type_name/fields/$field_id/storage");
326
    $this->assertFieldByXpath('//input[@id="edit-settings-uri-scheme-public" and @disabled="disabled"]', 'public', 'Upload destination setting disabled.');
327 328

    // Delete node and confirm that setting could be changed.
329
    $node->delete();
330
    $this->drupalGet("admin/structure/types/manage/$type_name/fields/$field_id/storage");
331
    $this->assertFieldByXpath('//input[@id="edit-settings-uri-scheme-public" and not(@disabled)]', 'public', 'Upload destination setting enabled.');
332 333 334 335 336 337 338 339
  }

  /**
   * Tests that download restrictions on private files work on comments.
   */
  function testPrivateFileComment() {
    $user = $this->drupalCreateUser(array('access comments'));

340
    // Grant the admin user required comment permissions.
341
    $roles = $this->adminUser->getRoles();
342
    user_role_grant_permissions($roles[1], array('administer comment fields', 'administer comments'));
343 344 345

    // Revoke access comments permission from anon user, grant post to
    // authenticated.
346 347
    user_role_revoke_permissions(RoleInterface::ANONYMOUS_ID, array('access comments'));
    user_role_grant_permissions(RoleInterface::AUTHENTICATED_ID, array('post comments', 'skip comment approval'));
348 349

    // Create a new field.
350
    $this->addDefaultCommentField('node', 'article');
351 352 353

    $name = strtolower($this->randomMachineName());
    $label = $this->randomMachineName();
354
    $storage_edit = array('settings[uri_scheme]' => 'private');
355
    $this->fieldUIAddNewField('admin/structure/comment/manage/comment', $name, $label, 'file', $storage_edit);
356

357
    // Manually clear cache on the tester side.
358
    \Drupal::entityManager()->clearCachedFieldDefinitions();
359

360 361
    // Create node.
    $edit = array(
362
      'title[0][value]' => $this->randomMachineName(),
363
    );
364
    $this->drupalPostForm('node/add/article', $edit, t('Save and publish'));
365
    $node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
366 367 368 369

    // Add a comment with a file.
    $text_file = $this->getTestFile('text');
    $edit = array(
370
      'files[field_' . $name . '_' . 0 . ']' => drupal_realpath($text_file->getFileUri()),
371
      'comment_body[0][value]' => $comment_body = $this->randomMachineName(),
372
    );
373
    $this->drupalPostForm('node/' . $node->id(), $edit, t('Save'));
374 375 376 377 378 379 380 381

    // Get the comment ID.
    preg_match('/comment-([0-9]+)/', $this->getUrl(), $matches);
    $cid = $matches[1];

    // Log in as normal user.
    $this->drupalLogin($user);

382
    $comment = Comment::load($cid);
383
    $comment_file = $comment->{'field_' . $name}->entity;
384
    $this->assertFileExists($comment_file, 'New file saved to disk on node creation.');
385
    // Test authenticated file download.
386
    $url = file_create_url($comment_file->getFileUri());
387
    $this->assertNotEqual($url, NULL, 'Confirmed that the URL is valid');
388
    $this->drupalGet(file_create_url($comment_file->getFileUri()));
389
    $this->assertResponse(200, 'Confirmed that the generated URL is correct by downloading the shipped file.');
390 391 392

    // Test anonymous file download.
    $this->drupalLogout();
393
    $this->drupalGet(file_create_url($comment_file->getFileUri()));
394
    $this->assertResponse(403, 'Confirmed that access is denied for the file without the needed permission.');
395 396

    // Unpublishes node.
397
    $this->drupalLogin($this->adminUser);
398
    $this->drupalPostForm('node/' . $node->id() . '/edit', array(), t('Save and unpublish'));
399 400 401

    // Ensures normal user can no longer download the file.
    $this->drupalLogin($user);
402
    $this->drupalGet(file_create_url($comment_file->getFileUri()));
403
    $this->assertResponse(403, 'Confirmed that access is denied for the file without the needed permission.');
404 405
  }

406 407 408 409 410
  /**
   * Tests validation with the Upload button.
   */
  function testWidgetValidation() {
    $type_name = 'article';
411
    $field_name = strtolower($this->randomMachineName());
412
    $this->createFileField($field_name, 'node', $type_name);
413 414 415 416 417
    $this->updateFileField($field_name, $type_name, array('file_extensions' => 'txt'));

    foreach (array('nojs', 'js') as $type) {
      // Create node and prepare files for upload.
      $node = $this->drupalCreateNode(array('type' => 'article'));
418
      $nid = $node->id();
419 420 421
      $this->drupalGet("node/$nid/edit");
      $test_file_text = $this->getTestFile('text');
      $test_file_image = $this->getTestFile('image');
422
      $name = 'files[' . $field_name . '_0]';
423 424

      // Upload file with incorrect extension, check for validation error.
425
      $edit[$name] = drupal_realpath($test_file_image->getFileUri());
426 427
      switch ($type) {
        case 'nojs':
428
          $this->drupalPostForm(NULL, $edit, t('Upload'));
429 430 431
          break;
        case 'js':
          $button = $this->xpath('//input[@type="submit" and @value="' . t('Upload') . '"]');
432
          $this->drupalPostAjaxForm(NULL, $edit, array((string) $button[0]['name'] => (string) $button[0]['value']));
433 434 435 436 437 438
          break;
      }
      $error_message = t('Only files with the following extensions are allowed: %files-allowed.', array('%files-allowed' => 'txt'));
      $this->assertRaw($error_message, t('Validation error when file with wrong extension uploaded (JSMode=%type).', array('%type' => $type)));

      // Upload file with correct extension, check that error message is removed.
439
      $edit[$name] = drupal_realpath($test_file_text->getFileUri());
440 441
      switch ($type) {
        case 'nojs':
442
          $this->drupalPostForm(NULL, $edit, t('Upload'));
443 444 445
          break;
        case 'js':
          $button = $this->xpath('//input[@type="submit" and @value="' . t('Upload') . '"]');
446
          $this->drupalPostAjaxForm(NULL, $edit, array((string) $button[0]['name'] => (string) $button[0]['value']));
447 448 449 450 451
          break;
      }
      $this->assertNoRaw($error_message, t('Validation error removed when file with correct extension uploaded (JSMode=%type).', array('%type' => $type)));
    }
  }
452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477

  /**
   * Tests file widget element.
   */
  public function testWidgetElement() {
    $field_name = Unicode::strtolower($this->randomMachineName());
    $html_name = str_replace('_', '-', $field_name);
    $this->createFileField($field_name, 'node', 'article', ['cardinality' => FieldStorageConfig::CARDINALITY_UNLIMITED]);
    $file = $this->getTestFile('text');
    $xpath = "//details[@data-drupal-selector='edit-$html_name']/div[@class='details-wrapper']/table";

    $this->drupalGet('node/add/article');

    $elements = $this->xpath($xpath);

    // If the field has no item, the table should not be visible.
    $this->assertIdentical(count($elements), 0);

    // Upload a file.
    $edit['files[' . $field_name . '_0][]'] = $this->container->get('file_system')->realpath($file->getFileUri());
    $this->drupalPostAjaxForm(NULL, $edit, "{$field_name}_0_upload_button");

    $elements = $this->xpath($xpath);

    // If the field has at least a item, the table should be visible.
    $this->assertIdentical(count($elements), 1);
478 479 480 481 482 483

    // Test for AJAX error when using progress bar on file field widget
    $key = $this->randomMachineName();
    $this->drupalPost('file/progress/' . $key, 'application/json', []);
    $this->assertNoResponse(500, t('No AJAX error when using progress bar on file field widget'));
    $this->assertText('Starting upload...');
484 485
  }

486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594
  /**
   * Tests exploiting the temporary file removal of another user using fid.
   */
  public function testTemporaryFileRemovalExploit() {
    // Create a victim user.
    $victim_user = $this->drupalCreateUser();

    // Create an attacker user.
    $attacker_user = $this->drupalCreateUser(array(
      'access content',
      'create article content',
      'edit any article content',
    ));

    // Log in as the attacker user.
    $this->drupalLogin($attacker_user);

    // Perform tests using the newly created users.
    $this->doTestTemporaryFileRemovalExploit($victim_user, $attacker_user);
  }

  /**
   * Tests exploiting the temporary file removal for anonymous users using fid.
   */
  public function testTemporaryFileRemovalExploitAnonymous() {
    // Set up an anonymous victim user.
    $victim_user = User::getAnonymousUser();

    // Set up an anonymous attacker user.
    $attacker_user = User::getAnonymousUser();

    // Set up permissions for anonymous attacker user.
    user_role_change_permissions(RoleInterface::ANONYMOUS_ID, array(
      'access content' => TRUE,
      'create article content' => TRUE,
      'edit any article content' => TRUE,
    ));

    // Log out so as to be the anonymous attacker user.
    $this->drupalLogout();

    // Perform tests using the newly set up anonymous users.
    $this->doTestTemporaryFileRemovalExploit($victim_user, $attacker_user);
  }

  /**
   * Helper for testing exploiting the temporary file removal using fid.
   *
   * @param \Drupal\user\UserInterface $victim_user
   *   The victim user.
   * @param \Drupal\user\UserInterface $attacker_user
   *   The attacker user.
   */
  protected function doTestTemporaryFileRemovalExploit(UserInterface $victim_user, UserInterface $attacker_user) {
    $type_name = 'article';
    $field_name = 'test_file_field';
    $this->createFileField($field_name, 'node', $type_name);

    $test_file = $this->getTestFile('text');
    foreach (array('nojs', 'js') as $type) {
      // Create a temporary file owned by the victim user. This will be as if
      // they had uploaded the file, but not saved the node they were editing
      // or creating.
      $victim_tmp_file = $this->createTemporaryFile('some text', $victim_user);
      $victim_tmp_file = File::load($victim_tmp_file->id());
      $this->assertTrue($victim_tmp_file->isTemporary(), 'New file saved to disk is temporary.');
      $this->assertFalse(empty($victim_tmp_file->id()), 'New file has an fid.');
      $this->assertEqual($victim_user->id(), $victim_tmp_file->getOwnerId(), 'New file belongs to the victim.');

      // Have attacker create a new node with a different uploaded file and
      // ensure it got uploaded successfully.
      $edit = [
        'title[0][value]' => $type . '-title' ,
      ];

      // Attach a file to a node.
      $edit['files[' . $field_name . '_0]'] = $this->container->get('file_system')->realpath($test_file->getFileUri());
      $this->drupalPostForm(Url::fromRoute('node.add', array('node_type' => $type_name)), $edit, t('Save'));
      $node = $this->drupalGetNodeByTitle($edit['title[0][value]']);

      /** @var \Drupal\file\FileInterface $node_file */
      $node_file = File::load($node->{$field_name}->target_id);
      $this->assertFileExists($node_file, 'A file was saved to disk on node creation');
      $this->assertEqual($attacker_user->id(), $node_file->getOwnerId(), 'New file belongs to the attacker.');

      // Ensure the file can be downloaded.
      $this->drupalGet(file_create_url($node_file->getFileUri()));
      $this->assertResponse(200, 'Confirmed that the generated URL is correct by downloading the shipped file.');

      // "Click" the remove button (emulating either a nojs or js submission).
      // In this POST request, the attacker "guesses" the fid of the victim's
      // temporary file and uses that to remove this file.
      $this->drupalGet($node->toUrl('edit-form'));
      switch ($type) {
        case 'nojs':
          $this->drupalPostForm(NULL, [$field_name . '[0][fids]' => (string) $victim_tmp_file->id()], 'Remove');
          break;

        case 'js':
          $this->drupalPostAjaxForm(NULL, [$field_name . '[0][fids]' => (string) $victim_tmp_file->id()], ["{$field_name}_0_remove_button" => 'Remove']);
          break;
      }

      // The victim's temporary file should not be removed by the attacker's
      // POST request.
      $this->assertFileExists($victim_tmp_file);
    }
  }

595
}