Chris@0: Chris@0: * Chris@0: * For the full copyright and license information, please view the LICENSE Chris@0: * file that was distributed with this source code. Chris@0: */ Chris@0: Chris@0: namespace Symfony\Component\DomCrawler; Chris@0: Chris@0: use Symfony\Component\DomCrawler\Field\ChoiceFormField; Chris@0: use Symfony\Component\DomCrawler\Field\FormField; Chris@0: Chris@0: /** Chris@0: * Form represents an HTML form. Chris@0: * Chris@0: * @author Fabien Potencier Chris@0: */ Chris@0: class Form extends Link implements \ArrayAccess Chris@0: { Chris@0: /** Chris@0: * @var \DOMElement Chris@0: */ Chris@0: private $button; Chris@0: Chris@0: /** Chris@0: * @var FormFieldRegistry Chris@0: */ Chris@0: private $fields; Chris@0: Chris@0: /** Chris@0: * @var string Chris@0: */ Chris@0: private $baseHref; Chris@0: Chris@0: /** Chris@0: * @param \DOMElement $node A \DOMElement instance Chris@0: * @param string $currentUri The URI of the page where the form is embedded Chris@0: * @param string $method The method to use for the link (if null, it defaults to the method defined by the form) Chris@0: * @param string $baseHref The URI of the used for relative links, but not for empty action Chris@0: * Chris@0: * @throws \LogicException if the node is not a button inside a form tag Chris@0: */ Chris@0: public function __construct(\DOMElement $node, $currentUri, $method = null, $baseHref = null) Chris@0: { Chris@0: parent::__construct($node, $currentUri, $method); Chris@0: $this->baseHref = $baseHref; Chris@0: Chris@0: $this->initialize(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets the form node associated with this form. Chris@0: * Chris@0: * @return \DOMElement A \DOMElement instance Chris@0: */ Chris@0: public function getFormNode() Chris@0: { Chris@0: return $this->node; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sets the value of the fields. Chris@0: * Chris@0: * @param array $values An array of field values Chris@0: * Chris@0: * @return $this Chris@0: */ Chris@0: public function setValues(array $values) Chris@0: { Chris@0: foreach ($values as $name => $value) { Chris@0: $this->fields->set($name, $value); Chris@0: } Chris@0: Chris@0: return $this; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets the field values. Chris@0: * Chris@0: * The returned array does not include file fields (@see getFiles). Chris@0: * Chris@0: * @return array An array of field values Chris@0: */ Chris@0: public function getValues() Chris@0: { Chris@17: $values = []; Chris@0: foreach ($this->fields->all() as $name => $field) { Chris@0: if ($field->isDisabled()) { Chris@0: continue; Chris@0: } Chris@0: Chris@0: if (!$field instanceof Field\FileFormField && $field->hasValue()) { Chris@0: $values[$name] = $field->getValue(); Chris@0: } Chris@0: } Chris@0: Chris@0: return $values; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets the file field values. Chris@0: * Chris@0: * @return array An array of file field values Chris@0: */ Chris@0: public function getFiles() Chris@0: { Chris@17: if (!\in_array($this->getMethod(), ['POST', 'PUT', 'DELETE', 'PATCH'])) { Chris@17: return []; Chris@0: } Chris@0: Chris@17: $files = []; Chris@0: Chris@0: foreach ($this->fields->all() as $name => $field) { Chris@0: if ($field->isDisabled()) { Chris@0: continue; Chris@0: } Chris@0: Chris@0: if ($field instanceof Field\FileFormField) { Chris@0: $files[$name] = $field->getValue(); Chris@0: } Chris@0: } Chris@0: Chris@0: return $files; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets the field values as PHP. Chris@0: * Chris@0: * This method converts fields with the array notation Chris@0: * (like foo[bar] to arrays) like PHP does. Chris@0: * Chris@0: * @return array An array of field values Chris@0: */ Chris@0: public function getPhpValues() Chris@0: { Chris@17: $values = []; Chris@0: foreach ($this->getValues() as $name => $value) { Chris@17: $qs = http_build_query([$name => $value], '', '&'); Chris@0: if (!empty($qs)) { Chris@0: parse_str($qs, $expandedValue); Chris@17: $varName = substr($name, 0, \strlen(key($expandedValue))); Chris@17: $values = array_replace_recursive($values, [$varName => current($expandedValue)]); Chris@0: } Chris@0: } Chris@0: Chris@0: return $values; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets the file field values as PHP. Chris@0: * Chris@0: * This method converts fields with the array notation Chris@0: * (like foo[bar] to arrays) like PHP does. Chris@0: * The returned array is consistent with the array for field values Chris@0: * (@see getPhpValues), rather than uploaded files found in $_FILES. Chris@0: * For a compound file field foo[bar] it will create foo[bar][name], Chris@0: * instead of foo[name][bar] which would be found in $_FILES. Chris@0: * Chris@0: * @return array An array of file field values Chris@0: */ Chris@0: public function getPhpFiles() Chris@0: { Chris@17: $values = []; Chris@0: foreach ($this->getFiles() as $name => $value) { Chris@17: $qs = http_build_query([$name => $value], '', '&'); Chris@0: if (!empty($qs)) { Chris@0: parse_str($qs, $expandedValue); Chris@17: $varName = substr($name, 0, \strlen(key($expandedValue))); Chris@12: Chris@12: array_walk_recursive( Chris@12: $expandedValue, Chris@12: function (&$value, $key) { Chris@12: if (ctype_digit($value) && ('size' === $key || 'error' === $key)) { Chris@12: $value = (int) $value; Chris@12: } Chris@12: } Chris@12: ); Chris@12: Chris@12: reset($expandedValue); Chris@12: Chris@17: $values = array_replace_recursive($values, [$varName => current($expandedValue)]); Chris@0: } Chris@0: } Chris@0: Chris@0: return $values; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets the URI of the form. Chris@0: * Chris@0: * The returned URI is not the same as the form "action" attribute. Chris@0: * This method merges the value if the method is GET to mimics Chris@0: * browser behavior. Chris@0: * Chris@0: * @return string The URI Chris@0: */ Chris@0: public function getUri() Chris@0: { Chris@0: $uri = parent::getUri(); Chris@0: Chris@17: if (!\in_array($this->getMethod(), ['POST', 'PUT', 'DELETE', 'PATCH'])) { Chris@0: $query = parse_url($uri, PHP_URL_QUERY); Chris@17: $currentParameters = []; Chris@0: if ($query) { Chris@0: parse_str($query, $currentParameters); Chris@0: } Chris@0: Chris@13: $queryString = http_build_query(array_merge($currentParameters, $this->getValues()), '', '&'); Chris@0: Chris@0: $pos = strpos($uri, '?'); Chris@0: $base = false === $pos ? $uri : substr($uri, 0, $pos); Chris@0: $uri = rtrim($base.'?'.$queryString, '?'); Chris@0: } Chris@0: Chris@0: return $uri; Chris@0: } Chris@0: Chris@0: protected function getRawUri() Chris@0: { Chris@12: // If the form was created from a button rather than the form node, check for HTML5 action overrides Chris@12: if ($this->button !== $this->node && $this->button->getAttribute('formaction')) { Chris@12: return $this->button->getAttribute('formaction'); Chris@12: } Chris@12: Chris@0: return $this->node->getAttribute('action'); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets the form method. Chris@0: * Chris@0: * If no method is defined in the form, GET is returned. Chris@0: * Chris@0: * @return string The method Chris@0: */ Chris@0: public function getMethod() Chris@0: { Chris@0: if (null !== $this->method) { Chris@0: return $this->method; Chris@0: } Chris@0: Chris@12: // If the form was created from a button rather than the form node, check for HTML5 method override Chris@12: if ($this->button !== $this->node && $this->button->getAttribute('formmethod')) { Chris@12: return strtoupper($this->button->getAttribute('formmethod')); Chris@12: } Chris@12: Chris@0: return $this->node->getAttribute('method') ? strtoupper($this->node->getAttribute('method')) : 'GET'; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns true if the named field exists. Chris@0: * Chris@0: * @param string $name The field name Chris@0: * Chris@0: * @return bool true if the field exists, false otherwise Chris@0: */ Chris@0: public function has($name) Chris@0: { Chris@0: return $this->fields->has($name); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Removes a field from the form. Chris@0: * Chris@0: * @param string $name The field name Chris@0: */ Chris@0: public function remove($name) Chris@0: { Chris@0: $this->fields->remove($name); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets a named field. Chris@0: * Chris@0: * @param string $name The field name Chris@0: * Chris@0: * @return FormField The field instance Chris@0: * Chris@0: * @throws \InvalidArgumentException When field is not present in this form Chris@0: */ Chris@0: public function get($name) Chris@0: { Chris@0: return $this->fields->get($name); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sets a named field. Chris@0: */ Chris@0: public function set(FormField $field) Chris@0: { Chris@0: $this->fields->add($field); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets all fields. Chris@0: * Chris@0: * @return FormField[] Chris@0: */ Chris@0: public function all() Chris@0: { Chris@0: return $this->fields->all(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns true if the named field exists. Chris@0: * Chris@0: * @param string $name The field name Chris@0: * Chris@0: * @return bool true if the field exists, false otherwise Chris@0: */ Chris@0: public function offsetExists($name) Chris@0: { Chris@0: return $this->has($name); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets the value of a field. Chris@0: * Chris@0: * @param string $name The field name Chris@0: * Chris@0: * @return FormField The associated Field instance Chris@0: * Chris@0: * @throws \InvalidArgumentException if the field does not exist Chris@0: */ Chris@0: public function offsetGet($name) Chris@0: { Chris@0: return $this->fields->get($name); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sets the value of a field. Chris@0: * Chris@0: * @param string $name The field name Chris@0: * @param string|array $value The value of the field Chris@0: * Chris@0: * @throws \InvalidArgumentException if the field does not exist Chris@0: */ Chris@0: public function offsetSet($name, $value) Chris@0: { Chris@0: $this->fields->set($name, $value); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Removes a field from the form. Chris@0: * Chris@0: * @param string $name The field name Chris@0: */ Chris@0: public function offsetUnset($name) Chris@0: { Chris@0: $this->fields->remove($name); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Disables validation. Chris@0: * Chris@0: * @return self Chris@0: */ Chris@0: public function disableValidation() Chris@0: { Chris@0: foreach ($this->fields->all() as $field) { Chris@0: if ($field instanceof Field\ChoiceFormField) { Chris@0: $field->disableValidation(); Chris@0: } Chris@0: } Chris@0: Chris@0: return $this; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sets the node for the form. Chris@0: * Chris@0: * Expects a 'submit' button \DOMElement and finds the corresponding form element, or the form element itself. Chris@0: * Chris@0: * @throws \LogicException If given node is not a button or input or does not have a form ancestor Chris@0: */ Chris@0: protected function setNode(\DOMElement $node) Chris@0: { Chris@0: $this->button = $node; Chris@17: if ('button' === $node->nodeName || ('input' === $node->nodeName && \in_array(strtolower($node->getAttribute('type')), ['submit', 'button', 'image']))) { Chris@0: if ($node->hasAttribute('form')) { Chris@0: // if the node has the HTML5-compliant 'form' attribute, use it Chris@0: $formId = $node->getAttribute('form'); Chris@0: $form = $node->ownerDocument->getElementById($formId); Chris@0: if (null === $form) { Chris@0: throw new \LogicException(sprintf('The selected node has an invalid form attribute (%s).', $formId)); Chris@0: } Chris@0: $this->node = $form; Chris@0: Chris@0: return; Chris@0: } Chris@0: // we loop until we find a form ancestor Chris@0: do { Chris@0: if (null === $node = $node->parentNode) { Chris@0: throw new \LogicException('The selected node does not have a form ancestor.'); Chris@0: } Chris@0: } while ('form' !== $node->nodeName); Chris@0: } elseif ('form' !== $node->nodeName) { Chris@0: throw new \LogicException(sprintf('Unable to submit on a "%s" tag.', $node->nodeName)); Chris@0: } Chris@0: Chris@0: $this->node = $node; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Adds form elements related to this form. Chris@0: * Chris@0: * Creates an internal copy of the submitted 'button' element and Chris@0: * the form node or the entire document depending on whether we need Chris@0: * to find non-descendant elements through HTML5 'form' attribute. Chris@0: */ Chris@0: private function initialize() Chris@0: { Chris@0: $this->fields = new FormFieldRegistry(); Chris@0: Chris@0: $xpath = new \DOMXPath($this->node->ownerDocument); Chris@0: Chris@0: // add submitted button if it has a valid name Chris@0: if ('form' !== $this->button->nodeName && $this->button->hasAttribute('name') && $this->button->getAttribute('name')) { Chris@0: if ('input' == $this->button->nodeName && 'image' == strtolower($this->button->getAttribute('type'))) { Chris@0: $name = $this->button->getAttribute('name'); Chris@0: $this->button->setAttribute('value', '0'); Chris@0: Chris@0: // temporarily change the name of the input node for the x coordinate Chris@0: $this->button->setAttribute('name', $name.'.x'); Chris@0: $this->set(new Field\InputFormField($this->button)); Chris@0: Chris@0: // temporarily change the name of the input node for the y coordinate Chris@0: $this->button->setAttribute('name', $name.'.y'); Chris@0: $this->set(new Field\InputFormField($this->button)); Chris@0: Chris@0: // restore the original name of the input node Chris@0: $this->button->setAttribute('name', $name); Chris@0: } else { Chris@0: $this->set(new Field\InputFormField($this->button)); Chris@0: } Chris@0: } Chris@0: Chris@0: // find form elements corresponding to the current form Chris@0: if ($this->node->hasAttribute('id')) { Chris@0: // corresponding elements are either descendants or have a matching HTML5 form attribute Chris@0: $formId = Crawler::xpathLiteral($this->node->getAttribute('id')); Chris@0: Chris@17: $fieldNodes = $xpath->query(sprintf('( descendant::input[@form=%s] | descendant::button[@form=%1$s] | descendant::textarea[@form=%1$s] | descendant::select[@form=%1$s] | //form[@id=%1$s]//input[not(@form)] | //form[@id=%1$s]//button[not(@form)] | //form[@id=%1$s]//textarea[not(@form)] | //form[@id=%1$s]//select[not(@form)] )[not(ancestor::template)]', $formId)); Chris@0: foreach ($fieldNodes as $node) { Chris@0: $this->addField($node); Chris@0: } Chris@0: } else { Chris@0: // do the xpath query with $this->node as the context node, to only find descendant elements Chris@0: // however, descendant elements with form attribute are not part of this form Chris@17: $fieldNodes = $xpath->query('( descendant::input[not(@form)] | descendant::button[not(@form)] | descendant::textarea[not(@form)] | descendant::select[not(@form)] )[not(ancestor::template)]', $this->node); Chris@0: foreach ($fieldNodes as $node) { Chris@0: $this->addField($node); Chris@0: } Chris@0: } Chris@0: Chris@0: if ($this->baseHref && '' !== $this->node->getAttribute('action')) { Chris@0: $this->currentUri = $this->baseHref; Chris@0: } Chris@0: } Chris@0: Chris@0: private function addField(\DOMElement $node) Chris@0: { Chris@0: if (!$node->hasAttribute('name') || !$node->getAttribute('name')) { Chris@0: return; Chris@0: } Chris@0: Chris@0: $nodeName = $node->nodeName; Chris@0: if ('select' == $nodeName || 'input' == $nodeName && 'checkbox' == strtolower($node->getAttribute('type'))) { Chris@0: $this->set(new Field\ChoiceFormField($node)); Chris@0: } elseif ('input' == $nodeName && 'radio' == strtolower($node->getAttribute('type'))) { Chris@0: // there may be other fields with the same name that are no choice Chris@0: // fields already registered (see https://github.com/symfony/symfony/issues/11689) Chris@0: if ($this->has($node->getAttribute('name')) && $this->get($node->getAttribute('name')) instanceof ChoiceFormField) { Chris@0: $this->get($node->getAttribute('name'))->addChoice($node); Chris@0: } else { Chris@0: $this->set(new Field\ChoiceFormField($node)); Chris@0: } Chris@0: } elseif ('input' == $nodeName && 'file' == strtolower($node->getAttribute('type'))) { Chris@0: $this->set(new Field\FileFormField($node)); Chris@17: } elseif ('input' == $nodeName && !\in_array(strtolower($node->getAttribute('type')), ['submit', 'button', 'image'])) { Chris@0: $this->set(new Field\InputFormField($node)); Chris@0: } elseif ('textarea' == $nodeName) { Chris@0: $this->set(new Field\TextareaFormField($node)); Chris@0: } Chris@0: } Chris@0: }