8584e016b0d7e61ca7b43a3a65be938695a0907b
[yaffs-website] / vendor / symfony / dom-crawler / Form.php
1 <?php
2
3 /*
4  * This file is part of the Symfony package.
5  *
6  * (c) Fabien Potencier <fabien@symfony.com>
7  *
8  * For the full copyright and license information, please view the LICENSE
9  * file that was distributed with this source code.
10  */
11
12 namespace Symfony\Component\DomCrawler;
13
14 use Symfony\Component\DomCrawler\Field\ChoiceFormField;
15 use Symfony\Component\DomCrawler\Field\FormField;
16
17 /**
18  * Form represents an HTML form.
19  *
20  * @author Fabien Potencier <fabien@symfony.com>
21  */
22 class Form extends Link implements \ArrayAccess
23 {
24     /**
25      * @var \DOMElement
26      */
27     private $button;
28
29     /**
30      * @var FormFieldRegistry
31      */
32     private $fields;
33
34     /**
35      * @var string
36      */
37     private $baseHref;
38
39     /**
40      * @param \DOMElement $node       A \DOMElement instance
41      * @param string      $currentUri The URI of the page where the form is embedded
42      * @param string      $method     The method to use for the link (if null, it defaults to the method defined by the form)
43      * @param string      $baseHref   The URI of the <base> used for relative links, but not for empty action
44      *
45      * @throws \LogicException if the node is not a button inside a form tag
46      */
47     public function __construct(\DOMElement $node, $currentUri, $method = null, $baseHref = null)
48     {
49         parent::__construct($node, $currentUri, $method);
50         $this->baseHref = $baseHref;
51
52         $this->initialize();
53     }
54
55     /**
56      * Gets the form node associated with this form.
57      *
58      * @return \DOMElement A \DOMElement instance
59      */
60     public function getFormNode()
61     {
62         return $this->node;
63     }
64
65     /**
66      * Sets the value of the fields.
67      *
68      * @param array $values An array of field values
69      *
70      * @return $this
71      */
72     public function setValues(array $values)
73     {
74         foreach ($values as $name => $value) {
75             $this->fields->set($name, $value);
76         }
77
78         return $this;
79     }
80
81     /**
82      * Gets the field values.
83      *
84      * The returned array does not include file fields (@see getFiles).
85      *
86      * @return array An array of field values
87      */
88     public function getValues()
89     {
90         $values = array();
91         foreach ($this->fields->all() as $name => $field) {
92             if ($field->isDisabled()) {
93                 continue;
94             }
95
96             if (!$field instanceof Field\FileFormField && $field->hasValue()) {
97                 $values[$name] = $field->getValue();
98             }
99         }
100
101         return $values;
102     }
103
104     /**
105      * Gets the file field values.
106      *
107      * @return array An array of file field values
108      */
109     public function getFiles()
110     {
111         if (!\in_array($this->getMethod(), array('POST', 'PUT', 'DELETE', 'PATCH'))) {
112             return array();
113         }
114
115         $files = array();
116
117         foreach ($this->fields->all() as $name => $field) {
118             if ($field->isDisabled()) {
119                 continue;
120             }
121
122             if ($field instanceof Field\FileFormField) {
123                 $files[$name] = $field->getValue();
124             }
125         }
126
127         return $files;
128     }
129
130     /**
131      * Gets the field values as PHP.
132      *
133      * This method converts fields with the array notation
134      * (like foo[bar] to arrays) like PHP does.
135      *
136      * @return array An array of field values
137      */
138     public function getPhpValues()
139     {
140         $values = array();
141         foreach ($this->getValues() as $name => $value) {
142             $qs = http_build_query(array($name => $value), '', '&');
143             if (!empty($qs)) {
144                 parse_str($qs, $expandedValue);
145                 $varName = substr($name, 0, \strlen(key($expandedValue)));
146                 $values = array_replace_recursive($values, array($varName => current($expandedValue)));
147             }
148         }
149
150         return $values;
151     }
152
153     /**
154      * Gets the file field values as PHP.
155      *
156      * This method converts fields with the array notation
157      * (like foo[bar] to arrays) like PHP does.
158      * The returned array is consistent with the array for field values
159      * (@see getPhpValues), rather than uploaded files found in $_FILES.
160      * For a compound file field foo[bar] it will create foo[bar][name],
161      * instead of foo[name][bar] which would be found in $_FILES.
162      *
163      * @return array An array of file field values
164      */
165     public function getPhpFiles()
166     {
167         $values = array();
168         foreach ($this->getFiles() as $name => $value) {
169             $qs = http_build_query(array($name => $value), '', '&');
170             if (!empty($qs)) {
171                 parse_str($qs, $expandedValue);
172                 $varName = substr($name, 0, \strlen(key($expandedValue)));
173
174                 array_walk_recursive(
175                     $expandedValue,
176                     function (&$value, $key) {
177                         if (ctype_digit($value) && ('size' === $key || 'error' === $key)) {
178                             $value = (int) $value;
179                         }
180                     }
181                 );
182
183                 reset($expandedValue);
184
185                 $values = array_replace_recursive($values, array($varName => current($expandedValue)));
186             }
187         }
188
189         return $values;
190     }
191
192     /**
193      * Gets the URI of the form.
194      *
195      * The returned URI is not the same as the form "action" attribute.
196      * This method merges the value if the method is GET to mimics
197      * browser behavior.
198      *
199      * @return string The URI
200      */
201     public function getUri()
202     {
203         $uri = parent::getUri();
204
205         if (!\in_array($this->getMethod(), array('POST', 'PUT', 'DELETE', 'PATCH'))) {
206             $query = parse_url($uri, PHP_URL_QUERY);
207             $currentParameters = array();
208             if ($query) {
209                 parse_str($query, $currentParameters);
210             }
211
212             $queryString = http_build_query(array_merge($currentParameters, $this->getValues()), '', '&');
213
214             $pos = strpos($uri, '?');
215             $base = false === $pos ? $uri : substr($uri, 0, $pos);
216             $uri = rtrim($base.'?'.$queryString, '?');
217         }
218
219         return $uri;
220     }
221
222     protected function getRawUri()
223     {
224         // If the form was created from a button rather than the form node, check for HTML5 action overrides
225         if ($this->button !== $this->node && $this->button->getAttribute('formaction')) {
226             return $this->button->getAttribute('formaction');
227         }
228
229         return $this->node->getAttribute('action');
230     }
231
232     /**
233      * Gets the form method.
234      *
235      * If no method is defined in the form, GET is returned.
236      *
237      * @return string The method
238      */
239     public function getMethod()
240     {
241         if (null !== $this->method) {
242             return $this->method;
243         }
244
245         // If the form was created from a button rather than the form node, check for HTML5 method override
246         if ($this->button !== $this->node && $this->button->getAttribute('formmethod')) {
247             return strtoupper($this->button->getAttribute('formmethod'));
248         }
249
250         return $this->node->getAttribute('method') ? strtoupper($this->node->getAttribute('method')) : 'GET';
251     }
252
253     /**
254      * Returns true if the named field exists.
255      *
256      * @param string $name The field name
257      *
258      * @return bool true if the field exists, false otherwise
259      */
260     public function has($name)
261     {
262         return $this->fields->has($name);
263     }
264
265     /**
266      * Removes a field from the form.
267      *
268      * @param string $name The field name
269      */
270     public function remove($name)
271     {
272         $this->fields->remove($name);
273     }
274
275     /**
276      * Gets a named field.
277      *
278      * @param string $name The field name
279      *
280      * @return FormField The field instance
281      *
282      * @throws \InvalidArgumentException When field is not present in this form
283      */
284     public function get($name)
285     {
286         return $this->fields->get($name);
287     }
288
289     /**
290      * Sets a named field.
291      */
292     public function set(FormField $field)
293     {
294         $this->fields->add($field);
295     }
296
297     /**
298      * Gets all fields.
299      *
300      * @return FormField[]
301      */
302     public function all()
303     {
304         return $this->fields->all();
305     }
306
307     /**
308      * Returns true if the named field exists.
309      *
310      * @param string $name The field name
311      *
312      * @return bool true if the field exists, false otherwise
313      */
314     public function offsetExists($name)
315     {
316         return $this->has($name);
317     }
318
319     /**
320      * Gets the value of a field.
321      *
322      * @param string $name The field name
323      *
324      * @return FormField The associated Field instance
325      *
326      * @throws \InvalidArgumentException if the field does not exist
327      */
328     public function offsetGet($name)
329     {
330         return $this->fields->get($name);
331     }
332
333     /**
334      * Sets the value of a field.
335      *
336      * @param string       $name  The field name
337      * @param string|array $value The value of the field
338      *
339      * @throws \InvalidArgumentException if the field does not exist
340      */
341     public function offsetSet($name, $value)
342     {
343         $this->fields->set($name, $value);
344     }
345
346     /**
347      * Removes a field from the form.
348      *
349      * @param string $name The field name
350      */
351     public function offsetUnset($name)
352     {
353         $this->fields->remove($name);
354     }
355
356     /**
357      * Disables validation.
358      *
359      * @return self
360      */
361     public function disableValidation()
362     {
363         foreach ($this->fields->all() as $field) {
364             if ($field instanceof Field\ChoiceFormField) {
365                 $field->disableValidation();
366             }
367         }
368
369         return $this;
370     }
371
372     /**
373      * Sets the node for the form.
374      *
375      * Expects a 'submit' button \DOMElement and finds the corresponding form element, or the form element itself.
376      *
377      * @throws \LogicException If given node is not a button or input or does not have a form ancestor
378      */
379     protected function setNode(\DOMElement $node)
380     {
381         $this->button = $node;
382         if ('button' === $node->nodeName || ('input' === $node->nodeName && \in_array(strtolower($node->getAttribute('type')), array('submit', 'button', 'image')))) {
383             if ($node->hasAttribute('form')) {
384                 // if the node has the HTML5-compliant 'form' attribute, use it
385                 $formId = $node->getAttribute('form');
386                 $form = $node->ownerDocument->getElementById($formId);
387                 if (null === $form) {
388                     throw new \LogicException(sprintf('The selected node has an invalid form attribute (%s).', $formId));
389                 }
390                 $this->node = $form;
391
392                 return;
393             }
394             // we loop until we find a form ancestor
395             do {
396                 if (null === $node = $node->parentNode) {
397                     throw new \LogicException('The selected node does not have a form ancestor.');
398                 }
399             } while ('form' !== $node->nodeName);
400         } elseif ('form' !== $node->nodeName) {
401             throw new \LogicException(sprintf('Unable to submit on a "%s" tag.', $node->nodeName));
402         }
403
404         $this->node = $node;
405     }
406
407     /**
408      * Adds form elements related to this form.
409      *
410      * Creates an internal copy of the submitted 'button' element and
411      * the form node or the entire document depending on whether we need
412      * to find non-descendant elements through HTML5 'form' attribute.
413      */
414     private function initialize()
415     {
416         $this->fields = new FormFieldRegistry();
417
418         $xpath = new \DOMXPath($this->node->ownerDocument);
419
420         // add submitted button if it has a valid name
421         if ('form' !== $this->button->nodeName && $this->button->hasAttribute('name') && $this->button->getAttribute('name')) {
422             if ('input' == $this->button->nodeName && 'image' == strtolower($this->button->getAttribute('type'))) {
423                 $name = $this->button->getAttribute('name');
424                 $this->button->setAttribute('value', '0');
425
426                 // temporarily change the name of the input node for the x coordinate
427                 $this->button->setAttribute('name', $name.'.x');
428                 $this->set(new Field\InputFormField($this->button));
429
430                 // temporarily change the name of the input node for the y coordinate
431                 $this->button->setAttribute('name', $name.'.y');
432                 $this->set(new Field\InputFormField($this->button));
433
434                 // restore the original name of the input node
435                 $this->button->setAttribute('name', $name);
436             } else {
437                 $this->set(new Field\InputFormField($this->button));
438             }
439         }
440
441         // find form elements corresponding to the current form
442         if ($this->node->hasAttribute('id')) {
443             // corresponding elements are either descendants or have a matching HTML5 form attribute
444             $formId = Crawler::xpathLiteral($this->node->getAttribute('id'));
445
446             $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)]', $formId));
447             foreach ($fieldNodes as $node) {
448                 $this->addField($node);
449             }
450         } else {
451             // do the xpath query with $this->node as the context node, to only find descendant elements
452             // however, descendant elements with form attribute are not part of this form
453             $fieldNodes = $xpath->query('descendant::input[not(@form)] | descendant::button[not(@form)] | descendant::textarea[not(@form)] | descendant::select[not(@form)]', $this->node);
454             foreach ($fieldNodes as $node) {
455                 $this->addField($node);
456             }
457         }
458
459         if ($this->baseHref && '' !== $this->node->getAttribute('action')) {
460             $this->currentUri = $this->baseHref;
461         }
462     }
463
464     private function addField(\DOMElement $node)
465     {
466         if (!$node->hasAttribute('name') || !$node->getAttribute('name')) {
467             return;
468         }
469
470         $nodeName = $node->nodeName;
471         if ('select' == $nodeName || 'input' == $nodeName && 'checkbox' == strtolower($node->getAttribute('type'))) {
472             $this->set(new Field\ChoiceFormField($node));
473         } elseif ('input' == $nodeName && 'radio' == strtolower($node->getAttribute('type'))) {
474             // there may be other fields with the same name that are no choice
475             // fields already registered (see https://github.com/symfony/symfony/issues/11689)
476             if ($this->has($node->getAttribute('name')) && $this->get($node->getAttribute('name')) instanceof ChoiceFormField) {
477                 $this->get($node->getAttribute('name'))->addChoice($node);
478             } else {
479                 $this->set(new Field\ChoiceFormField($node));
480             }
481         } elseif ('input' == $nodeName && 'file' == strtolower($node->getAttribute('type'))) {
482             $this->set(new Field\FileFormField($node));
483         } elseif ('input' == $nodeName && !\in_array(strtolower($node->getAttribute('type')), array('submit', 'button', 'image'))) {
484             $this->set(new Field\InputFormField($node));
485         } elseif ('textarea' == $nodeName) {
486             $this->set(new Field\TextareaFormField($node));
487         }
488     }
489 }