Version 1
[yaffs-website] / vendor / behat / mink-browserkit-driver / src / BrowserKitDriver.php
diff --git a/vendor/behat/mink-browserkit-driver/src/BrowserKitDriver.php b/vendor/behat/mink-browserkit-driver/src/BrowserKitDriver.php
new file mode 100644 (file)
index 0000000..cea8125
--- /dev/null
@@ -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;
+    }
+}