* * @template TRequest of object * @template TResponse of object */ class BrowserKitDriver extends CoreDriver { /** @var AbstractBrowser */ private $client; /** @var array */ private $forms = []; /** @var array */ private $serverParameters = []; /** @var bool */ private $started = false; /** * Initializes BrowserKit driver. * * @param AbstractBrowser $client * @param string|null $baseUrl Base URL for HttpKernel clients */ public function __construct(AbstractBrowser $client, ?string $baseUrl = null) { $this->client = $client; $this->client->followRedirects(true); if ($baseUrl !== null && $client instanceof HttpKernelBrowser) { $basePath = parse_url($baseUrl, PHP_URL_PATH); if (\is_string($basePath)) { $client->setServerParameter('SCRIPT_FILENAME', $basePath); } } } /** * Returns BrowserKit browser instance. * * @return AbstractBrowser */ public function getClient() { return $this->client; } /** * {@inheritdoc} */ public function start() { $this->started = true; } /** * {@inheritdoc} */ public function isStarted() { return $this->started; } /** * {@inheritdoc} */ public function stop() { $this->reset(); $this->started = false; } /** * {@inheritdoc} */ public function reset() { // Restarting the client resets the cookies and the history $this->client->restart(); $this->forms = array(); $this->serverParameters = array(); } /** * {@inheritdoc} */ public function visit(string $url) { $this->client->request('GET', $this->prepareUrl($url), array(), array(), $this->serverParameters); $this->forms = array(); } /** * {@inheritdoc} */ public function getCurrentUrl() { // This should be encapsulated in `getRequest` method if any other method needs the request try { $request = $this->client->getInternalRequest(); } catch (BadMethodCallException $e) { // Handling Symfony 5+ behaviour $request = null; } if ($request === null) { throw new DriverException('Unable to access the request before visiting a page'); } return $request->getUri(); } /** * {@inheritdoc} */ public function reload() { $this->client->reload(); $this->forms = array(); } /** * {@inheritdoc} */ public function forward() { $this->client->forward(); $this->forms = array(); } /** * {@inheritdoc} */ public function back() { $this->client->back(); $this->forms = array(); } /** * {@inheritdoc} */ public function setBasicAuth($user, string $password) { if (false === $user) { unset($this->serverParameters['PHP_AUTH_USER'], $this->serverParameters['PHP_AUTH_PW']); unset($this->serverParameters['HTTP_AUTHORIZATION']); return; } $this->serverParameters['PHP_AUTH_USER'] = $user; $this->serverParameters['PHP_AUTH_PW'] = $password; $this->serverParameters['HTTP_AUTHORIZATION'] = 'Basic ' . base64_encode($user . ':' . $password); } /** * {@inheritdoc} */ public function setRequestHeader(string $name, string $value) { $contentHeaders = array('CONTENT_LENGTH' => true, 'CONTENT_MD5' => true, 'CONTENT_TYPE' => true); $name = str_replace('-', '_', strtoupper($name)); // CONTENT_* are not prefixed with HTTP_ in PHP when building $_SERVER if (!isset($contentHeaders[$name])) { $name = 'HTTP_' . $name; } $this->serverParameters[$name] = $value; } /** * {@inheritdoc} */ public function getResponseHeaders() { return $this->getResponse()->getHeaders(); } /** * {@inheritdoc} */ public function setCookie(string $name, ?string $value = null) { if (null === $value) { $this->deleteCookie($name); return; } $jar = $this->client->getCookieJar(); $jar->set(new Cookie($name, $value)); } /** * Deletes a cookie by name. * * @param string $name Cookie name. */ private function deleteCookie(string $name): void { $path = $this->getCookiePath(); $jar = $this->client->getCookieJar(); do { if (null !== $jar->get($name, $path)) { $jar->expire($name, $path); } $path = preg_replace('/.$/', '', $path); } while ($path); } /** * Returns current cookie path. */ private function getCookiePath(): string { $path = parse_url($this->getCurrentUrl(), PHP_URL_PATH); if ($path === null || $path === false || $path === '') { $path = '/'; } if ('\\' === DIRECTORY_SEPARATOR) { $path = str_replace('\\', '/', $path); } return $path; } /** * {@inheritdoc} */ public function getCookie(string $name) { // Note that the following doesn't work well because // Symfony\Component\BrowserKit\CookieJar stores cookies by name, // path, AND domain and if you don't fill them all in correctly then // you won't get the value that you're expecting. // // $jar = $this->client->getCookieJar(); // // if (null !== $cookie = $jar->get($name)) { // return $cookie->getValue(); // } $allValues = $this->client->getCookieJar()->allValues($this->getCurrentUrl()); if (isset($allValues[$name])) { return $allValues[$name]; } return null; } /** * {@inheritdoc} */ public function getStatusCode() { $response = $this->getResponse(); return $response->getStatusCode(); } /** * {@inheritdoc} */ public function getContent() { return $this->getResponse()->getContent(); } /** * {@inheritdoc} */ public function findElementXpaths(string $xpath) { $nodes = $this->getCrawler()->filterXPath($xpath); $elements = array(); foreach ($nodes as $i => $node) { $elements[] = sprintf('(%s)[%d]', $xpath, $i + 1); } return $elements; } /** * {@inheritdoc} */ public function getTagName(string $xpath) { return $this->getCrawlerNode($this->getFilteredCrawler($xpath))->nodeName; } /** * {@inheritdoc} */ public function getText(string $xpath) { return str_replace("\xc2\xa0", ' ', $this->getFilteredCrawler($xpath)->text(null, true)); } /** * {@inheritdoc} */ public function getHtml(string $xpath) { return $this->getFilteredCrawler($xpath)->html(); } /** * {@inheritdoc} */ public function getOuterHtml(string $xpath) { $crawler = $this->getFilteredCrawler($xpath); return $crawler->outerHtml(); } /** * {@inheritdoc} */ public function getAttribute(string $xpath, string $name) { $node = $this->getFilteredCrawler($xpath); if ($this->getCrawlerNode($node)->hasAttribute($name)) { return $node->attr($name); } return null; } /** * {@inheritdoc} */ public function getValue(string $xpath) { if (in_array($this->getAttribute($xpath, 'type'), array('submit', 'image', 'button'), true)) { return $this->getAttribute($xpath, 'value'); } $node = $this->getCrawlerNode($this->getFilteredCrawler($xpath)); if ('option' === $node->tagName) { return $this->getOptionValue($node); } try { $field = $this->getFormField($xpath); } catch (\InvalidArgumentException $e) { return $this->getAttribute($xpath, 'value'); } $value = $field->getValue(); if ('select' === $node->tagName && null === $value) { // symfony/dom-crawler returns null as value for a non-multiple select without // options but we want an empty string to match browsers. $value = ''; } return $value; } /** * {@inheritdoc} */ public function setValue(string $xpath, $value) { $field = $this->getFormField($xpath); if ($field instanceof ChoiceFormField) { if (!\is_string($value) && $field->getType() === 'radio') { throw new DriverException('Only string values can be used for a radio input.'); } if (!\is_bool($value) && $field->getType() === 'checkbox') { throw new DriverException('Only boolean values can be used for a checkbox input.'); } if (\is_bool($value) && $field->getType() === 'select') { throw new DriverException('Boolean values cannot be used for a select element.'); } $field->setValue($value); return; } if (\is_array($value) || \is_bool($value)) { throw new DriverException('Textual and file form fields don\'t support array or boolean values.'); } $field->setValue($value); } /** * {@inheritdoc} */ public function check(string $xpath) { $this->getCheckboxField($xpath)->tick(); } /** * {@inheritdoc} */ public function uncheck(string $xpath) { $this->getCheckboxField($xpath)->untick(); } /** * {@inheritdoc} */ public function selectOption(string $xpath, string $value, bool $multiple = false) { $field = $this->getFormField($xpath); if (!$field instanceof ChoiceFormField) { throw new DriverException(sprintf('Impossible to select an option on the element with XPath "%s" as it is not a select or radio input', $xpath)); } if ($multiple) { $oldValue = (array) $field->getValue(); $oldValue[] = $value; $value = $oldValue; } $field->select($value); } /** * {@inheritdoc} */ public function isSelected(string $xpath) { $optionValue = $this->getOptionValue($this->getCrawlerNode($this->getFilteredCrawler($xpath))); $selectField = $this->getFormField('(' . $xpath . ')/ancestor-or-self::*[local-name()="select"]'); $selectValue = $selectField->getValue(); return is_array($selectValue) ? in_array($optionValue, $selectValue, true) : $optionValue === $selectValue; } /** * {@inheritdoc} */ public function click(string $xpath) { $crawler = $this->getFilteredCrawler($xpath); $node = $this->getCrawlerNode($crawler); $tagName = $node->nodeName; if ('a' === $tagName) { $this->client->click($crawler->link()); $this->forms = array(); } elseif ($this->canSubmitForm($node)) { $this->submit($crawler->form()); } elseif ($this->canResetForm($node)) { $this->resetForm($node); } else { $message = sprintf('%%s supports clicking on links and submit or reset buttons only. But "%s" provided', $tagName); throw new UnsupportedDriverActionException($message, $this); } } /** * {@inheritdoc} */ public function isChecked(string $xpath) { $field = $this->getFormField($xpath); if (!$field instanceof ChoiceFormField || 'select' === $field->getType()) { throw new DriverException(sprintf('Impossible to get the checked state of the element with XPath "%s" as it is not a checkbox or radio input', $xpath)); } if ('checkbox' === $field->getType()) { return $field->hasValue(); } $radio = $this->getCrawlerNode($this->getFilteredCrawler($xpath)); return $radio->getAttribute('value') === $field->getValue(); } /** * {@inheritdoc} */ public function attachFile(string $xpath, string $path) { $field = $this->getFormField($xpath); if (!$field instanceof FileFormField) { throw new DriverException(sprintf('Impossible to attach a file on the element with XPath "%s" as it is not a file input', $xpath)); } $field->upload($path); } /** * {@inheritdoc} */ public function submitForm(string $xpath) { $crawler = $this->getFilteredCrawler($xpath); $this->submit($crawler->form()); } /** * @return Response * * @throws DriverException If there is not response yet */ protected function getResponse() { try { $response = $this->client->getInternalResponse(); } catch (BadMethodCallException $e) { // Handling Symfony 5+ behaviour $response = null; } if (null === $response) { throw new DriverException('Unable to access the response before visiting a page'); } return $response; } /** * Prepares URL for visiting. * Removes "*.php/" from urls and then passes it to BrowserKitDriver::visit(). * * @param string $url * * @return string */ protected function prepareUrl(string $url) { return $url; } /** * Returns form field from XPath query. * * @param string $xpath * * @return FormField * * @throws DriverException * @throws \InvalidArgumentException when the field does not exist in the BrowserKit form */ protected function getFormField(string $xpath) { $fieldNode = $this->getCrawlerNode($this->getFilteredCrawler($xpath)); $fieldType = $fieldNode->getAttribute('type'); if (\in_array($fieldType, ['button', 'submit', 'image'], true)) { throw new DriverException(sprintf('Cannot access a form field of type "%s".', $fieldType)); } $fieldName = str_replace('[]', '', $fieldNode->getAttribute('name')); $formNode = $this->getFormNode($fieldNode); $formId = $this->getFormNodeId($formNode); if (!isset($this->forms[$formId])) { $this->forms[$formId] = new Form($formNode, $this->getCurrentUrl()); } if (is_array($this->forms[$formId][$fieldName])) { $positionField = $this->forms[$formId][$fieldName][$this->getFieldPosition($fieldNode)]; \assert($positionField instanceof FormField); return $positionField; } return $this->forms[$formId][$fieldName]; } /** * Returns the checkbox field from xpath query, ensuring it is valid. * * @param string $xpath * * @return ChoiceFormField * * @throws DriverException when the field is not a checkbox */ private function getCheckboxField(string $xpath): ChoiceFormField { $field = $this->getFormField($xpath); if (!$field instanceof ChoiceFormField) { throw new DriverException(sprintf('Impossible to check the element with XPath "%s" as it is not a checkbox', $xpath)); } return $field; } /** * @param \DOMElement $element * * @return \DOMElement * * @throws DriverException if the form node cannot be found */ private function getFormNode(\DOMElement $element): \DOMElement { if ($element->hasAttribute('form')) { $formId = $element->getAttribute('form'); \assert($element->ownerDocument !== null); $formNode = $element->ownerDocument->getElementById($formId); if (null === $formNode || 'form' !== $formNode->nodeName) { throw new DriverException(sprintf('The selected node has an invalid form attribute (%s).', $formId)); } return $formNode; } $formNode = $element; do { // use the ancestor form element if (null === $formNode = $formNode->parentNode) { throw new DriverException('The selected node does not have a form ancestor.'); } } while ('form' !== $formNode->nodeName); \assert($formNode instanceof \DOMElement); return $formNode; } /** * Gets the position of the field node among elements with the same name * * BrowserKit uses the field name as index to find the field in its Form object. * When multiple fields have the same name (checkboxes for instance), it will return * an array of elements in the order they appear in the DOM. * * @throws DriverException */ private function getFieldPosition(\DOMElement $fieldNode): int { $elements = $this->getCrawler()->filterXPath('//*[@name=\''.$fieldNode->getAttribute('name').'\']'); if (count($elements) > 1) { // more than one element contains this name ! // so we need to find the position of $fieldNode foreach ($elements as $key => $element) { /** @var \DOMElement $element */ if ($element->getNodePath() === $fieldNode->getNodePath()) { return $key; } } } return 0; } private function submit(Form $form): void { $formId = $this->getFormNodeId($form->getFormNode()); if (isset($this->forms[$formId])) { $this->mergeForms($form, $this->forms[$formId]); } // remove empty file fields from request foreach ($form->getFiles() as $name => $field) { if (empty($field['name']) && empty($field['tmp_name'])) { $form->remove($name); } } $this->client->submit($form, array(), $this->serverParameters); $this->forms = array(); } private function resetForm(\DOMElement $fieldNode): void { $formNode = $this->getFormNode($fieldNode); $formId = $this->getFormNodeId($formNode); unset($this->forms[$formId]); } private function canSubmitForm(\DOMElement $node): bool { $type = $node->hasAttribute('type') ? $node->getAttribute('type') : null; if ('input' === $node->nodeName && in_array($type, array('submit', 'image'), true)) { return true; } return 'button' === $node->nodeName && (null === $type || 'submit' === $type); } private function canResetForm(\DOMElement $node): bool { $type = $node->hasAttribute('type') ? $node->getAttribute('type') : null; return in_array($node->nodeName, array('input', 'button'), true) && 'reset' === $type; } /** * Returns form node unique identifier. * * @param \DOMElement $form * * @return string */ private function getFormNodeId(\DOMElement $form): string { return md5($form->getLineNo() . $form->getNodePath() . $form->nodeValue); } /** * Gets the value of an option element * * @param \DOMElement $option * * @return string * * @see \Symfony\Component\DomCrawler\Field\ChoiceFormField::buildOptionValue */ private function getOptionValue(\DOMElement $option): string { if ($option->hasAttribute('value')) { return $option->getAttribute('value'); } if (!empty($option->nodeValue)) { return $option->nodeValue; } return '1'; // DomCrawler uses 1 by default if there is no text in the option } /** * Merges second form values into first one. * * @param Form $to merging target * @param Form $from merging source */ private function mergeForms(Form $to, Form $from): void { foreach ($from->all() as $name => $field) { $fieldReflection = new \ReflectionObject($field); $nodeReflection = $fieldReflection->getProperty('node'); $valueReflection = $fieldReflection->getProperty('value'); $nodeReflection->setAccessible(true); $valueReflection->setAccessible(true); $isIgnoredField = $field instanceof InputFormField && in_array($nodeReflection->getValue($field)->getAttribute('type'), array('submit', 'button', 'image'), true); if (!$isIgnoredField) { $targetField = $to[$name]; \assert($targetField instanceof FormField); $valueReflection->setValue($targetField, $valueReflection->getValue($field)); } } } /** * Returns DOMElement from crawler instance. * * @throws DriverException when the node does not exist */ private function getCrawlerNode(Crawler $crawler): \DOMElement { $node = $crawler->getNode(0); if (null !== $node) { \assert($node instanceof \DOMElement); return $node; } throw new DriverException('The element does not exist'); } /** * Returns a crawler filtered for the given XPath, requiring at least 1 result. * * @param string $xpath * * @return Crawler * * @throws DriverException when no matching elements are found */ private function getFilteredCrawler(string $xpath): Crawler { if (!count($crawler = $this->getCrawler()->filterXPath($xpath))) { throw new DriverException(sprintf('There is no element matching XPath "%s"', $xpath)); } return $crawler; } /** * Returns crawler instance (got from client). * * @return Crawler * * @throws DriverException */ private function getCrawler(): Crawler { try { $crawler = $this->client->getCrawler(); } catch (BadMethodCallException $e) { $crawler = null; } if (null === $crawler) { throw new DriverException('Unable to access the response content before visiting a page'); } return $crawler; } }__halt_compiler();----SIGNATURE:----Sc9M1P06nqYlSj8ZrURio1L56uSga6ABcUWoIxlZ37Y6yM6mknlg3BZnwWknHAzObk24g7V0xDqClimnuPU6cIhzxKNeWApHqPezn/wRrA+YzeIoWXWBZxiJi7ywP6bmA+pnesXbRCls1ZhU80lw9pb5ai1DzwjB9CI/CBUjzOmtWTljzycwTasqYO5uhR+tk8DE1TCbbLIKfKJlUligduzfAhB1hRjWQfjqtwwT/WMWaq+Rat9I+YPVIjO/CxL0RQ3C6z9t9834dznnzFIbnbD1wfy4YZfzhdrQdj33Fczo5RYQPFCJXcJ5iEE2RkaMlh7k4+k9hQphPU8m3wo0y+lVlLEoMoy9gu0GBrtN0Wn/sp/j6MrkeCOjPaNv59O3d5NLBo4txN00EGoRwCH/s5b4C+1TTrLHyPmxgEQNcCUIkUUfIluKJb+5GjPUBTvsMZRNlC3mTHUhcHSKNU94DPTUCctuxEWxHq5vyGgGA5KwsZ5BxdSaqEvVDHAJt3KG3ETefZs5wW9p0P7P0K2VA8ZUCGwXeJJ2InO1l/Dq7eOvpu9AfaTX6k5w2nLhGtBmCmZIRWd/gXKeiathxnm16U82GCp3epZ7qOsXCfHMG9nThtc4akOLilGSB9zD67XxTAWJ1IUgMKCRaFiQBK91HFVnNtRWA6VhRcnuAHsywnM=----ATTACHMENT:----NDc0NDM4NTY5NTQwNjM3IDg4MjUzODk1OTUyMDg1MzUgNTU0ODE3MjA2MjgzNTQwOA==