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

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

8
namespace Drupal\simpletest;
9

10
use Drupal\block\Entity\Block;
11
use Drupal\Component\FileCache\FileCacheFactory;
12
use Drupal\Component\Serialization\Json;
13
use Drupal\Component\Serialization\Yaml;
14
use Drupal\Component\Utility\Html;
15
use Drupal\Component\Utility\NestedArray;
16
use Drupal\Component\Utility\UrlHelper;
17
use Drupal\Core\Cache\Cache;
18
use Drupal\Component\Utility\SafeMarkup;
19
use Drupal\Core\Database\Database;
20
use Drupal\Core\DrupalKernel;
21
use Drupal\Core\Entity\EntityInterface;
22
use Drupal\Core\EventSubscriber\AjaxResponseSubscriber;
23
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
24
use Drupal\Core\Extension\MissingDependencyException;
25
use Drupal\Core\Render\Element;
26
use Drupal\Core\Session\AccountInterface;
27
use Drupal\Core\Session\AnonymousUserSession;
28
use Drupal\Core\Session\UserSession;
29
use Drupal\Core\Site\Settings;
30
use Drupal\Core\StreamWrapper\PublicStream;
31
use Drupal\Core\Url;
32
use Drupal\node\Entity\NodeType;
33
use Symfony\Component\DependencyInjection\ContainerInterface;
34
use Symfony\Component\HttpFoundation\Request;
35
use Zend\Diactoros\Uri;
36 37 38

/**
 * Test case for typical Drupal tests.
39 40
 *
 * @ingroup testing
41
 */
42 43
abstract class WebTestBase extends TestBase {

44 45
  use AssertContentTrait;

46 47 48 49 50 51
  use UserCreationTrait {
    createUser as drupalCreateUser;
    createRole as drupalCreateRole;
    createAdminRole as drupalCreateAdminRole;
  }

52 53 54 55 56
  /**
   * The profile to install as a basis for testing.
   *
   * @var string
   */
57
  protected $profile = 'testing';
58

59 60 61 62 63 64 65 66 67 68 69 70 71 72
  /**
   * The URL currently loaded in the internal browser.
   *
   * @var string
   */
  protected $url;

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

73 74 75 76 77 78 79
  /**
   * Whether or not to assert the presence of the X-Drupal-Ajax-Token.
   *
   * @var bool
   */
  protected $assertAjaxHeader = TRUE;

80 81 82 83 84 85 86
  /**
   * The headers of the page currently loaded in the internal browser.
   *
   * @var Array
   */
  protected $headers;

87 88 89 90 91
  /**
   * The cookies of the page currently loaded in the internal browser.
   *
   * @var array
   */
92
  protected $cookies = array();
93

94 95 96 97
  /**
   * Indicates that headers should be dumped if verbose output is enabled.
   *
   * Headers are dumped to verbose by drupalGet(), drupalHead(), and
98
   * drupalPostForm().
99 100 101 102 103
   *
   * @var bool
   */
  protected $dumpHeaders = FALSE;

104 105 106
  /**
   * The current user logged in using the internal browser.
   *
107
   * @var \Drupal\Core\Session\AccountInterface|bool
108 109 110
   */
  protected $loggedInUser = FALSE;

111 112 113 114 115 116 117 118
  /**
   * The "#1" admin user.
   *
   * @var \Drupal\Core\Session\AccountInterface
   */
  protected $rootUser;


119 120 121 122 123 124 125 126 127 128 129
  /**
   * 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.
   *
130 131
   * \Drupal\simpletest\WebTestBase itself never sets this but always obeys what
   * is set.
132 133 134
   */
  protected $additionalCurlOptions = array();

135 136 137 138 139 140 141
  /**
   * The original batch, before it was changed for testing purposes.
   *
   * @var array
   */
  protected $originalBatch;

142
  /**
143
   * The original user, before it was changed to a clean uid = 1 for testing.
144 145 146 147 148
   *
   * @var object
   */
  protected $originalUser = NULL;

149
  /**
150
   * The original shutdown handlers array, before it was cleaned for testing.
151 152 153 154 155
   *
   * @var array
   */
  protected $originalShutdownCallbacks = array();

156 157 158
  /**
   * The current session ID, if available.
   */
159
  protected $sessionId = NULL;
160

161 162 163 164 165
  /**
   * Whether the files were copied to the test files directory.
   */
  protected $generatedTestFiles = FALSE;

166 167 168 169 170
  /**
   * The maximum number of redirects to follow when handling responses.
   */
  protected $maximumRedirects = 5;

171 172 173
  /**
   * The number of redirects followed during the handling of a request.
   */
174
  protected $redirectCount;
175

176 177 178 179 180 181 182 183 184 185 186 187 188 189 190

  /**
   * The number of meta refresh redirects to follow, or NULL if unlimited.
   *
   * @var null|int
   */
  protected $maximumMetaRefreshCount = NULL;

  /**
   * The number of meta refresh redirects followed during ::drupalGet().
   *
   * @var int
   */
  protected $metaRefreshCount = 0;

191 192
  /**
   * The kernel used in this test.
193 194
   *
   * @var \Drupal\Core\DrupalKernel
195 196 197
   */
  protected $kernel;

198 199 200 201 202
  /**
   * The config directories used in this test.
   */
  protected $configDirectories = array();

203 204 205 206 207 208 209
  /**
   * Cookies to set on curl requests.
   *
   * @var array
   */
  protected $curlCookies = array();

210 211 212 213 214 215 216
  /**
   * An array of custom translations suitable for drupal_rewrite_settings().
   *
   * @var array
   */
  protected $customTranslations;

217 218 219 220 221 222 223
  /**
   * The class loader to use for installation and initialization of setup.
   *
   * @var \Symfony\Component\Classloader\Classloader
   */
  protected $classLoader;

224
  /**
225
   * Constructor for \Drupal\simpletest\WebTestBase.
226 227 228 229
   */
  function __construct($test_id = NULL) {
    parent::__construct($test_id);
    $this->skipClasses[__CLASS__] = TRUE;
230
    $this->classLoader = require DRUPAL_ROOT . '/autoload.php';
231 232
  }

233 234 235
  /**
   * Get a node from the database based on its title.
   *
236
   * @param string|\Drupal\Component\Utility\SafeStringInterface $title
237
   *   A node title, usually generated by $this->randomMachineName().
238
   * @param $reset
239
   *   (optional) Whether to reset the entity cache.
240
   *
241
   * @return \Drupal\node\NodeInterface
242
   *   A node entity matching $title.
243
   */
244
  function drupalGetNodeByTitle($title, $reset = FALSE) {
245
    if ($reset) {
246
      \Drupal::entityManager()->getStorage('node')->resetCache();
247
    }
248 249
    // Cast SafeStringInterface objects to string.
    $title = (string) $title;
250
    $nodes = entity_load_multiple_by_properties('node', array('title' => $title));
251 252 253 254 255
    // Load the first node returned from the database.
    $returned_node = reset($nodes);
    return $returned_node;
  }

256 257 258
  /**
   * Creates a node based on default settings.
   *
259 260 261 262 263 264 265 266 267 268 269 270 271
   * @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
272
   *       $settings['body'][0] = array(
273
   *         'value' => $this->randomMachineName(32),
274 275 276 277 278
   *         'format' => filter_default_format(),
   *       );
   *     @endcode
   *   - title: Random string.
   *   - type: 'page'.
279
   *   - uid: The currently logged in user, or anonymous.
280
   *
281
   * @return \Drupal\node\NodeInterface
282 283 284
   *   The created node entity.
   */
  protected function drupalCreateNode(array $settings = array()) {
285
    // Populate defaults array.
286
    $settings += array(
287 288 289 290
      'body'      => array(array(
        'value' => $this->randomMachineName(32),
        'format' => filter_default_format(),
      )),
291
      'title'     => $this->randomMachineName(8),
292
      'type'      => 'page',
293
      'uid'       => \Drupal::currentUser()->id(),
294
    );
295 296
    $node = entity_create('node', $settings);
    $node->save();
297 298 299 300 301 302 303

    return $node;
  }

  /**
   * Creates a custom content type based on default settings.
   *
304
   * @param array $values
305 306
   *   An array of settings to change from the defaults.
   *   Example: 'type' => 'foo'.
307
   *
308
   * @return \Drupal\node\Entity\NodeType
309
   *   Created content type.
310
   */
311
  protected function drupalCreateContentType(array $values = array()) {
312
    // Find a non-existent random type name.
313 314
    if (!isset($values['type'])) {
      do {
315
        $id = strtolower($this->randomMachineName(8));
316
      } while (NodeType::load($id));
317 318 319 320 321 322 323
    }
    else {
      $id = $values['type'];
    }
    $values += array(
      'type' => $id,
      'name' => $id,
324
    );
325 326
    $type = entity_create('node_type', $values);
    $status = $type->save();
327
    node_add_body_field($type);
328
    \Drupal::service('router.builder')->rebuild();
329

330
    $this->assertEqual($status, SAVED_NEW, SafeMarkup::format('Created content type %type.', array('%type' => $type->id())));
331

332 333 334
    return $type;
  }

335 336 337 338 339
  /**
   * Builds the renderable view of an entity.
   *
   * Entities postpone the composition of their renderable arrays to #pre_render
   * functions in order to maximize cache efficacy. This means that the full
340 341
   * renderable array for an entity is constructed in drupal_render(). Some
   * tests require the complete renderable array for an entity outside of the
342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359
   * drupal_render process in order to verify the presence of specific values.
   * This method isolates the steps in the render process that produce an
   * entity's renderable array.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity to prepare a renderable array for.
   * @param string $view_mode
   *   (optional) The view mode that should be used to build the entity.
   * @param null $langcode
   *   (optional) For which language the entity should be prepared, defaults to
   *   the current content language.
   * @param bool $reset
   *   (optional) Whether to clear the cache for this entity.
   * @return array
   *
   * @see drupal_render()
   */
  protected function drupalBuildEntityView(EntityInterface $entity, $view_mode = 'full', $langcode = NULL, $reset = FALSE) {
360 361 362 363
    $ensure_fully_built = function(&$elements) use (&$ensure_fully_built) {
      // If the default values for this element have not been loaded yet, populate
      // them.
      if (isset($elements['#type']) && empty($elements['#defaults_loaded'])) {
364
        $elements += \Drupal::service('element_info')->getInfo($elements['#type']);
365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382
      }

      // Make any final changes to the element before it is rendered. This means
      // that the $element or the children can be altered or corrected before the
      // element is rendered into the final text.
      if (isset($elements['#pre_render'])) {
        foreach ($elements['#pre_render'] as $callable) {
          $elements = call_user_func($callable, $elements);
        }
      }

      // And recurse.
      $children = Element::children($elements, TRUE);
      foreach ($children as $key) {
        $ensure_fully_built($elements[$key]);
      }
    };

383 384 385 386
    $render_controller = $this->container->get('entity.manager')->getViewBuilder($entity->getEntityTypeId());
    if ($reset) {
      $render_controller->resetCache(array($entity->id()));
    }
387 388
    $build = $render_controller->view($entity, $view_mode, $langcode);
    $ensure_fully_built($build);
389

390
    return $build;
391 392
  }

393 394 395 396 397
  /**
   * Creates a block instance based on default settings.
   *
   * @param string $plugin_id
   *   The plugin ID of the block type for this block instance.
398 399
   * @param array $settings
   *   (optional) An associative array of settings for the block entity.
400
   *   Override the defaults by specifying the key and value in the array, for
401 402 403
   *   example:
   *   @code
   *     $this->drupalPlaceBlock('system_powered_by_block', array(
404
   *       'label' => t('Hello, world!'),
405 406 407
   *     ));
   *   @endcode
   *   The following defaults are provided:
408
   *   - label: Random string.
409
   *   - ID: Random string.
410
   *   - region: 'sidebar_first'.
411
   *   - theme: The default theme.
412
   *   - visibility: Empty array.
413
   *
414
   * @return \Drupal\block\Entity\Block
415
   *   The block entity.
416 417 418 419
   *
   * @todo
   *   Add support for creating custom block instances.
   */
420 421
  protected function drupalPlaceBlock($plugin_id, array $settings = array()) {
    $settings += array(
422
      'plugin' => $plugin_id,
423
      'region' => 'sidebar_first',
424
      'id' => strtolower($this->randomMachineName(8)),
425
      'theme' => $this->config('system.theme')->get('default'),
426
      'label' => $this->randomMachineName(8),
427
      'visibility' => array(),
428
      'weight' => 0,
429
    );
430 431
    $values = [];
    foreach (array('region', 'id', 'theme', 'plugin', 'weight', 'visibility') as $key) {
432
      $values[$key] = $settings[$key];
433
      // Remove extra values that do not belong in the settings array.
434 435
      unset($settings[$key]);
    }
436 437
    foreach ($values['visibility'] as $id => $visibility) {
      $values['visibility'][$id]['id'] = $id;
438
    }
439
    $values['settings'] = $settings;
440 441 442
    $block = entity_create('block', $values);
    $block->save();
    return $block;
443 444
  }

445 446 447 448 449 450 451 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 478 479
  /**
   * Checks to see whether a block appears on the page.
   *
   * @param \Drupal\block\Entity\Block $block
   *   The block entity to find on the page.
   */
  protected function assertBlockAppears(Block $block) {
    $result = $this->findBlockInstance($block);
    $this->assertTrue(!empty($result), format_string('Ensure the block @id appears on the page', array('@id' => $block->id())));
  }

  /**
   * Checks to see whether a block does not appears on the page.
   *
   * @param \Drupal\block\Entity\Block $block
   *   The block entity to find on the page.
   */
  protected function assertNoBlockAppears(Block $block) {
    $result = $this->findBlockInstance($block);
    $this->assertFalse(!empty($result), format_string('Ensure the block @id does not appear on the page', array('@id' => $block->id())));
  }

  /**
   * Find a block instance on the page.
   *
   * @param \Drupal\block\Entity\Block $block
   *   The block entity to find on the page.
   *
   * @return array
   *   The result from the xpath query.
   */
  protected function findBlockInstance(Block $block) {
    return $this->xpath('//div[@id = :id]', array(':id' => 'block-' . $block->id()));
  }

480
  /**
481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501
   * Gets a list of files that can be used in tests.
   *
   * The first time this method is called, it will call
   * simpletest_generate_file() to generate binary and ASCII text files in the
   * public:// directory. It will also copy all files in
   * core/modules/simpletest/files to public://. These contain image, SQL, PHP,
   * JavaScript, and HTML files.
   *
   * All filenames are prefixed with their type and have appropriate extensions:
   * - text-*.txt
   * - binary-*.txt
   * - html-*.html and html-*.txt
   * - image-*.png, image-*.jpg, and image-*.gif
   * - javascript-*.txt and javascript-*.script
   * - php-*.txt and php-*.php
   * - sql-*.txt and sql-*.sql
   *
   * Any subsequent calls will not generate any new files, or copy the files
   * over again. However, if a test class adds a new file to public:// that
   * is prefixed with one of the above types, it will get returned as well, even
   * on subsequent calls.
502
   *
503
   * @param $type
504 505
   *   File type, possible values: 'binary', 'html', 'image', 'javascript',
   *   'php', 'sql', 'text'.
506
   * @param $size
507 508
   *   (optional) File size in bytes to match. Defaults to NULL, which will not
   *   filter the returned list by size.
509
   *
510
   * @return
511
   *   List of files in public:// that match the filter(s).
512
   */
513
  protected function drupalGetTestFiles($type, $size = NULL) {
514 515 516 517 518 519 520 521
    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');
      }

522
      // Generate ASCII text test files.
523 524 525
      $lines = array(16, 256, 1024, 2048, 20480);
      $count = 0;
      foreach ($lines as $line) {
526
        simpletest_generate_file('text-' . $count++, 64, $line, 'text');
527 528 529 530 531 532
      }

      // 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) {
533
        file_unmanaged_copy($file->uri, PublicStream::basePath());
534
      }
535

536 537 538 539
      $this->generatedTestFiles = TRUE;
    }

    $files = array();
540 541
    // Make sure type is valid.
    if (in_array($type, array('binary', 'html', 'image', 'javascript', 'php', 'sql', 'text'))) {
542
      $files = file_scan_directory('public://', '/' . $type . '\-.*/');
543 544 545 546

      // If size is set then remove any files that are not of that size.
      if ($size !== NULL) {
        foreach ($files as $file) {
547
          $stats = stat($file->uri);
548
          if ($stats['size'] != $size) {
549
            unset($files[$file->uri]);
550 551 552 553 554 555 556 557 558
          }
        }
      }
    }
    usort($files, array($this, 'drupalCompareFiles'));
    return $files;
  }

  /**
559
   * Compare two files based on size and file name.
560
   */
561
  protected function drupalCompareFiles($file1, $file2) {
562
    $compare_size = filesize($file1->uri) - filesize($file2->uri);
563 564 565
    if ($compare_size) {
      // Sort by file size.
      return $compare_size;
566 567
    }
    else {
568 569
      // The files were the same size, so sort alphabetically.
      return strnatcmp($file1->name, $file2->name);
570 571 572 573
    }
  }

  /**
574 575 576 577 578
   * 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.
   *
579
   * Please note that neither the current user nor the passed-in user object is
580 581 582
   * 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()),
583
   * e.g. to log in the same user again, then it must be re-assigned manually.
584 585 586 587 588 589 590
   * For example:
   * @code
   *   // Create a user.
   *   $account = $this->drupalCreateUser(array());
   *   $this->drupalLogin($account);
   *   // Load real user object.
   *   $pass_raw = $account->pass_raw;
591
   *   $account = User::load($account->id());
592 593
   *   $account->pass_raw = $pass_raw;
   * @endcode
594
   *
595
   * @param \Drupal\Core\Session\AccountInterface $account
596
   *   User object representing the user to log in.
597 598
   *
   * @see drupalCreateUser()
599
   */
600
  protected function drupalLogin(AccountInterface $account) {
601
    if ($this->loggedInUser) {
602 603 604 605
      $this->drupalLogout();
    }

    $edit = array(
606
      'name' => $account->getUsername(),
607
      'pass' => $account->pass_raw
608
    );
609
    $this->drupalPostForm('user/login', $edit, t('Log in'));
610

611
    // @see WebTestBase::drupalUserIsLoggedIn()
612 613
    if (isset($this->sessionId)) {
      $account->session_id = $this->sessionId;
614
    }
615
    $pass = $this->assert($this->drupalUserIsLoggedIn($account), format_string('User %name successfully logged in.', array('%name' => $account->getUsername())), 'User login');
616
    if ($pass) {
617
      $this->loggedInUser = $account;
618
      $this->container->get('current_user')->setAccount($account);
619
    }
620 621
  }

622 623 624
  /**
   * Returns whether a given user account is logged in.
   *
625
   * @param \Drupal\user\UserInterface $account
626 627 628
   *   The user account object to check.
   */
  protected function drupalUserIsLoggedIn($account) {
629 630 631 632 633
    $logged_in = FALSE;

    if (isset($account->session_id)) {
      $session_handler = $this->container->get('session_handler.storage');
      $logged_in = (bool) $session_handler->read($account->session_id);
634
    }
635

636
    return $logged_in;
637 638
  }

639 640 641 642
  /**
   * Logs a user out of the internal browser and confirms.
   *
   * Confirms logout by checking the login page.
643
   */
644
  protected function drupalLogout() {
645 646 647
    // 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.
648
    $this->drupalGet('user/logout', array('query' => array('destination' => 'user/login')));
649 650 651
    $this->assertResponse(200, 'User was logged out.');
    $pass = $this->assertField('name', 'Username field found.', 'Logout');
    $pass = $pass && $this->assertField('pass', 'Password field found.', 'Logout');
652

653
    if ($pass) {
654 655
      // @see WebTestBase::drupalUserIsLoggedIn()
      unset($this->loggedInUser->session_id);
656
      $this->loggedInUser = FALSE;
657
      $this->container->get('current_user')->setAccount(new AnonymousUserSession());
658
    }
659 660
  }

661 662 663
  /**
   * Sets up a Drupal site for running functional and integration tests.
   *
664 665
   * Installs Drupal with the installation profile specified in
   * \Drupal\simpletest\WebTestBase::$profile into the prefixed database.
666
   *
667 668 669
   * Afterwards, installs any additional modules specified in the static
   * \Drupal\simpletest\WebTestBase::$modules property of each class in the
   * class hierarchy.
670 671 672 673 674 675 676
   *
   * 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.
   */
  protected function setUp() {
677 678
    // Preserve original batch for later restoration.
    $this->setBatch();
679

680 681
    // Initialize user 1 and session name.
    $this->initUserSession();
682

683 684 685
    // Get parameters for install_drupal() before removing global variables.
    $parameters = $this->installParameters();

686 687 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 720 721 722 723 724 725 726 727
    // Prepare the child site settings.
    $this->prepareSettings();

    // Execute the non-interactive installer.
    $this->doInstall($parameters);

    // Import new settings.php written by the installer.
    $this->initSettings();

    // Initialize the request and container post-install.
    $container = $this->initKernel(\Drupal::request());

    // Initialize and override certain configurations.
    $this->initConfig($container);

    // Collect modules to install.
    $this->installModulesFromClassProperty($container);

    // Restore the original batch.
    $this->restoreBatch();

    // Reset/rebuild everything.
    $this->rebuildAll();
  }

  /**
   * Execute the non-interactive installer.
   *
   * @param array $parameters
   *   Parameters to pass to install_drupal().
   *
   * @see install_drupal()
   */
  protected function doInstall(array $parameters = []) {
    require_once DRUPAL_ROOT . '/core/includes/install.core.inc';
    install_drupal($this->classLoader, $this->installParameters());
  }

  /**
   * Prepares site settings and services before installation.
   */
  protected function prepareSettings() {
728 729 730 731
    // Prepare installer settings that are not install_drupal() parameters.
    // Copy and prepare an actual settings.php, so as to resemble a regular
    // installation.
    // Not using File API; a potential error must trigger a PHP warning.
732 733
    $directory = DRUPAL_ROOT . '/' . $this->siteDirectory;
    copy(DRUPAL_ROOT . '/sites/default/default.settings.php', $directory . '/settings.php');
734 735 736 737

    // All file system paths are created by System module during installation.
    // @see system_requirements()
    // @see TestBase::prepareEnvironment()
738
    $settings['settings']['file_public_path'] = (object) [
739
      'value' => $this->publicFilesDirectory,
740
      'required' => TRUE,
741 742
    ];
    $settings['settings']['file_private_path'] = (object) [
743
      'value' => $this->privateFilesDirectory,
744
      'required' => TRUE,
745
    ];
746 747 748
    // Save the original site directory path, so that extensions in the
    // site-specific directory can still be discovered in the test site
    // environment.
749
    // @see \Drupal\Core\Extension\ExtensionDiscovery::scan()
750
    $settings['settings']['test_parent_site'] = (object) [
751 752
      'value' => $this->originalSite,
      'required' => TRUE,
753
    ];
754
    // Add the parent profile's search path to the child site's search paths.
755
    // @see \Drupal\Core\Extension\ExtensionDiscovery::getProfileDirectories()
756
    $settings['conf']['simpletest.settings']['parent_profile'] = (object) [
757 758
      'value' => $this->originalProfile,
      'required' => TRUE,
759 760
    ];
    $settings['settings']['apcu_ensure_unique_prefix'] = (object) [
761 762
      'value' => FALSE,
      'required' => TRUE,
763
    ];
764
    $this->writeSettings($settings);
765 766 767 768 769 770 771
    // Allow for test-specific overrides.
    $settings_testing_file = DRUPAL_ROOT . '/' . $this->originalSite . '/settings.testing.php';
    if (file_exists($settings_testing_file)) {
      // Copy the testing-specific settings.php overrides in place.
      copy($settings_testing_file, $directory . '/settings.testing.php');
      // Add the name of the testing class to settings.php and include the
      // testing specific overrides
772
      file_put_contents($directory . '/settings.php', "\n\$test_class = '" . get_class($this) ."';\n" . 'include DRUPAL_ROOT . \'/\' . $site_path . \'/settings.testing.php\';' ."\n", FILE_APPEND);
773 774
    }
    $settings_services_file = DRUPAL_ROOT . '/' . $this->originalSite . '/testing.services.yml';
775 776 777
    if (!file_exists($settings_services_file)) {
      // Otherwise, use the default services as a starting point for overrides.
      $settings_services_file = DRUPAL_ROOT . '/sites/default/default.services.yml';
778
    }
779 780
    // Copy the testing-specific service overrides in place.
    copy($settings_services_file, $directory . '/services.yml');
781 782 783
    if ($this->strictConfigSchema) {
      // Add a listener to validate configuration schema on save.
      $yaml = new \Symfony\Component\Yaml\Yaml();
784 785
      $content = file_get_contents($directory . '/services.yml');
      $services = $yaml->parse($content);
786 787 788 789 790 791 792
      $services['services']['simpletest.config_schema_checker'] = [
        'class' => 'Drupal\Core\Config\Testing\ConfigSchemaChecker',
        'arguments' => ['@config.typed'],
        'tags' => [['name' => 'event_subscriber']]
      ];
      file_put_contents($directory . '/services.yml', $yaml->dump($services));
    }
793
    // Since Drupal is bootstrapped already, install_begin_request() will not
794 795
    // bootstrap again. Hence, we have to reload the newly written custom
    // settings.php manually.
796 797
    Settings::initialize(DRUPAL_ROOT, $this->siteDirectory, $this->classLoader);
  }
798

799 800 801 802 803
  /**
   * Initialize settings created during install.
   */
  protected function initSettings() {
    Settings::initialize(DRUPAL_ROOT, $this->siteDirectory, $this->classLoader);
804 805 806 807 808 809 810 811 812 813
    foreach ($GLOBALS['config_directories'] as $type => $path) {
      $this->configDirectories[$type] = $path;
    }

    // After writing settings.php, the installer removes write permissions
    // from the site directory. To allow drupal_generate_test_ua() to write
    // a file containing the private key for drupal_valid_test_ua(), the site
    // directory has to be writable.
    // TestBase::restoreEnvironment() will delete the entire site directory.
    // Not using File API; a potential error must trigger a PHP warning.
814 815
    chmod(DRUPAL_ROOT . '/' . $this->siteDirectory, 0777);
  }
816

817 818 819 820 821 822 823
  /**
   * Initialize various configurations post-installation.
   *
   * @param \Symfony\Component\DependencyInjection\ContainerInterface $container
   *   The container.
   */
  protected function initConfig(ContainerInterface $container) {
824
    $config = $container->get('config.factory');
825

826 827 828 829
    // Manually create and configure private and temporary files directories.
    // While these could be preset/enforced in settings.php like the public
    // files directory above, some tests expect them to be configurable in the
    // UI. If declared in settings.php, they would no longer be configurable.
830 831
    file_prepare_directory($this->privateFilesDirectory, FILE_CREATE_DIRECTORY);
    file_prepare_directory($this->tempFilesDirectory, FILE_CREATE_DIRECTORY);
832
    $config->getEditable('system.file')
833
      ->set('path.temporary', $this->tempFilesDirectory)
834 835 836
      ->save();

    // Manually configure the test mail collector implementation to prevent
837
    // tests from sending out emails and collect them in state instead.
838 839
    // While this should be enforced via settings.php prior to installation,
    // some tests expect to be able to test mail system implementations.
840
    $config->getEditable('system.mail')
841
      ->set('interface.default', 'test_mail_collector')
842 843
      ->save();

844 845 846
    // By default, verbosely display all errors and disable all production
    // environment optimizations for all tests to avoid needless overhead and
    // ensure a sane default experience for test authors.
847
    // @see https://www.drupal.org/node/2259167
848
    $config->getEditable('system.logging')
849 850
      ->set('error_level', 'verbose')
      ->save();
851
    $config->getEditable('system.performance')
852 853
      ->set('css.preprocess', FALSE)
      ->set('js.preprocess', FALSE)
854
      ->save();
855 856 857 858 859 860 861 862 863

    // Set an explicit time zone to not rely on the system one, which may vary
    // from setup to setup. The Australia/Sydney time zone is chosen so all
    // tests are run using an edge case scenario (UTC+10 and DST). This choice
    // is made to prevent time zone related regressions and reduce the
    // fragility of the testing system in general.
    $config->getEditable('system.date')
      ->set('timezone.default', 'Australia/Sydney')
      ->save();
864
  }
865

866 867 868 869
  /**
   * Reset and rebuild the environment after setup.
   */
  protected function rebuildAll() {
870 871 872
    // Reset/rebuild all data structures after enabling the modules, primarily
    // to synchronize all data structures and caches between the test runner and
    // the child site.
873
    // @see \Drupal\Core\DrupalKernel::bootCode()
874 875
    // @todo Test-specific setUp() methods may set up further fixtures; find a
    //   way to execute this after setUp() is done, or to eliminate it entirely.
876
    $this->resetAll();
877
    $this->kernel->prepareLegacyRequest(\Drupal::request());
878

879 880 881 882 883
    // Explicitly call register() again on the container registered in \Drupal.
    // @todo This should already be called through
    //   DrupalKernel::prepareLegacyRequest() -> DrupalKernel::boot() but that
    //   appears to be calling a different container.
    $this->container->get('stream_wrapper_manager')->register();
884 885
  }

886 887 888 889 890
  /**
   * Returns the parameters that will be used when Simpletest installs Drupal.
   *
   * @see install_drupal()
   * @see install_state_defaults()
891 892 893
   *
   * @return array
   *   Array of parameters for use in install_drupal().
894 895 896
   */
  protected function installParameters() {
    $connection_info = Database::getConnectionInfo();
897
    $driver = $connection_info['default']['driver'];
898
    $connection_info['default']['prefix'] = $connection_info['default']['prefix']['default'];
899 900 901 902
    unset($connection_info['default']['driver']);
    unset($connection_info['default']['namespace']);
    unset($connection_info['default']['pdo']);
    unset($connection_info['default']['init_commands']);
903 904 905 906 907 908 909
    // Remove database connection info that is not used by SQLite.
    if ($driver == 'sqlite') {
      unset($connection_info['default']['username']);
      unset($connection_info['default']['password']);
      unset($connection_info['default']['host']);
      unset($connection_info['default']['port']);
    }
910 911 912 913 914 915 916
    $parameters = array(
      'interactive' => FALSE,
      'parameters' => array(
        'profile' => $this->profile,
        'langcode' => 'en',
      ),
      'forms' => array(
917 918 919 920
        'install_settings_form' => array(
          'driver' => $driver,
          $driver => $connection_info['default'],
        ),
921 922 923 924
        'install_configure_form' => array(
          'site_name' => 'Drupal',
          'site_mail' => 'simpletest@example.com',
          'account' => array(
925 926
            'name' => $this->rootUser->name,
            'mail' => $this->rootUser->getEmail(),
927
            'pass' => array(
928 929
              'pass1' => $this->rootUser->pass_raw,
              'pass2' => $this->rootUser->pass_raw,
930 931
            ),
          ),
932 933 934
          // \Drupal\Core\Render\Element\Checkboxes::valueCallback() requires
          // NULL instead of FALSE values for programmatic form submissions to
          // disable a checkbox.
935 936 937 938 939 940 941
          'update_status_module' => array(
            1 => NULL,
            2 => NULL,
          ),
        ),
      ),
    );
942 943 944

    // If we only have one db driver available, we cannot set the driver.
    include_once DRUPAL_ROOT . '/core/includes/install.inc';
945
    if (count($this->getDatabaseTypes()) == 1) {
946 947
      unset($parameters['forms']['install_settings_form']['driver']);
    }
948 949 950
    return $parameters;
  }

951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041
  /**
   * Preserve the original batch, and instantiate the test batch.
   */
  protected function setBatch() {
    // 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();

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

  /**
   * Restore the original batch.
   *
   * @see ::setBatch
   */
  protected function restoreBatch() {
    // Restore the original Simpletest batch.
    $batch = &batch_get();
    $batch = $this->originalBatch;
  }

  /**
   * Initializes user 1 for the site to be installed.
   */
  protected function initUserSession() {
    // Define information about the user 1 account.
    $this->rootUser = new UserSession(array(
      'uid' => 1,
      'name' => 'admin',
      'mail' => 'admin@example.com',
      'pass_raw' => $this->randomMachineName(),
    ));

    // The child site derives its session name from the database prefix when
    // running web tests.
    $this->generateSessionName($this->databasePrefix);
  }

  /**
   * Initializes the kernel after installation.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   Request object.
   *
   * @return \Symfony\Component\DependencyInjection\ContainerInterface
   *   The container.
   */
  protected function initKernel(Request $request) {
    $this->kernel = DrupalKernel::createFromRequest($request, $this->classLoader, 'prod', TRUE);
    $this->kernel->prepareLegacyRequest($request);
    // Force the container to be built from scratch instead of loaded from the
    // disk. This forces us to not accidentally load the parent site.
    return $this->kernel->rebuildContainer();
  }

  /**
   * Install modules defined by `static::$modules`.
   *
   * @param \Symfony\Component\DependencyInjection\ContainerInterface $container
   *   The container.
   */
  protected function installModulesFromClassProperty(ContainerInterface $container) {
    $class = get_class($this);
    $modules = [];
    while ($class) {
      if (property_exists($class, 'modules')) {
        $modules = array_merge($modules, $class::$modules);
      }
      $class = get_parent_class($class);
    }
    if ($modules) {
      $modules = array_unique($modules);
      try {
        $success = $container->get('module_installer')->install($modules, TRUE);
        $this->assertTrue($success, SafeMarkup::format('Enabled modules: %modules', ['%modules' => implode(', ', $modules)]));
      }
      catch (MissingDependencyException $e) {
        // The exception message has all the details.
        $this->fail($e->getMessage());
      }

      $this->rebuildContainer();
    }
  }

1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052
  /**
   * Returns all supported database driver installer objects.
   *
   * This wraps drupal_get_database_types() for use without a current container.
   *
   * @return \Drupal\Core\Database\Install\Tasks[]
   *   An array of available database driver installer objects.
   */
  protected function getDatabaseTypes() {
    \Drupal::setContainer($this->originalContainer);
    $database_types = drupal_get_database_types();
1053
    \Drupal::unsetContainer();
1054 1055 1056
    return $database_types;
  }

1057
  /**
1058
   * Rewrites the settings.php file of the test site.
1059
   *
1060 1061 1062
   * @param array $settings
   *   An array of settings to write out, in the format expected by
   *   drupal_rewrite_settings().
1063 1064 1065
   *
   * @see drupal_rewrite_settings()
   */
1066
  protected function writeSettings(array $settings) {
1067
    include_once DRUPAL_ROOT . '/core/includes/install.inc';
1068 1069 1070 1071 1072
    $filename = $this->siteDirectory . '/settings.php';
    // system_requirements() removes write permissions from settings.php
    // whenever it is invoked.
    // Not using File API; a potential error must trigger a PHP warning.
    chmod($filename, 0666);
1073 1074 1075
    drupal_rewrite_settings($settings, $filename);
  }

1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091
  /**
   * Changes parameters in the services.yml file.
   *
   * @param $name
   *   The name of the parameter.
   * @param $value
   *   The value of the parameter.
   */
  protected function setContainerParameter($name, $value) {
    $filename = $this->siteDirectory . '/services.yml';
    chmod($filename, 0666);

    $services = Yaml::decode(file_get_contents($filename));
    $services['parameters'][$name] = $value;
    file_put_contents($filename, Yaml::encode($services));

1092 1093 1094
    // Ensure that the cache is deleted for the yaml file loader.
    $file_cache = FileCacheFactory::get('container_yaml_loader');
    $file_cache->delete($filename);
1095 1096
  }

1097
  /**
1098
   * Queues custom translations to be written to settings.php.
1099
   *
1100 1101
   * Use WebTestBase::writeCustomTranslations() to apply and write the queued
   * translations.
1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113
   *
   * @param string $langcode
   *   The langcode to add translations for.
   * @param array $values
   *   Array of values containing the untranslated string and its translation.
   *   For example:
   *   @code
   *   array(
   *     '' => array('Sunday' => 'domingo'),
   *     'Long month name' => array('March' => 'marzo'),
   *   );
   *   @endcode
1114 1115
   *   Pass an empty array to remove all existing custom translations for the
   *   given $langcode.
1116 1117
   */
  protected function addCustomTranslations($langcode, array $values) {
1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129
    // If $values is empty, then the test expects all custom translations to be
    // cleared.
    if (empty($values)) {
      $this->customTranslations[$langcode] = array();
    }
    // Otherwise, $values are expected to be merged into previously passed
    // values, while retaining keys that are not explicitly set.
    else {
      foreach ($values as $context => $translations) {
        foreach ($translations as $original => $translation) {
          $this->customTranslations[$langcode][$context][$original] = $translation;
        }
1130 1131 1132 1133 1134
      }
    }
  }

  /**
1135 1136 1137 1138
   * Writes custom translations to the test site's settings.php.
   *
   * Use TestBase::addCustomTranslations() to queue custom translations before
   * calling this method.
1139 1140
   */
  protected function writeCustomTranslations() {
1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156
    $settings = array();
    foreach ($this->customTranslations as $langcode => $values) {
      $settings_key = 'locale_custom_strings_' . $langcode;

      // Update in-memory settings directly.
      $this->settingsSet($settings_key, $values);

      $settings['settings'][$settings_key] = (object) array(
        'value' => $values,
        'required' => TRUE,
      );
    }
    // Only rewrite settings if there are any translation changes to write.
    if (!empty($settings)) {
      $this->writeSettings($settings);
    }
1157 1158
  }

1159
  /**
1160 1161
   * Rebuilds \Drupal::getContainer().
   *
1162 1163 1164 1165
   * Use this to update the test process's kernel with a new service container.
   * For example, when the list of enabled modules is changed via the internal
   * browser the test process's kernel has a service container with an out of
   * date module list.
1166 1167 1168 1169
   *
   * @see TestBase::prepareEnvironment()
   * @see TestBase::restoreEnvironment()
   *
1170
   * @todo Fix https://www.drupal.org/node/2021959 so that module enable/disable
1171 1172 1173
   *   changes are immediately reflected in \Drupal::getContainer(). Until then,
   *   tests can invoke this workaround when requiring services from newly
   *   enabled modules to be immediately available in the same request.
1174
   */
1175 1176 1177
  protected function rebuildContainer() {
    // Rebuild the kernel and bring it back to a fully bootstrapped state.
    $this->container = $this->kernel->rebuildContainer();
1178

1179 1180 1181 1182 1183
    // Make sure the url generator has a request object, otherwise calls to
    // $this->drupalGet() will fail.
    $this->prepareRequestForGenerator();
  }

1184
  /**
1185
   * Resets all data structures after having enabled new modules.
1186
   *
1187 1188 1189
   * This method is called by \Drupal\simpletest\WebTestBase::setUp() after
   * enabling the requested modules. It must be called again when additional
   * modules are enabled later.
1190 1191
   */
  protected function resetAll() {
1192
    // Clear all database and static caches and rebuild data structures.
1193
    drupal_flush_all_caches();
1194
    $this->container = \Drupal::getContainer();
1195

1196
    // Reset static variables and reload permissions.
1197 1198 1199
    $this->refreshVariables();
  }

1200
  /**
1201
   * Refreshes in-memory configuration and state information.
1202
   *
1203 1204
   * Useful after a page request is made that changes configuration or state in
   * a different thread.
1205
   *
1206
   * In other words calling a settings page with $this->drupalPostForm() with a
1207 1208
   * changed value would update configuration to reflect that change, but in the
   * thread that made the call (thread running the test) the changed values
1209 1210
   * would not be picked up.
   *
1211
   * This method clears the cache and loads a fresh copy.
1212
   */
1213
  protected function refreshVariables() {
1214
    // Clear the tag cache.
1215
    \Drupal::service('cache_tags.invalidator')->resetChecksums();
1216 1217 1218 1219 1220
    foreach (Cache::getBins() as $backend) {
      if (is_callable(array($backend, 'reset'))) {
        $backend->reset();
      }
    }
1221

1222 1223
    $this->container->get('config.factory')->reset();
    $this->container->get('state')->resetCache();
1224 1225
  }

1226
  /**
1227 1228 1229 1230
   * Cleans up after testing.
   *
   * Deletes created files and temporary files directory, deletes the tables
   * created by setUp(), and resets the database prefix.
1231
   */
1232
  protected function tearDown() {
1233 1234 1235 1236
    // Destroy the testing kernel.
    if (isset($this->kernel)) {
      $this->kernel->shutdown();
    }
1237
    parent::tearDown();
1238

1239 1240 1241
    // Ensure that the maximum meta refresh count is reset.
    $this->maximumMetaRefreshCount = NULL;

1242 1243 1244
    // Ensure that internal logged in variable and cURL options are reset.
    $this->loggedInUser = FALSE;
    $this->additionalCurlOptions = array();
1245

1246 1247
    // Close the CURL handler and reset the cookies array used for upgrade
    // testing so test classes containing multiple tests are not polluted.
1248
    $this->curlClose();
1249
    $this->curlCookies = array();
1250
    $this->cookies = array();
1251 1252 1253
  }

  /**
1254
   * Initializes the cURL connection.
1255
   *
1256 1257 1258 1259
   * 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.
1260
   */
1261
  protected function curlInitialize() {
1262
    global $base_url;
1263

1264 1265
    if (!isset($this->curlHandle)) {
      $this->curlHandle = curl_init();
1266 1267 1268 1269

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

1273
      $curl_options = array(
1274
        CURLOPT_COOKIEJAR => $this->cookieFile,
1275
        CURLOPT_URL => $base_url,
1276
        CURLOPT_FOLLOWLOCATION => FALSE,
1277
        CURLOPT_RETURNTRANSFER => TRUE,
1278 1279 1280 1281
        // Required to make the tests run on HTTPS.
        CURLOPT_SSL_VERIFYPEER => FALSE,
        // Required to make the tests run on HTTPS.
        CURLOPT_SSL_VERIFYHOST => FALSE,
1282
        CURLOPT_HEADERFUNCTION => array(&$this, 'curlHeaderCallback'),
1283
        CURLOPT_USERAGENT => $this->databasePrefix,
1284 1285
        // Disable support for the @ prefix for uploading files.
        CURLOPT_SAFE_UPLOAD => TRUE,
1286
      );
1287 1288 1289
      if (isset($this->httpAuthCredentials)) {
        $curl_options[CURLOPT_HTTPAUTH] = $this->httpAuthMethod;
        $curl_options[CURLOPT_USERPWD] = $this->httpAuthCredentials;
1290
      }
1291 1292 1293 1294 1295 1296
      // 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.');
      }
1297
    }
1298 1299
    // We set the user agent header on each request so as to use the current
    // time and a new uniqid.
1300
    if (preg_match('/simpletest\d+/', $this->databasePrefix, $matches)) {
1301 1302
      curl_setopt($this->curlHandle, CURLOPT_USERAGENT, drupal_generate_test_ua($matches[0]));
    }
1303 1304 1305
  }

  /**
1306
   * Initializes and executes a cURL request.
1307
   *
1308
   * @param $curl_options
1309 1310 1311
   *   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
1312
   * @param $redirect
1313 1314 1315
   *   FALSE if this is an initial request, TRUE if this request is the result
   *   of a redirect.
   *
1316
   * @return
1317 1318 1319
   *   The content returned from the call to curl_exec().
   *
   * @see curlInitialize()
1320
   */
1321
  protected function curlExec($curl_options, $redirect = FALSE) {
1322
    $this->curlInitialize();
1323

1324 1325 1326 1327 1328 1329 1330 1331 1332 1333
    if (!empty($curl_options[CURLOPT_URL])) {
      // 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 (strpos($curl_options[CURLOPT_URL], '#')) {
        $original_url = $curl_options[CURLOPT_URL];
        $curl_options[CURLOPT_URL] = strtok($curl_options[CURLOPT_URL], '#');
      }
1334 1335
    }

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

1338 1339 1340 1341 1342 1343 1344 1345
    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:';
    }
1346

1347 1348 1349 1350
    $cookies = array();
    if (!empty($this->curlCookies)) {
      $cookies = $this->curlCookies;
    }
1351 1352 1353 1354 1355
    // In order to debug web tests you need to either set a cookie, have the
    // Xdebug session in the URL or set an environment variable in case of CLI
    // requests. If the developer listens to connection on the parent site, by
    // default the cookie is not forwarded to the client side, so you cannot
    // debug the code running on the child site. In order to make debuggers work
1356 1357
    // this bit of information is forwarded. Make sure that the debugger listens
    // to at least three external connections.
1358 1359 1360 1361
    $request = \Drupal::request();
    $cookie_params = $request->cookies;
    if ($cookie_params->has('XDEBUG_SESSION')) {
      $cookies[] = 'XDEBUG_SESSION=' . $cookie_params->get('XDEBUG_SESSION');
1362
    }
1363
    // For CLI requests, the information is stored in $_SERVER.
1364 1365
    $server = $request->server;
    if ($server->has('XDEBUG_CONFIG')) {
1366
      // $_SERVER['XDEBUG_CONFIG'] has the form "key1=value1 key2=value2 ...".
1367
      $pairs = explode(' ', $server->get('XDEBUG_CONFIG'));
1368 1369 1370 1371 1372 1373 1374 1375
      foreach ($pairs as $pair) {
        list($key, $value) = explode('=', $pair);
        // Account for key-value pairs being separated by multiple spaces.
        if (trim($key, ' ') == 'idekey') {
          $cookies[] = 'XDEBUG_SESSION=' . trim($value, ' ');