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