initial commit

This commit is contained in:
boris
2025-09-30 09:24:25 +01:00
committed by boris
parent a783a12c97
commit c7770ea03b
4695 changed files with 525784 additions and 0 deletions

View File

@@ -0,0 +1,697 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\BrowserKit;
use Symfony\Component\BrowserKit\Exception\BadMethodCallException;
use Symfony\Component\BrowserKit\Exception\InvalidArgumentException;
use Symfony\Component\BrowserKit\Exception\LogicException;
use Symfony\Component\BrowserKit\Exception\RuntimeException;
use Symfony\Component\DomCrawler\Crawler;
use Symfony\Component\DomCrawler\Form;
use Symfony\Component\DomCrawler\Link;
use Symfony\Component\Process\PhpProcess;
/**
* Simulates a browser.
*
* To make the actual request, you need to implement the doRequest() method.
*
* If you want to be able to run requests in their own process (insulated flag),
* you need to also implement the getScript() method.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @template TRequest of object
* @template TResponse of object
*/
abstract class AbstractBrowser
{
protected History $history;
protected CookieJar $cookieJar;
protected array $server = [];
protected Request $internalRequest;
/** @psalm-var TRequest */
protected object $request;
protected Response $internalResponse;
/** @psalm-var TResponse */
protected object $response;
protected Crawler $crawler;
protected bool $useHtml5Parser = true;
protected bool $insulated = false;
protected ?string $redirect;
protected bool $followRedirects = true;
protected bool $followMetaRefresh = false;
private int $maxRedirects = -1;
private int $redirectCount = 0;
private array $redirects = [];
private bool $isMainRequest = true;
/**
* @param array $server The server parameters (equivalent of $_SERVER)
*/
public function __construct(array $server = [], ?History $history = null, ?CookieJar $cookieJar = null)
{
$this->setServerParameters($server);
$this->history = $history ?? new History();
$this->cookieJar = $cookieJar ?? new CookieJar();
}
/**
* Sets whether to automatically follow redirects or not.
*/
public function followRedirects(bool $followRedirects = true): void
{
$this->followRedirects = $followRedirects;
}
/**
* Sets whether to automatically follow meta refresh redirects or not.
*/
public function followMetaRefresh(bool $followMetaRefresh = true): void
{
$this->followMetaRefresh = $followMetaRefresh;
}
/**
* Returns whether client automatically follows redirects or not.
*/
public function isFollowingRedirects(): bool
{
return $this->followRedirects;
}
/**
* Sets the maximum number of redirects that crawler can follow.
*/
public function setMaxRedirects(int $maxRedirects): void
{
$this->maxRedirects = $maxRedirects < 0 ? -1 : $maxRedirects;
$this->followRedirects = -1 !== $this->maxRedirects;
}
/**
* Returns the maximum number of redirects that crawler can follow.
*/
public function getMaxRedirects(): int
{
return $this->maxRedirects;
}
/**
* Sets the insulated flag.
*
* @throws LogicException When Symfony Process Component is not installed
*/
public function insulate(bool $insulated = true): void
{
if ($insulated && !class_exists(\Symfony\Component\Process\Process::class)) {
throw new LogicException('Unable to isolate requests as the Symfony Process Component is not installed. Try running "composer require symfony/process".');
}
$this->insulated = $insulated;
}
/**
* Sets server parameters.
*/
public function setServerParameters(array $server): void
{
$this->server = array_merge([
'HTTP_USER_AGENT' => 'Symfony BrowserKit',
], $server);
}
/**
* Sets single server parameter.
*/
public function setServerParameter(string $key, string $value): void
{
$this->server[$key] = $value;
}
/**
* Gets single server parameter for specified key.
*/
public function getServerParameter(string $key, mixed $default = ''): mixed
{
return $this->server[$key] ?? $default;
}
public function xmlHttpRequest(string $method, string $uri, array $parameters = [], array $files = [], array $server = [], ?string $content = null, bool $changeHistory = true): Crawler
{
$this->setServerParameter('HTTP_X_REQUESTED_WITH', 'XMLHttpRequest');
try {
return $this->request($method, $uri, $parameters, $files, $server, $content, $changeHistory);
} finally {
unset($this->server['HTTP_X_REQUESTED_WITH']);
}
}
/**
* Converts the request parameters into a JSON string and uses it as request content.
*/
public function jsonRequest(string $method, string $uri, array $parameters = [], array $server = [], bool $changeHistory = true): Crawler
{
$content = json_encode($parameters, \JSON_PRESERVE_ZERO_FRACTION);
$this->setServerParameter('CONTENT_TYPE', 'application/json');
$this->setServerParameter('HTTP_ACCEPT', 'application/json');
try {
return $this->request($method, $uri, [], [], $server, $content, $changeHistory);
} finally {
unset($this->server['CONTENT_TYPE']);
unset($this->server['HTTP_ACCEPT']);
}
}
/**
* Returns the History instance.
*/
public function getHistory(): History
{
return $this->history;
}
/**
* Returns the CookieJar instance.
*/
public function getCookieJar(): CookieJar
{
return $this->cookieJar;
}
/**
* Returns the current Crawler instance.
*/
public function getCrawler(): Crawler
{
return $this->crawler ?? throw new BadMethodCallException(\sprintf('The "request()" method must be called before "%s()".', __METHOD__));
}
/**
* Sets whether parsing should be done using "masterminds/html5".
*
* @return $this
*/
public function useHtml5Parser(bool $useHtml5Parser): static
{
$this->useHtml5Parser = $useHtml5Parser;
return $this;
}
/**
* Returns the current BrowserKit Response instance.
*/
public function getInternalResponse(): Response
{
return $this->internalResponse ?? throw new BadMethodCallException(\sprintf('The "request()" method must be called before "%s()".', __METHOD__));
}
/**
* Returns the current origin response instance.
*
* The origin response is the response instance that is returned
* by the code that handles requests.
*
* @psalm-return TResponse
*
* @see doRequest()
*/
public function getResponse(): object
{
return $this->response ?? throw new BadMethodCallException(\sprintf('The "request()" method must be called before "%s()".', __METHOD__));
}
/**
* Returns the current BrowserKit Request instance.
*/
public function getInternalRequest(): Request
{
return $this->internalRequest ?? throw new BadMethodCallException(\sprintf('The "request()" method must be called before "%s()".', __METHOD__));
}
/**
* Returns the current origin Request instance.
*
* The origin request is the request instance that is sent
* to the code that handles requests.
*
* @psalm-return TRequest
*
* @see doRequest()
*/
public function getRequest(): object
{
return $this->request ?? throw new BadMethodCallException(\sprintf('The "request()" method must be called before "%s()".', __METHOD__));
}
/**
* Clicks on a given link.
*
* @param array $serverParameters An array of server parameters
*/
public function click(Link $link, array $serverParameters = []): Crawler
{
if ($link instanceof Form) {
return $this->submit($link, [], $serverParameters);
}
return $this->request($link->getMethod(), $link->getUri(), [], [], $serverParameters);
}
/**
* Clicks the first link (or clickable image) that contains the given text.
*
* @param string $linkText The text of the link or the alt attribute of the clickable image
* @param array $serverParameters An array of server parameters
*/
public function clickLink(string $linkText, array $serverParameters = []): Crawler
{
$crawler = $this->crawler ?? throw new BadMethodCallException(\sprintf('The "request()" method must be called before "%s()".', __METHOD__));
return $this->click($crawler->selectLink($linkText)->link(), $serverParameters);
}
/**
* Submits a form.
*
* @param array $values An array of form field values
* @param array $serverParameters An array of server parameters
*/
public function submit(Form $form, array $values = [], array $serverParameters = []): Crawler
{
$form->setValues($values);
return $this->request($form->getMethod(), $form->getUri(), $form->getPhpValues(), $form->getPhpFiles(), $serverParameters);
}
/**
* Finds the first form that contains a button with the given content and
* uses it to submit the given form field values.
*
* @param string $button The text content, id, value or name of the form <button> or <input type="submit">
* @param array $fieldValues Use this syntax: ['my_form[name]' => '...', 'my_form[email]' => '...']
* @param string $method The HTTP method used to submit the form
* @param array $serverParameters These values override the ones stored in $_SERVER (HTTP headers must include an HTTP_ prefix as PHP does)
*/
public function submitForm(string $button, array $fieldValues = [], string $method = 'POST', array $serverParameters = []): Crawler
{
$crawler = $this->crawler ?? throw new BadMethodCallException(\sprintf('The "request()" method must be called before "%s()".', __METHOD__));
$buttonNode = $crawler->selectButton($button);
if (0 === $buttonNode->count()) {
throw new InvalidArgumentException(\sprintf('There is no button with "%s" as its content, id, value or name.', $button));
}
$form = $buttonNode->form($fieldValues, $method);
return $this->submit($form, [], $serverParameters);
}
/**
* Calls a URI.
*
* @param string $method The request method
* @param string $uri The URI to fetch
* @param array $parameters The Request parameters
* @param array $files The files
* @param array $server The server parameters (HTTP headers are referenced with an HTTP_ prefix as PHP does)
* @param string $content The raw body data
* @param bool $changeHistory Whether to update the history or not (only used internally for back(), forward(), and reload())
*/
public function request(string $method, string $uri, array $parameters = [], array $files = [], array $server = [], ?string $content = null, bool $changeHistory = true): Crawler
{
if ($this->isMainRequest) {
$this->redirectCount = 0;
} else {
++$this->redirectCount;
}
$originalUri = $uri;
$uri = $this->getAbsoluteUri($uri);
$server = array_merge($this->server, $server);
if (!empty($server['HTTP_HOST']) && !parse_url($originalUri, \PHP_URL_HOST)) {
$uri = preg_replace('{^(https?\://)'.preg_quote($this->extractHost($uri)).'}', '${1}'.$server['HTTP_HOST'], $uri);
}
if (isset($server['HTTPS']) && !parse_url($originalUri, \PHP_URL_SCHEME)) {
$uri = preg_replace('{^'.parse_url($uri, \PHP_URL_SCHEME).'}', $server['HTTPS'] ? 'https' : 'http', $uri);
}
if (!isset($server['HTTP_REFERER']) && !$this->history->isEmpty()) {
$server['HTTP_REFERER'] = $this->history->current()->getUri();
}
if (empty($server['HTTP_HOST'])) {
$server['HTTP_HOST'] = $this->extractHost($uri);
}
$server['HTTPS'] = 'https' === parse_url($uri, \PHP_URL_SCHEME);
$this->internalRequest = new Request($uri, $method, $parameters, $files, $this->cookieJar->allValues($uri), $server, $content);
$this->request = $this->filterRequest($this->internalRequest);
if (true === $changeHistory) {
$this->history->add($this->internalRequest);
}
if ($this->insulated) {
$this->response = $this->doRequestInProcess($this->request);
} else {
$this->response = $this->doRequest($this->request);
}
$this->internalResponse = $this->filterResponse($this->response);
$this->cookieJar->updateFromResponse($this->internalResponse, $uri);
$status = $this->internalResponse->getStatusCode();
if ($status >= 300 && $status < 400) {
$this->redirect = $this->internalResponse->getHeader('Location');
} else {
$this->redirect = null;
}
if ($this->followRedirects && $this->redirect) {
$this->redirects[serialize($this->history->current())] = true;
return $this->crawler = $this->followRedirect();
}
$this->crawler = $this->createCrawlerFromContent($this->internalRequest->getUri(), $this->internalResponse->getContent(), $this->internalResponse->getHeader('Content-Type') ?? '');
// Check for meta refresh redirect
if ($this->followMetaRefresh && null !== $redirect = $this->getMetaRefreshUrl()) {
$this->redirect = $redirect;
$this->redirects[serialize($this->history->current())] = true;
$this->crawler = $this->followRedirect();
}
return $this->crawler;
}
/**
* Makes a request in another process.
*
* @psalm-param TRequest $request
*
* @return object
*
* @psalm-return TResponse
*
* @throws \RuntimeException When processing returns exit code
*/
protected function doRequestInProcess(object $request)
{
$deprecationsFile = tempnam(sys_get_temp_dir(), 'deprec');
putenv('SYMFONY_DEPRECATIONS_SERIALIZE='.$deprecationsFile);
$_ENV['SYMFONY_DEPRECATIONS_SERIALIZE'] = $deprecationsFile;
$process = new PhpProcess($this->getScript($request), null, null);
$process->run();
if (file_exists($deprecationsFile)) {
$deprecations = file_get_contents($deprecationsFile);
unlink($deprecationsFile);
foreach ($deprecations ? unserialize($deprecations) : [] as $deprecation) {
if ($deprecation[0]) {
// unsilenced on purpose
trigger_error($deprecation[1], \E_USER_DEPRECATED);
} else {
@trigger_error($deprecation[1], \E_USER_DEPRECATED);
}
}
}
if (!$process->isSuccessful() || !preg_match('/^O\:\d+\:/', $process->getOutput())) {
throw new RuntimeException(\sprintf('OUTPUT: %s ERROR OUTPUT: %s.', $process->getOutput(), $process->getErrorOutput()));
}
return unserialize($process->getOutput());
}
/**
* Makes a request.
*
* @psalm-param TRequest $request
*
* @return object
*
* @psalm-return TResponse
*/
abstract protected function doRequest(object $request);
/**
* Returns the script to execute when the request must be insulated.
*
* @param object $request An origin request instance
*
* @psalm-param TRequest $request
*
* @return string
*
* @throws LogicException When this abstract class is not implemented
*/
protected function getScript(object $request)
{
throw new LogicException('To insulate requests, you need to override the getScript() method.');
}
/**
* Filters the BrowserKit request to the origin one.
*
* @return object
*
* @psalm-return TRequest
*/
protected function filterRequest(Request $request)
{
return $request;
}
/**
* Filters the origin response to the BrowserKit one.
*
* @psalm-param TResponse $response
*
* @return Response
*/
protected function filterResponse(object $response)
{
return $response;
}
/**
* Creates a crawler.
*
* This method returns null if the DomCrawler component is not available.
*/
protected function createCrawlerFromContent(string $uri, string $content, string $type): ?Crawler
{
if (!class_exists(Crawler::class)) {
return null;
}
$crawler = new Crawler(null, $uri, null, $this->useHtml5Parser);
$crawler->addContent($content, $type);
return $crawler;
}
/**
* Goes back in the browser history.
*/
public function back(): Crawler
{
do {
$request = $this->history->back();
} while (\array_key_exists(serialize($request), $this->redirects));
return $this->requestFromRequest($request, false);
}
/**
* Goes forward in the browser history.
*/
public function forward(): Crawler
{
do {
$request = $this->history->forward();
} while (\array_key_exists(serialize($request), $this->redirects));
return $this->requestFromRequest($request, false);
}
/**
* Reloads the current browser.
*/
public function reload(): Crawler
{
return $this->requestFromRequest($this->history->current(), false);
}
/**
* Follow redirects?
*
* @throws LogicException If request was not a redirect
*/
public function followRedirect(): Crawler
{
if (!isset($this->redirect)) {
throw new LogicException('The request was not redirected.');
}
if (-1 !== $this->maxRedirects) {
if ($this->redirectCount > $this->maxRedirects) {
$this->redirectCount = 0;
throw new LogicException(\sprintf('The maximum number (%d) of redirections was reached.', $this->maxRedirects));
}
}
$request = $this->internalRequest;
if (\in_array($this->internalResponse->getStatusCode(), [301, 302, 303])) {
$method = 'GET';
$files = [];
$content = null;
} else {
$method = $request->getMethod();
$files = $request->getFiles();
$content = $request->getContent();
}
if ('GET' === strtoupper($method)) {
// Don't forward parameters for GET request as it should reach the redirection URI
$parameters = [];
} else {
$parameters = $request->getParameters();
}
$server = $request->getServer();
$server = $this->updateServerFromUri($server, $this->redirect);
$this->isMainRequest = false;
$response = $this->request($method, $this->redirect, $parameters, $files, $server, $content);
$this->isMainRequest = true;
return $response;
}
/**
* @see https://dev.w3.org/html5/spec-preview/the-meta-element.html#attr-meta-http-equiv-refresh
*/
private function getMetaRefreshUrl(): ?string
{
$metaRefresh = $this->getCrawler()->filter('head meta[http-equiv="refresh"]');
foreach ($metaRefresh->extract(['content']) as $content) {
if (preg_match('/^\s*0\s*;\s*URL\s*=\s*(?|\'([^\']++)|"([^"]++)|([^\'"].*))/i', $content, $m)) {
return str_replace("\t\r\n", '', rtrim($m[1]));
}
}
return null;
}
/**
* Restarts the client.
*
* It flushes history and all cookies.
*/
public function restart(): void
{
$this->cookieJar->clear();
$this->history->clear();
}
/**
* Takes a URI and converts it to absolute if it is not already absolute.
*/
protected function getAbsoluteUri(string $uri): string
{
// already absolute?
if (str_starts_with($uri, 'http://') || str_starts_with($uri, 'https://')) {
return $uri;
}
if (!$this->history->isEmpty()) {
$currentUri = $this->history->current()->getUri();
} else {
$currentUri = \sprintf('http%s://%s/',
isset($this->server['HTTPS']) ? 's' : '',
$this->server['HTTP_HOST'] ?? 'localhost'
);
}
// protocol relative URL
if ('' !== trim($uri, '/') && str_starts_with($uri, '//')) {
return parse_url($currentUri, \PHP_URL_SCHEME).':'.$uri;
}
// anchor or query string parameters?
if (!$uri || '#' === $uri[0] || '?' === $uri[0]) {
return preg_replace('/[#?].*?$/', '', $currentUri).$uri;
}
if ('/' !== $uri[0]) {
$path = parse_url($currentUri, \PHP_URL_PATH);
if (!str_ends_with($path, '/')) {
$path = substr($path, 0, strrpos($path, '/') + 1);
}
$uri = $path.$uri;
}
return preg_replace('#^(.*?//[^/]+)\/.*$#', '$1', $currentUri).$uri;
}
/**
* Makes a request from a Request object directly.
*
* @param bool $changeHistory Whether to update the history or not (only used internally for back(), forward(), and reload())
*/
protected function requestFromRequest(Request $request, bool $changeHistory = true): Crawler
{
return $this->request($request->getMethod(), $request->getUri(), $request->getParameters(), $request->getFiles(), $request->getServer(), $request->getContent(), $changeHistory);
}
private function updateServerFromUri(array $server, string $uri): array
{
$server['HTTP_HOST'] = $this->extractHost($uri);
$scheme = parse_url($uri, \PHP_URL_SCHEME);
$server['HTTPS'] = null === $scheme ? $server['HTTPS'] : 'https' === $scheme;
unset($server['HTTP_IF_NONE_MATCH'], $server['HTTP_IF_MODIFIED_SINCE']);
return $server;
}
private function extractHost(string $uri): ?string
{
$host = parse_url($uri, \PHP_URL_HOST);
if ($port = parse_url($uri, \PHP_URL_PORT)) {
return $host.':'.$port;
}
return $host;
}
}

78
vendor/symfony/browser-kit/CHANGELOG.md vendored Normal file
View File

@@ -0,0 +1,78 @@
CHANGELOG
=========
6.4
---
* Add argument `$serverParameters` to `AbstractBrowser::click()` and `AbstractBrowser::clickLink()`
6.3
---
* Add `AbstractBrowser::useHtml5Parser()`
6.1
---
* Add `toArray` method to `Response`
5.3
---
* Added `jsonRequest` method to `AbstractBrowser`
* Allowed sending a body with GET requests when a content-type is defined
5.2.0
-----
* [BC BREAK] Request parameters are now casted to string in `Request::__construct()`.
4.3.0
-----
* Added PHPUnit constraints: `BrowserCookieValueSame` and `BrowserHasCookie`
* Added `HttpBrowser`, an implementation of a browser with the HttpClient component
* Renamed `Client` to `AbstractBrowser`
* Marked `Response` final.
* Deprecated `Response::buildHeader()`
* Deprecated `Response::getStatus()`, use `Response::getStatusCode()` instead
4.2.0
-----
* The method `Client::submit()` will have a new `$serverParameters` argument
in version 5.0, not defining it is deprecated
* Added ability to read the "samesite" attribute of cookies using `Cookie::getSameSite()`
3.4.0
-----
* [BC BREAK] Client will skip redirects during history navigation
(back and forward calls) according to W3C Browsers recommendation
3.3.0
-----
* [BC BREAK] The request method is dropped from POST to GET when the response
status code is 301.
3.2.0
-----
* Client HTTP user agent has been changed to 'Symfony BrowserKit'
2.3.0
-----
* [BC BREAK] `Client::followRedirect()` won't redirect responses with
a non-3xx Status Code and `Location` header anymore, as per
http://tools.ietf.org/html/rfc2616#section-14.30
* added `Client::getInternalRequest()` and `Client::getInternalResponse()` to
have access to the BrowserKit internal request and response objects
2.1.0
-----
* [BC BREAK] The CookieJar internals have changed to allow cookies with the
same name on different sub-domains/sub-paths

299
vendor/symfony/browser-kit/Cookie.php vendored Normal file
View File

@@ -0,0 +1,299 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\BrowserKit;
use Symfony\Component\BrowserKit\Exception\InvalidArgumentException;
use Symfony\Component\BrowserKit\Exception\UnexpectedValueException;
/**
* Cookie represents an HTTP cookie.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class Cookie
{
/**
* Handles dates as defined by RFC 2616 section 3.3.1, and also some other
* non-standard, but common formats.
*/
private const DATE_FORMATS = [
'D, d M Y H:i:s T',
'D, d-M-y H:i:s T',
'D, d-M-Y H:i:s T',
'D, d-m-y H:i:s T',
'D, d-m-Y H:i:s T',
'D M j G:i:s Y',
'D M d H:i:s Y T',
];
protected string $value;
protected ?string $expires = null;
protected string $path;
protected string $rawValue;
/**
* Sets a cookie.
*
* @param string $name The cookie name
* @param string|null $value The value of the cookie
* @param string|null $expires The time the cookie expires
* @param string|null $path The path on the server in which the cookie will be available on
* @param string $domain The domain that the cookie is available
* @param bool $secure Indicates that the cookie should only be transmitted over a secure HTTPS connection from the client
* @param bool $httponly The cookie httponly flag
* @param bool $encodedValue Whether the value is encoded or not
* @param string|null $samesite The cookie samesite attribute
*/
public function __construct(
private string $name,
?string $value,
?string $expires = null,
?string $path = null,
private string $domain = '',
private bool $secure = false,
private bool $httponly = true,
bool $encodedValue = false,
private ?string $samesite = null,
) {
if ($encodedValue) {
$this->rawValue = $value ?? '';
$this->value = urldecode($this->rawValue);
} else {
$this->value = $value ?? '';
$this->rawValue = rawurlencode($this->value);
}
$this->path = $path ?: '/';
if (null !== $expires) {
$timestampAsDateTime = \DateTimeImmutable::createFromFormat('U', $expires);
if (false === $timestampAsDateTime) {
throw new UnexpectedValueException(\sprintf('The cookie expiration time "%s" is not valid.', $expires));
}
$this->expires = $timestampAsDateTime->format('U');
}
}
/**
* Returns the HTTP representation of the Cookie.
*/
public function __toString(): string
{
$cookie = \sprintf('%s=%s', $this->name, $this->rawValue);
if (null !== $this->expires) {
$dateTime = \DateTimeImmutable::createFromFormat('U', $this->expires, new \DateTimeZone('GMT'));
$cookie .= '; expires='.str_replace('+0000', '', $dateTime->format(self::DATE_FORMATS[0]));
}
if ('' !== $this->domain) {
$cookie .= '; domain='.$this->domain;
}
if ($this->path) {
$cookie .= '; path='.$this->path;
}
if ($this->secure) {
$cookie .= '; secure';
}
if ($this->httponly) {
$cookie .= '; httponly';
}
if (null !== $this->samesite) {
$cookie .= '; samesite='.$this->samesite;
}
return $cookie;
}
/**
* Creates a Cookie instance from a Set-Cookie header value.
*
* @throws InvalidArgumentException
*/
public static function fromString(string $cookie, ?string $url = null): static
{
$parts = explode(';', $cookie);
if (!str_contains($parts[0], '=')) {
throw new InvalidArgumentException(\sprintf('The cookie string "%s" is not valid.', $parts[0]));
}
[$name, $value] = explode('=', array_shift($parts), 2);
$values = [
'name' => trim($name),
'value' => trim($value),
'expires' => null,
'path' => '/',
'domain' => '',
'secure' => false,
'httponly' => false,
'passedRawValue' => true,
'samesite' => null,
];
if (null !== $url) {
if (false === ($urlParts = parse_url($url)) || !isset($urlParts['host'])) {
throw new InvalidArgumentException(\sprintf('The URL "%s" is not valid.', $url));
}
$values['domain'] = $urlParts['host'];
$values['path'] = isset($urlParts['path']) ? substr($urlParts['path'], 0, strrpos($urlParts['path'], '/')) : '';
}
foreach ($parts as $part) {
$part = trim($part);
if ('secure' === strtolower($part)) {
// Ignore the secure flag if the original URI is not given or is not HTTPS
if (null === $url || !isset($urlParts['scheme']) || 'https' !== $urlParts['scheme']) {
continue;
}
$values['secure'] = true;
continue;
}
if ('httponly' === strtolower($part)) {
$values['httponly'] = true;
continue;
}
if (2 === \count($elements = explode('=', $part, 2))) {
if ('expires' === strtolower($elements[0])) {
$elements[1] = self::parseDate($elements[1]);
}
$values[strtolower($elements[0])] = $elements[1];
}
}
return new static(
$values['name'],
$values['value'],
$values['expires'],
$values['path'],
$values['domain'],
$values['secure'],
$values['httponly'],
$values['passedRawValue'],
$values['samesite']
);
}
private static function parseDate(string $dateValue): ?string
{
// trim single quotes around date if present
if (($length = \strlen($dateValue)) > 1 && "'" === $dateValue[0] && "'" === $dateValue[$length - 1]) {
$dateValue = substr($dateValue, 1, -1);
}
foreach (self::DATE_FORMATS as $dateFormat) {
if (false !== $date = \DateTimeImmutable::createFromFormat($dateFormat, $dateValue, new \DateTimeZone('GMT'))) {
return $date->format('U');
}
}
// attempt a fallback for unusual formatting
if (false !== $date = date_create_immutable($dateValue, new \DateTimeZone('GMT'))) {
return $date->format('U');
}
return null;
}
/**
* Gets the name of the cookie.
*/
public function getName(): string
{
return $this->name;
}
/**
* Gets the value of the cookie.
*/
public function getValue(): string
{
return $this->value;
}
/**
* Gets the raw value of the cookie.
*/
public function getRawValue(): string
{
return $this->rawValue;
}
/**
* Gets the expires time of the cookie.
*/
public function getExpiresTime(): ?string
{
return $this->expires;
}
/**
* Gets the path of the cookie.
*/
public function getPath(): string
{
return $this->path;
}
/**
* Gets the domain of the cookie.
*/
public function getDomain(): string
{
return $this->domain;
}
/**
* Returns the secure flag of the cookie.
*/
public function isSecure(): bool
{
return $this->secure;
}
/**
* Returns the httponly flag of the cookie.
*/
public function isHttpOnly(): bool
{
return $this->httponly;
}
/**
* Returns true if the cookie has expired.
*/
public function isExpired(): bool
{
return null !== $this->expires && 0 != $this->expires && $this->expires <= time();
}
/**
* Gets the samesite attribute of the cookie.
*/
public function getSameSite(): ?string
{
return $this->samesite;
}
}

218
vendor/symfony/browser-kit/CookieJar.php vendored Normal file
View File

@@ -0,0 +1,218 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\BrowserKit;
use Symfony\Component\BrowserKit\Exception\InvalidArgumentException;
/**
* CookieJar.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class CookieJar
{
protected array $cookieJar = [];
public function set(Cookie $cookie): void
{
$this->cookieJar[$cookie->getDomain()][$cookie->getPath()][$cookie->getName()] = $cookie;
}
/**
* Gets a cookie by name.
*
* You should never use an empty domain, but if you do so,
* this method returns the first cookie for the given name/path
* (this behavior ensures a BC behavior with previous versions of
* Symfony).
*/
public function get(string $name, string $path = '/', ?string $domain = null): ?Cookie
{
$this->flushExpiredCookies();
foreach ($this->cookieJar as $cookieDomain => $pathCookies) {
if ($cookieDomain && $domain) {
$cookieDomain = '.'.ltrim($cookieDomain, '.');
if (!str_ends_with('.'.$domain, $cookieDomain)) {
continue;
}
}
foreach ($pathCookies as $cookiePath => $namedCookies) {
if (!str_starts_with($path, $cookiePath)) {
continue;
}
if (isset($namedCookies[$name])) {
return $namedCookies[$name];
}
}
}
return null;
}
/**
* Removes a cookie by name.
*
* You should never use an empty domain, but if you do so,
* all cookies for the given name/path expire (this behavior
* ensures a BC behavior with previous versions of Symfony).
*/
public function expire(string $name, ?string $path = '/', ?string $domain = null): void
{
$path ??= '/';
if (!$domain) {
// an empty domain means any domain
// this should never happen but it allows for a better BC
$domains = array_keys($this->cookieJar);
} else {
$domains = [$domain];
}
foreach ($domains as $domain) {
unset($this->cookieJar[$domain][$path][$name]);
if (empty($this->cookieJar[$domain][$path])) {
unset($this->cookieJar[$domain][$path]);
if (empty($this->cookieJar[$domain])) {
unset($this->cookieJar[$domain]);
}
}
}
}
/**
* Removes all the cookies from the jar.
*/
public function clear(): void
{
$this->cookieJar = [];
}
/**
* Updates the cookie jar from a response Set-Cookie headers.
*
* @param string[] $setCookies Set-Cookie headers from an HTTP response
*/
public function updateFromSetCookie(array $setCookies, ?string $uri = null): void
{
$cookies = [];
foreach ($setCookies as $cookie) {
foreach (explode(',', $cookie) as $i => $part) {
if (0 === $i || preg_match('/^(?P<token>\s*[0-9A-Za-z!#\$%\&\'\*\+\-\.^_`\|~]+)=/', $part)) {
$cookies[] = ltrim($part);
} else {
$cookies[\count($cookies) - 1] .= ','.$part;
}
}
}
foreach ($cookies as $cookie) {
try {
$this->set(Cookie::fromString($cookie, $uri));
} catch (InvalidArgumentException) {
// invalid cookies are just ignored
}
}
}
/**
* Updates the cookie jar from a Response object.
*/
public function updateFromResponse(Response $response, ?string $uri = null): void
{
$this->updateFromSetCookie($response->getHeader('Set-Cookie', false), $uri);
}
/**
* Returns not yet expired cookies.
*
* @return Cookie[]
*/
public function all(): array
{
$this->flushExpiredCookies();
$flattenedCookies = [];
foreach ($this->cookieJar as $path) {
foreach ($path as $cookies) {
foreach ($cookies as $cookie) {
$flattenedCookies[] = $cookie;
}
}
}
return $flattenedCookies;
}
/**
* Returns not yet expired cookie values for the given URI.
*/
public function allValues(string $uri, bool $returnsRawValue = false): array
{
$this->flushExpiredCookies();
$parts = array_replace(['path' => '/'], parse_url($uri));
$cookies = [];
foreach ($this->cookieJar as $domain => $pathCookies) {
if ($domain) {
$domain = '.'.ltrim($domain, '.');
if (!str_ends_with('.'.$parts['host'], $domain)) {
continue;
}
}
foreach ($pathCookies as $path => $namedCookies) {
if (!str_starts_with($parts['path'], $path)) {
continue;
}
foreach ($namedCookies as $cookie) {
if ($cookie->isSecure() && 'https' !== $parts['scheme']) {
continue;
}
$cookies[$cookie->getName()] = $returnsRawValue ? $cookie->getRawValue() : $cookie->getValue();
}
}
}
return $cookies;
}
/**
* Returns not yet expired raw cookie values for the given URI.
*/
public function allRawValues(string $uri): array
{
return $this->allValues($uri, true);
}
/**
* Removes all expired cookies.
*/
public function flushExpiredCookies(): void
{
foreach ($this->cookieJar as $domain => $pathCookies) {
foreach ($pathCookies as $path => $namedCookies) {
foreach ($namedCookies as $name => $cookie) {
if ($cookie->isExpired()) {
unset($this->cookieJar[$domain][$path][$name]);
}
}
}
}
}
}

View File

@@ -0,0 +1,16 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\BrowserKit\Exception;
class BadMethodCallException extends \BadMethodCallException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\BrowserKit\Exception;
/**
* Base ExceptionInterface for the BrowserKit component.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
interface ExceptionInterface extends \Throwable
{
}

View File

@@ -0,0 +1,16 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\BrowserKit\Exception;
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,16 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\BrowserKit\Exception;
class JsonException extends \JsonException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,16 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\BrowserKit\Exception;
class LogicException extends \LogicException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,16 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\BrowserKit\Exception;
class RuntimeException extends \RuntimeException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,16 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\BrowserKit\Exception;
class UnexpectedValueException extends \UnexpectedValueException implements ExceptionInterface
{
}

94
vendor/symfony/browser-kit/History.php vendored Normal file
View File

@@ -0,0 +1,94 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\BrowserKit;
use Symfony\Component\BrowserKit\Exception\LogicException;
/**
* History.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class History
{
protected array $stack = [];
protected int $position = -1;
/**
* Clears the history.
*/
public function clear(): void
{
$this->stack = [];
$this->position = -1;
}
/**
* Adds a Request to the history.
*/
public function add(Request $request): void
{
$this->stack = \array_slice($this->stack, 0, $this->position + 1);
$this->stack[] = clone $request;
$this->position = \count($this->stack) - 1;
}
/**
* Returns true if the history is empty.
*/
public function isEmpty(): bool
{
return 0 === \count($this->stack);
}
/**
* Goes back in the history.
*
* @throws LogicException if the stack is already on the first page
*/
public function back(): Request
{
if ($this->position < 1) {
throw new LogicException('You are already on the first page.');
}
return clone $this->stack[--$this->position];
}
/**
* Goes forward in the history.
*
* @throws LogicException if the stack is already on the last page
*/
public function forward(): Request
{
if ($this->position > \count($this->stack) - 2) {
throw new LogicException('You are already on the last page.');
}
return clone $this->stack[++$this->position];
}
/**
* Returns the current element in the history.
*
* @throws LogicException if the stack is empty
*/
public function current(): Request
{
if (-1 === $this->position) {
throw new LogicException('The page history is empty.');
}
return clone $this->stack[$this->position];
}
}

View File

@@ -0,0 +1,161 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\BrowserKit;
use Symfony\Component\BrowserKit\Exception\LogicException;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\Mime\Part\AbstractPart;
use Symfony\Component\Mime\Part\DataPart;
use Symfony\Component\Mime\Part\Multipart\FormDataPart;
use Symfony\Component\Mime\Part\TextPart;
use Symfony\Contracts\HttpClient\HttpClientInterface;
/**
* An implementation of a browser using the HttpClient component
* to make real HTTP requests.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @template-extends AbstractBrowser<Request, Response>
*/
class HttpBrowser extends AbstractBrowser
{
private HttpClientInterface $client;
public function __construct(?HttpClientInterface $client = null, ?History $history = null, ?CookieJar $cookieJar = null)
{
if (!$client && !class_exists(HttpClient::class)) {
throw new LogicException(\sprintf('You cannot use "%s" as the HttpClient component is not installed. Try running "composer require symfony/http-client".', __CLASS__));
}
$this->client = $client ?? HttpClient::create();
parent::__construct([], $history, $cookieJar);
}
/**
* @param Request $request
*/
protected function doRequest(object $request): Response
{
$headers = $this->getHeaders($request);
[$body, $extraHeaders] = $this->getBodyAndExtraHeaders($request, $headers);
$response = $this->client->request($request->getMethod(), $request->getUri(), [
'headers' => array_merge($headers, $extraHeaders),
'body' => $body,
'max_redirects' => 0,
]);
return new Response($response->getContent(false), $response->getStatusCode(), $response->getHeaders(false));
}
/**
* @return array [$body, $headers]
*/
private function getBodyAndExtraHeaders(Request $request, array $headers): array
{
if (\in_array($request->getMethod(), ['GET', 'HEAD']) && !isset($headers['content-type'])) {
return ['', []];
}
if (!class_exists(AbstractPart::class)) {
throw new LogicException('You cannot pass non-empty bodies as the Mime component is not installed. Try running "composer require symfony/mime".');
}
if (null !== $content = $request->getContent()) {
if (isset($headers['content-type'])) {
return [$content, []];
}
$part = new TextPart($content, 'utf-8', 'plain', '8bit');
return [$part->bodyToString(), $part->getPreparedHeaders()->toArray()];
}
$fields = $request->getParameters();
if ($uploadedFiles = $this->getUploadedFiles($request->getFiles())) {
$part = new FormDataPart(array_replace_recursive($fields, $uploadedFiles));
return [$part->bodyToIterable(), $part->getPreparedHeaders()->toArray()];
}
if (!$fields) {
return ['', []];
}
array_walk_recursive($fields, $caster = static function (&$v) use (&$caster) {
if (\is_object($v)) {
if ($vars = get_object_vars($v)) {
array_walk_recursive($vars, $caster);
$v = $vars;
} elseif ($v instanceof \Stringable) {
$v = (string) $v;
}
}
});
return [http_build_query($fields, '', '&'), ['Content-Type' => 'application/x-www-form-urlencoded']];
}
protected function getHeaders(Request $request): array
{
$headers = [];
foreach ($request->getServer() as $key => $value) {
$key = strtolower(str_replace('_', '-', $key));
$contentHeaders = ['content-length' => true, 'content-md5' => true, 'content-type' => true];
if (str_starts_with($key, 'http-')) {
$headers[substr($key, 5)] = $value;
} elseif (isset($contentHeaders[$key])) {
// CONTENT_* are not prefixed with HTTP_
$headers[$key] = $value;
}
}
$cookies = [];
foreach ($this->getCookieJar()->allRawValues($request->getUri()) as $name => $value) {
$cookies[] = $name.'='.$value;
}
if ($cookies) {
$headers['cookie'] = implode('; ', $cookies);
}
return $headers;
}
/**
* Recursively go through the list. If the file has a tmp_name, convert it to a DataPart.
* Keep the original hierarchy.
*/
private function getUploadedFiles(array $files): array
{
$uploadedFiles = [];
foreach ($files as $name => $file) {
if (!\is_array($file)) {
return $uploadedFiles;
}
if (!isset($file['tmp_name'])) {
$uploadedFiles[$name] = $this->getUploadedFiles($file);
continue;
}
if ('' === $file['tmp_name']) {
$uploadedFiles[$name] = new DataPart('', '');
continue;
}
$uploadedFiles[$name] = DataPart::fromPath($file['tmp_name'], $file['name']);
}
return $uploadedFiles;
}
}

19
vendor/symfony/browser-kit/LICENSE vendored Normal file
View File

@@ -0,0 +1,19 @@
Copyright (c) 2004-present Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

17
vendor/symfony/browser-kit/README.md vendored Normal file
View File

@@ -0,0 +1,17 @@
BrowserKit Component
====================
The BrowserKit component simulates the behavior of a web browser, allowing you
to make requests, click on links and submit forms programmatically.
The component comes with a concrete implementation that uses the HttpClient
component to make real HTTP requests.
Resources
---------
* [Documentation](https://symfony.com/doc/current/components/browser_kit/introduction.html)
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
* [Report issues](https://github.com/symfony/symfony/issues) and
[send Pull Requests](https://github.com/symfony/symfony/pulls)
in the [main Symfony repository](https://github.com/symfony/symfony)

99
vendor/symfony/browser-kit/Request.php vendored Normal file
View File

@@ -0,0 +1,99 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\BrowserKit;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class Request
{
/**
* @param string $uri The request URI
* @param string $method The HTTP method request
* @param array $parameters The request parameters
* @param array $files An array of uploaded files
* @param array $cookies An array of cookies
* @param array $server An array of server parameters
* @param string|null $content The raw body data
*/
public function __construct(
protected string $uri,
protected string $method,
protected array $parameters = [],
protected array $files = [],
protected array $cookies = [],
protected array $server = [],
protected ?string $content = null,
) {
array_walk_recursive($parameters, static function (&$value) {
$value = (string) $value;
});
$this->parameters = $parameters;
}
/**
* Gets the request URI.
*/
public function getUri(): string
{
return $this->uri;
}
/**
* Gets the request HTTP method.
*/
public function getMethod(): string
{
return $this->method;
}
/**
* Gets the request parameters.
*/
public function getParameters(): array
{
return $this->parameters;
}
/**
* Gets the request server files.
*/
public function getFiles(): array
{
return $this->files;
}
/**
* Gets the request cookies.
*/
public function getCookies(): array
{
return $this->cookies;
}
/**
* Gets the request server parameters.
*/
public function getServer(): array
{
return $this->server;
}
/**
* Gets the request raw body data.
*/
public function getContent(): ?string
{
return $this->content;
}
}

109
vendor/symfony/browser-kit/Response.php vendored Normal file
View File

@@ -0,0 +1,109 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\BrowserKit;
use Symfony\Component\BrowserKit\Exception\JsonException;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
final class Response
{
private array $jsonData;
/**
* The headers array is a set of key/value pairs. If a header is present multiple times
* then the value is an array of all the values.
*
* @param string $content The content of the response
* @param int $status The response status code (302 "Found" by default)
* @param array $headers An array of headers
*/
public function __construct(
private string $content = '',
private int $status = 200,
private array $headers = [],
) {
}
/**
* Converts the response object to string containing all headers and the response content.
*/
public function __toString(): string
{
$headers = '';
foreach ($this->headers as $name => $value) {
if (\is_string($value)) {
$headers .= \sprintf("%s: %s\n", $name, $value);
} else {
foreach ($value as $headerValue) {
$headers .= \sprintf("%s: %s\n", $name, $headerValue);
}
}
}
return $headers."\n".$this->content;
}
public function getContent(): string
{
return $this->content;
}
public function getStatusCode(): int
{
return $this->status;
}
public function getHeaders(): array
{
return $this->headers;
}
/**
* @return string|array|null The first header value if $first is true, an array of values otherwise
*/
public function getHeader(string $header, bool $first = true): string|array|null
{
$normalizedHeader = str_replace('-', '_', strtolower($header));
foreach ($this->headers as $key => $value) {
if (str_replace('-', '_', strtolower($key)) === $normalizedHeader) {
if ($first) {
return \is_array($value) ? (\count($value) ? $value[0] : '') : $value;
}
return \is_array($value) ? $value : [$value];
}
}
return $first ? null : [];
}
public function toArray(): array
{
if (isset($this->jsonData)) {
return $this->jsonData;
}
try {
$content = json_decode($this->content, true, flags: \JSON_BIGINT_AS_STRING | \JSON_THROW_ON_ERROR);
} catch (\JsonException $e) {
throw new JsonException($e->getMessage(), $e->getCode(), $e);
}
if (!\is_array($content)) {
throw new JsonException(\sprintf('JSON content was expected to decode to an array, "%s" returned.', get_debug_type($content)));
}
return $this->jsonData = $content;
}
}

View File

@@ -0,0 +1,62 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\BrowserKit\Test\Constraint;
use PHPUnit\Framework\Constraint\Constraint;
use Symfony\Component\BrowserKit\AbstractBrowser;
final class BrowserCookieValueSame extends Constraint
{
public function __construct(
private string $name,
private string $value,
private bool $raw = false,
private string $path = '/',
private ?string $domain = null,
) {
}
public function toString(): string
{
$str = \sprintf('has cookie "%s"', $this->name);
if ('/' !== $this->path) {
$str .= \sprintf(' with path "%s"', $this->path);
}
if ($this->domain) {
$str .= \sprintf(' for domain "%s"', $this->domain);
}
$str .= \sprintf(' with %svalue "%s"', $this->raw ? 'raw ' : '', $this->value);
return $str;
}
/**
* @param AbstractBrowser $browser
*/
protected function matches($browser): bool
{
$cookie = $browser->getCookieJar()->get($this->name, $this->path, $this->domain);
if (!$cookie) {
return false;
}
return $this->value === ($this->raw ? $cookie->getRawValue() : $cookie->getValue());
}
/**
* @param AbstractBrowser $browser
*/
protected function failureDescription($browser): string
{
return 'the Browser '.$this->toString();
}
}

View File

@@ -0,0 +1,54 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\BrowserKit\Test\Constraint;
use PHPUnit\Framework\Constraint\Constraint;
use Symfony\Component\BrowserKit\AbstractBrowser;
final class BrowserHasCookie extends Constraint
{
public function __construct(
private string $name,
private string $path = '/',
private ?string $domain = null,
) {
}
public function toString(): string
{
$str = \sprintf('has cookie "%s"', $this->name);
if ('/' !== $this->path) {
$str .= \sprintf(' with path "%s"', $this->path);
}
if ($this->domain) {
$str .= \sprintf(' for domain "%s"', $this->domain);
}
return $str;
}
/**
* @param AbstractBrowser $browser
*/
protected function matches($browser): bool
{
return null !== $browser->getCookieJar()->get($this->name, $this->path, $this->domain);
}
/**
* @param AbstractBrowser $browser
*/
protected function failureDescription($browser): string
{
return 'the Browser '.$this->toString();
}
}

View File

@@ -0,0 +1,35 @@
{
"name": "symfony/browser-kit",
"type": "library",
"description": "Simulates the behavior of a web browser, allowing you to make requests, click on links and submit forms programmatically",
"keywords": [],
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"require": {
"php": ">=8.2",
"symfony/dom-crawler": "^6.4|^7.0"
},
"require-dev": {
"symfony/css-selector": "^6.4|^7.0",
"symfony/http-client": "^6.4|^7.0",
"symfony/mime": "^6.4|^7.0",
"symfony/process": "^6.4|^7.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\BrowserKit\\": "" },
"exclude-from-classmap": [
"/Tests/"
]
},
"minimum-stability": "dev"
}