Upgraded drupal core with security updates
[yaffs-website] / web / core / tests / Drupal / Tests / BrowserTestBase.php
1 <?php
2
3 namespace Drupal\Tests;
4
5 use Behat\Mink\Driver\GoutteDriver;
6 use Behat\Mink\Element\Element;
7 use Behat\Mink\Mink;
8 use Behat\Mink\Selector\SelectorsHandler;
9 use Behat\Mink\Session;
10 use Drupal\Component\Render\FormattableMarkup;
11 use Drupal\Component\Serialization\Json;
12 use Drupal\Component\Utility\Html;
13 use Drupal\Component\Utility\UrlHelper;
14 use Drupal\Core\Database\Database;
15 use Drupal\Core\Session\AccountInterface;
16 use Drupal\Core\Session\AnonymousUserSession;
17 use Drupal\Core\Site\Settings;
18 use Drupal\Core\StreamWrapper\StreamWrapperInterface;
19 use Drupal\Core\Test\FunctionalTestSetupTrait;
20 use Drupal\Core\Test\TestRunnerKernel;
21 use Drupal\Core\Test\TestSetupTrait;
22 use Drupal\Core\Url;
23 use Drupal\Core\Utility\Error;
24 use Drupal\FunctionalTests\AssertLegacyTrait;
25 use Drupal\simpletest\AssertHelperTrait;
26 use Drupal\simpletest\ContentTypeCreationTrait;
27 use Drupal\simpletest\BlockCreationTrait;
28 use Drupal\simpletest\NodeCreationTrait;
29 use Drupal\simpletest\UserCreationTrait;
30 use Symfony\Component\CssSelector\CssSelectorConverter;
31 use Symfony\Component\HttpFoundation\Request;
32 use Psr\Http\Message\RequestInterface;
33 use Psr\Http\Message\ResponseInterface;
34
35 /**
36  * Provides a test case for functional Drupal tests.
37  *
38  * Tests extending BrowserTestBase must exist in the
39  * Drupal\Tests\yourmodule\Functional namespace and live in the
40  * modules/yourmodule/tests/src/Functional directory.
41  *
42  * @ingroup testing
43  */
44 abstract class BrowserTestBase extends \PHPUnit_Framework_TestCase {
45
46   use FunctionalTestSetupTrait;
47   use TestSetupTrait;
48   use AssertHelperTrait;
49   use BlockCreationTrait {
50     placeBlock as drupalPlaceBlock;
51   }
52   use AssertLegacyTrait;
53   use RandomGeneratorTrait;
54   use SessionTestTrait;
55   use NodeCreationTrait {
56     getNodeByTitle as drupalGetNodeByTitle;
57     createNode as drupalCreateNode;
58   }
59   use ContentTypeCreationTrait {
60     createContentType as drupalCreateContentType;
61   }
62   use ConfigTestTrait;
63   use UserCreationTrait {
64     createRole as drupalCreateRole;
65     createUser as drupalCreateUser;
66   }
67   use XdebugRequestTrait;
68
69   /**
70    * The database prefix of this test run.
71    *
72    * @var string
73    */
74   protected $databasePrefix;
75
76   /**
77    * Time limit in seconds for the test.
78    *
79    * @var int
80    */
81   protected $timeLimit = 500;
82
83   /**
84    * The translation file directory for the test environment.
85    *
86    * This is set in BrowserTestBase::prepareEnvironment().
87    *
88    * @var string
89    */
90   protected $translationFilesDirectory;
91
92   /**
93    * The config importer that can be used in a test.
94    *
95    * @var \Drupal\Core\Config\ConfigImporter
96    */
97   protected $configImporter;
98
99   /**
100    * Modules to enable.
101    *
102    * The test runner will merge the $modules lists from this class, the class
103    * it extends, and so on up the class hierarchy. It is not necessary to
104    * include modules in your list that a parent class has already declared.
105    *
106    * @var string[]
107    *
108    * @see \Drupal\Tests\BrowserTestBase::installDrupal()
109    */
110   protected static $modules = [];
111
112   /**
113    * The profile to install as a basis for testing.
114    *
115    * @var string
116    */
117   protected $profile = 'testing';
118
119   /**
120    * The current user logged in using the Mink controlled browser.
121    *
122    * @var \Drupal\user\UserInterface
123    */
124   protected $loggedInUser = FALSE;
125
126   /**
127    * An array of custom translations suitable for drupal_rewrite_settings().
128    *
129    * @var array
130    */
131   protected $customTranslations;
132
133   /*
134    * Mink class for the default driver to use.
135    *
136    * Shoud be a fully qualified class name that implements
137    * Behat\Mink\Driver\DriverInterface.
138    *
139    * Value can be overridden using the environment variable MINK_DRIVER_CLASS.
140    *
141    * @var string.
142    */
143   protected $minkDefaultDriverClass = GoutteDriver::class;
144
145   /*
146    * Mink default driver params.
147    *
148    * If it's an array its contents are used as constructor params when default
149    * Mink driver class is instantiated.
150    *
151    * Can be overridden using the environment variable MINK_DRIVER_ARGS. In this
152    * case that variable should be a JSON array, for example:
153    * '["firefox", null, "http://localhost:4444/wd/hub"]'.
154    *
155    *
156    * @var array
157    */
158   protected $minkDefaultDriverArgs;
159
160   /**
161    * Mink session manager.
162    *
163    * This will not be initialized if there was an error during the test setup.
164    *
165    * @var \Behat\Mink\Mink|null
166    */
167   protected $mink;
168
169   /**
170    * {@inheritdoc}
171    *
172    * Browser tests are run in separate processes to prevent collisions between
173    * code that may be loaded by tests.
174    */
175   protected $runTestInSeparateProcess = TRUE;
176
177   /**
178    * {@inheritdoc}
179    */
180   protected $preserveGlobalState = FALSE;
181
182   /**
183    * Class name for HTML output logging.
184    *
185    * @var string
186    */
187   protected $htmlOutputClassName;
188
189   /**
190    * Directory name for HTML output logging.
191    *
192    * @var string
193    */
194   protected $htmlOutputDirectory;
195
196   /**
197    * Counter storage for HTML output logging.
198    *
199    * @var string
200    */
201   protected $htmlOutputCounterStorage;
202
203   /**
204    * Counter for HTML output logging.
205    *
206    * @var int
207    */
208   protected $htmlOutputCounter = 1;
209
210   /**
211    * HTML output output enabled.
212    *
213    * @var bool
214    */
215   protected $htmlOutputEnabled = FALSE;
216
217   /**
218    * The file name to write the list of URLs to.
219    *
220    * This file is read by the PHPUnit result printer.
221    *
222    * @var string
223    *
224    * @see \Drupal\Tests\Listeners\HtmlOutputPrinter
225    */
226   protected $htmlOutputFile;
227
228   /**
229    * HTML output test ID.
230    *
231    * @var int
232    */
233   protected $htmlOutputTestId;
234
235   /**
236    * The base URL.
237    *
238    * @var string
239    */
240   protected $baseUrl;
241
242   /**
243    * The original array of shutdown function callbacks.
244    *
245    * @var array
246    */
247   protected $originalShutdownCallbacks = [];
248
249   /**
250    * The number of meta refresh redirects to follow, or NULL if unlimited.
251    *
252    * @var null|int
253    */
254   protected $maximumMetaRefreshCount = NULL;
255
256   /**
257    * The number of meta refresh redirects followed during ::drupalGet().
258    *
259    * @var int
260    */
261   protected $metaRefreshCount = 0;
262
263   /**
264    * Initializes Mink sessions.
265    */
266   protected function initMink() {
267     $driver = $this->getDefaultDriverInstance();
268
269     if ($driver instanceof GoutteDriver) {
270       // Turn off curl timeout. Having a timeout is not a problem in a normal
271       // test running, but it is a problem when debugging. Also, disable SSL
272       // peer verification so that testing under HTTPS always works.
273       /** @var \GuzzleHttp\Client $client */
274       $client = $this->container->get('http_client_factory')->fromOptions([
275         'timeout' => NULL,
276         'verify' => FALSE,
277       ]);
278
279       // Inject a Guzzle middleware to generate debug output for every request
280       // performed in the test.
281       $handler_stack = $client->getConfig('handler');
282       $handler_stack->push($this->getResponseLogHandler());
283
284       $driver->getClient()->setClient($client);
285     }
286
287     $selectors_handler = new SelectorsHandler([
288       'hidden_field_selector' => new HiddenFieldSelector()
289     ]);
290     $session = new Session($driver, $selectors_handler);
291     $this->mink = new Mink();
292     $this->mink->registerSession('default', $session);
293     $this->mink->setDefaultSessionName('default');
294     $this->registerSessions();
295
296     // According to the W3C WebDriver specification a cookie can only be set if
297     // the cookie domain is equal to the domain of the active document. When the
298     // browser starts up the active document is not our domain but 'about:blank'
299     // or similar. To be able to set our User-Agent and Xdebug cookies at the
300     // start of the test we now do a request to the front page so the active
301     // document matches the domain.
302     // @see https://w3c.github.io/webdriver/webdriver-spec.html#add-cookie
303     // @see https://www.w3.org/Bugs/Public/show_bug.cgi?id=20975
304     $session = $this->getSession();
305     $session->visit($this->baseUrl);
306
307     return $session;
308   }
309
310   /**
311    * Gets an instance of the default Mink driver.
312    *
313    * @return Behat\Mink\Driver\DriverInterface
314    *   Instance of default Mink driver.
315    *
316    * @throws \InvalidArgumentException
317    *   When provided default Mink driver class can't be instantiated.
318    */
319   protected function getDefaultDriverInstance() {
320     // Get default driver params from environment if availables.
321     if ($arg_json = getenv('MINK_DRIVER_ARGS')) {
322       $this->minkDefaultDriverArgs = json_decode($arg_json);
323     }
324
325     // Get and check default driver class from environment if availables.
326     if ($minkDriverClass = getenv('MINK_DRIVER_CLASS')) {
327       if (class_exists($minkDriverClass)) {
328         $this->minkDefaultDriverClass = $minkDriverClass;
329       }
330       else {
331         throw new \InvalidArgumentException("Can't instantiate provided $minkDriverClass class by environment as default driver class.");
332       }
333     }
334
335     if (is_array($this->minkDefaultDriverArgs)) {
336       // Use ReflectionClass to instantiate class with received params.
337       $reflector = new \ReflectionClass($this->minkDefaultDriverClass);
338       $driver = $reflector->newInstanceArgs($this->minkDefaultDriverArgs);
339     }
340     else {
341       $driver = new $this->minkDefaultDriverClass();
342     }
343     return $driver;
344   }
345
346   /**
347    * Provides a Guzzle middleware handler to log every response received.
348    *
349    * @return callable
350    *   The callable handler that will do the logging.
351    */
352   protected function getResponseLogHandler() {
353     return function (callable $handler) {
354       return function (RequestInterface $request, array $options) use ($handler) {
355         return $handler($request, $options)
356           ->then(function (ResponseInterface $response) use ($request) {
357             if ($this->htmlOutputEnabled) {
358
359               $caller = $this->getTestMethodCaller();
360               $html_output = 'Called from ' . $caller['function'] . ' line ' . $caller['line'];
361               $html_output .= '<hr />' . $request->getMethod() . ' request to: ' . $request->getUri();
362
363               // On redirect responses (status code starting with '3') we need
364               // to remove the meta tag that would do a browser refresh. We
365               // don't want to redirect developers away when they look at the
366               // debug output file in their browser.
367               $body = $response->getBody();
368               $status_code = (string) $response->getStatusCode();
369               if ($status_code[0] === '3') {
370                 $body = preg_replace('#<meta http-equiv="refresh" content=.+/>#', '', $body, 1);
371               }
372               $html_output .= '<hr />' . $body;
373               $html_output .= $this->formatHtmlOutputHeaders($response->getHeaders());
374
375               $this->htmlOutput($html_output);
376             }
377             return $response;
378           });
379       };
380     };
381   }
382
383   /**
384    * Registers additional Mink sessions.
385    *
386    * Tests wishing to use a different driver or change the default driver should
387    * override this method.
388    *
389    * @code
390    *   // Register a new session that uses the MinkPonyDriver.
391    *   $pony = new MinkPonyDriver();
392    *   $session = new Session($pony);
393    *   $this->mink->registerSession('pony', $session);
394    * @endcode
395    */
396   protected function registerSessions() {}
397
398   /**
399    * {@inheritdoc}
400    */
401   protected function setUp() {
402     global $base_url;
403     parent::setUp();
404
405     // Get and set the domain of the environment we are running our test
406     // coverage against.
407     $base_url = getenv('SIMPLETEST_BASE_URL');
408     if (!$base_url) {
409       throw new \Exception(
410         'You must provide a SIMPLETEST_BASE_URL environment variable to run some PHPUnit based functional tests.'
411       );
412     }
413
414     // Setup $_SERVER variable.
415     $parsed_url = parse_url($base_url);
416     $host = $parsed_url['host'] . (isset($parsed_url['port']) ? ':' . $parsed_url['port'] : '');
417     $path = isset($parsed_url['path']) ? rtrim(rtrim($parsed_url['path']), '/') : '';
418     $port = isset($parsed_url['port']) ? $parsed_url['port'] : 80;
419
420     $this->baseUrl = $base_url;
421
422     // If the passed URL schema is 'https' then setup the $_SERVER variables
423     // properly so that testing will run under HTTPS.
424     if ($parsed_url['scheme'] === 'https') {
425       $_SERVER['HTTPS'] = 'on';
426     }
427     $_SERVER['HTTP_HOST'] = $host;
428     $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
429     $_SERVER['SERVER_ADDR'] = '127.0.0.1';
430     $_SERVER['SERVER_PORT'] = $port;
431     $_SERVER['SERVER_SOFTWARE'] = NULL;
432     $_SERVER['SERVER_NAME'] = 'localhost';
433     $_SERVER['REQUEST_URI'] = $path . '/';
434     $_SERVER['REQUEST_METHOD'] = 'GET';
435     $_SERVER['SCRIPT_NAME'] = $path . '/index.php';
436     $_SERVER['SCRIPT_FILENAME'] = $path . '/index.php';
437     $_SERVER['PHP_SELF'] = $path . '/index.php';
438     $_SERVER['HTTP_USER_AGENT'] = 'Drupal command line';
439
440     // Install Drupal test site.
441     $this->prepareEnvironment();
442     $this->installDrupal();
443
444     // Setup Mink.
445     $session = $this->initMink();
446
447     $cookies = $this->extractCookiesFromRequest(\Drupal::request());
448     foreach ($cookies as $cookie_name => $values) {
449       foreach ($values as $value) {
450         $session->setCookie($cookie_name, $value);
451       }
452     }
453
454     // Creates the directory to store browser output in if a file to write
455     // URLs to has been created by \Drupal\Tests\Listeners\HtmlOutputPrinter.
456     $browser_output_file = getenv('BROWSERTEST_OUTPUT_FILE');
457     $this->htmlOutputEnabled = is_file($browser_output_file);
458     if ($this->htmlOutputEnabled) {
459       $this->htmlOutputFile = $browser_output_file;
460       $this->htmlOutputClassName = str_replace("\\", "_", get_called_class());
461       $this->htmlOutputDirectory = DRUPAL_ROOT . '/sites/simpletest/browser_output';
462       if (file_prepare_directory($this->htmlOutputDirectory, FILE_CREATE_DIRECTORY) && !file_exists($this->htmlOutputDirectory . '/.htaccess')) {
463         file_put_contents($this->htmlOutputDirectory . '/.htaccess', "<IfModule mod_expires.c>\nExpiresActive Off\n</IfModule>\n");
464       }
465       $this->htmlOutputCounterStorage = $this->htmlOutputDirectory . '/' . $this->htmlOutputClassName . '.counter';
466       $this->htmlOutputTestId = str_replace('sites/simpletest/', '', $this->siteDirectory);
467       if (is_file($this->htmlOutputCounterStorage)) {
468         $this->htmlOutputCounter = max(1, (int) file_get_contents($this->htmlOutputCounterStorage)) + 1;
469       }
470     }
471   }
472
473   /**
474    * Ensures test files are deletable within file_unmanaged_delete_recursive().
475    *
476    * Some tests chmod generated files to be read only. During
477    * BrowserTestBase::cleanupEnvironment() and other cleanup operations,
478    * these files need to get deleted too.
479    *
480    * @param string $path
481    *   The file path.
482    */
483   public static function filePreDeleteCallback($path) {
484     // When the webserver runs with the same system user as phpunit, we can
485     // make read-only files writable again. If not, chmod will fail while the
486     // file deletion still works if file permissions have been configured
487     // correctly. Thus, we ignore any problems while running chmod.
488     @chmod($path, 0700);
489   }
490
491   /**
492    * Clean up the Simpletest environment.
493    */
494   protected function cleanupEnvironment() {
495     // Remove all prefixed tables.
496     $original_connection_info = Database::getConnectionInfo('simpletest_original_default');
497     $original_prefix = $original_connection_info['default']['prefix']['default'];
498     $test_connection_info = Database::getConnectionInfo('default');
499     $test_prefix = $test_connection_info['default']['prefix']['default'];
500     if ($original_prefix != $test_prefix) {
501       $tables = Database::getConnection()->schema()->findTables('%');
502       foreach ($tables as $table) {
503         if (Database::getConnection()->schema()->dropTable($table)) {
504           unset($tables[$table]);
505         }
506       }
507     }
508
509     // Delete test site directory.
510     file_unmanaged_delete_recursive($this->siteDirectory, [$this, 'filePreDeleteCallback']);
511   }
512
513   /**
514    * {@inheritdoc}
515    */
516   protected function tearDown() {
517     parent::tearDown();
518
519     // Destroy the testing kernel.
520     if (isset($this->kernel)) {
521       $this->cleanupEnvironment();
522       $this->kernel->shutdown();
523     }
524
525     // Ensure that internal logged in variable is reset.
526     $this->loggedInUser = FALSE;
527
528     if ($this->mink) {
529       $this->mink->stopSessions();
530     }
531
532     // Restore original shutdown callbacks.
533     if (function_exists('drupal_register_shutdown_function')) {
534       $callbacks = &drupal_register_shutdown_function();
535       $callbacks = $this->originalShutdownCallbacks;
536     }
537   }
538
539   /**
540    * Returns Mink session.
541    *
542    * @param string $name
543    *   (optional) Name of the session. Defaults to the active session.
544    *
545    * @return \Behat\Mink\Session
546    *   The active Mink session object.
547    */
548   public function getSession($name = NULL) {
549     return $this->mink->getSession($name);
550   }
551
552   /**
553    * Returns WebAssert object.
554    *
555    * @param string $name
556    *   (optional) Name of the session. Defaults to the active session.
557    *
558    * @return \Drupal\Tests\WebAssert
559    *   A new web-assert option for asserting the presence of elements with.
560    */
561   public function assertSession($name = NULL) {
562     return new WebAssert($this->getSession($name), $this->baseUrl);
563   }
564
565   /**
566    * Prepare for a request to testing site.
567    *
568    * The testing site is protected via a SIMPLETEST_USER_AGENT cookie that is
569    * checked by drupal_valid_test_ua().
570    *
571    * @see drupal_valid_test_ua()
572    */
573   protected function prepareRequest() {
574     $session = $this->getSession();
575     $session->setCookie('SIMPLETEST_USER_AGENT', drupal_generate_test_ua($this->databasePrefix));
576   }
577
578   /**
579    * Builds an a absolute URL from a system path or a URL object.
580    *
581    * @param string|\Drupal\Core\Url $path
582    *   A system path or a URL.
583    * @param array $options
584    *   Options to be passed to Url::fromUri().
585    *
586    * @return string
587    *   An absolute URL stsring.
588    */
589   protected function buildUrl($path, array $options = []) {
590     if ($path instanceof Url) {
591       $url_options = $path->getOptions();
592       $options = $url_options + $options;
593       $path->setOptions($options);
594       return $path->setAbsolute()->toString();
595     }
596     // The URL generator service is not necessarily available yet; e.g., in
597     // interactive installer tests.
598     elseif ($this->container->has('url_generator')) {
599       $force_internal = isset($options['external']) && $options['external'] == FALSE;
600       if (!$force_internal && UrlHelper::isExternal($path)) {
601         return Url::fromUri($path, $options)->toString();
602       }
603       else {
604         $uri = $path === '<front>' ? 'base:/' : 'base:/' . $path;
605         // Path processing is needed for language prefixing.  Skip it when a
606         // path that may look like an external URL is being used as internal.
607         $options['path_processing'] = !$force_internal;
608         return Url::fromUri($uri, $options)
609           ->setAbsolute()
610           ->toString();
611       }
612     }
613     else {
614       return $this->getAbsoluteUrl($path);
615     }
616   }
617
618   /**
619    * Retrieves a Drupal path or an absolute path.
620    *
621    * @param string|\Drupal\Core\Url $path
622    *   Drupal path or URL to load into Mink controlled browser.
623    * @param array $options
624    *   (optional) Options to be forwarded to the url generator.
625    * @param string[] $headers
626    *   An array containing additional HTTP request headers, the array keys are
627    *   the header names and the array values the header values. This is useful
628    *   to set for example the "Accept-Language" header for requesting the page
629    *   in a different language. Note that not all headers are supported, for
630    *   example the "Accept" header is always overridden by the browser. For
631    *   testing REST APIs it is recommended to directly use an HTTP client such
632    *   as Guzzle instead.
633    *
634    * @return string
635    *   The retrieved HTML string, also available as $this->getRawContent()
636    */
637   protected function drupalGet($path, array $options = [], array $headers = []) {
638     $options['absolute'] = TRUE;
639     $url = $this->buildUrl($path, $options);
640
641     $session = $this->getSession();
642
643     $this->prepareRequest();
644     foreach ($headers as $header_name => $header_value) {
645       $session->setRequestHeader($header_name, $header_value);
646     }
647
648     $session->visit($url);
649     $out = $session->getPage()->getContent();
650
651     // Ensure that any changes to variables in the other thread are picked up.
652     $this->refreshVariables();
653
654     // Replace original page output with new output from redirected page(s).
655     if ($new = $this->checkForMetaRefresh()) {
656       $out = $new;
657       // We are finished with all meta refresh redirects, so reset the counter.
658       $this->metaRefreshCount = 0;
659     }
660
661     // Log only for JavascriptTestBase tests because for Goutte we log with
662     // ::getResponseLogHandler.
663     if ($this->htmlOutputEnabled && !($this->getSession()->getDriver() instanceof GoutteDriver)) {
664       $html_output = 'GET request to: ' . $url .
665         '<hr />Ending URL: ' . $this->getSession()->getCurrentUrl();
666       $html_output .= '<hr />' . $out;
667       $html_output .= $this->getHtmlOutputHeaders();
668       $this->htmlOutput($html_output);
669     }
670
671     return $out;
672   }
673
674   /**
675    * Takes a path and returns an absolute path.
676    *
677    * @param string $path
678    *   A path from the Mink controlled browser content.
679    *
680    * @return string
681    *   The $path with $base_url prepended, if necessary.
682    */
683   protected function getAbsoluteUrl($path) {
684     global $base_url, $base_path;
685
686     $parts = parse_url($path);
687     if (empty($parts['host'])) {
688       // Ensure that we have a string (and no xpath object).
689       $path = (string) $path;
690       // Strip $base_path, if existent.
691       $length = strlen($base_path);
692       if (substr($path, 0, $length) === $base_path) {
693         $path = substr($path, $length);
694       }
695       // Ensure that we have an absolute path.
696       if (empty($path) || $path[0] !== '/') {
697         $path = '/' . $path;
698       }
699       // Finally, prepend the $base_url.
700       $path = $base_url . $path;
701     }
702     return $path;
703   }
704
705   /**
706    * Logs in a user using the Mink controlled browser.
707    *
708    * If a user is already logged in, then the current user is logged out before
709    * logging in the specified user.
710    *
711    * Please note that neither the current user nor the passed-in user object is
712    * populated with data of the logged in user. If you need full access to the
713    * user object after logging in, it must be updated manually. If you also need
714    * access to the plain-text password of the user (set by drupalCreateUser()),
715    * e.g. to log in the same user again, then it must be re-assigned manually.
716    * For example:
717    * @code
718    *   // Create a user.
719    *   $account = $this->drupalCreateUser(array());
720    *   $this->drupalLogin($account);
721    *   // Load real user object.
722    *   $pass_raw = $account->passRaw;
723    *   $account = User::load($account->id());
724    *   $account->passRaw = $pass_raw;
725    * @endcode
726    *
727    * @param \Drupal\Core\Session\AccountInterface $account
728    *   User object representing the user to log in.
729    *
730    * @see drupalCreateUser()
731    */
732   protected function drupalLogin(AccountInterface $account) {
733     if ($this->loggedInUser) {
734       $this->drupalLogout();
735     }
736
737     $this->drupalGet('user/login');
738     $this->assertSession()->statusCodeEquals(200);
739     $this->submitForm([
740       'name' => $account->getUsername(),
741       'pass' => $account->passRaw,
742     ], t('Log in'));
743
744     // @see BrowserTestBase::drupalUserIsLoggedIn()
745     $account->sessionId = $this->getSession()->getCookie($this->getSessionName());
746     $this->assertTrue($this->drupalUserIsLoggedIn($account), new FormattableMarkup('User %name successfully logged in.', ['%name' => $account->getAccountName()]));
747
748     $this->loggedInUser = $account;
749     $this->container->get('current_user')->setAccount($account);
750   }
751
752   /**
753    * Logs a user out of the Mink controlled browser and confirms.
754    *
755    * Confirms logout by checking the login page.
756    */
757   protected function drupalLogout() {
758     // Make a request to the logout page, and redirect to the user page, the
759     // idea being if you were properly logged out you should be seeing a login
760     // screen.
761     $assert_session = $this->assertSession();
762     $this->drupalGet('user/logout', ['query' => ['destination' => 'user']]);
763     $assert_session->statusCodeEquals(200);
764     $assert_session->fieldExists('name');
765     $assert_session->fieldExists('pass');
766
767     // @see BrowserTestBase::drupalUserIsLoggedIn()
768     unset($this->loggedInUser->sessionId);
769     $this->loggedInUser = FALSE;
770     $this->container->get('current_user')->setAccount(new AnonymousUserSession());
771   }
772
773   /**
774    * Fills and submits a form.
775    *
776    * @param array $edit
777    *   Field data in an associative array. Changes the current input fields
778    *   (where possible) to the values indicated.
779    *
780    *   A checkbox can be set to TRUE to be checked and should be set to FALSE to
781    *   be unchecked.
782    * @param string $submit
783    *   Value of the submit button whose click is to be emulated. For example,
784    *   t('Save'). The processing of the request depends on this value. For
785    *   example, a form may have one button with the value t('Save') and another
786    *   button with the value t('Delete'), and execute different code depending
787    *   on which one is clicked.
788    * @param string $form_html_id
789    *   (optional) HTML ID of the form to be submitted. On some pages
790    *   there are many identical forms, so just using the value of the submit
791    *   button is not enough. For example: 'trigger-node-presave-assign-form'.
792    *   Note that this is not the Drupal $form_id, but rather the HTML ID of the
793    *   form, which is typically the same thing but with hyphens replacing the
794    *   underscores.
795    */
796   protected function submitForm(array $edit, $submit, $form_html_id = NULL) {
797     $assert_session = $this->assertSession();
798
799     // Get the form.
800     if (isset($form_html_id)) {
801       $form = $assert_session->elementExists('xpath', "//form[@id='$form_html_id']");
802       $submit_button = $assert_session->buttonExists($submit, $form);
803       $action = $form->getAttribute('action');
804     }
805     else {
806       $submit_button = $assert_session->buttonExists($submit);
807       $form = $assert_session->elementExists('xpath', './ancestor::form', $submit_button);
808       $action = $form->getAttribute('action');
809     }
810
811     // Edit the form values.
812     foreach ($edit as $name => $value) {
813       $field = $assert_session->fieldExists($name, $form);
814
815       // Provide support for the values '1' and '0' for checkboxes instead of
816       // TRUE and FALSE.
817       // @todo Get rid of supporting 1/0 by converting all tests cases using
818       // this to boolean values.
819       $field_type = $field->getAttribute('type');
820       if ($field_type === 'checkbox') {
821         $value = (bool) $value;
822       }
823
824       $field->setValue($value);
825     }
826
827     // Submit form.
828     $this->prepareRequest();
829     $submit_button->press();
830
831     // Ensure that any changes to variables in the other thread are picked up.
832     $this->refreshVariables();
833
834     // Check if there are any meta refresh redirects (like Batch API pages).
835     if ($this->checkForMetaRefresh()) {
836       // We are finished with all meta refresh redirects, so reset the counter.
837       $this->metaRefreshCount = 0;
838     }
839
840     // Log only for JavascriptTestBase tests because for Goutte we log with
841     // ::getResponseLogHandler.
842     if ($this->htmlOutputEnabled && !($this->getSession()->getDriver() instanceof GoutteDriver)) {
843       $out = $this->getSession()->getPage()->getContent();
844       $html_output = 'POST request to: ' . $action .
845         '<hr />Ending URL: ' . $this->getSession()->getCurrentUrl();
846       $html_output .= '<hr />' . $out;
847       $html_output .= $this->getHtmlOutputHeaders();
848       $this->htmlOutput($html_output);
849     }
850
851   }
852
853   /**
854    * Executes a form submission.
855    *
856    * It will be done as usual POST request with Mink.
857    *
858    * @param \Drupal\Core\Url|string $path
859    *   Location of the post form. Either a Drupal path or an absolute path or
860    *   NULL to post to the current page. For multi-stage forms you can set the
861    *   path to NULL and have it post to the last received page. Example:
862    *
863    *   @code
864    *   // First step in form.
865    *   $edit = array(...);
866    *   $this->drupalPostForm('some_url', $edit, t('Save'));
867    *
868    *   // Second step in form.
869    *   $edit = array(...);
870    *   $this->drupalPostForm(NULL, $edit, t('Save'));
871    *   @endcode
872    * @param array $edit
873    *   Field data in an associative array. Changes the current input fields
874    *   (where possible) to the values indicated.
875    *
876    *   When working with form tests, the keys for an $edit element should match
877    *   the 'name' parameter of the HTML of the form. For example, the 'body'
878    *   field for a node has the following HTML:
879    *   @code
880    *   <textarea id="edit-body-und-0-value" class="text-full form-textarea
881    *    resize-vertical" placeholder="" cols="60" rows="9"
882    *    name="body[0][value]"></textarea>
883    *   @endcode
884    *   When testing this field using an $edit parameter, the code becomes:
885    *   @code
886    *   $edit["body[0][value]"] = 'My test value';
887    *   @endcode
888    *
889    *   A checkbox can be set to TRUE to be checked and should be set to FALSE to
890    *   be unchecked. Multiple select fields can be tested using 'name[]' and
891    *   setting each of the desired values in an array:
892    *   @code
893    *   $edit = array();
894    *   $edit['name[]'] = array('value1', 'value2');
895    *   @endcode
896    *   @todo change $edit to disallow NULL as a value for Drupal 9.
897    *     https://www.drupal.org/node/2802401
898    * @param string $submit
899    *   Value of the submit button whose click is to be emulated. For example,
900    *   t('Save'). The processing of the request depends on this value. For
901    *   example, a form may have one button with the value t('Save') and another
902    *   button with the value t('Delete'), and execute different code depending
903    *   on which one is clicked.
904    *
905    *   This function can also be called to emulate an Ajax submission. In this
906    *   case, this value needs to be an array with the following keys:
907    *   - path: A path to submit the form values to for Ajax-specific processing.
908    *   - triggering_element: If the value for the 'path' key is a generic Ajax
909    *     processing path, this needs to be set to the name of the element. If
910    *     the name doesn't identify the element uniquely, then this should
911    *     instead be an array with a single key/value pair, corresponding to the
912    *     element name and value. The \Drupal\Core\Form\FormAjaxResponseBuilder
913    *     uses this to find the #ajax information for the element, including
914    *     which specific callback to use for processing the request.
915    *
916    *   This can also be set to NULL in order to emulate an Internet Explorer
917    *   submission of a form with a single text field, and pressing ENTER in that
918    *   textfield: under these conditions, no button information is added to the
919    *   POST data.
920    * @param array $options
921    *   Options to be forwarded to the url generator.
922    */
923   protected function drupalPostForm($path, $edit, $submit, array $options = []) {
924     if (is_object($submit)) {
925       // Cast MarkupInterface objects to string.
926       $submit = (string) $submit;
927     }
928     if ($edit === NULL) {
929       $edit = [];
930     }
931     if (is_array($edit)) {
932       $edit = $this->castSafeStrings($edit);
933     }
934
935     if (isset($path)) {
936       $this->drupalGet($path, $options);
937     }
938
939     $this->submitForm($edit, $submit);
940   }
941
942   /**
943    * Helper function to get the options of select field.
944    *
945    * @param \Behat\Mink\Element\NodeElement|string $select
946    *   Name, ID, or Label of select field to assert.
947    * @param \Behat\Mink\Element\Element $container
948    *   (optional) Container element to check against. Defaults to current page.
949    *
950    * @return array
951    *   Associative array of option keys and values.
952    */
953   protected function getOptions($select, Element $container = NULL) {
954     if (is_string($select)) {
955       $select = $this->assertSession()->selectExists($select, $container);
956     }
957     $options = [];
958     /* @var \Behat\Mink\Element\NodeElement $option */
959     foreach ($select->findAll('xpath', '//option') as $option) {
960       $label = $option->getText();
961       $value = $option->getAttribute('value') ?: $label;
962       $options[$value] = $label;
963     }
964     return $options;
965   }
966
967   /**
968    * Installs Drupal into the Simpletest site.
969    */
970   public function installDrupal() {
971     $this->initUserSession();
972     $this->prepareSettings();
973     $this->doInstall();
974     $this->initSettings();
975     $container = $this->initKernel(\Drupal::request());
976     $this->initConfig($container);
977     $this->installModulesFromClassProperty($container);
978     $this->rebuildAll();
979   }
980
981   /**
982    * Returns the parameters that will be used when Simpletest installs Drupal.
983    *
984    * @see install_drupal()
985    * @see install_state_defaults()
986    */
987   protected function installParameters() {
988     $connection_info = Database::getConnectionInfo();
989     $driver = $connection_info['default']['driver'];
990     $connection_info['default']['prefix'] = $connection_info['default']['prefix']['default'];
991     unset($connection_info['default']['driver']);
992     unset($connection_info['default']['namespace']);
993     unset($connection_info['default']['pdo']);
994     unset($connection_info['default']['init_commands']);
995     $parameters = [
996       'interactive' => FALSE,
997       'parameters' => [
998         'profile' => $this->profile,
999         'langcode' => 'en',
1000       ],
1001       'forms' => [
1002         'install_settings_form' => [
1003           'driver' => $driver,
1004           $driver => $connection_info['default'],
1005         ],
1006         'install_configure_form' => [
1007           'site_name' => 'Drupal',
1008           'site_mail' => 'simpletest@example.com',
1009           'account' => [
1010             'name' => $this->rootUser->name,
1011             'mail' => $this->rootUser->getEmail(),
1012             'pass' => [
1013               'pass1' => $this->rootUser->pass_raw,
1014               'pass2' => $this->rootUser->pass_raw,
1015             ],
1016           ],
1017           // form_type_checkboxes_value() requires NULL instead of FALSE values
1018           // for programmatic form submissions to disable a checkbox.
1019           'enable_update_status_module' => NULL,
1020           'enable_update_status_emails' => NULL,
1021         ],
1022       ],
1023     ];
1024     return $parameters;
1025   }
1026
1027   /**
1028    * Prepares the current environment for running the test.
1029    *
1030    * Also sets up new resources for the testing environment, such as the public
1031    * filesystem and configuration directories.
1032    *
1033    * This method is private as it must only be called once by
1034    * BrowserTestBase::setUp() (multiple invocations for the same test would have
1035    * unpredictable consequences) and it must not be callable or overridable by
1036    * test classes.
1037    */
1038   protected function prepareEnvironment() {
1039     // Bootstrap Drupal so we can use Drupal's built in functions.
1040     $this->classLoader = require __DIR__ . '/../../../../autoload.php';
1041     $request = Request::createFromGlobals();
1042     $kernel = TestRunnerKernel::createFromRequest($request, $this->classLoader);
1043     // TestRunnerKernel expects the working directory to be DRUPAL_ROOT.
1044     chdir(DRUPAL_ROOT);
1045     $kernel->prepareLegacyRequest($request);
1046     $this->prepareDatabasePrefix();
1047
1048     $this->originalSite = $kernel->findSitePath($request);
1049
1050     // Create test directory ahead of installation so fatal errors and debug
1051     // information can be logged during installation process.
1052     file_prepare_directory($this->siteDirectory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
1053
1054     // Prepare filesystem directory paths.
1055     $this->publicFilesDirectory = $this->siteDirectory . '/files';
1056     $this->privateFilesDirectory = $this->siteDirectory . '/private';
1057     $this->tempFilesDirectory = $this->siteDirectory . '/temp';
1058     $this->translationFilesDirectory = $this->siteDirectory . '/translations';
1059
1060     // Ensure the configImporter is refreshed for each test.
1061     $this->configImporter = NULL;
1062
1063     // Unregister all custom stream wrappers of the parent site.
1064     $wrappers = \Drupal::service('stream_wrapper_manager')->getWrappers(StreamWrapperInterface::ALL);
1065     foreach ($wrappers as $scheme => $info) {
1066       stream_wrapper_unregister($scheme);
1067     }
1068
1069     // Reset statics.
1070     drupal_static_reset();
1071
1072     // Ensure there is no service container.
1073     $this->container = NULL;
1074     \Drupal::unsetContainer();
1075
1076     // Unset globals.
1077     unset($GLOBALS['config_directories']);
1078     unset($GLOBALS['config']);
1079     unset($GLOBALS['conf']);
1080
1081     // Log fatal errors.
1082     ini_set('log_errors', 1);
1083     ini_set('error_log', DRUPAL_ROOT . '/' . $this->siteDirectory . '/error.log');
1084
1085     // Change the database prefix.
1086     $this->changeDatabasePrefix();
1087
1088     // After preparing the environment and changing the database prefix, we are
1089     // in a valid test environment.
1090     drupal_valid_test_ua($this->databasePrefix);
1091
1092     // Reset settings.
1093     new Settings([
1094       // For performance, simply use the database prefix as hash salt.
1095       'hash_salt' => $this->databasePrefix,
1096     ]);
1097
1098     drupal_set_time_limit($this->timeLimit);
1099
1100     // Save and clean the shutdown callbacks array because it is static cached
1101     // and will be changed by the test run. Otherwise it will contain callbacks
1102     // from both environments and the testing environment will try to call the
1103     // handlers defined by the original one.
1104     $callbacks = &drupal_register_shutdown_function();
1105     $this->originalShutdownCallbacks = $callbacks;
1106     $callbacks = [];
1107   }
1108
1109   /**
1110    * Returns whether a given user account is logged in.
1111    *
1112    * @param \Drupal\Core\Session\AccountInterface $account
1113    *   The user account object to check.
1114    *
1115    * @return bool
1116    *   Return TRUE if the user is logged in, FALSE otherwise.
1117    */
1118   protected function drupalUserIsLoggedIn(AccountInterface $account) {
1119     $logged_in = FALSE;
1120
1121     if (isset($account->sessionId)) {
1122       $session_handler = $this->container->get('session_handler.storage');
1123       $logged_in = (bool) $session_handler->read($account->sessionId);
1124     }
1125
1126     return $logged_in;
1127   }
1128
1129   /**
1130    * Clicks the element with the given CSS selector.
1131    *
1132    * @param string $css_selector
1133    *   The CSS selector identifying the element to click.
1134    */
1135   protected function click($css_selector) {
1136     $this->getSession()->getDriver()->click($this->cssSelectToXpath($css_selector));
1137   }
1138
1139   /**
1140    * Prevents serializing any properties.
1141    *
1142    * Browser tests are run in a separate process. To do this PHPUnit creates a
1143    * script to run the test. If it fails, the test result object will contain a
1144    * stack trace which includes the test object. It will attempt to serialize
1145    * it. Returning an empty array prevents it from serializing anything it
1146    * should not.
1147    *
1148    * @return array
1149    *   An empty array.
1150    *
1151    * @see vendor/phpunit/phpunit/src/Util/PHP/Template/TestCaseMethod.tpl.dist
1152    */
1153   public function __sleep() {
1154     return [];
1155   }
1156
1157   /**
1158    * Logs a HTML output message in a text file.
1159    *
1160    * The link to the HTML output message will be printed by the results printer.
1161    *
1162    * @param string $message
1163    *   The HTML output message to be stored.
1164    *
1165    * @see \Drupal\Tests\Listeners\VerbosePrinter::printResult()
1166    */
1167   protected function htmlOutput($message) {
1168     if (!$this->htmlOutputEnabled) {
1169       return;
1170     }
1171     $message = '<hr />ID #' . $this->htmlOutputCounter . ' (<a href="' . $this->htmlOutputClassName . '-' . ($this->htmlOutputCounter - 1) . '-' . $this->htmlOutputTestId . '.html">Previous</a> | <a href="' . $this->htmlOutputClassName . '-' . ($this->htmlOutputCounter + 1) . '-' . $this->htmlOutputTestId . '.html">Next</a>)<hr />' . $message;
1172     $html_output_filename = $this->htmlOutputClassName . '-' . $this->htmlOutputCounter . '-' . $this->htmlOutputTestId . '.html';
1173     file_put_contents($this->htmlOutputDirectory . '/' . $html_output_filename, $message);
1174     file_put_contents($this->htmlOutputCounterStorage, $this->htmlOutputCounter++);
1175     file_put_contents($this->htmlOutputFile, file_create_url('sites/simpletest/browser_output/' . $html_output_filename) . "\n", FILE_APPEND);
1176   }
1177
1178   /**
1179    * Returns headers in HTML output format.
1180    *
1181    * @return string
1182    *   HTML output headers.
1183    */
1184   protected function getHtmlOutputHeaders() {
1185     return $this->formatHtmlOutputHeaders($this->getSession()->getResponseHeaders());
1186   }
1187
1188   /**
1189    * Formats HTTP headers as string for HTML output logging.
1190    *
1191    * @param array[] $headers
1192    *   Headers that should be formatted.
1193    *
1194    * @return string
1195    *   The formatted HTML string.
1196    */
1197   protected function formatHtmlOutputHeaders(array $headers) {
1198     $flattened_headers = array_map(function($header) {
1199       if (is_array($header)) {
1200         return implode(';', array_map('trim', $header));
1201       }
1202       else {
1203         return $header;
1204       }
1205     }, $headers);
1206     return '<hr />Headers: <pre>' . Html::escape(var_export($flattened_headers, TRUE)) . '</pre>';
1207   }
1208
1209   /**
1210    * Translates a CSS expression to its XPath equivalent.
1211    *
1212    * The search is relative to the root element (HTML tag normally) of the page.
1213    *
1214    * @param string $selector
1215    *   CSS selector to use in the search.
1216    * @param bool $html
1217    *   (optional) Enables HTML support. Disable it for XML documents.
1218    * @param string $prefix
1219    *   (optional) The prefix for the XPath expression.
1220    *
1221    * @return string
1222    *   The equivalent XPath of a CSS expression.
1223    */
1224   protected function cssSelectToXpath($selector, $html = TRUE, $prefix = 'descendant-or-self::') {
1225     return (new CssSelectorConverter($html))->toXPath($selector, $prefix);
1226   }
1227
1228   /**
1229    * Searches elements using a CSS selector in the raw content.
1230    *
1231    * The search is relative to the root element (HTML tag normally) of the page.
1232    *
1233    * @param string $selector
1234    *   CSS selector to use in the search.
1235    *
1236    * @return \Behat\Mink\Element\NodeElement[]
1237    *   The list of elements on the page that match the selector.
1238    */
1239   protected function cssSelect($selector) {
1240     return $this->getSession()->getPage()->findAll('css', $selector);
1241   }
1242
1243   /**
1244    * Follows a link by complete name.
1245    *
1246    * Will click the first link found with this link text.
1247    *
1248    * If the link is discovered and clicked, the test passes. Fail otherwise.
1249    *
1250    * @param string|\Drupal\Component\Render\MarkupInterface $label
1251    *   Text between the anchor tags.
1252    * @param int $index
1253    *   (optional) The index number for cases where multiple links have the same
1254    *   text. Defaults to 0.
1255    */
1256   protected function clickLink($label, $index = 0) {
1257     $label = (string) $label;
1258     $links = $this->getSession()->getPage()->findAll('named', ['link', $label]);
1259     $links[$index]->click();
1260   }
1261
1262   /**
1263    * Retrieves the plain-text content from the current page.
1264    */
1265   protected function getTextContent() {
1266     return $this->getSession()->getPage()->getText();
1267   }
1268
1269   /**
1270    * Performs an xpath search on the contents of the internal browser.
1271    *
1272    * The search is relative to the root element (HTML tag normally) of the page.
1273    *
1274    * @param string $xpath
1275    *   The xpath string to use in the search.
1276    * @param array $arguments
1277    *   An array of arguments with keys in the form ':name' matching the
1278    *   placeholders in the query. The values may be either strings or numeric
1279    *   values.
1280    *
1281    * @return \Behat\Mink\Element\NodeElement[]
1282    *   The list of elements matching the xpath expression.
1283    */
1284   protected function xpath($xpath, array $arguments = []) {
1285     $xpath = $this->assertSession()->buildXPathQuery($xpath, $arguments);
1286     return $this->getSession()->getPage()->findAll('xpath', $xpath);
1287   }
1288
1289   /**
1290    * Configuration accessor for tests. Returns non-overridden configuration.
1291    *
1292    * @param string $name
1293    *   Configuration name.
1294    *
1295    * @return \Drupal\Core\Config\Config
1296    *   The configuration object with original configuration data.
1297    */
1298   protected function config($name) {
1299     return $this->container->get('config.factory')->getEditable($name);
1300   }
1301
1302   /**
1303    * Returns all response headers.
1304    *
1305    * @return array
1306    *   The HTTP headers values.
1307    *
1308    * @deprecated Scheduled for removal in Drupal 9.0.0.
1309    *   Use $this->getSession()->getResponseHeaders() instead.
1310    */
1311   protected function drupalGetHeaders() {
1312     return $this->getSession()->getResponseHeaders();
1313   }
1314
1315   /**
1316    * Gets the value of an HTTP response header.
1317    *
1318    * If multiple requests were required to retrieve the page, only the headers
1319    * from the last request will be checked by default.
1320    *
1321    * @param string $name
1322    *   The name of the header to retrieve. Names are case-insensitive (see RFC
1323    *   2616 section 4.2).
1324    *
1325    * @return string|null
1326    *   The HTTP header value or NULL if not found.
1327    */
1328   protected function drupalGetHeader($name) {
1329     return $this->getSession()->getResponseHeader($name);
1330   }
1331
1332   /**
1333    * Get the current URL from the browser.
1334    *
1335    * @return string
1336    *   The current URL.
1337    */
1338   protected function getUrl() {
1339     return $this->getSession()->getCurrentUrl();
1340   }
1341
1342   /**
1343    * Gets the JavaScript drupalSettings variable for the currently-loaded page.
1344    *
1345    * @return array
1346    *   The JSON decoded drupalSettings value from the current page.
1347    */
1348   protected function getDrupalSettings() {
1349     $html = $this->getSession()->getPage()->getHtml();
1350     if (preg_match('@<script type="application/json" data-drupal-selector="drupal-settings-json">([^<]*)</script>@', $html, $matches)) {
1351       return Json::decode($matches[1]);
1352     }
1353     return [];
1354   }
1355
1356   /**
1357    * {@inheritdoc}
1358    */
1359   public static function assertEquals($expected, $actual, $message = '', $delta = 0.0, $maxDepth = 10, $canonicalize = FALSE, $ignoreCase = FALSE) {
1360     // Cast objects implementing MarkupInterface to string instead of
1361     // relying on PHP casting them to string depending on what they are being
1362     // comparing with.
1363     $expected = static::castSafeStrings($expected);
1364     $actual = static::castSafeStrings($actual);
1365     parent::assertEquals($expected, $actual, $message, $delta, $maxDepth, $canonicalize, $ignoreCase);
1366   }
1367
1368   /**
1369    * Retrieves the current calling line in the class under test.
1370    *
1371    * @return array
1372    *   An associative array with keys 'file', 'line' and 'function'.
1373    */
1374   protected function getTestMethodCaller() {
1375     $backtrace = debug_backtrace();
1376     // Find the test class that has the test method.
1377     while ($caller = Error::getLastCaller($backtrace)) {
1378       if (isset($caller['class']) && $caller['class'] === get_class($this)) {
1379         break;
1380       }
1381       // If the test method is implemented by a test class's parent then the
1382       // class name of $this will not be part of the backtrace.
1383       // In that case we process the backtrace until the caller is not a
1384       // subclass of $this and return the previous caller.
1385       if (isset($last_caller) && (!isset($caller['class']) || !is_subclass_of($this, $caller['class']))) {
1386         // Return the last caller since that has to be the test class.
1387         $caller = $last_caller;
1388         break;
1389       }
1390       // Otherwise we have not reached our test class yet: save the last caller
1391       // and remove an element from to backtrace to process the next call.
1392       $last_caller = $caller;
1393       array_shift($backtrace);
1394     }
1395
1396     return $caller;
1397   }
1398
1399   /**
1400    * Checks for meta refresh tag and if found call drupalGet() recursively.
1401    *
1402    * This function looks for the http-equiv attribute to be set to "Refresh" and
1403    * is case-sensitive.
1404    *
1405    * @return string|false
1406    *   Either the new page content or FALSE.
1407    */
1408   protected function checkForMetaRefresh() {
1409     $refresh = $this->cssSelect('meta[http-equiv="Refresh"]');
1410     if (!empty($refresh) && (!isset($this->maximumMetaRefreshCount) || $this->metaRefreshCount < $this->maximumMetaRefreshCount)) {
1411       // Parse the content attribute of the meta tag for the format:
1412       // "[delay]: URL=[page_to_redirect_to]".
1413       if (preg_match('/\d+;\s*URL=(?<url>.*)/i', $refresh[0]->getAttribute('content'), $match)) {
1414         $this->metaRefreshCount++;
1415         return $this->drupalGet($this->getAbsoluteUrl(Html::decodeEntities($match['url'])));
1416       }
1417     }
1418     return FALSE;
1419   }
1420
1421 }