Client.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  1. <?php
  2. namespace Symfony\Components\BrowserKit;
  3. use Symfony\Components\DomCrawler\Crawler;
  4. use Symfony\Components\DomCrawler\Link;
  5. use Symfony\Components\DomCrawler\Form;
  6. use Symfony\Components\Process\PhpProcess;
  7. /*
  8. * This file is part of the symfony package.
  9. *
  10. * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
  11. *
  12. * For the full copyright and license information, please view the LICENSE
  13. * file that was distributed with this source code.
  14. */
  15. /**
  16. * Client simulates a browser.
  17. *
  18. * To make the actual request, you need to implement the doRequest() method.
  19. *
  20. * If you want to be able to run requests in their own process (insulated flag),
  21. * you need to also implement the getScript() method.
  22. *
  23. * @package Symfony
  24. * @subpackage Components_BrowserKit
  25. * @author Fabien Potencier <fabien.potencier@symfony-project.com>
  26. */
  27. abstract class Client
  28. {
  29. protected $history;
  30. protected $cookieJar;
  31. protected $server;
  32. protected $request;
  33. protected $response;
  34. protected $crawler;
  35. protected $insulated;
  36. protected $redirect;
  37. protected $followRedirects;
  38. /**
  39. * Constructor.
  40. *
  41. * @param array $server The server parameters (equivalent of $_SERVER)
  42. * @param Symfony\Components\BrowserKit\History $history A History instance to store the browser history
  43. * @param Symfony\Components\BrowserKit\CookieJar $cookieJar A CookieJar instance to store the cookies
  44. */
  45. public function __construct(array $server = array(), History $history = null, CookieJar $cookieJar = null)
  46. {
  47. $this->setServerParameters($server);
  48. $this->history = null === $history ? new History() : $history;
  49. $this->cookieJar = null === $cookieJar ? new CookieJar() : $cookieJar;
  50. $this->insulated = false;
  51. $this->followRedirects = true;
  52. }
  53. /**
  54. * Sets whether to automatically follow redirects or not.
  55. *
  56. * @param Boolean $followRedirect Whether to follow redirects
  57. */
  58. public function followRedirects($followRedirect = true)
  59. {
  60. $this->followRedirects = (Boolean) $followRedirect;
  61. }
  62. /**
  63. * Sets the insulated flag.
  64. *
  65. * @param Boolean $insulated Whether to insulate the requests or not
  66. */
  67. public function insulate($insulated = true)
  68. {
  69. if (!class_exists('Symfony\\Components\\Process\\Process'))
  70. {
  71. // @codeCoverageIgnoreStart
  72. throw new \RuntimeException('Unable to isolate requests as the Symfony Process Component is not installed.');
  73. // @codeCoverageIgnoreEnd
  74. }
  75. $this->insulated = (Boolean) $insulated;
  76. }
  77. /**
  78. * Sets server parameters.
  79. *
  80. * @param array $server An array of server parameters
  81. */
  82. public function setServerParameters(array $server)
  83. {
  84. $this->server = array_merge(array(
  85. 'HTTP_HOST' => 'localhost',
  86. 'HTTP_USER_AGENT' => 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3',
  87. ), $server);
  88. }
  89. /**
  90. * Returns the History instance.
  91. *
  92. * @return Symfony\Components\BrowserKit\History A History instance
  93. */
  94. public function getHistory()
  95. {
  96. return $this->history;
  97. }
  98. /**
  99. * Returns the CookieJar instance.
  100. *
  101. * @return Symfony\Components\BrowserKit\CookieJar A CookieJar instance
  102. */
  103. public function getCookieJar()
  104. {
  105. return $this->cookieJar;
  106. }
  107. /**
  108. * Returns the current Crawler instance.
  109. *
  110. * @return Symfony\Components\DomCrawler\Crawler A Crawler instance
  111. */
  112. public function getCrawler()
  113. {
  114. return $this->crawler;
  115. }
  116. /**
  117. * Returns the current Response instance.
  118. *
  119. * @return Symfony\Components\BrowserKit\Response A Response instance
  120. */
  121. public function getResponse()
  122. {
  123. return $this->response;
  124. }
  125. /**
  126. * Returns the current Request instance.
  127. *
  128. * @return Symfony\Components\BrowserKit\Request A Request instance
  129. */
  130. public function getRequest()
  131. {
  132. return $this->request;
  133. }
  134. /**
  135. * Clicks on a given link.
  136. *
  137. * @param Symfony\Components\BrowserKit\Link $link A Link instance
  138. */
  139. public function click(Link $link)
  140. {
  141. return $this->request($link->getMethod(), $link->getUri());
  142. }
  143. /**
  144. * Submits a form.
  145. *
  146. * @param Symfony\Components\BrowserKit\Form $form A Form instance
  147. * @param array $values An array of form field values
  148. */
  149. public function submit(Form $form, array $values = array())
  150. {
  151. $form->setValues($values);
  152. return $this->request($form->getMethod(), $form->getUri(), $form->getPhpValues(), array(), $form->getPhpFiles());
  153. }
  154. /**
  155. * Calls a URI.
  156. *
  157. * @param string $method The request method
  158. * @param string $uri The URI to fetch
  159. * @param array $parameters The Request parameters
  160. * @param array $headers The headers
  161. * @param array $files The files
  162. * @param array $server The server parameters
  163. * @param Boolean $changeHistory Whether to update the history or not (only used internally for back(), forward(), and reload())
  164. *
  165. * @return Symfony\Components\DomCrawler\Crawler
  166. */
  167. public function request($method, $uri, $parameters = array(), $headers = array(), $files = array(), $server = array(), $changeHistory = true)
  168. {
  169. $uri = $this->getAbsoluteUri($uri);
  170. $server = array_merge($this->server, $server);
  171. if (!$this->history->isEmpty())
  172. {
  173. $server['HTTP_REFERER'] = $this->history->current()->getUri();
  174. }
  175. $server['HTTP_HOST'] = parse_url($uri, PHP_URL_HOST);
  176. $server['HTTPS'] = 'https' == parse_url($uri, PHP_URL_SCHEME);
  177. $request = new Request($uri, $method, $parameters, $files, $this->cookieJar->getValues($uri), $server);
  178. $this->request = $this->filterRequest($request);
  179. if (true === $changeHistory)
  180. {
  181. $this->history->add($request);
  182. }
  183. if ($this->insulated)
  184. {
  185. $this->response = $this->doRequestInProcess($this->request);
  186. }
  187. else
  188. {
  189. $this->response = $this->doRequest($this->request);
  190. }
  191. $response = $this->filterResponse($this->response);
  192. $this->cookieJar->updateFromResponse($response);
  193. $this->redirect = $response->getHeader('Location');
  194. if ($this->followRedirects && $this->redirect)
  195. {
  196. return $this->crawler = $this->followRedirect();
  197. }
  198. return $this->crawler = $this->createCrawlerFromContent($request->getUri(), $response->getContent(), $response->getHeader('Content-Type'));
  199. }
  200. /**
  201. * Makes a request in another process.
  202. *
  203. * @param Request $request A Request instance
  204. *
  205. * @param Response $response A Response instance
  206. */
  207. protected function doRequestInProcess($request)
  208. {
  209. $process = new PhpProcess($this->getScript($request));
  210. $process->run();
  211. if ($process->getExitCode() > 0)
  212. {
  213. throw new \RuntimeException($process->getErrorOutput());
  214. }
  215. return unserialize($process->getOutput());
  216. }
  217. /**
  218. * Makes a request.
  219. *
  220. * @param Request $request A Request instance
  221. *
  222. * @param Response $response A Response instance
  223. */
  224. abstract protected function doRequest($request);
  225. /**
  226. * Returns the script to execute when the request must be insulated.
  227. *
  228. * @param Request $request A Request instance
  229. */
  230. protected function getScript($request)
  231. {
  232. // @codeCoverageIgnoreStart
  233. throw new \LogicException('To insulate requests, you need to override the getScript() method.');
  234. // @codeCoverageIgnoreEnd
  235. }
  236. protected function filterRequest(Request $request)
  237. {
  238. return $request;
  239. }
  240. protected function filterResponse($response)
  241. {
  242. return $response;
  243. }
  244. protected function createCrawlerFromContent($uri, $content, $type)
  245. {
  246. $crawler = new Crawler(null, $uri);
  247. $crawler->addContent($content, $type);
  248. return $crawler;
  249. }
  250. /**
  251. * Goes back in the browser history.
  252. */
  253. public function back()
  254. {
  255. return $this->requestFromRequest($this->history->back(), false);
  256. }
  257. /**
  258. * Goes forward in the browser history.
  259. */
  260. public function forward()
  261. {
  262. return $this->requestFromRequest($this->history->forward(), false);
  263. }
  264. /**
  265. * Reloads the current browser.
  266. */
  267. public function reload()
  268. {
  269. return $this->requestFromRequest($this->history->current(), false);
  270. }
  271. /**
  272. * Follow redirects?
  273. *
  274. * @throws \LogicException If request was not a redirect
  275. *
  276. * @return Symfony\Components\BrowserKit\Client
  277. */
  278. public function followRedirect()
  279. {
  280. if (empty($this->redirect))
  281. {
  282. throw new \LogicException('The request was not redirected.');
  283. }
  284. return $this->request('get', $this->redirect);
  285. }
  286. /**
  287. * Restarts the client.
  288. *
  289. * It flushes all cookies.
  290. */
  291. public function restart()
  292. {
  293. $this->cookieJar->clear();
  294. $this->history->clear();
  295. }
  296. protected function getAbsoluteUri($uri)
  297. {
  298. // already absolute?
  299. if ('http' === substr($uri, 0, 4))
  300. {
  301. return $uri;
  302. }
  303. if (!$this->history->isEmpty())
  304. {
  305. $currentUri = $this->history->current()->getUri();
  306. }
  307. else
  308. {
  309. $currentUri = sprintf('http%s://%s/',
  310. isset($this->server['HTTPS']) ? 's' : '',
  311. isset($this->server['HTTP_HOST']) ? $this->server['HTTP_HOST'] : 'localhost'
  312. );
  313. }
  314. // anchor?
  315. if (!$uri || '#' == $uri[0])
  316. {
  317. return preg_replace('/#.*?$/', '', $currentUri).$uri;
  318. }
  319. if ('/' !== $uri[0])
  320. {
  321. $path = parse_url($currentUri, PHP_URL_PATH);
  322. if ('/' !== substr($path, -1))
  323. {
  324. $path = substr($path, 0, strrpos($path, '/') + 1);
  325. }
  326. $uri = $path.$uri;
  327. }
  328. return preg_replace('#^(.*?//[^/]+)\/.*$#', '$1', $currentUri).$uri;
  329. }
  330. /**
  331. * Makes a request from a Request object directly.
  332. *
  333. * @param Symfony\Components\BrowserKit\Request $request A Request instance
  334. * @param Boolean $changeHistory Whether to update the history or not (only used internally for back(), forward(), and reload())
  335. *
  336. * @return Symfony\Components\DomCrawler\Crawler
  337. */
  338. protected function requestFromRequest(Request $request, $changeHistory = true)
  339. {
  340. return $this->request($request->getMethod(), $request->getUri(), $request->getParameters(), $request->getFiles(), array(), $request->getServer(), $changeHistory);
  341. }
  342. }