├── AbstractBrowser.php ├── CHANGELOG.md ├── Cookie.php ├── CookieJar.php ├── Exception ├── BadMethodCallException.php ├── ExceptionInterface.php ├── InvalidArgumentException.php ├── JsonException.php ├── LogicException.php ├── RuntimeException.php └── UnexpectedValueException.php ├── History.php ├── HttpBrowser.php ├── LICENSE ├── README.md ├── Request.php ├── Response.php ├── Test └── Constraint │ ├── BrowserCookieValueSame.php │ └── BrowserHasCookie.php └── composer.json /AbstractBrowser.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\BrowserKit; 13 | 14 | use Symfony\Component\BrowserKit\Exception\BadMethodCallException; 15 | use Symfony\Component\BrowserKit\Exception\InvalidArgumentException; 16 | use Symfony\Component\BrowserKit\Exception\LogicException; 17 | use Symfony\Component\BrowserKit\Exception\RuntimeException; 18 | use Symfony\Component\DomCrawler\Crawler; 19 | use Symfony\Component\DomCrawler\Form; 20 | use Symfony\Component\DomCrawler\Link; 21 | use Symfony\Component\Process\PhpProcess; 22 | 23 | /** 24 | * Simulates a browser. 25 | * 26 | * To make the actual request, you need to implement the doRequest() method. 27 | * 28 | * If you want to be able to run requests in their own process (insulated flag), 29 | * you need to also implement the getScript() method. 30 | * 31 | * @author Fabien Potencier 32 | * 33 | * @template TRequest of object 34 | * @template TResponse of object 35 | */ 36 | abstract class AbstractBrowser 37 | { 38 | protected History $history; 39 | protected CookieJar $cookieJar; 40 | protected array $server = []; 41 | protected Request $internalRequest; 42 | /** @psalm-var TRequest */ 43 | protected object $request; 44 | protected Response $internalResponse; 45 | /** @psalm-var TResponse */ 46 | protected object $response; 47 | protected Crawler $crawler; 48 | protected bool $useHtml5Parser = true; 49 | protected bool $insulated = false; 50 | protected ?string $redirect; 51 | protected bool $followRedirects = true; 52 | protected bool $followMetaRefresh = false; 53 | 54 | private int $maxRedirects = -1; 55 | private int $redirectCount = 0; 56 | private array $redirects = []; 57 | private bool $isMainRequest = true; 58 | 59 | /** 60 | * @param array $server The server parameters (equivalent of $_SERVER) 61 | */ 62 | public function __construct(array $server = [], ?History $history = null, ?CookieJar $cookieJar = null) 63 | { 64 | $this->setServerParameters($server); 65 | $this->history = $history ?? new History(); 66 | $this->cookieJar = $cookieJar ?? new CookieJar(); 67 | } 68 | 69 | /** 70 | * Sets whether to automatically follow redirects or not. 71 | */ 72 | public function followRedirects(bool $followRedirects = true): void 73 | { 74 | $this->followRedirects = $followRedirects; 75 | } 76 | 77 | /** 78 | * Sets whether to automatically follow meta refresh redirects or not. 79 | */ 80 | public function followMetaRefresh(bool $followMetaRefresh = true): void 81 | { 82 | $this->followMetaRefresh = $followMetaRefresh; 83 | } 84 | 85 | /** 86 | * Returns whether client automatically follows redirects or not. 87 | */ 88 | public function isFollowingRedirects(): bool 89 | { 90 | return $this->followRedirects; 91 | } 92 | 93 | /** 94 | * Sets the maximum number of redirects that crawler can follow. 95 | */ 96 | public function setMaxRedirects(int $maxRedirects): void 97 | { 98 | $this->maxRedirects = $maxRedirects < 0 ? -1 : $maxRedirects; 99 | $this->followRedirects = -1 !== $this->maxRedirects; 100 | } 101 | 102 | /** 103 | * Returns the maximum number of redirects that crawler can follow. 104 | */ 105 | public function getMaxRedirects(): int 106 | { 107 | return $this->maxRedirects; 108 | } 109 | 110 | /** 111 | * Sets the insulated flag. 112 | * 113 | * @throws LogicException When Symfony Process Component is not installed 114 | */ 115 | public function insulate(bool $insulated = true): void 116 | { 117 | if ($insulated && !class_exists(\Symfony\Component\Process\Process::class)) { 118 | throw new LogicException('Unable to isolate requests as the Symfony Process Component is not installed. Try running "composer require symfony/process".'); 119 | } 120 | 121 | $this->insulated = $insulated; 122 | } 123 | 124 | /** 125 | * Sets server parameters. 126 | */ 127 | public function setServerParameters(array $server): void 128 | { 129 | $this->server = array_merge([ 130 | 'HTTP_USER_AGENT' => 'Symfony BrowserKit', 131 | ], $server); 132 | } 133 | 134 | /** 135 | * Sets single server parameter. 136 | */ 137 | public function setServerParameter(string $key, string $value): void 138 | { 139 | $this->server[$key] = $value; 140 | } 141 | 142 | /** 143 | * Gets single server parameter for specified key. 144 | */ 145 | public function getServerParameter(string $key, mixed $default = ''): mixed 146 | { 147 | return $this->server[$key] ?? $default; 148 | } 149 | 150 | public function xmlHttpRequest(string $method, string $uri, array $parameters = [], array $files = [], array $server = [], ?string $content = null, bool $changeHistory = true): Crawler 151 | { 152 | $this->setServerParameter('HTTP_X_REQUESTED_WITH', 'XMLHttpRequest'); 153 | 154 | try { 155 | return $this->request($method, $uri, $parameters, $files, $server, $content, $changeHistory); 156 | } finally { 157 | unset($this->server['HTTP_X_REQUESTED_WITH']); 158 | } 159 | } 160 | 161 | /** 162 | * Converts the request parameters into a JSON string and uses it as request content. 163 | */ 164 | public function jsonRequest(string $method, string $uri, array $parameters = [], array $server = [], bool $changeHistory = true): Crawler 165 | { 166 | $content = json_encode($parameters, \JSON_PRESERVE_ZERO_FRACTION); 167 | 168 | $this->setServerParameter('CONTENT_TYPE', 'application/json'); 169 | $this->setServerParameter('HTTP_ACCEPT', 'application/json'); 170 | 171 | try { 172 | return $this->request($method, $uri, [], [], $server, $content, $changeHistory); 173 | } finally { 174 | unset($this->server['CONTENT_TYPE']); 175 | unset($this->server['HTTP_ACCEPT']); 176 | } 177 | } 178 | 179 | /** 180 | * Returns the History instance. 181 | */ 182 | public function getHistory(): History 183 | { 184 | return $this->history; 185 | } 186 | 187 | /** 188 | * Returns the CookieJar instance. 189 | */ 190 | public function getCookieJar(): CookieJar 191 | { 192 | return $this->cookieJar; 193 | } 194 | 195 | /** 196 | * Returns the current Crawler instance. 197 | */ 198 | public function getCrawler(): Crawler 199 | { 200 | return $this->crawler ?? throw new BadMethodCallException(\sprintf('The "request()" method must be called before "%s()".', __METHOD__)); 201 | } 202 | 203 | /** 204 | * Sets whether parsing should be done using "masterminds/html5". 205 | * 206 | * @return $this 207 | */ 208 | public function useHtml5Parser(bool $useHtml5Parser): static 209 | { 210 | $this->useHtml5Parser = $useHtml5Parser; 211 | 212 | return $this; 213 | } 214 | 215 | /** 216 | * Returns the current BrowserKit Response instance. 217 | */ 218 | public function getInternalResponse(): Response 219 | { 220 | return $this->internalResponse ?? throw new BadMethodCallException(\sprintf('The "request()" method must be called before "%s()".', __METHOD__)); 221 | } 222 | 223 | /** 224 | * Returns the current origin response instance. 225 | * 226 | * The origin response is the response instance that is returned 227 | * by the code that handles requests. 228 | * 229 | * @psalm-return TResponse 230 | * 231 | * @see doRequest() 232 | */ 233 | public function getResponse(): object 234 | { 235 | return $this->response ?? throw new BadMethodCallException(\sprintf('The "request()" method must be called before "%s()".', __METHOD__)); 236 | } 237 | 238 | /** 239 | * Returns the current BrowserKit Request instance. 240 | */ 241 | public function getInternalRequest(): Request 242 | { 243 | return $this->internalRequest ?? throw new BadMethodCallException(\sprintf('The "request()" method must be called before "%s()".', __METHOD__)); 244 | } 245 | 246 | /** 247 | * Returns the current origin Request instance. 248 | * 249 | * The origin request is the request instance that is sent 250 | * to the code that handles requests. 251 | * 252 | * @psalm-return TRequest 253 | * 254 | * @see doRequest() 255 | */ 256 | public function getRequest(): object 257 | { 258 | return $this->request ?? throw new BadMethodCallException(\sprintf('The "request()" method must be called before "%s()".', __METHOD__)); 259 | } 260 | 261 | /** 262 | * Clicks on a given link. 263 | * 264 | * @param array $serverParameters An array of server parameters 265 | */ 266 | public function click(Link $link, array $serverParameters = []): Crawler 267 | { 268 | if ($link instanceof Form) { 269 | return $this->submit($link, [], $serverParameters); 270 | } 271 | 272 | return $this->request($link->getMethod(), $link->getUri(), [], [], $serverParameters); 273 | } 274 | 275 | /** 276 | * Clicks the first link (or clickable image) that contains the given text. 277 | * 278 | * @param string $linkText The text of the link or the alt attribute of the clickable image 279 | * @param array $serverParameters An array of server parameters 280 | */ 281 | public function clickLink(string $linkText, array $serverParameters = []): Crawler 282 | { 283 | $crawler = $this->crawler ?? throw new BadMethodCallException(\sprintf('The "request()" method must be called before "%s()".', __METHOD__)); 284 | 285 | return $this->click($crawler->selectLink($linkText)->link(), $serverParameters); 286 | } 287 | 288 | /** 289 | * Submits a form. 290 | * 291 | * @param array $values An array of form field values 292 | * @param array $serverParameters An array of server parameters 293 | */ 294 | public function submit(Form $form, array $values = [], array $serverParameters = []): Crawler 295 | { 296 | $form->setValues($values); 297 | 298 | return $this->request($form->getMethod(), $form->getUri(), $form->getPhpValues(), $form->getPhpFiles(), $serverParameters); 299 | } 300 | 301 | /** 302 | * Finds the first form that contains a button with the given content and 303 | * uses it to submit the given form field values. 304 | * 305 | * @param string $button The text content, id, value or name of the form