WebTestBase.php 114 KB
Newer Older
1 2 3
<?php

/**
4 5
 * @file
 * Definition of Drupal\simpletest\WebTestBase.
6
 */
7

8
namespace Drupal\simpletest;
9

10
use Drupal\Core\DrupalKernel;
11 12 13 14 15 16 17
use Drupal\Core\Database\Database;
use Drupal\Core\Database\ConnectionNotDefinedException;
use PDO;
use stdClass;
use DOMDocument;
use DOMXPath;
use SimpleXMLElement;
18 19 20 21

/**
 * Test case for typical Drupal tests.
 */
22 23
abstract class WebTestBase extends TestBase {

24 25 26 27 28
  /**
   * The profile to install as a basis for testing.
   *
   * @var string
   */
29
  protected $profile = 'testing';
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
  /**
   * The URL currently loaded in the internal browser.
   *
   * @var string
   */
  protected $url;

  /**
   * The handle of the current cURL connection.
   *
   * @var resource
   */
  protected $curlHandle;

  /**
   * The headers of the page currently loaded in the internal browser.
   *
   * @var Array
   */
  protected $headers;

  /**
   * The content of the page currently loaded in the internal browser.
   *
   * @var string
   */
  protected $content;

  /**
   * The content of the page currently loaded in the internal browser (plain text version).
   *
   * @var string
   */
  protected $plainTextContent;

66 67 68 69 70 71 72
  /**
   * The value of the Drupal.settings JavaScript variable for the page currently loaded in the internal browser.
   *
   * @var Array
   */
  protected $drupalSettings;

73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
  /**
   * The parsed version of the page.
   *
   * @var SimpleXMLElement
   */
  protected $elements = NULL;

  /**
   * The current user logged in using the internal browser.
   *
   * @var bool
   */
  protected $loggedInUser = FALSE;

  /**
   * The current cookie file used by cURL.
   *
   * We do not reuse the cookies in further runs, so we do not need a file
   * but we still need cookie handling, so we set the jar to NULL.
   */
  protected $cookieFile = NULL;

  /**
   * Additional cURL options.
   *
98 99
   * Drupal\simpletest\WebTestBase itself never sets this but always obeys what is
   * set.
100 101 102 103 104 105 106 107 108 109
   */
  protected $additionalCurlOptions = array();

  /**
   * The original user, before it was changed to a clean uid = 1 for testing purposes.
   *
   * @var object
   */
  protected $originalUser = NULL;

110 111 112 113 114 115 116
  /**
   * The original shutdown handlers array, before it was cleaned for testing purposes.
   *
   * @var array
   */
  protected $originalShutdownCallbacks = array();

117 118 119 120 121
  /**
   * HTTP authentication method
   */
  protected $httpauth_method = CURLAUTH_BASIC;

122 123 124 125 126
  /**
   * HTTP authentication credentials (<username>:<password>).
   */
  protected $httpauth_credentials = NULL;

127 128 129 130 131 132 133 134 135 136
  /**
   * The current session name, if available.
   */
  protected $session_name = NULL;

  /**
   * The current session ID, if available.
   */
  protected $session_id = NULL;

137 138 139 140 141
  /**
   * Whether the files were copied to the test files directory.
   */
  protected $generatedTestFiles = FALSE;

142 143 144 145 146
  /**
   * The maximum number of redirects to follow when handling responses.
   */
  protected $maximumRedirects = 5;

147 148 149 150 151
  /**
   * The number of redirects followed during the handling of a request.
   */
  protected $redirect_count;

152 153 154 155 156
  /**
   * The kernel used in this test.
   */
  protected $kernel;

157
  /**
158
   * Constructor for Drupal\simpletest\WebTestBase.
159 160 161 162 163 164
   */
  function __construct($test_id = NULL) {
    parent::__construct($test_id);
    $this->skipClasses[__CLASS__] = TRUE;
  }

165 166 167
  /**
   * Get a node from the database based on its title.
   *
168
   * @param $title
169
   *   A node title, usually generated by $this->randomName().
170
   * @param $reset
171
   *   (optional) Whether to reset the entity cache.
172 173
   *
   * @return
174
   *   A node entity matching $title.
175
   */
176
  function drupalGetNodeByTitle($title, $reset = FALSE) {
177 178 179 180
    if ($reset) {
      entity_get_controller('node')->resetCache();
    }
    $nodes = entity_load_multiple_by_properties('node', array('title' => $title));
181 182 183 184 185
    // Load the first node returned from the database.
    $returned_node = reset($nodes);
    return $returned_node;
  }

186 187 188
  /**
   * Creates a node based on default settings.
   *
189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226
   * @param array $settings
   *   (optional) An associative array of settings for the node, as used in
   *   entity_create(). Override the defaults by specifying the key and value
   *   in the array, for example:
   *   @code
   *     $this->drupalCreateNode(array(
   *       'title' => t('Hello, world!'),
   *       'type' => 'article',
   *     ));
   *   @endcode
   *   The following defaults are provided:
   *   - body: Random string using the default filter format:
   *     @code
   *       $settings['body'][LANGUAGE_NOT_SPECIFIED][0] = array(
   *         'value' => $this->randomName(32),
   *         'format' => filter_default_format(),
   *       );
   *     @endcode
   *   - title: Random string.
   *   - comment: COMMENT_NODE_OPEN.
   *   - changed: REQUEST_TIME.
   *   - promote: NODE_NOT_PROMOTED.
   *   - log: Empty string.
   *   - status: NODE_PUBLISHED.
   *   - sticky: NODE_NOT_STICKY.
   *   - type: 'page'.
   *   - langcode: LANGCODE_NOT_SPECIFIED. (If a 'langcode' key is provided in
   *     the array, this language code will also be used for a randomly
   *     generated body field for that language, and the body for
   *     LANGUAGE_NOT_SPECIFIED will remain empty.)
   *   - uid: The currently logged in user, or the user running test.
   *   - revision: 1. (Backwards-compatible binary flag indicating whether a
   *     new revision should be created; use 1 to specify a new revision.)
   *
   * @return Drupal\node\Node
   *   The created node entity.
   */
  protected function drupalCreateNode(array $settings = array()) {
227
    // Populate defaults array.
228
    $settings += array(
229
      'body'      => array(LANGUAGE_NOT_SPECIFIED => array(array())),
230
      'title'     => $this->randomName(8),
231
      'changed'   => REQUEST_TIME,
232
      'promote'   => NODE_NOT_PROMOTED,
233 234
      'revision'  => 1,
      'log'       => '',
235 236
      'status'    => NODE_PUBLISHED,
      'sticky'    => NODE_NOT_STICKY,
237
      'type'      => 'page',
238
      'langcode'  => LANGUAGE_NOT_SPECIFIED,
239
    );
240

241 242 243 244 245 246 247
    // Add in comment settings for nodes.
    if (module_exists('comment')) {
      $settings += array(
        'comment' => COMMENT_NODE_OPEN,
      );
    }

248 249 250
    // Use the original node's created time for existing nodes.
    if (isset($settings['created']) && !isset($settings['date'])) {
      $settings['date'] = format_date($settings['created'], 'custom', 'Y-m-d H:i:s O');
251
    }
252 253 254 255 256 257 258 259 260 261 262 263 264

    // If the node's user uid is not specified manually, use the currently
    // logged in user if available, or else the user running the test.
    if (!isset($settings['uid'])) {
      if ($this->loggedInUser) {
        $settings['uid'] = $this->loggedInUser->uid;
      }
      else {
        global $user;
        $settings['uid'] = $user->uid;
      }
    }

265 266 267
    // Merge body field value and format separately.
    $body = array(
      'value' => $this->randomName(32),
268
      'format' => filter_default_format(),
269
    );
270 271 272
    if (empty($settings['body'][$settings['langcode']])) {
      $settings['body'][$settings['langcode']][0] = array();
    }
273
    $settings['body'][$settings['langcode']][0] += $body;
274

275
    $node = entity_create('node', $settings);
276 277 278
    if (!empty($settings['revision'])) {
      $node->setNewRevision();
    }
279
    $node->save();
280

281
    // Small hack to link revisions to our test user.
282 283 284 285
    db_update('node_revision')
      ->fields(array('uid' => $node->uid))
      ->condition('vid', $node->vid)
      ->execute();
286 287 288 289 290 291
    return $node;
  }

  /**
   * Creates a custom content type based on default settings.
   *
292
   * @param $settings
293 294
   *   An array of settings to change from the defaults.
   *   Example: 'type' => 'foo'.
295 296
   * @return
   *   Created content type.
297
   */
298
  protected function drupalCreateContentType($settings = array()) {
299
    // Find a non-existent random type name.
300
    do {
301
      $name = strtolower($this->randomName(8));
302
    } while (node_type_load($name));
303

304
    // Populate defaults array.
305 306 307
    $defaults = array(
      'type' => $name,
      'name' => $name,
308
      'base' => 'node_content',
309 310 311 312 313 314 315
      'description' => '',
      'help' => '',
      'title_label' => 'Title',
      'body_label' => 'Body',
      'has_title' => 1,
      'has_body' => 1,
    );
316
    // Imposed values for a custom type.
317 318 319 320 321 322 323 324 325
    $forced = array(
      'orig_type' => '',
      'old_type' => '',
      'module' => 'node',
      'custom' => 1,
      'modified' => 1,
      'locked' => 0,
    );
    $type = $forced + $settings + $defaults;
326
    $type = (object) $type;
327

328
    $saved_type = node_type_save($type);
329
    node_types_rebuild();
330
    menu_router_rebuild();
331
    node_add_body_field($type);
332

333
    $this->assertEqual($saved_type, SAVED_NEW, t('Created content type %type.', array('%type' => $type->type)));
334

335 336 337
    // Reset permissions so that permissions for this content type are available.
    $this->checkPermissions(array(), TRUE);

338 339 340 341 342 343
    return $type;
  }

  /**
   * Get a list files that can be used in tests.
   *
344 345 346 347 348 349
   * @param $type
   *   File type, possible values: 'binary', 'html', 'image', 'javascript', 'php', 'sql', 'text'.
   * @param $size
   *   File size in bytes to match. Please check the tests/files folder.
   * @return
   *   List of files that match filter.
350
   */
351
  protected function drupalGetTestFiles($type, $size = NULL) {
352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370
    if (empty($this->generatedTestFiles)) {
      // Generate binary test files.
      $lines = array(64, 1024);
      $count = 0;
      foreach ($lines as $line) {
        simpletest_generate_file('binary-' . $count++, 64, $line, 'binary');
      }

      // Generate text test files.
      $lines = array(16, 256, 1024, 2048, 20480);
      $count = 0;
      foreach ($lines as $line) {
        simpletest_generate_file('text-' . $count++, 64, $line);
      }

      // Copy other test files from simpletest.
      $original = drupal_get_path('module', 'simpletest') . '/files';
      $files = file_scan_directory($original, '/(html|image|javascript|php|sql)-.*/');
      foreach ($files as $file) {
371
        file_unmanaged_copy($file->uri, variable_get('file_public_path', conf_path() . '/files'));
372
      }
373

374 375 376 377
      $this->generatedTestFiles = TRUE;
    }

    $files = array();
378 379
    // Make sure type is valid.
    if (in_array($type, array('binary', 'html', 'image', 'javascript', 'php', 'sql', 'text'))) {
380
      $files = file_scan_directory('public://', '/' . $type . '\-.*/');
381 382 383 384

      // If size is set then remove any files that are not of that size.
      if ($size !== NULL) {
        foreach ($files as $file) {
385
          $stats = stat($file->uri);
386
          if ($stats['size'] != $size) {
387
            unset($files[$file->uri]);
388 389 390 391 392 393 394 395 396
          }
        }
      }
    }
    usort($files, array($this, 'drupalCompareFiles'));
    return $files;
  }

  /**
397
   * Compare two files based on size and file name.
398
   */
399
  protected function drupalCompareFiles($file1, $file2) {
400
    $compare_size = filesize($file1->uri) - filesize($file2->uri);
401 402 403
    if ($compare_size) {
      // Sort by file size.
      return $compare_size;
404 405
    }
    else {
406 407
      // The files were the same size, so sort alphabetically.
      return strnatcmp($file1->name, $file2->name);
408 409 410 411
    }
  }

  /**
412
   * Create a user with a given set of permissions.
413
   *
414 415 416 417 418
   * @param array $permissions
   *   Array of permission names to assign to user. Note that the user always
   *   has the default permissions derived from the "authenticated users" role.
   *
   * @return object|false
419
   *   A fully loaded user object with pass_raw property, or FALSE if account
420 421
   *   creation fails.
   */
422 423 424 425 426 427 428 429
  protected function drupalCreateUser(array $permissions = array()) {
    // Create a role with the given permission set, if any.
    $rid = FALSE;
    if ($permissions) {
      $rid = $this->drupalCreateRole($permissions);
      if (!$rid) {
        return FALSE;
      }
430 431 432 433 434
    }

    // Create a user assigned to that role.
    $edit = array();
    $edit['name']   = $this->randomName();
435
    $edit['mail']   = $edit['name'] . '@example.com';
436 437
    $edit['pass']   = user_password();
    $edit['status'] = 1;
438 439 440
    if ($rid) {
      $edit['roles'] = array($rid => $rid);
    }
441

442 443
    $account = entity_create('user', $edit);
    $account->save();
444 445 446 447 448 449 450 451 452 453 454 455 456 457

    $this->assertTrue(!empty($account->uid), t('User created with name %name and pass %pass', array('%name' => $edit['name'], '%pass' => $edit['pass'])), t('User login'));
    if (empty($account->uid)) {
      return FALSE;
    }

    // Add the raw password so that we can log in as this user.
    $account->pass_raw = $edit['pass'];
    return $account;
  }

  /**
   * Internal helper function; Create a role with specified permissions.
   *
458
   * @param array $permissions
459
   *   Array of permission names to assign to role.
460 461 462 463 464 465
   * @param string $rid
   *   (optional) The role ID (machine name). Defaults to a random name.
   * @param string $name
   *   (optional) The label for the role. Defaults to a random string.
   *
   * @return string
466
   *   Role ID of newly created role, or FALSE if role creation failed.
467
   */
468 469 470 471 472 473 474 475
  protected function drupalCreateRole(array $permissions, $rid = NULL, $name = NULL) {
    // Generate a random, lowercase machine name if none was passed.
    if (!isset($rid)) {
      $rid = strtolower($this->randomName(8));
    }
    // Generate a random label.
    if (!isset($name)) {
      $name = $this->randomString(8);
476 477
    }

478
    // Check the all the permissions strings are valid.
479 480 481 482
    if (!$this->checkPermissions($permissions)) {
      return FALSE;
    }

483
    // Create new role.
484
    $role = new stdClass();
485
    $role->rid = $rid;
486
    $role->name = $name;
487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507
    $result = user_role_save($role);

    $this->assertIdentical($result, SAVED_NEW, t('Created role ID @rid with name @name.', array(
      '@name' => var_export($role->name, TRUE),
      '@rid' => var_export($role->rid, TRUE),
    )), t('Role'));

    if ($result === SAVED_NEW) {
      // Grant the specified permissions to the role, if any.
      if (!empty($permissions)) {
        user_role_grant_permissions($role->rid, $permissions);

        $assigned_permissions = db_query('SELECT permission FROM {role_permission} WHERE rid = :rid', array(':rid' => $role->rid))->fetchCol();
        $missing_permissions = array_diff($permissions, $assigned_permissions);
        if (!$missing_permissions) {
          $this->pass(t('Created permissions: @perms', array('@perms' => implode(', ', $permissions))), t('Role'));
        }
        else {
          $this->fail(t('Failed to create permissions: @perms', array('@perms' => implode(', ', $missing_permissions))), t('Role'));
        }
      }
508 509 510 511 512 513 514
      return $role->rid;
    }
    else {
      return FALSE;
    }
  }

515 516 517
  /**
   * Check to make sure that the array of permissions are valid.
   *
518 519 520 521 522 523
   * @param $permissions
   *   Permissions to check.
   * @param $reset
   *   Reset cached available permissions.
   * @return
   *   TRUE or FALSE depending on whether the permissions are valid.
524
   */
525
  protected function checkPermissions(array $permissions, $reset = FALSE) {
526
    $available = &drupal_static(__FUNCTION__);
527 528

    if (!isset($available) || $reset) {
529
      $available = array_keys(module_invoke_all('permission'));
530 531 532 533 534 535 536 537 538 539 540 541
    }

    $valid = TRUE;
    foreach ($permissions as $permission) {
      if (!in_array($permission, $available)) {
        $this->fail(t('Invalid permission %permission.', array('%permission' => $permission)), t('Role'));
        $valid = FALSE;
      }
    }
    return $valid;
  }

542
  /**
543 544 545 546 547
   * Log in a user with the internal browser.
   *
   * If a user is already logged in, then the current user is logged out before
   * logging in the specified user.
   *
548
   * Please note that neither the global $user nor the passed-in user object is
549 550 551
   * populated with data of the logged in user. If you need full access to the
   * user object after logging in, it must be updated manually. If you also need
   * access to the plain-text password of the user (set by drupalCreateUser()),
552
   * e.g. to log in the same user again, then it must be re-assigned manually.
553 554 555 556 557 558 559 560 561 562
   * For example:
   * @code
   *   // Create a user.
   *   $account = $this->drupalCreateUser(array());
   *   $this->drupalLogin($account);
   *   // Load real user object.
   *   $pass_raw = $account->pass_raw;
   *   $account = user_load($account->uid);
   *   $account->pass_raw = $pass_raw;
   * @endcode
563
   *
564
   * @param $user
565
   *   User object representing the user to log in.
566 567
   *
   * @see drupalCreateUser()
568
   */
569
  protected function drupalLogin($user) {
570
    if ($this->loggedInUser) {
571 572 573 574 575 576 577 578 579
      $this->drupalLogout();
    }

    $edit = array(
      'name' => $user->name,
      'pass' => $user->pass_raw
    );
    $this->drupalPost('user', $edit, t('Log in'));

580 581 582
    // If a "log out" link appears on the page, it is almost certainly because
    // the login was successful.
    $pass = $this->assertLink(t('Log out'), 0, t('User %name successfully logged in.', array('%name' => $user->name)), t('User login'));
583

584 585 586
    if ($pass) {
      $this->loggedInUser = $user;
    }
587 588
  }

589 590 591 592 593
  /**
   * Generate a token for the currently logged in user.
   */
  protected function drupalGetToken($value = '') {
    $private_key = drupal_get_private_key();
594
    return drupal_hmac_base64($value, $this->session_id . $private_key);
595 596
  }

597 598 599
  /*
   * Logs a user out of the internal browser, then check the login page to confirm logout.
   */
600
  protected function drupalLogout() {
601 602 603
    // Make a request to the logout page, and redirect to the user page, the
    // idea being if you were properly logged out you should be seeing a login
    // screen.
604 605
    $this->drupalGet('user/logout', array('query' => array('destination' => 'user')));
    $this->assertResponse(200, t('User was logged out.'));
606 607 608
    $pass = $this->assertField('name', t('Username field found.'), t('Logout'));
    $pass = $pass && $this->assertField('pass', t('Password field found.'), t('Logout'));

609 610 611
    if ($pass) {
      $this->loggedInUser = FALSE;
    }
612 613
  }

614 615 616 617
  /**
   * Sets up a Drupal site for running functional and integration tests.
   *
   * Generates a random database prefix and installs Drupal with the specified
618 619 620
   * installation profile in Drupal\simpletest\WebTestBase::$profile into the
   * prefixed database. Afterwards, installs any additional modules specified by
   * the test.
621 622 623 624 625 626 627 628 629 630
   *
   * After installation all caches are flushed and several configuration values
   * are reset to the values of the parent site executing the test, since the
   * default values may be incompatible with the environment in which tests are
   * being executed.
   *
   * @param ...
   *   List of modules to enable for the duration of the test. This can be
   *   either a single array or a variable number of string arguments.
   *
631 632 633
   * @see Drupal\simpletest\WebTestBase::prepareDatabasePrefix()
   * @see Drupal\simpletest\WebTestBase::changeDatabasePrefix()
   * @see Drupal\simpletest\WebTestBase::prepareEnvironment()
634 635
   */
  protected function setUp() {
636
    global $user, $conf;
637

638 639 640 641 642 643 644
    // When running tests through the Simpletest UI (vs. on the command line),
    // Simpletest's batch conflicts with the installer's batch. Batch API does
    // not support the concept of nested batches (in which the nested is not
    // progressive), so we need to temporarily pretend there was no batch.
    // Backup the currently running Simpletest batch.
    $this->originalBatch = batch_get();

645 646 647
    // Create the database prefix for this test.
    $this->prepareDatabasePrefix();

648 649
    // Prepare the environment for running tests.
    $this->prepareEnvironment();
650 651 652
    if (!$this->setupEnvironment) {
      return FALSE;
    }
653

654 655 656 657 658 659 660 661 662
    // Reset all statics and variables to perform tests in a clean environment.
    $conf = array();
    drupal_static_reset();

    // Change the database prefix.
    // All static variables need to be reset before the database prefix is
    // changed, since Drupal\Core\Utility\CacheArray implementations attempt to
    // write back to persistent caches when they are destructed.
    $this->changeDatabasePrefix();
663 664 665
    if (!$this->setupDatabasePrefix) {
      return FALSE;
    }
666

667 668 669 670 671 672 673
    // Set the 'simpletest_parent_profile' variable to add the parent profile's
    // search path to the child site's search paths.
    // @see drupal_system_listing()
    $conf['simpletest_parent_profile'] = $this->originalProfile;

    // Set installer parameters.
    // @see install.php, install.core.inc
674
    $connection_info = Database::getConnectionInfo();
675 676 677 678 679 680 681 682 683 684 685 686
    $this->root_user = (object) array(
      'name' => 'admin',
      'mail' => 'admin@example.com',
      'pass_raw' => $this->randomName(),
    );
    $settings = array(
      'interactive' => FALSE,
      'parameters' => array(
        'profile' => $this->profile,
        'langcode' => 'en',
      ),
      'forms' => array(
687
        'install_settings_form' => $connection_info['default'],
688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719
        'install_configure_form' => array(
          'site_name' => 'Drupal',
          'site_mail' => 'simpletest@example.com',
          'account' => array(
            'name' => $this->root_user->name,
            'mail' => $this->root_user->mail,
            'pass' => array(
              'pass1' => $this->root_user->pass_raw,
              'pass2' => $this->root_user->pass_raw,
            ),
          ),
          // form_type_checkboxes_value() requires NULL instead of FALSE values
          // for programmatic form submissions to disable a checkbox.
          'update_status_module' => array(
            1 => NULL,
            2 => NULL,
          ),
        ),
      ),
    );

    // Replace the global $user session with an anonymous user to resemble a
    // regular installation.
    $user = drupal_anonymous_user();

    // Reset the static batch to remove Simpletest's batch operations.
    $batch = &batch_get();
    $batch = array();

    // Execute the non-interactive installer.
    require_once DRUPAL_ROOT . '/core/includes/install.core.inc';
    install_drupal($settings);
720
    $this->rebuildContainer();
721 722 723 724

    // Restore the original Simpletest batch.
    $batch = &batch_get();
    $batch = $this->originalBatch;
725

726 727 728
    // Revert install_begin_request() cache and lock service overrides.
    unset($conf['cache_classes']);
    unset($conf['lock_backend']);
729

730
    // Set path variables.
731 732 733
    variable_set('file_public_path', $this->public_files_directory);
    variable_set('file_private_path', $this->private_files_directory);
    variable_set('file_temporary_path', $this->temp_files_directory);
734
    variable_set('locale_translate_file_directory', $this->translation_files_directory);
735

736
    // Set 'parent_profile' of simpletest to add the parent profile's
737 738
    // search path to the child site's search paths.
    // @see drupal_system_listing()
739
    config('simpletest.settings')->set('parent_profile', $this->originalProfile)->save();
740

741 742
    // Collect modules to install.
    $class = get_class($this);
743
    $modules = array();
744 745 746 747 748 749
    while ($class) {
      if (property_exists($class, 'modules')) {
        $modules = array_merge($modules, $class::$modules);
      }
      $class = get_parent_class($class);
    }
750
    if ($modules) {
751 752
      $success = module_enable($modules, TRUE);
      $this->assertTrue($success, t('Enabled modules: %modules', array('%modules' => implode(', ', $modules))));
753
      $this->rebuildContainer();
754
    }
755

756 757
    // Reset/rebuild all data structures after enabling the modules.
    $this->resetAll();
758

759
    // Use the test mail class instead of the default mail handler class.
760
    variable_set('mail_system', array('default-system' => 'Drupal\Core\Mail\VariableLog'));
761

762
    drupal_set_time_limit($this->timeLimit);
763
    $this->setup = TRUE;
764 765
  }

766 767 768
  /**
   * Reset all data structures after having enabled new modules.
   *
769
   * This method is called by Drupal\simpletest\WebTestBase::setUp() after enabling
770 771 772 773
   * the requested modules. It must be called again when additional modules
   * are enabled later.
   */
  protected function resetAll() {
774
    // Clear all database and static caches and rebuild data structures.
775 776 777 778 779 780 781
    drupal_flush_all_caches();

    // Reload global $conf array and permissions.
    $this->refreshVariables();
    $this->checkPermissions(array(), TRUE);
  }

782 783 784 785 786 787 788 789 790 791 792 793
  /**
   * Refresh the in-memory set of variables. Useful after a page request is made
   * that changes a variable in a different thread.
   *
   * In other words calling a settings page with $this->drupalPost() with a changed
   * value would update a variable to reflect that change, but in the thread that
   * made the call (thread running the test) the changed variable would not be
   * picked up.
   *
   * This method clears the variables cache and loads a fresh copy from the database
   * to ensure that the most up-to-date set of variables is loaded.
   */
794
  protected function refreshVariables() {
795
    global $conf;
796
    cache('bootstrap')->delete('variables');
797
    $conf = variable_initialize();
798 799
  }

800 801 802 803
  /**
   * Delete created files and temporary files directory, delete the tables created by setUp(),
   * and reset the database prefix.
   */
804
  protected function tearDown() {
805 806 807 808
    // Destroy the testing kernel.
    if (isset($this->kernel)) {
      $this->kernel->shutdown();
    }
809
    parent::tearDown();
810

811 812 813
    // Ensure that internal logged in variable and cURL options are reset.
    $this->loggedInUser = FALSE;
    $this->additionalCurlOptions = array();
814

815 816
    // Reload module list and implementations to ensure that test module hooks
    // aren't called after tests.
817
    system_list_reset();
818
    module_list_reset();
819
    module_implements_reset();
820

821 822
    // Reset the Field API.
    field_cache_clear();
823

824 825
    // Rebuild caches.
    $this->refreshVariables();
826

827 828
    // Close the CURL handler.
    $this->curlClose();
829 830 831
  }

  /**
832
   * Initializes the cURL connection.
833
   *
834 835 836 837
   * If the simpletest_httpauth_credentials variable is set, this function will
   * add HTTP authentication headers. This is necessary for testing sites that
   * are protected by login credentials from public access.
   * See the description of $curl_options for other options.
838
   */
839
  protected function curlInitialize() {
840
    global $base_url;
841

842 843
    if (!isset($this->curlHandle)) {
      $this->curlHandle = curl_init();
844 845 846 847 848 849 850

      // Some versions/configurations of cURL break on a NULL cookie jar, so
      // supply a real file.
      if (empty($this->cookieFile)) {
        $this->cookieFile = $this->public_files_directory . '/cookie.jar';
      }

851
      $curl_options = array(
852
        CURLOPT_COOKIEJAR => $this->cookieFile,
853
        CURLOPT_URL => $base_url,
854
        CURLOPT_FOLLOWLOCATION => FALSE,
855
        CURLOPT_RETURNTRANSFER => TRUE,
856 857
        CURLOPT_SSL_VERIFYPEER => FALSE, // Required to make the tests run on HTTPS.
        CURLOPT_SSL_VERIFYHOST => FALSE, // Required to make the tests run on HTTPS.
858
        CURLOPT_HEADERFUNCTION => array(&$this, 'curlHeaderCallback'),
859
        CURLOPT_USERAGENT => $this->databasePrefix,
860
      );
861
      if (isset($this->httpauth_credentials)) {
862
        $curl_options[CURLOPT_HTTPAUTH] = $this->httpauth_method;
863
        $curl_options[CURLOPT_USERPWD] = $this->httpauth_credentials;
864
      }
865 866 867 868 869 870
      // curl_setopt_array() returns FALSE if any of the specified options
      // cannot be set, and stops processing any further options.
      $result = curl_setopt_array($this->curlHandle, $this->additionalCurlOptions + $curl_options);
      if (!$result) {
        throw new \UnexpectedValueException('One or more cURL options could not be set.');
      }
871 872 873

      // By default, the child session name should be the same as the parent.
      $this->session_name = session_name();
874
    }
875 876
    // We set the user agent header on each request so as to use the current
    // time and a new uniqid.
877
    if (preg_match('/simpletest\d+/', $this->databasePrefix, $matches)) {
878 879
      curl_setopt($this->curlHandle, CURLOPT_USERAGENT, drupal_generate_test_ua($matches[0]));
    }
880 881 882
  }

  /**
883
   * Initializes and executes a cURL request.
884
   *
885
   * @param $curl_options
886 887 888
   *   An associative array of cURL options to set, where the keys are constants
   *   defined by the cURL library. For a list of valid options, see
   *   http://www.php.net/manual/function.curl-setopt.php
889
   * @param $redirect
890 891 892
   *   FALSE if this is an initial request, TRUE if this request is the result
   *   of a redirect.
   *
893
   * @return
894 895 896
   *   The content returned from the call to curl_exec().
   *
   * @see curlInitialize()
897
   */
898
  protected function curlExec($curl_options, $redirect = FALSE) {
899
    $this->curlInitialize();
900 901 902 903 904 905 906 907 908 909 910

    // cURL incorrectly handles URLs with a fragment by including the
    // fragment in the request to the server, causing some web servers
    // to reject the request citing "400 - Bad Request". To prevent
    // this, we strip the fragment from the request.
    // TODO: Remove this for Drupal 8, since fixed in curl 7.20.0.
    if (!empty($curl_options[CURLOPT_URL]) && strpos($curl_options[CURLOPT_URL], '#')) {
      $original_url = $curl_options[CURLOPT_URL];
      $curl_options[CURLOPT_URL] = strtok($curl_options[CURLOPT_URL], '#');
    }

911
    $url = empty($curl_options[CURLOPT_URL]) ? curl_getinfo($this->curlHandle, CURLINFO_EFFECTIVE_URL) : $curl_options[CURLOPT_URL];
912

913 914 915 916 917 918 919 920
    if (!empty($curl_options[CURLOPT_POST])) {
      // This is a fix for the Curl library to prevent Expect: 100-continue
      // headers in POST requests, that may cause unexpected HTTP response
      // codes from some webservers (like lighttpd that returns a 417 error
      // code). It is done by setting an empty "Expect" header field that is
      // not overwritten by Curl.
      $curl_options[CURLOPT_HTTPHEADER][] = 'Expect:';
    }
921
    curl_setopt_array($this->curlHandle, $this->additionalCurlOptions + $curl_options);
922

923 924 925 926 927 928 929 930 931 932 933 934 935 936 937
    if (!$redirect) {
      // Reset headers, the session ID and the redirect counter.
      $this->session_id = NULL;
      $this->headers = array();
      $this->redirect_count = 0;
    }

    $content = curl_exec($this->curlHandle);
    $status = curl_getinfo($this->curlHandle, CURLINFO_HTTP_CODE);

    // cURL incorrectly handles URLs with fragments, so instead of
    // letting cURL handle redirects we take of them ourselves to
    // to prevent fragments being sent to the web server as part
    // of the request.
    // TODO: Remove this for Drupal 8, since fixed in curl 7.20.0.
938
    if (in_array($status, array(300, 301, 302, 303, 305, 307)) && $this->redirect_count < $this->maximumRedirects) {
939 940 941 942 943 944 945 946
      if ($this->drupalGetHeader('location')) {
        $this->redirect_count++;
        $curl_options = array();
        $curl_options[CURLOPT_URL] = $this->drupalGetHeader('location');
        $curl_options[CURLOPT_HTTPGET] = TRUE;
        return $this->curlExec($curl_options, TRUE);
      }
    }
947

948
    $this->drupalSetContent($content, isset($original_url) ? $original_url : curl_getinfo($this->curlHandle, CURLINFO_EFFECTIVE_URL));
949 950
    $message_vars = array(
      '!method' => !empty($curl_options[CURLOPT_NOBODY]) ? 'HEAD' : (empty($curl_options[CURLOPT_POSTFIELDS]) ? 'GET' : 'POST'),
951 952
      '@url' => isset($original_url) ? $original_url : $url,
      '@status' => $status,
953
      '!length' => format_size(strlen($this->drupalGetContent()))
954 955
    );
    $message = t('!method @url returned @status (!length).', $message_vars);
956
    $this->assertTrue($this->drupalGetContent() !== FALSE, $message, t('Browser'));
957
    return $this->drupalGetContent();
958 959
  }

960 961 962 963 964
  /**
   * Reads headers and registers errors received from the tested site.
   *
   * @see _drupal_log_error().
   *
965 966 967 968
   * @param $curlHandler
   *   The cURL handler.
   * @param $header
   *   An header.
969
   */
970
  protected function curlHeaderCallback($curlHandler, $header) {
971 972 973 974 975 976 977 978 979 980
    // Header fields can be extended over multiple lines by preceding each
    // extra line with at least one SP or HT. They should be joined on receive.
    // Details are in RFC2616 section 4.
    if ($header[0] == ' ' || $header[0] == "\t") {
      // Normalize whitespace between chucks.
      $this->headers[] = array_pop($this->headers) . ' ' . trim($header);
    }
    else {
      $this->headers[] = $header;
    }
981

982 983
    // Errors are being sent via X-Drupal-Assertion-* headers,
    // generated by _drupal_log_error() in the exact form required
984
    // by Drupal\simpletest\WebTestBase::error().
985
    if (preg_match('/^X-Drupal-Assertion-[0-9]+: (.*)$/', $header, $matches)) {
986
      // Call Drupal\simpletest\WebTestBase::error() with the parameters from the header.
987 988
      call_user_func_array(array(&$this, 'error'), unserialize(urldecode($matches[1])));
    }
989

990 991 992 993 994 995 996 997 998 999 1000 1001 1002
    // Save cookies.
    if (preg_match('/^Set-Cookie: ([^=]+)=(.+)/', $header, $matches)) {
      $name = $matches[1];
      $parts = array_map('trim', explode(';', $matches[2]));
      $value = array_shift($parts);
      $this->cookies[$name] = array('value' => $value, 'secure' => in_array('secure', $parts));
      if ($name == $this->session_name) {
        if ($value != 'deleted') {
          $this->session_id = $value;
        }
        else {
          $this->session_id = NULL;
        }
1003 1004 1005
      }
    }

1006 1007 1008 1009
    // This is required by cURL.
    return strlen($header);
  }

1010 1011 1012 1013
  /**
   * Close the cURL handler and unset the handler.
   */
  protected function curlClose() {
1014 1015 1016
    if (isset($this->curlHandle)) {
      curl_close($this->curlHandle);
      unset($this->curlHandle);
1017 1018 1019 1020
    }
  }

  /**
1021
   * Parse content returned from curlExec using DOM and SimpleXML.
1022
   *
1023 1024
   * @return
   *   A SimpleXMLElement or FALSE on failure.