WebTestBase.php 119 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\Component\Utility\NestedArray;
11
use Drupal\Core\DrupalKernel;
12
13
14
15
16
17
18
use Drupal\Core\Database\Database;
use Drupal\Core\Database\ConnectionNotDefinedException;
use PDO;
use stdClass;
use DOMDocument;
use DOMXPath;
use SimpleXMLElement;
19
use Drupal\Core\Datetime\DrupalDateTime;
20
21
22
23

/**
 * Test case for typical Drupal tests.
 */
24
25
abstract class WebTestBase extends TestBase {

26
27
28
29
30
  /**
   * The profile to install as a basis for testing.
   *
   * @var string
   */
31
  protected $profile = 'testing';
32

33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
  /**
   * 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;

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

75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
  /**
   * 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.
   *
100
101
   * Drupal\simpletest\WebTestBase itself never sets this but always obeys what is
   * set.
102
103
104
105
106
107
108
109
110
111
   */
  protected $additionalCurlOptions = array();

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

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

119
120
121
122
123
  /**
   * HTTP authentication method
   */
  protected $httpauth_method = CURLAUTH_BASIC;

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

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

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

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

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

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

154
155
156
157
158
  /**
   * The kernel used in this test.
   */
  protected $kernel;

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

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

188
189
190
  /**
   * Creates a node based on default settings.
   *
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
   * @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'.
217
   *   - langcode: LANGUAGE_NOT_SPECIFIED. (If a 'langcode' key is provided in
218
219
220
221
222
223
224
   *     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.)
   *
225
   * @return \Drupal\node\Plugin\Core\Entity\Node
226
227
228
   *   The created node entity.
   */
  protected function drupalCreateNode(array $settings = array()) {
229
    // Populate defaults array.
230
    $settings += array(
231
      'body'      => array(LANGUAGE_NOT_SPECIFIED => array(array())),
232
      'title'     => $this->randomName(8),
233
      'changed'   => REQUEST_TIME,
234
      'promote'   => NODE_NOT_PROMOTED,
235
236
      'revision'  => 1,
      'log'       => '',
237
238
      'status'    => NODE_PUBLISHED,
      'sticky'    => NODE_NOT_STICKY,
239
      'type'      => 'page',
240
      'langcode'  => LANGUAGE_NOT_SPECIFIED,
241
    );
242

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

250
251
252
    // 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');
253
    }
254
255
256
257
258
259
260
261
262
263
264
265
266

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

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

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

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

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

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

330
    $saved_type = node_type_save($type);
331
    node_types_rebuild();
332
    menu_router_rebuild();
333
    node_add_body_field($type);
334

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

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

340
341
342
    return $type;
  }

343
344
345
346
347
348
349
350
  /**
   * Creates a block instance based on default settings.
   *
   * Note: Until this can be done programmatically, the active user account
   * must have permission to administer blocks.
   *
   * @param string $plugin_id
   *   The plugin ID of the block type for this block instance.
351
352
353
   * @param array $values
   *   (optional) An associative array of values for the block entity.
   *   Override the defaults by specifying the key and value in the array, for
354
355
356
   *   example:
   *   @code
   *     $this->drupalPlaceBlock('system_powered_by_block', array(
357
   *       'label' => t('Hello, world!'),
358
359
360
   *     ));
   *   @endcode
   *   The following defaults are provided:
361
   *   - label: Random string.
362
363
   *   - machine_name: Random string.
   *   - region: 'sidebar_first'.
364
365
366
   *   - theme: The default theme.
   * @param array $settings
   *   (optional) An associative array of plugin-specific settings.
367
   *
368
369
   * @return \Drupal\block\Plugin\Core\Entity\Block
   *   The block entity.
370
371
372
373
   *
   * @todo
   *   Add support for creating custom block instances.
   */
374
375
376
377
  protected function drupalPlaceBlock($plugin_id, array $values = array(), array $settings = array()) {
    $values += array(
      'plugin' => $plugin_id,
      'label' => $this->randomName(8),
378
      'region' => 'sidebar_first',
379
      'theme' => config('system.theme')->get('default'),
380
381
      'machine_name' => strtolower($this->randomName(8)),
      'settings' => $settings,
382
    );
383
384
385
386
387
    // Build the ID out of the theme and machine_name.
    $values['id'] = $values['theme'] . '.' . $values['machine_name'];
    $block = entity_create('block', $values);
    $block->save();
    return $block;
388
389
  }

390
391
392
  /**
   * Get a list files that can be used in tests.
   *
393
394
395
396
397
398
   * @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.
399
   */
400
  protected function drupalGetTestFiles($type, $size = NULL) {
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
    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) {
420
        file_unmanaged_copy($file->uri, variable_get('file_public_path', conf_path() . '/files'));
421
      }
422

423
424
425
426
      $this->generatedTestFiles = TRUE;
    }

    $files = array();
427
428
    // Make sure type is valid.
    if (in_array($type, array('binary', 'html', 'image', 'javascript', 'php', 'sql', 'text'))) {
429
      $files = file_scan_directory('public://', '/' . $type . '\-.*/');
430
431
432
433

      // If size is set then remove any files that are not of that size.
      if ($size !== NULL) {
        foreach ($files as $file) {
434
          $stats = stat($file->uri);
435
          if ($stats['size'] != $size) {
436
            unset($files[$file->uri]);
437
438
439
440
441
442
443
444
445
          }
        }
      }
    }
    usort($files, array($this, 'drupalCompareFiles'));
    return $files;
  }

  /**
446
   * Compare two files based on size and file name.
447
   */
448
  protected function drupalCompareFiles($file1, $file2) {
449
    $compare_size = filesize($file1->uri) - filesize($file2->uri);
450
451
452
    if ($compare_size) {
      // Sort by file size.
      return $compare_size;
453
454
    }
    else {
455
456
      // The files were the same size, so sort alphabetically.
      return strnatcmp($file1->name, $file2->name);
457
458
459
460
    }
  }

  /**
461
   * Create a user with a given set of permissions.
462
   *
463
464
465
   * @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.
466
   * @param string $name
467
   *   The user name.
468
   *
469
   * @return \Drupal\user\Plugin\Core\Entity\User|false
470
   *   A fully loaded user object with pass_raw property, or FALSE if account
471
472
   *   creation fails.
   */
473
  protected function drupalCreateUser(array $permissions = array(), $name = NULL) {
474
475
476
477
478
479
480
    // Create a role with the given permission set, if any.
    $rid = FALSE;
    if ($permissions) {
      $rid = $this->drupalCreateRole($permissions);
      if (!$rid) {
        return FALSE;
      }
481
482
483
484
    }

    // Create a user assigned to that role.
    $edit = array();
485
    $edit['name']   = !empty($name) ? $name : $this->randomName();
486
    $edit['mail']   = $edit['name'] . '@example.com';
487
488
    $edit['pass']   = user_password();
    $edit['status'] = 1;
489
490
491
    if ($rid) {
      $edit['roles'] = array($rid => $rid);
    }
492

493
494
    $account = entity_create('user', $edit);
    $account->save();
495
496
497
498
499
500
501
502
503
504
505
506
507
508

    $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.
   *
509
   * @param array $permissions
510
   *   Array of permission names to assign to role.
511
512
513
514
   * @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.
515
516
517
   * @param integer $weight
   *   (optional) The weight for the role. Defaults NULL so that entity_create()
   *   sets the weight to maximum + 1.
518
519
   *
   * @return string
520
   *   Role ID of newly created role, or FALSE if role creation failed.
521
   */
522
  protected function drupalCreateRole(array $permissions, $rid = NULL, $name = NULL, $weight = NULL) {
523
524
525
526
527
528
529
    // 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);
530
531
    }

532
    // Check the all the permissions strings are valid.
533
534
535
536
    if (!$this->checkPermissions($permissions)) {
      return FALSE;
    }

537
    // Create new role.
538
539
540
541
542
543
544
545
    $role = entity_create('user_role', array(
      'id' => $rid,
      'label' => $name,
    ));
    if (!is_null($weight)) {
      $role->set('weight', $weight);
    }
    $result = $role->save();
546
547

    $this->assertIdentical($result, SAVED_NEW, t('Created role ID @rid with name @name.', array(
548
549
      '@name' => var_export($role->label(), TRUE),
      '@rid' => var_export($role->id(), TRUE),
550
551
552
553
554
    )), t('Role'));

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

557
        $assigned_permissions = db_query('SELECT permission FROM {role_permission} WHERE rid = :rid', array(':rid' => $role->id()))->fetchCol();
558
559
560
561
562
563
564
565
        $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'));
        }
      }
566
      return $role->id();
567
568
569
570
571
572
    }
    else {
      return FALSE;
    }
  }

573
574
575
  /**
   * Check to make sure that the array of permissions are valid.
   *
576
577
578
579
580
581
   * @param $permissions
   *   Permissions to check.
   * @param $reset
   *   Reset cached available permissions.
   * @return
   *   TRUE or FALSE depending on whether the permissions are valid.
582
   */
583
  protected function checkPermissions(array $permissions, $reset = FALSE) {
584
    $available = &drupal_static(__FUNCTION__);
585
586

    if (!isset($available) || $reset) {
587
      $available = array_keys(module_invoke_all('permission'));
588
589
590
591
592
593
594
595
596
597
598
599
    }

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

600
  /**
601
602
603
604
605
   * 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.
   *
606
   * Please note that neither the global $user nor the passed-in user object is
607
608
609
   * 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()),
610
   * e.g. to log in the same user again, then it must be re-assigned manually.
611
612
613
614
615
616
617
618
619
620
   * 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
621
   *
622
   * @param \Drupal\user\Plugin\Core\Entity\User $account
623
   *   User object representing the user to log in.
624
625
   *
   * @see drupalCreateUser()
626
   */
627
  protected function drupalLogin($account) {
628
    if ($this->loggedInUser) {
629
630
631
632
      $this->drupalLogout();
    }

    $edit = array(
633
634
      'name' => $account->name,
      'pass' => $account->pass_raw
635
636
637
    );
    $this->drupalPost('user', $edit, t('Log in'));

638
639
    // @see WebTestBase::drupalUserIsLoggedIn()
    if (isset($this->session_id)) {
640
      $account->session_id = $this->session_id;
641
    }
642
    $pass = $this->assert($this->drupalUserIsLoggedIn($account), format_string('User %name successfully logged in.', array('%name' => $account->name)), 'User login');
643
    if ($pass) {
644
      $this->loggedInUser = $account;
645
    }
646
647
  }

648
649
650
651
652
653
654
655
656
657
658
659
660
661
  /**
   * Returns whether a given user account is logged in.
   *
   * @param \Drupal\user\User $account
   *   The user account object to check.
   */
  protected function drupalUserIsLoggedIn($account) {
    if (!isset($account->session_id)) {
      return FALSE;
    }
    // @see _drupal_session_read()
    return (bool) db_query("SELECT sid FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.sid = :sid", array(':sid' => $account->session_id))->fetchField();
  }

662
663
664
665
666
  /**
   * Generate a token for the currently logged in user.
   */
  protected function drupalGetToken($value = '') {
    $private_key = drupal_get_private_key();
667
    return drupal_hmac_base64($value, $this->session_id . $private_key);
668
669
  }

670
671
672
  /*
   * Logs a user out of the internal browser, then check the login page to confirm logout.
   */
673
  protected function drupalLogout() {
674
675
676
    // 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.
677
678
    $this->drupalGet('user/logout', array('query' => array('destination' => 'user')));
    $this->assertResponse(200, t('User was logged out.'));
679
680
681
    $pass = $this->assertField('name', t('Username field found.'), t('Logout'));
    $pass = $pass && $this->assertField('pass', t('Password field found.'), t('Logout'));

682
    if ($pass) {
683
684
      // @see WebTestBase::drupalUserIsLoggedIn()
      unset($this->loggedInUser->session_id);
685
686
      $this->loggedInUser = FALSE;
    }
687
688
  }

689
690
691
692
  /**
   * Sets up a Drupal site for running functional and integration tests.
   *
   * Generates a random database prefix and installs Drupal with the specified
693
694
695
   * installation profile in Drupal\simpletest\WebTestBase::$profile into the
   * prefixed database. Afterwards, installs any additional modules specified by
   * the test.
696
697
698
699
700
701
702
703
704
705
   *
   * 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.
   *
706
707
708
   * @see Drupal\simpletest\WebTestBase::prepareDatabasePrefix()
   * @see Drupal\simpletest\WebTestBase::changeDatabasePrefix()
   * @see Drupal\simpletest\WebTestBase::prepareEnvironment()
709
710
   */
  protected function setUp() {
711
    global $user, $conf;
712

713
714
715
716
717
718
719
    // 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();

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

723
724
    // Prepare the environment for running tests.
    $this->prepareEnvironment();
725
726
727
    if (!$this->setupEnvironment) {
      return FALSE;
    }
728

729
730
731
732
733
734
735
736
737
    // 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();
738
739
740
    if (!$this->setupDatabasePrefix) {
      return FALSE;
    }
741

742
743
744
745
746
747
748
    // 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
749
    $connection_info = Database::getConnectionInfo();
750
    $this->root_user = (object) array(
751
      'uid' => 1,
752
753
754
755
756
757
758
759
760
761
762
      'name' => 'admin',
      'mail' => 'admin@example.com',
      'pass_raw' => $this->randomName(),
    );
    $settings = array(
      'interactive' => FALSE,
      'parameters' => array(
        'profile' => $this->profile,
        'langcode' => 'en',
      ),
      'forms' => array(
763
        'install_settings_form' => $connection_info['default'],
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
        '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();
792
793
794
795
796
797
798
799
    $variable_groups = array(
      'system.file' => array(
        'path.private' =>  $this->private_files_directory,
        'path.temporary' =>  $this->temp_files_directory,
      ),
      'locale.settings' =>  array(
        'translation.path' => $this->translation_files_directory,
      ),
800
    );
801
802
803
804
    foreach ($variable_groups as $config_base => $variables) {
      foreach ($variables as $name => $value) {
        NestedArray::setValue($GLOBALS['conf'], array_merge(array($config_base), explode('.', $name)), $value);
      }
805
    }
806
    $GLOBALS['conf']['file_public_path'] = $this->public_files_directory;
807
808
809
    // Execute the non-interactive installer.
    require_once DRUPAL_ROOT . '/core/includes/install.core.inc';
    install_drupal($settings);
810
    $this->rebuildContainer();
811
812
813
814

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

816
    // Revert install_begin_request() cache service overrides.
817
    unset($conf['cache_classes']);
818

819
820
    // Set path variables.

821
    // Set 'parent_profile' of simpletest to add the parent profile's
822
823
    // search path to the child site's search paths.
    // @see drupal_system_listing()
824
    config('simpletest.settings')->set('parent_profile', $this->originalProfile)->save();
825

826
827
    // Collect modules to install.
    $class = get_class($this);
828
    $modules = array();
829
830
831
832
833
834
    while ($class) {
      if (property_exists($class, 'modules')) {
        $modules = array_merge($modules, $class::$modules);
      }
      $class = get_parent_class($class);
    }
835
    if ($modules) {
836
      $modules = array_unique($modules);
837
838
      $success = module_enable($modules, TRUE);
      $this->assertTrue($success, t('Enabled modules: %modules', array('%modules' => implode(', ', $modules))));
839
      $this->rebuildContainer();
840
    }
841

842
843
    // Reset/rebuild all data structures after enabling the modules.
    $this->resetAll();
844

845
846
847
848
849
850
851
852
853
854
855
    // Now make sure that the file path configurations are saved. This is done
    // after we install the modules to override default values.
    foreach ($variable_groups as $config_base => $variables) {
      $config = config($config_base);
      foreach ($variables as $name => $value) {
        $config->set($name, $value);
      }
      $config->save();
    }
    variable_set('file_public_path', $this->public_files_directory);

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

859
    drupal_set_time_limit($this->timeLimit);
860
861
862
863
864
865
    // Temporary fix so that when running from run-tests.sh we don't get an
    // empty current path which would indicate we're on the home page.
    $path = current_path();
    if (empty($path)) {
      _current_path('run-tests');
    }
866
    $this->setup = TRUE;
867
868
  }

869
870
871
  /**
   * Reset all data structures after having enabled new modules.
   *
872
   * This method is called by Drupal\simpletest\WebTestBase::setUp() after enabling
873
874
875
876
   * the requested modules. It must be called again when additional modules
   * are enabled later.
   */
  protected function resetAll() {
877
    // Clear all database and static caches and rebuild data structures.
878
879
880
881
882
883
884
    drupal_flush_all_caches();

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

885
886
887
888
889
890
891
892
893
894
895
896
  /**
   * 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.
   */
897
  protected function refreshVariables() {
898
    global $conf;
899
    cache('bootstrap')->delete('variables');
900
    $conf = variable_initialize();
901
    drupal_container()->get('config.factory')->reset();
902
903
  }

904
905
906
907
  /**
   * Delete created files and temporary files directory, delete the tables created by setUp(),
   * and reset the database prefix.
   */
908
  protected function tearDown() {
909
910
911
912
    // Destroy the testing kernel.
    if (isset($this->kernel)) {
      $this->kernel->shutdown();
    }
913
    parent::tearDown();
914

915
916
917
    // Ensure that internal logged in variable and cURL options are reset.
    $this->loggedInUser = FALSE;
    $this->additionalCurlOptions = array();
918

919
920
    // Close the CURL handler.
    $this->curlClose();
921
922
923
  }

  /**
924
   * Initializes the cURL connection.
925
   *
926
927
928
929
   * 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.
930
   */
931
  protected function curlInitialize() {
932
    global $base_url;
933

934
935
    if (!isset($this->curlHandle)) {
      $this->curlHandle = curl_init();
936
937
938
939
940
941
942

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

943
      $curl_options = array(
944
        CURLOPT_COOKIEJAR => $this->cookieFile,
945
        CURLOPT_URL => $base_url,
946
        CURLOPT_FOLLOWLOCATION => FALSE,
947
        CURLOPT_RETURNTRANSFER => TRUE,
948
949
        CURLOPT_SSL_VERIFYPEER => FALSE, // Required to make the tests run on HTTPS.
        CURLOPT_SSL_VERIFYHOST => FALSE, // Required to make the tests run on HTTPS.
950
        CURLOPT_HEADERFUNCTION => array(&$this, 'curlHeaderCallback'),
951
        CURLOPT_USERAGENT => $this->databasePrefix,
952
      );
953
      if (isset($this->httpauth_credentials)) {
954
        $curl_options[CURLOPT_HTTPAUTH] = $this->httpauth_method;
955
        $curl_options[CURLOPT_USERPWD] = $this->httpauth_credentials;
956
      }
957
958
959
960
961
962
      // 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.');
      }
963
964
965

      // By default, the child session name should be the same as the parent.
      $this->session_name = session_name();
966
    }
967
968
    // We set the user agent header on each request so as to use the current
    // time and a new uniqid.
969
    if (preg_match('/simpletest\d+/', $this->databasePrefix, $matches)) {
970
971
      curl_setopt($this->curlHandle, CURLOPT_USERAGENT, drupal_generate_test_ua($matches[0]));
    }
972
973
974
  }

  /**
975
   * Initializes and executes a cURL request.
976
   *
977
   * @param $curl_options
978
979
980
   *   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
981
   * @param $redirect
982
983
984
   *   FALSE if this is an initial request, TRUE if this request is the result
   *   of a redirect.
   *
985
   * @return
986
987
988
   *   The content returned from the call to curl_exec().
   *
   * @see curlInitialize()
989
   */
990
  protected function curlExec($curl_options, $redirect = FALSE) {
991
    $this->curlInitialize();
992
993
994
995
996
997
998
999
1000

    // 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], '#');
For faster browsing, not all history is shown. View entire blame