123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862 |
- <?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;
- }
- }
|