WebTestBase.php 96.1 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 number of redirects followed during the handling of a request.
   */
  protected $redirect_count;

147 148 149 150 151
  /**
   * The kernel used in this test.
   */
  protected $kernel;

152
  /**
153
   * Constructor for Drupal\simpletest\WebTestBase.
154 155 156 157 158 159
   */
  function __construct($test_id = NULL) {
    parent::__construct($test_id);
    $this->skipClasses[__CLASS__] = TRUE;
  }

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

181 182 183
  /**
   * Creates a node based on default settings.
   *
184 185
   * @param $settings
   *   An associative array of settings to change from the defaults, keys are
186
   *   node properties, for example 'title' => 'Hello, world!'.
187
   * @return
188
   *   Created node entity.
189
   */
190
  protected function drupalCreateNode($settings = array()) {
191
    // Populate defaults array.
192
    $settings += array(
193
      'body'      => array(LANGUAGE_NOT_SPECIFIED => array(array())),
194
      'title'     => $this->randomName(8),
195
      'comment'   => 2,
196
      'changed'   => REQUEST_TIME,
197 198 199 200 201 202 203 204
      'moderate'  => 0,
      'promote'   => 0,
      'revision'  => 1,
      'log'       => '',
      'status'    => 1,
      'sticky'    => 0,
      'type'      => 'page',
      'revisions' => NULL,
205
      'langcode'  => LANGUAGE_NOT_SPECIFIED,
206
    );
207 208 209 210

    // 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');
211
    }
212 213 214 215 216 217 218 219 220 221 222 223 224

    // 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;
      }
    }

225 226 227
    // Merge body field value and format separately.
    $body = array(
      'value' => $this->randomName(32),
228
      'format' => filter_default_format(),
229
    );
230
    $settings['body'][$settings['langcode']][0] += $body;
231

232 233
    $node = entity_create('node', $settings);
    $node->save();
234

235
    // Small hack to link revisions to our test user.
236 237 238 239
    db_update('node_revision')
      ->fields(array('uid' => $node->uid))
      ->condition('vid', $node->vid)
      ->execute();
240 241 242 243 244 245
    return $node;
  }

  /**
   * Creates a custom content type based on default settings.
   *
246
   * @param $settings
247 248
   *   An array of settings to change from the defaults.
   *   Example: 'type' => 'foo'.
249 250
   * @return
   *   Created content type.
251
   */
252
  protected function drupalCreateContentType($settings = array()) {
253
    // Find a non-existent random type name.
254
    do {
255
      $name = strtolower($this->randomName(8));
256
    } while (node_type_load($name));
257

258
    // Populate defaults array.
259 260 261
    $defaults = array(
      'type' => $name,
      'name' => $name,
262
      'base' => 'node_content',
263 264 265 266 267 268 269
      'description' => '',
      'help' => '',
      'title_label' => 'Title',
      'body_label' => 'Body',
      'has_title' => 1,
      'has_body' => 1,
    );
270
    // Imposed values for a custom type.
271 272 273 274 275 276 277 278 279
    $forced = array(
      'orig_type' => '',
      'old_type' => '',
      'module' => 'node',
      'custom' => 1,
      'modified' => 1,
      'locked' => 0,
    );
    $type = $forced + $settings + $defaults;
280
    $type = (object) $type;
281

282
    $saved_type = node_type_save($type);
283
    node_types_rebuild();
284
    menu_router_rebuild();
285
    node_add_body_field($type);
286

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

289 290 291
    // Reset permissions so that permissions for this content type are available.
    $this->checkPermissions(array(), TRUE);

292 293 294 295 296 297
    return $type;
  }

  /**
   * Get a list files that can be used in tests.
   *
298 299 300 301 302 303
   * @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.
304
   */
305
  protected function drupalGetTestFiles($type, $size = NULL) {
306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324
    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) {
325
        file_unmanaged_copy($file->uri, variable_get('file_public_path', conf_path() . '/files'));
326
      }
327

328 329 330 331
      $this->generatedTestFiles = TRUE;
    }

    $files = array();
332 333
    // Make sure type is valid.
    if (in_array($type, array('binary', 'html', 'image', 'javascript', 'php', 'sql', 'text'))) {
334
      $files = file_scan_directory('public://', '/' . $type . '\-.*/');
335 336 337 338

      // If size is set then remove any files that are not of that size.
      if ($size !== NULL) {
        foreach ($files as $file) {
339
          $stats = stat($file->uri);
340
          if ($stats['size'] != $size) {
341
            unset($files[$file->uri]);
342 343 344 345 346 347 348 349 350
          }
        }
      }
    }
    usort($files, array($this, 'drupalCompareFiles'));
    return $files;
  }

  /**
351
   * Compare two files based on size and file name.
352
   */
353
  protected function drupalCompareFiles($file1, $file2) {
354
    $compare_size = filesize($file1->uri) - filesize($file2->uri);
355 356 357
    if ($compare_size) {
      // Sort by file size.
      return $compare_size;
358 359
    }
    else {
360 361
      // The files were the same size, so sort alphabetically.
      return strnatcmp($file1->name, $file2->name);
362 363 364 365
    }
  }

  /**
366
   * Create a user with a given set of permissions.
367
   *
368 369 370 371 372
   * @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
373
   *   A fully loaded user object with pass_raw property, or FALSE if account
374 375
   *   creation fails.
   */
376 377 378 379 380 381 382 383
  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;
      }
384 385 386 387 388
    }

    // Create a user assigned to that role.
    $edit = array();
    $edit['name']   = $this->randomName();
389
    $edit['mail']   = $edit['name'] . '@example.com';
390 391
    $edit['pass']   = user_password();
    $edit['status'] = 1;
392 393 394
    if ($rid) {
      $edit['roles'] = array($rid => $rid);
    }
395

396 397
    $account = entity_create('user', $edit);
    $account->save();
398 399 400 401 402 403 404 405 406 407 408 409 410 411

    $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.
   *
412
   * @param array $permissions
413
   *   Array of permission names to assign to role.
414 415 416 417 418 419
   * @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
420
   *   Role ID of newly created role, or FALSE if role creation failed.
421
   */
422 423 424 425 426 427 428 429
  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);
430 431
    }

432
    // Check the all the permissions strings are valid.
433 434 435 436
    if (!$this->checkPermissions($permissions)) {
      return FALSE;
    }

437
    // Create new role.
438
    $role = new stdClass();
439
    $role->rid = $rid;
440
    $role->name = $name;
441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461
    $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'));
        }
      }
462 463 464 465 466 467 468
      return $role->rid;
    }
    else {
      return FALSE;
    }
  }

469 470 471
  /**
   * Check to make sure that the array of permissions are valid.
   *
472 473 474 475 476 477
   * @param $permissions
   *   Permissions to check.
   * @param $reset
   *   Reset cached available permissions.
   * @return
   *   TRUE or FALSE depending on whether the permissions are valid.
478
   */
479
  protected function checkPermissions(array $permissions, $reset = FALSE) {
480
    $available = &drupal_static(__FUNCTION__);
481 482

    if (!isset($available) || $reset) {
483
      $available = array_keys(module_invoke_all('permission'));
484 485 486 487 488 489 490 491 492 493 494 495
    }

    $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;
  }

496
  /**
497 498 499 500 501
   * 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.
   *
502
   * Please note that neither the global $user nor the passed-in user object is
503 504 505
   * 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()),
506
   * e.g. to log in the same user again, then it must be re-assigned manually.
507 508 509 510 511 512 513 514 515 516
   * 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
517
   *
518
   * @param $user
519
   *   User object representing the user to log in.
520 521
   *
   * @see drupalCreateUser()
522
   */
523
  protected function drupalLogin($user) {
524
    if ($this->loggedInUser) {
525 526 527 528 529 530 531 532 533
      $this->drupalLogout();
    }

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

534 535 536
    // 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'));
537

538 539 540
    if ($pass) {
      $this->loggedInUser = $user;
    }
541 542
  }

543 544 545 546 547
  /**
   * Generate a token for the currently logged in user.
   */
  protected function drupalGetToken($value = '') {
    $private_key = drupal_get_private_key();
548
    return drupal_hmac_base64($value, $this->session_id . $private_key);
549 550
  }

551 552 553
  /*
   * Logs a user out of the internal browser, then check the login page to confirm logout.
   */
554
  protected function drupalLogout() {
555 556 557
    // 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.
558 559
    $this->drupalGet('user/logout', array('query' => array('destination' => 'user')));
    $this->assertResponse(200, t('User was logged out.'));
560 561 562
    $pass = $this->assertField('name', t('Username field found.'), t('Logout'));
    $pass = $pass && $this->assertField('pass', t('Password field found.'), t('Logout'));

563 564 565
    if ($pass) {
      $this->loggedInUser = FALSE;
    }
566 567
  }

568 569 570 571
  /**
   * Sets up a Drupal site for running functional and integration tests.
   *
   * Generates a random database prefix and installs Drupal with the specified
572 573 574
   * installation profile in Drupal\simpletest\WebTestBase::$profile into the
   * prefixed database. Afterwards, installs any additional modules specified by
   * the test.
575 576 577 578 579 580 581 582 583 584
   *
   * 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.
   *
585 586 587
   * @see Drupal\simpletest\WebTestBase::prepareDatabasePrefix()
   * @see Drupal\simpletest\WebTestBase::changeDatabasePrefix()
   * @see Drupal\simpletest\WebTestBase::prepareEnvironment()
588 589
   */
  protected function setUp() {
590
    global $user, $conf;
591
    $language_interface = language(LANGUAGE_TYPE_INTERFACE);
592 593 594 595

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

596 597
    // Prepare the environment for running tests.
    $this->prepareEnvironment();
598 599 600
    if (!$this->setupEnvironment) {
      return FALSE;
    }
601

602 603 604 605 606 607 608 609 610
    // 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();
611 612 613
    if (!$this->setupDatabasePrefix) {
      return FALSE;
    }
614

615 616 617 618 619 620 621
    // Preset the 'install_profile' system variable, so the first call into
    // system_rebuild_module_data() (in drupal_install_system()) will register
    // the test's profile as a module. Without this, the installation profile of
    // the parent site (executing the test) is registered, and the test
    // profile's hook_install() and other hook implementations are never invoked.
    $conf['install_profile'] = $this->profile;

622
    // Perform the actual Drupal installation.
623
    include_once DRUPAL_ROOT . '/core/includes/install.inc';
624
    drupal_install_system();
625

626
    // Set path variables.
627 628 629
    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);
630

631
    // Set the 'simpletest_parent_profile' variable to add the parent profile's
632 633 634
    // search path to the child site's search paths.
    // @see drupal_system_listing()
    // @todo This may need to be primed like 'install_profile' above.
635
    variable_set('simpletest_parent_profile', $this->originalProfile);
636

637 638 639
    // Include the testing profile.
    variable_set('install_profile', $this->profile);
    $profile_details = install_profile_info($this->profile, 'en');
640

641
    // Install the modules specified by the testing profile.
642
    module_enable($profile_details['dependencies'], FALSE);
643

644 645
    // Collect modules to install.
    $class = get_class($this);
646
    $modules = array();
647 648 649 650 651 652
    while ($class) {
      if (property_exists($class, 'modules')) {
        $modules = array_merge($modules, $class::$modules);
      }
      $class = get_parent_class($class);
    }
653
    if ($modules) {
654 655
      $success = module_enable($modules, TRUE);
      $this->assertTrue($success, t('Enabled modules: %modules', array('%modules' => implode(', ', $modules))));
656
    }
657

658 659
    // Run the profile tasks.
    $install_profile_module_exists = db_query("SELECT 1 FROM {system} WHERE type = 'module' AND name = :name", array(
660 661
      ':name' => $this->profile,
    ))->fetchField();
662 663 664
    if ($install_profile_module_exists) {
      module_enable(array($this->profile), FALSE);
    }
665

666 667 668 669 670 671 672 673 674 675 676 677
    // Create a new DrupalKernel for testing purposes, now that all required
    // modules have been enabled. This also stores a new dependency injection
    // container in drupal_container(). Drupal\simpletest\TestBase::tearDown()
    // restores the original container.
    // @see Drupal\Core\DrupalKernel::initializeContainer()
    $this->kernel = new DrupalKernel('testing', FALSE);
    // Booting the kernel is necessary to initialize the new DIC. While
    // normally the kernel gets booted on demand in
    // Symfony\Component\HttpKernel\handle(), this kernel needs manual booting
    // as it is not used to handle a request.
    $this->kernel->boot();

678 679
    // Reset/rebuild all data structures after enabling the modules.
    $this->resetAll();
680

681 682 683 684
    // Run cron once in that environment, as install.php does at the end of
    // the installation process.
    drupal_cron_run();

685 686
    // Ensure that the session is not written to the new environment and replace
    // the global $user session with uid 1 from the new test site.
687
    drupal_save_session(FALSE);
688
    // Login as uid 1.
689
    $user = user_load(1);
690

691
    // Restore necessary variables.
692
    variable_set('install_task', 'done');
693
    config('system.site')->set('mail', 'simpletest@example.com')->save();
694
    variable_set('date_default_timezone', date_default_timezone_get());
695

696
    // Set up English language.
697
    unset($conf['language_default']);
698
    $language_interface = language_default();
699

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

703
    drupal_set_time_limit($this->timeLimit);
704
    $this->setup = TRUE;
705 706
  }

707 708 709
  /**
   * Reset all data structures after having enabled new modules.
   *
710
   * This method is called by Drupal\simpletest\WebTestBase::setUp() after enabling
711 712 713 714
   * the requested modules. It must be called again when additional modules
   * are enabled later.
   */
  protected function resetAll() {
715
    // Clear all database and static caches and rebuild data structures.
716 717 718 719 720 721 722
    drupal_flush_all_caches();

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

723 724 725 726 727 728 729 730 731 732 733 734
  /**
   * 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.
   */
735
  protected function refreshVariables() {
736
    global $conf;
737
    cache('bootstrap')->delete('variables');
738
    $conf = variable_initialize();
739 740
  }

741 742 743 744
  /**
   * Delete created files and temporary files directory, delete the tables created by setUp(),
   * and reset the database prefix.
   */
745
  protected function tearDown() {
746 747 748 749 750 751
    // Ensure that TestBase::changeDatabasePrefix() has run and TestBase::$setup
    // was not tricked into TRUE, since the following code would delete the
    // entire parent site otherwise.
    if (!$this->setupDatabasePrefix) {
      return FALSE;
    }
752 753 754 755
    // Destroy the testing kernel.
    if (isset($this->kernel)) {
      $this->kernel->shutdown();
    }
756
    // Remove all prefixed tables.
757 758 759 760 761 762
    $connection_info = Database::getConnectionInfo('default');
    $tables = db_find_tables($connection_info['default']['prefix']['default'] . '%');
    if (empty($tables)) {
      $this->fail('Failed to find test tables to drop.');
    }
    $prefix_length = strlen($connection_info['default']['prefix']['default']);
763
    foreach ($tables as $table) {
764
      if (db_drop_table(substr($table, $prefix_length))) {
765 766 767 768 769
        unset($tables[$table]);
      }
    }
    if (!empty($tables)) {
      $this->fail('Failed to drop all prefixed tables.');
770
    }
771

772
    parent::tearDown();
773

774 775 776
    // Ensure that internal logged in variable and cURL options are reset.
    $this->loggedInUser = FALSE;
    $this->additionalCurlOptions = array();
777

778 779
    // Reload module list and implementations to ensure that test module hooks
    // aren't called after tests.
780
    system_list_reset();
781
    module_list_reset();
782
    module_implements_reset();
783

784 785
    // Reset the Field API.
    field_cache_clear();
786

787 788
    // Rebuild caches.
    $this->refreshVariables();
789

790 791
    // Close the CURL handler.
    $this->curlClose();
792 793 794
  }

  /**
795
   * Initializes the cURL connection.
796
   *
797 798 799 800
   * 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.
801
   */
802
  protected function curlInitialize() {
803
    global $base_url;
804

805 806
    if (!isset($this->curlHandle)) {
      $this->curlHandle = curl_init();
807 808 809 810 811 812 813

      // 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';
      }

814
      $curl_options = array(
815
        CURLOPT_COOKIEJAR => $this->cookieFile,
816
        CURLOPT_URL => $base_url,
817
        CURLOPT_FOLLOWLOCATION => FALSE,
818
        CURLOPT_RETURNTRANSFER => TRUE,
819 820
        CURLOPT_SSL_VERIFYPEER => FALSE, // Required to make the tests run on HTTPS.
        CURLOPT_SSL_VERIFYHOST => FALSE, // Required to make the tests run on HTTPS.
821
        CURLOPT_HEADERFUNCTION => array(&$this, 'curlHeaderCallback'),
822
        CURLOPT_USERAGENT => $this->databasePrefix,
823
      );
824
      if (isset($this->httpauth_credentials)) {
825
        $curl_options[CURLOPT_HTTPAUTH] = $this->httpauth_method;
826
        $curl_options[CURLOPT_USERPWD] = $this->httpauth_credentials;
827
      }
828 829 830 831 832 833
      // 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.');
      }
834 835 836

      // By default, the child session name should be the same as the parent.
      $this->session_name = session_name();
837
    }
838 839
    // We set the user agent header on each request so as to use the current
    // time and a new uniqid.
840
    if (preg_match('/simpletest\d+/', $this->databasePrefix, $matches)) {
841 842
      curl_setopt($this->curlHandle, CURLOPT_USERAGENT, drupal_generate_test_ua($matches[0]));
    }
843 844 845
  }

  /**
846
   * Initializes and executes a cURL request.
847
   *
848
   * @param $curl_options
849 850 851
   *   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
852
   * @param $redirect
853 854 855
   *   FALSE if this is an initial request, TRUE if this request is the result
   *   of a redirect.
   *
856
   * @return
857 858 859
   *   The content returned from the call to curl_exec().
   *
   * @see curlInitialize()
860
   */
861
  protected function curlExec($curl_options, $redirect = FALSE) {
862
    $this->curlInitialize();
863 864 865 866 867 868 869 870 871 872 873

    // 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], '#');
    }

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

876 877 878 879 880 881 882 883
    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:';
    }
884
    curl_setopt_array($this->curlHandle, $this->additionalCurlOptions + $curl_options);
885

886 887 888 889 890 891 892 893 894 895 896 897 898 899 900
    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.
901
    if (in_array($status, array(300, 301, 302, 303, 305, 307)) && $this->redirect_count < variable_get('simpletest_maximum_redirects', 5)) {
902 903 904 905 906 907 908 909
      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);
      }
    }
910

911
    $this->drupalSetContent($content, isset($original_url) ? $original_url : curl_getinfo($this->curlHandle, CURLINFO_EFFECTIVE_URL));
912 913
    $message_vars = array(
      '!method' => !empty($curl_options[CURLOPT_NOBODY]) ? 'HEAD' : (empty($curl_options[CURLOPT_POSTFIELDS]) ? 'GET' : 'POST'),
914 915
      '@url' => isset($original_url) ? $original_url : $url,
      '@status' => $status,
916
      '!length' => format_size(strlen($this->drupalGetContent()))
917 918
    );
    $message = t('!method @url returned @status (!length).', $message_vars);
919
    $this->assertTrue($this->drupalGetContent() !== FALSE, $message, t('Browser'));
920
    return $this->drupalGetContent();
921 922
  }

923 924 925 926 927
  /**
   * Reads headers and registers errors received from the tested site.
   *
   * @see _drupal_log_error().
   *
928 929 930 931
   * @param $curlHandler
   *   The cURL handler.
   * @param $header
   *   An header.
932
   */
933
  protected function curlHeaderCallback($curlHandler, $header) {
934 935 936 937 938 939 940 941 942 943
    // 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;
    }
944

945 946
    // Errors are being sent via X-Drupal-Assertion-* headers,
    // generated by _drupal_log_error() in the exact form required
947
    // by Drupal\simpletest\WebTestBase::error().
948
    if (preg_match('/^X-Drupal-Assertion-[0-9]+: (.*)$/', $header, $matches)) {
949
      // Call Drupal\simpletest\WebTestBase::error() with the parameters from the header.
950 951
      call_user_func_array(array(&$this, 'error'), unserialize(urldecode($matches[1])));
    }
952

953 954 955 956 957 958 959 960 961 962 963 964 965
    // 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;
        }
966 967 968
      }
    }

969 970 971 972
    // This is required by cURL.
    return strlen($header);
  }

973 974 975 976
  /**
   * Close the cURL handler and unset the handler.
   */
  protected function curlClose() {
977 978 979
    if (isset($this->curlHandle)) {
      curl_close($this->curlHandle);
      unset($this->curlHandle);
980 981 982 983
    }
  }

  /**
984
   * Parse content returned from curlExec using DOM and SimpleXML.
985
   *
986 987
   * @return
   *   A SimpleXMLElement or FALSE on failure.
988 989 990
   */
  protected function parse() {
    if (!$this->elements) {
991 992
      // DOM can load HTML soup. But, HTML soup can throw warnings, suppress
      // them.
993
      $htmlDom = new DOMDocument();
994
      @$htmlDom->loadHTML('<?xml encoding="UTF-8">' . $this->drupalGetContent());
995
      if ($htmlDom) {
996
        $this->pass(t('Valid HTML found on "@path"', array('@path' => $this->getUrl())), t('Browser'));
997 998
        // It's much easier to work with simplexml than DOM, luckily enough
        // we can just simply import our DOM tree.
999
        $this->elements = simplexml_import_dom($htmlDom);
1000 1001
      }
    }
1002 1003 1004
    if (!$this->elements) {
      $this->fail(t('Parsed page successfully.'), t('Browser'));
    }
1005

1006 1007 1008 1009 1010 1011
    return $this->elements;
  }

  /**
   * Retrieves a Drupal path or an absolute path.
   *
1012
   * @param $path
1013
   *   Drupal path or URL to load into internal browser
1014
   * @param $options
1015
   *   Options to be forwarded to url().
1016 1017 1018
   * @param $headers
   *   An array containing additional HTTP request headers, each formatted as
   *   "name: value".
1019
   * @return
1020
   *   The retrieved HTML string, also available as $this->drupalGetContent()
1021
   */
1022
  protected function drupalGet($path, array $options = array(), array $headers = array()) {
1023
    $options['absolute'] = TRUE;
1024

1025 1026
    // We re-using a CURL connection here. If that connection still has certain
    // options set, it might change the GET into a POST. Make sure we clear out
1027
    // previous options.