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

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

8
namespace Drupal\simpletest;
9

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

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

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

31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
  /**
   * The URL currently loaded in the internal browser.
   *
   * @var string
   */
  protected $url;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

186
187
188
  /**
   * Creates a node based on default settings.
   *
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
   * @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'.
215
   *   - langcode: LANGUAGE_NOT_SPECIFIED. (If a 'langcode' key is provided in
216
217
218
219
220
221
222
223
224
225
226
   *     the array, this language code will also be used for a randomly
   *     generated body field for that language, and the body for
   *     LANGUAGE_NOT_SPECIFIED will remain empty.)
   *   - uid: The currently logged in user, or the user running test.
   *   - revision: 1. (Backwards-compatible binary flag indicating whether a
   *     new revision should be created; use 1 to specify a new revision.)
   *
   * @return Drupal\node\Node
   *   The created node entity.
   */
  protected function drupalCreateNode(array $settings = array()) {
227
    // Populate defaults array.
228
    $settings += array(
229
      'body'      => array(LANGUAGE_NOT_SPECIFIED => array(array())),
230
      'title'     => $this->randomName(8),
231
      'changed'   => REQUEST_TIME,
232
      'promote'   => NODE_NOT_PROMOTED,
233
234
      'revision'  => 1,
      'log'       => '',
235
236
      'status'    => NODE_PUBLISHED,
      'sticky'    => NODE_NOT_STICKY,
237
      'type'      => 'page',
238
      'langcode'  => LANGUAGE_NOT_SPECIFIED,
239
    );
240

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

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

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

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

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

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

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

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

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

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

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

338
339
340
341
342
343
    return $type;
  }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    // Set installer parameters.
    // @see install.php, install.core.inc
674
    $connection_info = Database::getConnectionInfo();
675
676
677
678
679
680
681
682
683
684
685
686
    $this->root_user = (object) array(
      'name' => 'admin',
      'mail' => 'admin@example.com',
      'pass_raw' => $this->randomName(),
    );
    $settings = array(
      'interactive' => FALSE,
      'parameters' => array(
        'profile' => $this->profile,
        'langcode' => 'en',
      ),
      'forms' => array(
687
        'install_settings_form' => $connection_info['default'],
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
        '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();
716
717
718
719
720
721
722
723
724
    $variables = array(
      'file_public_path' =>  $this->public_files_directory,
      'file_private_path' =>  $this->private_files_directory,
      'file_temporary_path' =>  $this->temp_files_directory,
      'locale_translate_file_directory' =>  $this->translation_files_directory,
    );
    foreach ($variables as $name => $value) {
      $GLOBALS['conf'][$name] = $value;
    }
725
726
727
    // Execute the non-interactive installer.
    require_once DRUPAL_ROOT . '/core/includes/install.core.inc';
    install_drupal($settings);
728
    $this->rebuildContainer();
729
730
731
    foreach ($variables as $name => $value) {
      variable_set($name, $value);
    }
732
733
734
735

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

737
738
739
    // Revert install_begin_request() cache and lock service overrides.
    unset($conf['cache_classes']);
    unset($conf['lock_backend']);
740

741
742
    // Set path variables.

743
    // Set 'parent_profile' of simpletest to add the parent profile's
744
745
    // search path to the child site's search paths.
    // @see drupal_system_listing()
746
    config('simpletest.settings')->set('parent_profile', $this->originalProfile)->save();
747

748
749
    // Collect modules to install.
    $class = get_class($this);
750
    $modules = array();
751
752
753
754
755
756
    while ($class) {
      if (property_exists($class, 'modules')) {
        $modules = array_merge($modules, $class::$modules);
      }
      $class = get_parent_class($class);
    }
757
    if ($modules) {
758
759
      $success = module_enable($modules, TRUE);
      $this->assertTrue($success, t('Enabled modules: %modules', array('%modules' => implode(', ', $modules))));
760
      $this->rebuildContainer();
761
    }
762

763
764
    // Reset/rebuild all data structures after enabling the modules.
    $this->resetAll();
765

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

769
    drupal_set_time_limit($this->timeLimit);
770
771
772
773
774
775
    // 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');
    }
776
    $this->setup = TRUE;
777
778
  }

779
780
781
  /**
   * Reset all data structures after having enabled new modules.
   *
782
   * This method is called by Drupal\simpletest\WebTestBase::setUp() after enabling
783
784
785
786
   * the requested modules. It must be called again when additional modules
   * are enabled later.
   */
  protected function resetAll() {
787
    // Clear all database and static caches and rebuild data structures.
788
789
790
791
792
793
794
    drupal_flush_all_caches();

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

795
796
797
798
799
800
801
802
803
804
805
806
  /**
   * 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.
   */
807
  protected function refreshVariables() {
808
    global $conf;
809
    cache('bootstrap')->delete('variables');
810
    $conf = variable_initialize();
811
812
  }

813
814
815
816
  /**
   * Delete created files and temporary files directory, delete the tables created by setUp(),
   * and reset the database prefix.
   */
817
  protected function tearDown() {
818
819
820
821
    // Destroy the testing kernel.
    if (isset($this->kernel)) {
      $this->kernel->shutdown();
    }
822
    parent::tearDown();
823

824
825
826
    // Ensure that internal logged in variable and cURL options are reset.
    $this->loggedInUser = FALSE;
    $this->additionalCurlOptions = array();
827

828
829
    // Reload module list and implementations to ensure that test module hooks
    // aren't called after tests.
830
    system_list_reset();
831
    module_list_reset();
832
    module_implements_reset();
833

834
835
    // Reset the Field API.
    field_cache_clear();
836

837
838
    // Rebuild caches.
    $this->refreshVariables();
839

840
841
    // Close the CURL handler.
    $this->curlClose();
842
843
844
  }

  /**
845
   * Initializes the cURL connection.
846
   *
847
848
849
850
   * 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.
851
   */
852
  protected function curlInitialize() {
853
    global $base_url;
854

855
856
    if (!isset($this->curlHandle)) {
      $this->curlHandle = curl_init();
857
858
859
860
861
862
863

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

864
      $curl_options = array(
865
        CURLOPT_COOKIEJAR => $this->cookieFile,
866
        CURLOPT_URL => $base_url,
867
        CURLOPT_FOLLOWLOCATION => FALSE,
868
        CURLOPT_RETURNTRANSFER => TRUE,
869
870
        CURLOPT_SSL_VERIFYPEER => FALSE, // Required to make the tests run on HTTPS.
        CURLOPT_SSL_VERIFYHOST => FALSE, // Required to make the tests run on HTTPS.
871
        CURLOPT_HEADERFUNCTION => array(&$this, 'curlHeaderCallback'),
872
        CURLOPT_USERAGENT => $this->databasePrefix,
873
      );
874
      if (isset($this->httpauth_credentials)) {
875
        $curl_options[CURLOPT_HTTPAUTH] = $this->httpauth_method;
876
        $curl_options[CURLOPT_USERPWD] = $this->httpauth_credentials;
877
      }
878
879
880
881
882
883
      // 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.');
      }
884
885
886

      // By default, the child session name should be the same as the parent.
      $this->session_name = session_name();
887
    }
888
889
    // We set the user agent header on each request so as to use the current
    // time and a new uniqid.
890
    if (preg_match('/simpletest\d+/', $this->databasePrefix, $matches)) {
891
892
      curl_setopt($this->curlHandle, CURLOPT_USERAGENT, drupal_generate_test_ua($matches[0]));
    }
893
894
895
  }

  /**
896
   * Initializes and executes a cURL request.
897
   *
898
   * @param $curl_options
899
900
901
   *   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
902
   * @param $redirect
903
904
905
   *   FALSE if this is an initial request, TRUE if this request is the result
   *   of a redirect.
   *
906
   * @return
907
908
909
   *   The content returned from the call to curl_exec().
   *
   * @see curlInitialize()
910
   */
911
  protected function curlExec($curl_options, $redirect = FALSE) {
912
    $this->curlInitialize();
913
914
915
916
917
918
919
920
921
922
923

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

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

926
927
928
929
930
931
932
933
    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:';
    }
934
    curl_setopt_array($this->curlHandle, $this->additionalCurlOptions + $curl_options);
935

936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
    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.
951
    if (in_array($status, array(300, 301, 302, 303, 305, 307)) && $this->redirect_count < $this->maximumRedirects) {
952
953
954
955
956
957
958
959
      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);
      }
    }
960

961
    $this->drupalSetContent($content, isset($original_url) ? $original_url : curl_getinfo($this->curlHandle, CURLINFO_EFFECTIVE_URL));
962
963
    $message_vars = array(
      '!method' => !empty($curl_options[CURLOPT_NOBODY]) ? 'HEAD' : (empty($curl_options[CURLOPT_POSTFIELDS]) ? 'GET' : 'POST'),
964
965
      '@url' => isset($original_url) ? $original_url : $url,
      '@status' => $status,
966
      '!length' => format_size(strlen($this->drupalGetContent()))
967
968
    );
    $message = t('!method @url returned @status (!length).', $message_vars);
969
    $this->assertTrue($this->drupalGetContent() !== FALSE, $message, t('Browser'));
970
    return $this->drupalGetContent();
971
972
  }

973
974
975
976
977
  /**
   * Reads headers and registers errors received from the tested site.
   *
   * @see _drupal_log_error().
   *
978
979
980
981
   * @param $curlHandler
   *   The cURL handler.
   * @param $header
   *   An header.
982
   */
983
  protected function curlHeaderCallback($curlHandler, $header) {
984
985
986
987
988
989
990
991
992
993
    // 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;
    }
994

995
996
    // Errors are being sent via X-Drupal-Assertion-* headers,
    // generated by _drupal_log_error() in the exact form required
997
    // by Drupal\simpletest\WebTestBase::error().
998
    if (preg_match('/^X-Drupal-Assertion-[0-9]+: (.*)$/', $header, $matches)) {
999
      // Call Drupal\simpletest\WebTestBase::error() with the parameters from the header.
1000
      call_user_func_array(array(&$this, 'error'), unserialize(urldecode($matches[1])));
For faster browsing, not all history is shown. View entire blame