Client.php 12 KB

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