4 * This file is part of the Symfony package.
6 * (c) Fabien Potencier <fabien@symfony.com>
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
12 namespace Symfony\Component\BrowserKit;
14 use Symfony\Component\DomCrawler\Crawler;
15 use Symfony\Component\DomCrawler\Form;
16 use Symfony\Component\DomCrawler\Link;
17 use Symfony\Component\Process\PhpProcess;
20 * Client simulates a browser.
22 * To make the actual request, you need to implement the doRequest() method.
24 * If you want to be able to run requests in their own process (insulated flag),
25 * you need to also implement the getScript() method.
27 * @author Fabien Potencier <fabien@symfony.com>
33 protected $server = array();
34 protected $internalRequest;
36 protected $internalResponse;
39 protected $insulated = false;
41 protected $followRedirects = true;
43 private $maxRedirects = -1;
44 private $redirectCount = 0;
45 private $redirects = array();
46 private $isMainRequest = true;
49 * @param array $server The server parameters (equivalent of $_SERVER)
50 * @param History $history A History instance to store the browser history
51 * @param CookieJar $cookieJar A CookieJar instance to store the cookies
53 public function __construct(array $server = array(), History $history = null, CookieJar $cookieJar = null)
55 $this->setServerParameters($server);
56 $this->history = $history ?: new History();
57 $this->cookieJar = $cookieJar ?: new CookieJar();
61 * Sets whether to automatically follow redirects or not.
63 * @param bool $followRedirect Whether to follow redirects
65 public function followRedirects($followRedirect = true)
67 $this->followRedirects = (bool) $followRedirect;
71 * Returns whether client automatically follows redirects or not.
75 public function isFollowingRedirects()
77 return $this->followRedirects;
81 * Sets the maximum number of redirects that crawler can follow.
83 * @param int $maxRedirects
85 public function setMaxRedirects($maxRedirects)
87 $this->maxRedirects = $maxRedirects < 0 ? -1 : $maxRedirects;
88 $this->followRedirects = -1 != $this->maxRedirects;
92 * Returns the maximum number of redirects that crawler can follow.
96 public function getMaxRedirects()
98 return $this->maxRedirects;
102 * Sets the insulated flag.
104 * @param bool $insulated Whether to insulate the requests or not
106 * @throws \RuntimeException When Symfony Process Component is not installed
108 public function insulate($insulated = true)
110 if ($insulated && !class_exists('Symfony\\Component\\Process\\Process')) {
111 throw new \RuntimeException('Unable to isolate requests as the Symfony Process Component is not installed.');
114 $this->insulated = (bool) $insulated;
118 * Sets server parameters.
120 * @param array $server An array of server parameters
122 public function setServerParameters(array $server)
124 $this->server = array_merge(array(
125 'HTTP_USER_AGENT' => 'Symfony BrowserKit',
130 * Sets single server parameter.
132 * @param string $key A key of the parameter
133 * @param string $value A value of the parameter
135 public function setServerParameter($key, $value)
137 $this->server[$key] = $value;
141 * Gets single server parameter for specified key.
143 * @param string $key A key of the parameter to get
144 * @param string $default A default value when key is undefined
146 * @return string A value of the parameter
148 public function getServerParameter($key, $default = '')
150 return isset($this->server[$key]) ? $this->server[$key] : $default;
154 * Returns the History instance.
156 * @return History A History instance
158 public function getHistory()
160 return $this->history;
164 * Returns the CookieJar instance.
166 * @return CookieJar A CookieJar instance
168 public function getCookieJar()
170 return $this->cookieJar;
174 * Returns the current Crawler instance.
176 * @return Crawler|null A Crawler instance
178 public function getCrawler()
180 return $this->crawler;
184 * Returns the current BrowserKit Response instance.
186 * @return Response|null A BrowserKit Response instance
188 public function getInternalResponse()
190 return $this->internalResponse;
194 * Returns the current origin response instance.
196 * The origin response is the response instance that is returned
197 * by the code that handles requests.
199 * @return object|null A response instance
203 public function getResponse()
205 return $this->response;
209 * Returns the current BrowserKit Request instance.
211 * @return Request|null A BrowserKit Request instance
213 public function getInternalRequest()
215 return $this->internalRequest;
219 * Returns the current origin Request instance.
221 * The origin request is the request instance that is sent
222 * to the code that handles requests.
224 * @return object|null A Request instance
228 public function getRequest()
230 return $this->request;
234 * Clicks on a given link.
238 public function click(Link $link)
240 if ($link instanceof Form) {
241 return $this->submit($link);
244 return $this->request($link->getMethod(), $link->getUri());
250 * @param Form $form A Form instance
251 * @param array $values An array of form field values
255 public function submit(Form $form, array $values = array())
257 $form->setValues($values);
259 return $this->request($form->getMethod(), $form->getUri(), $form->getPhpValues(), $form->getPhpFiles());
265 * @param string $method The request method
266 * @param string $uri The URI to fetch
267 * @param array $parameters The Request parameters
268 * @param array $files The files
269 * @param array $server The server parameters (HTTP headers are referenced with a HTTP_ prefix as PHP does)
270 * @param string $content The raw body data
271 * @param bool $changeHistory Whether to update the history or not (only used internally for back(), forward(), and reload())
275 public function request($method, $uri, array $parameters = array(), array $files = array(), array $server = array(), $content = null, $changeHistory = true)
277 if ($this->isMainRequest) {
278 $this->redirectCount = 0;
280 ++$this->redirectCount;
283 $uri = $this->getAbsoluteUri($uri);
285 $server = array_merge($this->server, $server);
287 if (isset($server['HTTPS'])) {
288 $uri = preg_replace('{^'.parse_url($uri, PHP_URL_SCHEME).'}', $server['HTTPS'] ? 'https' : 'http', $uri);
291 if (!$this->history->isEmpty()) {
292 $server['HTTP_REFERER'] = $this->history->current()->getUri();
295 if (empty($server['HTTP_HOST'])) {
296 $server['HTTP_HOST'] = $this->extractHost($uri);
299 $server['HTTPS'] = 'https' == parse_url($uri, PHP_URL_SCHEME);
301 $this->internalRequest = new Request($uri, $method, $parameters, $files, $this->cookieJar->allValues($uri), $server, $content);
303 $this->request = $this->filterRequest($this->internalRequest);
305 if (true === $changeHistory) {
306 $this->history->add($this->internalRequest);
309 if ($this->insulated) {
310 $this->response = $this->doRequestInProcess($this->request);
312 $this->response = $this->doRequest($this->request);
315 $this->internalResponse = $this->filterResponse($this->response);
317 $this->cookieJar->updateFromResponse($this->internalResponse, $uri);
319 $status = $this->internalResponse->getStatus();
321 if ($status >= 300 && $status < 400) {
322 $this->redirect = $this->internalResponse->getHeader('Location');
324 $this->redirect = null;
327 if ($this->followRedirects && $this->redirect) {
328 $this->redirects[serialize($this->history->current())] = true;
330 return $this->crawler = $this->followRedirect();
333 return $this->crawler = $this->createCrawlerFromContent($this->internalRequest->getUri(), $this->internalResponse->getContent(), $this->internalResponse->getHeader('Content-Type'));
337 * Makes a request in another process.
339 * @param object $request An origin request instance
341 * @return object An origin response instance
343 * @throws \RuntimeException When processing returns exit code
345 protected function doRequestInProcess($request)
347 $deprecationsFile = tempnam(sys_get_temp_dir(), 'deprec');
348 putenv('SYMFONY_DEPRECATIONS_SERIALIZE='.$deprecationsFile);
349 $_ENV['SYMFONY_DEPRECATIONS_SERIALIZE'] = $deprecationsFile;
350 $process = new PhpProcess($this->getScript($request), null, null);
353 if (file_exists($deprecationsFile)) {
354 $deprecations = file_get_contents($deprecationsFile);
355 unlink($deprecationsFile);
356 foreach ($deprecations ? unserialize($deprecations) : array() as $deprecation) {
357 if ($deprecation[0]) {
358 @trigger_error($deprecation[1], E_USER_DEPRECATED);
360 @trigger_error($deprecation[1], E_USER_DEPRECATED);
365 if (!$process->isSuccessful() || !preg_match('/^O\:\d+\:/', $process->getOutput())) {
366 throw new \RuntimeException(sprintf('OUTPUT: %s ERROR OUTPUT: %s', $process->getOutput(), $process->getErrorOutput()));
369 return unserialize($process->getOutput());
375 * @param object $request An origin request instance
377 * @return object An origin response instance
379 abstract protected function doRequest($request);
382 * Returns the script to execute when the request must be insulated.
384 * @param object $request An origin request instance
386 * @throws \LogicException When this abstract class is not implemented
388 protected function getScript($request)
390 throw new \LogicException('To insulate requests, you need to override the getScript() method.');
394 * Filters the BrowserKit request to the origin one.
396 * @param Request $request The BrowserKit Request to filter
398 * @return object An origin request instance
400 protected function filterRequest(Request $request)
406 * Filters the origin response to the BrowserKit one.
408 * @param object $response The origin response to filter
410 * @return Response An BrowserKit Response instance
412 protected function filterResponse($response)
420 * This method returns null if the DomCrawler component is not available.
422 * @param string $uri A URI
423 * @param string $content Content for the crawler to use
424 * @param string $type Content type
426 * @return Crawler|null
428 protected function createCrawlerFromContent($uri, $content, $type)
430 if (!class_exists('Symfony\Component\DomCrawler\Crawler')) {
434 $crawler = new Crawler(null, $uri);
435 $crawler->addContent($content, $type);
441 * Goes back in the browser history.
445 public function back()
448 $request = $this->history->back();
449 } while (array_key_exists(serialize($request), $this->redirects));
451 return $this->requestFromRequest($request, false);
455 * Goes forward in the browser history.
459 public function forward()
462 $request = $this->history->forward();
463 } while (array_key_exists(serialize($request), $this->redirects));
465 return $this->requestFromRequest($request, false);
469 * Reloads the current browser.
473 public function reload()
475 return $this->requestFromRequest($this->history->current(), false);
483 * @throws \LogicException If request was not a redirect
485 public function followRedirect()
487 if (empty($this->redirect)) {
488 throw new \LogicException('The request was not redirected.');
491 if (-1 !== $this->maxRedirects) {
492 if ($this->redirectCount > $this->maxRedirects) {
493 $this->redirectCount = 0;
494 throw new \LogicException(sprintf('The maximum number (%d) of redirections was reached.', $this->maxRedirects));
498 $request = $this->internalRequest;
500 if (\in_array($this->internalResponse->getStatus(), array(301, 302, 303))) {
505 $method = $request->getMethod();
506 $files = $request->getFiles();
507 $content = $request->getContent();
510 if ('GET' === strtoupper($method)) {
511 // Don't forward parameters for GET request as it should reach the redirection URI
512 $parameters = array();
514 $parameters = $request->getParameters();
517 $server = $request->getServer();
518 $server = $this->updateServerFromUri($server, $this->redirect);
520 $this->isMainRequest = false;
522 $response = $this->request($method, $this->redirect, $parameters, $files, $server, $content);
524 $this->isMainRequest = true;
530 * Restarts the client.
532 * It flushes history and all cookies.
534 public function restart()
536 $this->cookieJar->clear();
537 $this->history->clear();
541 * Takes a URI and converts it to absolute if it is not already absolute.
543 * @param string $uri A URI
545 * @return string An absolute URI
547 protected function getAbsoluteUri($uri)
550 if (0 === strpos($uri, 'http://') || 0 === strpos($uri, 'https://')) {
554 if (!$this->history->isEmpty()) {
555 $currentUri = $this->history->current()->getUri();
557 $currentUri = sprintf('http%s://%s/',
558 isset($this->server['HTTPS']) ? 's' : '',
559 isset($this->server['HTTP_HOST']) ? $this->server['HTTP_HOST'] : 'localhost'
563 // protocol relative URL
564 if (0 === strpos($uri, '//')) {
565 return parse_url($currentUri, PHP_URL_SCHEME).':'.$uri;
568 // anchor or query string parameters?
569 if (!$uri || '#' == $uri[0] || '?' == $uri[0]) {
570 return preg_replace('/[#?].*?$/', '', $currentUri).$uri;
573 if ('/' !== $uri[0]) {
574 $path = parse_url($currentUri, PHP_URL_PATH);
576 if ('/' !== substr($path, -1)) {
577 $path = substr($path, 0, strrpos($path, '/') + 1);
583 return preg_replace('#^(.*?//[^/]+)\/.*$#', '$1', $currentUri).$uri;
587 * Makes a request from a Request object directly.
589 * @param Request $request A Request instance
590 * @param bool $changeHistory Whether to update the history or not (only used internally for back(), forward(), and reload())
594 protected function requestFromRequest(Request $request, $changeHistory = true)
596 return $this->request($request->getMethod(), $request->getUri(), $request->getParameters(), $request->getFiles(), $request->getServer(), $request->getContent(), $changeHistory);
599 private function updateServerFromUri($server, $uri)
601 $server['HTTP_HOST'] = $this->extractHost($uri);
602 $scheme = parse_url($uri, PHP_URL_SCHEME);
603 $server['HTTPS'] = null === $scheme ? $server['HTTPS'] : 'https' == $scheme;
604 unset($server['HTTP_IF_NONE_MATCH'], $server['HTTP_IF_MODIFIED_SINCE']);
609 private function extractHost($uri)
611 $host = parse_url($uri, PHP_URL_HOST);
613 if ($port = parse_url($uri, PHP_URL_PORT)) {
614 return $host.':'.$port;