Mercurial > hg > isophonics-drupal-site
diff vendor/behat/mink-browserkit-driver/src/BrowserKitDriver.php @ 0:4c8ae668cc8c
Initial import (non-working)
author | Chris Cannam |
---|---|
date | Wed, 29 Nov 2017 16:09:58 +0000 |
parents | |
children | c2387f117808 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/vendor/behat/mink-browserkit-driver/src/BrowserKitDriver.php Wed Nov 29 16:09:58 2017 +0000 @@ -0,0 +1,862 @@ +<?php + +/* + * This file is part of the Behat\Mink. + * (c) Konstantin Kudryashov <ever.zet@gmail.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Behat\Mink\Driver; + +use Behat\Mink\Exception\DriverException; +use Behat\Mink\Exception\UnsupportedDriverActionException; +use Symfony\Component\BrowserKit\Client; +use Symfony\Component\BrowserKit\Cookie; +use Symfony\Component\BrowserKit\Response; +use Symfony\Component\DomCrawler\Crawler; +use Symfony\Component\DomCrawler\Field\ChoiceFormField; +use Symfony\Component\DomCrawler\Field\FileFormField; +use Symfony\Component\DomCrawler\Field\FormField; +use Symfony\Component\DomCrawler\Field\InputFormField; +use Symfony\Component\DomCrawler\Field\TextareaFormField; +use Symfony\Component\DomCrawler\Form; +use Symfony\Component\HttpKernel\Client as HttpKernelClient; + +/** + * Symfony2 BrowserKit driver. + * + * @author Konstantin Kudryashov <ever.zet@gmail.com> + */ +class BrowserKitDriver extends CoreDriver +{ + private $client; + + /** + * @var Form[] + */ + private $forms = array(); + private $serverParameters = array(); + private $started = false; + private $removeScriptFromUrl = false; + private $removeHostFromUrl = false; + + /** + * Initializes BrowserKit driver. + * + * @param Client $client BrowserKit client instance + * @param string|null $baseUrl Base URL for HttpKernel clients + */ + public function __construct(Client $client, $baseUrl = null) + { + $this->client = $client; + $this->client->followRedirects(true); + + if ($baseUrl !== null && $client instanceof HttpKernelClient) { + $client->setServerParameter('SCRIPT_FILENAME', parse_url($baseUrl, PHP_URL_PATH)); + } + } + + /** + * Returns BrowserKit HTTP client instance. + * + * @return Client + */ + public function getClient() + { + return $this->client; + } + + /** + * Tells driver to remove hostname from URL. + * + * @param Boolean $remove + * + * @deprecated Deprecated as of 1.2, to be removed in 2.0. Pass the base url in the constructor instead. + */ + public function setRemoveHostFromUrl($remove = true) + { + trigger_error( + 'setRemoveHostFromUrl() is deprecated as of 1.2 and will be removed in 2.0. Pass the base url in the constructor instead.', + E_USER_DEPRECATED + ); + $this->removeHostFromUrl = (bool) $remove; + } + + /** + * Tells driver to remove script name from URL. + * + * @param Boolean $remove + * + * @deprecated Deprecated as of 1.2, to be removed in 2.0. Pass the base url in the constructor instead. + */ + public function setRemoveScriptFromUrl($remove = true) + { + trigger_error( + 'setRemoveScriptFromUrl() is deprecated as of 1.2 and will be removed in 2.0. Pass the base url in the constructor instead.', + E_USER_DEPRECATED + ); + $this->removeScriptFromUrl = (bool) $remove; + } + + /** + * {@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($url) + { + $this->client->request('GET', $this->prepareUrl($url), array(), array(), $this->serverParameters); + $this->forms = array(); + } + + /** + * {@inheritdoc} + */ + public function getCurrentUrl() + { + $request = $this->client->getInternalRequest(); + + 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, $password) + { + if (false === $user) { + unset($this->serverParameters['PHP_AUTH_USER'], $this->serverParameters['PHP_AUTH_PW']); + + return; + } + + $this->serverParameters['PHP_AUTH_USER'] = $user; + $this->serverParameters['PHP_AUTH_PW'] = $password; + } + + /** + * {@inheritdoc} + */ + public function setRequestHeader($name, $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($name, $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($name) + { + $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. + * + * @return string + */ + private function getCookiePath() + { + $path = dirname(parse_url($this->getCurrentUrl(), PHP_URL_PATH)); + + if ('\\' === DIRECTORY_SEPARATOR) { + $path = str_replace('\\', '/', $path); + } + + return $path; + } + + /** + * {@inheritdoc} + */ + public function getCookie($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() + { + return $this->getResponse()->getStatus(); + } + + /** + * {@inheritdoc} + */ + public function getContent() + { + return $this->getResponse()->getContent(); + } + + /** + * {@inheritdoc} + */ + public function findElementXpaths($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($xpath) + { + return $this->getCrawlerNode($this->getFilteredCrawler($xpath))->nodeName; + } + + /** + * {@inheritdoc} + */ + public function getText($xpath) + { + $text = $this->getFilteredCrawler($xpath)->text(); + $text = str_replace("\n", ' ', $text); + $text = preg_replace('/ {2,}/', ' ', $text); + + return trim($text); + } + + /** + * {@inheritdoc} + */ + public function getHtml($xpath) + { + // cut the tag itself (making innerHTML out of outerHTML) + return preg_replace('/^\<[^\>]+\>|\<[^\>]+\>$/', '', $this->getOuterHtml($xpath)); + } + + /** + * {@inheritdoc} + */ + public function getOuterHtml($xpath) + { + $node = $this->getCrawlerNode($this->getFilteredCrawler($xpath)); + + return $node->ownerDocument->saveHTML($node); + } + + /** + * {@inheritdoc} + */ + public function getAttribute($xpath, $name) + { + $node = $this->getFilteredCrawler($xpath); + + if ($this->getCrawlerNode($node)->hasAttribute($name)) { + return $node->attr($name); + } + + return null; + } + + /** + * {@inheritdoc} + */ + public function getValue($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'); + } + + return $field->getValue(); + } + + /** + * {@inheritdoc} + */ + public function setValue($xpath, $value) + { + $this->getFormField($xpath)->setValue($value); + } + + /** + * {@inheritdoc} + */ + public function check($xpath) + { + $this->getCheckboxField($xpath)->tick(); + } + + /** + * {@inheritdoc} + */ + public function uncheck($xpath) + { + $this->getCheckboxField($xpath)->untick(); + } + + /** + * {@inheritdoc} + */ + public function selectOption($xpath, $value, $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($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($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($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($xpath, $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($xpath) + { + $crawler = $this->getFilteredCrawler($xpath); + + $this->submit($crawler->form()); + } + + /** + * @return Response + * + * @throws DriverException If there is not response yet + */ + protected function getResponse() + { + $response = $this->client->getInternalResponse(); + + 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($url) + { + $replacement = ($this->removeHostFromUrl ? '' : '$1') . ($this->removeScriptFromUrl ? '' : '$2'); + + return preg_replace('#(https?\://[^/]+)(/[^/\.]+\.php)?#', $replacement, $url); + } + + /** + * Returns form field from XPath query. + * + * @param string $xpath + * + * @return FormField + * + * @throws DriverException + */ + protected function getFormField($xpath) + { + $fieldNode = $this->getCrawlerNode($this->getFilteredCrawler($xpath)); + $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])) { + return $this->forms[$formId][$fieldName][$this->getFieldPosition($fieldNode)]; + } + + 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($xpath) + { + $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) + { + if ($element->hasAttribute('form')) { + $formId = $element->getAttribute('form'); + $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); + + 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. + * + * @param \DOMElement $fieldNode + * + * @return integer + */ + private function getFieldPosition(\DOMElement $fieldNode) + { + $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) + { + $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); + } + } + + foreach ($form->all() as $field) { + // Add a fix for https://github.com/symfony/symfony/pull/10733 to support Symfony versions which are not fixed + if ($field instanceof TextareaFormField && null === $field->getValue()) { + $field->setValue(''); + } + } + + $this->client->submit($form); + + $this->forms = array(); + } + + private function resetForm(\DOMElement $fieldNode) + { + $formNode = $this->getFormNode($fieldNode); + $formId = $this->getFormNodeId($formNode); + unset($this->forms[$formId]); + } + + /** + * Determines if a node can submit a form. + * + * @param \DOMElement $node Node. + * + * @return boolean + */ + private function canSubmitForm(\DOMElement $node) + { + $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); + } + + /** + * Determines if a node can reset a form. + * + * @param \DOMElement $node Node. + * + * @return boolean + */ + private function canResetForm(\DOMElement $node) + { + $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) + { + 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) + { + 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) + { + 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) { + $valueReflection->setValue($to[$name], $valueReflection->getValue($field)); + } + } + } + + /** + * Returns DOMElement from crawler instance. + * + * @param Crawler $crawler + * + * @return \DOMElement + * + * @throws DriverException when the node does not exist + */ + private function getCrawlerNode(Crawler $crawler) + { + $node = null; + + if ($crawler instanceof \Iterator) { + // for symfony 2.3 compatibility as getNode is not public before symfony 2.4 + $crawler->rewind(); + $node = $crawler->current(); + } else { + $node = $crawler->getNode(0); + } + + if (null !== $node) { + 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($xpath) + { + 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 = $this->client->getCrawler(); + + if (null === $crawler) { + throw new DriverException('Unable to access the response content before visiting a page'); + } + + return $crawler; + } +}