1 <?php
2 /**
3 * @package Joomla.Platform
4 * @subpackage Form
5 *
6 * @copyright Copyright (C) 2005 - 2017 Open Source Matters, Inc. All rights reserved.
7 * @license GNU General Public License version 2 or later; see LICENSE
8 */
9
10 defined('JPATH_PLATFORM') or die;
11
12 use Joomla\Registry\Registry;
13 use Joomla\Utilities\ArrayHelper;
14
15 jimport('joomla.filesystem.path');
16
17 /**
18 * Form Class for the Joomla Platform.
19 *
20 * This class implements a robust API for constructing, populating, filtering, and validating forms.
21 * It uses XML definitions to construct form fields and a variety of field and rule classes to
22 * render and validate the form.
23 *
24 * @link http://www.w3.org/TR/html4/interact/forms.html
25 * @link http://www.w3.org/TR/html5/forms.html
26 * @since 11.1
27 */
28 class JForm
29 {
30 /**
31 * The Registry data store for form fields during display.
32 *
33 * @var Registry
34 * @since 11.1
35 */
36 protected $data;
37
38 /**
39 * The form object errors array.
40 *
41 * @var array
42 * @since 11.1
43 */
44 protected $errors = array();
45
46 /**
47 * The name of the form instance.
48 *
49 * @var string
50 * @since 11.1
51 */
52 protected $name;
53
54 /**
55 * The form object options for use in rendering and validation.
56 *
57 * @var array
58 * @since 11.1
59 */
60 protected $options = array();
61
62 /**
63 * The form XML definition.
64 *
65 * @var SimpleXMLElement
66 * @since 11.1
67 */
68 protected $xml;
69
70 /**
71 * Form instances.
72 *
73 * @var JForm[]
74 * @since 11.1
75 */
76 protected static $forms = array();
77
78 /**
79 * Alows extensions to implement repeating elements
80 *
81 * @var boolean
82 * @since 3.2
83 */
84 public $repeat = false;
85
86 /**
87 * Method to instantiate the form object.
88 *
89 * @param string $name The name of the form.
90 * @param array $options An array of form options.
91 *
92 * @since 11.1
93 */
94 public function __construct($name, array $options = array())
95 {
96 // Set the name for the form.
97 $this->name = $name;
98
99 // Initialise the Registry data.
100 $this->data = new Registry;
101
102 // Set the options if specified.
103 $this->options['control'] = isset($options['control']) ? $options['control'] : false;
104 }
105
106 /**
107 * Method to bind data to the form.
108 *
109 * @param mixed $data An array or object of data to bind to the form.
110 *
111 * @return boolean True on success.
112 *
113 * @since 11.1
114 */
115 public function bind($data)
116 {
117 // Make sure there is a valid JForm XML document.
118 if (!($this->xml instanceof SimpleXMLElement))
119 {
120 return false;
121 }
122
123 // The data must be an object or array.
124 if (!is_object($data) && !is_array($data))
125 {
126 return false;
127 }
128
129 $this->bindLevel(null, $data);
130
131 return true;
132 }
133
134 /**
135 * Method to bind data to the form for the group level.
136 *
137 * @param string $group The dot-separated form group path on which to bind the data.
138 * @param mixed $data An array or object of data to bind to the form for the group level.
139 *
140 * @return void
141 *
142 * @since 11.1
143 */
144 protected function bindLevel($group, $data)
145 {
146 // Ensure the input data is an array.
147 if (is_object($data))
148 {
149 if ($data instanceof Registry)
150 {
151 // Handle a Registry.
152 $data = $data->toArray();
153 }
154 elseif ($data instanceof JObject)
155 {
156 // Handle a JObject.
157 $data = $data->getProperties();
158 }
159 else
160 {
161 // Handle other types of objects.
162 $data = (array) $data;
163 }
164 }
165
166 // Process the input data.
167 foreach ($data as $k => $v)
168 {
169 $level = $group ? $group . '.' . $k : $k;
170
171 if ($this->findField($k, $group))
172 {
173 // If the field exists set the value.
174 $this->data->set($level, $v);
175 }
176 elseif (is_object($v) || ArrayHelper::isAssociative($v))
177 {
178 // If the value is an object or an associative array, hand it off to the recursive bind level method.
179 $this->bindLevel($level, $v);
180 }
181 }
182 }
183
184 /**
185 * Method to filter the form data.
186 *
187 * @param array $data An array of field values to filter.
188 * @param string $group The dot-separated form group path on which to filter the fields.
189 *
190 * @return mixed Array or false.
191 *
192 * @since 11.1
193 */
194 public function filter($data, $group = null)
195 {
196 // Make sure there is a valid JForm XML document.
197 if (!($this->xml instanceof SimpleXMLElement))
198 {
199 return false;
200 }
201
202 $input = new Registry($data);
203 $output = new Registry;
204
205 // Get the fields for which to filter the data.
206 $fields = $this->findFieldsByGroup($group);
207
208 if (!$fields)
209 {
210 // PANIC!
211 return false;
212 }
213
214 // Filter the fields.
215 foreach ($fields as $field)
216 {
217 $name = (string) $field['name'];
218
219 // Get the field groups for the element.
220 $attrs = $field->xpath('ancestor::fields[@name]/@name');
221 $groups = array_map('strval', $attrs ? $attrs : array());
222 $group = implode('.', $groups);
223
224 $key = $group ? $group . '.' . $name : $name;
225
226 // Filter the value if it exists.
227 if ($input->exists($key))
228 {
229 $output->set($key, $this->filterField($field, $input->get($key, (string) $field['default'])));
230 }
231 }
232
233 return $output->toArray();
234 }
235
236 /**
237 * Return all errors, if any.
238 *
239 * @return array Array of error messages or RuntimeException objects.
240 *
241 * @since 11.1
242 */
243 public function getErrors()
244 {
245 return $this->errors;
246 }
247
248 /**
249 * Method to get a form field represented as a JFormField object.
250 *
251 * @param string $name The name of the form field.
252 * @param string $group The optional dot-separated form group path on which to find the field.
253 * @param mixed $value The optional value to use as the default for the field.
254 *
255 * @return JFormField|boolean The JFormField object for the field or boolean false on error.
256 *
257 * @since 11.1
258 */
259 public function getField($name, $group = null, $value = null)
260 {
261 // Make sure there is a valid JForm XML document.
262 if (!($this->xml instanceof SimpleXMLElement))
263 {
264 return false;
265 }
266
267 // Attempt to find the field by name and group.
268 $element = $this->findField($name, $group);
269
270 // If the field element was not found return false.
271 if (!$element)
272 {
273 return false;
274 }
275
276 return $this->loadField($element, $group, $value);
277 }
278
279 /**
280 * Method to get an attribute value from a field XML element. If the attribute doesn't exist or
281 * is null then the optional default value will be used.
282 *
283 * @param string $name The name of the form field for which to get the attribute value.
284 * @param string $attribute The name of the attribute for which to get a value.
285 * @param mixed $default The optional default value to use if no attribute value exists.
286 * @param string $group The optional dot-separated form group path on which to find the field.
287 *
288 * @return mixed The attribute value for the field.
289 *
290 * @since 11.1
291 * @throws UnexpectedValueException
292 */
293 public function getFieldAttribute($name, $attribute, $default = null, $group = null)
294 {
295 // Make sure there is a valid JForm XML document.
296 if (!($this->xml instanceof SimpleXMLElement))
297 {
298 throw new UnexpectedValueException(sprintf('%s::getFieldAttribute `xml` is not an instance of SimpleXMLElement', get_class($this)));
299 }
300
301 // Find the form field element from the definition.
302 $element = $this->findField($name, $group);
303
304 // If the element exists and the attribute exists for the field return the attribute value.
305 if (($element instanceof SimpleXMLElement) && strlen((string) $element[$attribute]))
306 {
307 return (string) $element[$attribute];
308 }
309
310 // Otherwise return the given default value.
311 else
312 {
313 return $default;
314 }
315 }
316
317 /**
318 * Method to get an array of JFormField objects in a given fieldset by name. If no name is
319 * given then all fields are returned.
320 *
321 * @param string $set The optional name of the fieldset.
322 *
323 * @return array The array of JFormField objects in the fieldset.
324 *
325 * @since 11.1
326 */
327 public function getFieldset($set = null)
328 {
329 $fields = array();
330
331 // Get all of the field elements in the fieldset.
332 if ($set)
333 {
334 $elements = $this->findFieldsByFieldset($set);
335 }
336
337 // Get all fields.
338 else
339 {
340 $elements = $this->findFieldsByGroup();
341 }
342
343 // If no field elements were found return empty.
344 if (empty($elements))
345 {
346 return $fields;
347 }
348
349 // Build the result array from the found field elements.
350 foreach ($elements as $element)
351 {
352 // Get the field groups for the element.
353 $attrs = $element->xpath('ancestor::fields[@name]/@name');
354 $groups = array_map('strval', $attrs ? $attrs : array());
355 $group = implode('.', $groups);
356
357 // If the field is successfully loaded add it to the result array.
358 if ($field = $this->loadField($element, $group))
359 {
360 $fields[$field->id] = $field;
361 }
362 }
363
364 return $fields;
365 }
366
367 /**
368 * Method to get an array of fieldset objects optionally filtered over a given field group.
369 *
370 * @param string $group The dot-separated form group path on which to filter the fieldsets.
371 *
372 * @return array The array of fieldset objects.
373 *
374 * @since 11.1
375 */
376 public function getFieldsets($group = null)
377 {
378 $fieldsets = array();
379 $sets = array();
380
381 // Make sure there is a valid JForm XML document.
382 if (!($this->xml instanceof SimpleXMLElement))
383 {
384 return $fieldsets;
385 }
386
387 if ($group)
388 {
389 // Get the fields elements for a given group.
390 $elements = &$this->findGroup($group);
391
392 foreach ($elements as &$element)
393 {
394 // Get an array of <fieldset /> elements and fieldset attributes within the fields element.
395 if ($tmp = $element->xpath('descendant::fieldset[@name] | descendant::field[@fieldset]/@fieldset'))
396 {
397 $sets = array_merge($sets, (array) $tmp);
398 }
399 }
400 }
401 else
402 {
403 // Get an array of <fieldset /> elements and fieldset attributes.
404 $sets = $this->xml->xpath('//fieldset[@name and not(ancestor::field/form/*)] | //field[@fieldset and not(ancestor::field/form/*)]/@fieldset');
405 }
406
407 // If no fieldsets are found return empty.
408 if (empty($sets))
409 {
410 return $fieldsets;
411 }
412
413 // Process each found fieldset.
414 foreach ($sets as $set)
415 {
416 if ((string) $set['hidden'] == 'true')
417 {
418 continue;
419 }
420
421 // Are we dealing with a fieldset element?
422 if ((string) $set['name'])
423 {
424 // Only create it if it doesn't already exist.
425 if (empty($fieldsets[(string) $set['name']]))
426 {
427 // Build the fieldset object.
428 $fieldset = (object) array('name' => '', 'label' => '', 'description' => '');
429
430 foreach ($set->attributes() as $name => $value)
431 {
432 $fieldset->$name = (string) $value;
433 }
434
435 // Add the fieldset object to the list.
436 $fieldsets[$fieldset->name] = $fieldset;
437 }
438 }
439
440 // Must be dealing with a fieldset attribute.
441 else
442 {
443 // Only create it if it doesn't already exist.
444 if (empty($fieldsets[(string) $set]))
445 {
446 // Attempt to get the fieldset element for data (throughout the entire form document).
447 $tmp = $this->xml->xpath('//fieldset[@name="' . (string) $set . '"]');
448
449 // If no element was found, build a very simple fieldset object.
450 if (empty($tmp))
451 {
452 $fieldset = (object) array('name' => (string) $set, 'label' => '', 'description' => '');
453 }
454
455 // Build the fieldset object from the element.
456 else
457 {
458 $fieldset = (object) array('name' => '', 'label' => '', 'description' => '');
459
460 foreach ($tmp[0]->attributes() as $name => $value)
461 {
462 $fieldset->$name = (string) $value;
463 }
464 }
465
466 // Add the fieldset object to the list.
467 $fieldsets[$fieldset->name] = $fieldset;
468 }
469 }
470 }
471
472 return $fieldsets;
473 }
474
475 /**
476 * Method to get the form control. This string serves as a container for all form fields. For
477 * example, if there is a field named 'foo' and a field named 'bar' and the form control is
478 * empty the fields will be rendered like: `<input name="foo" />` and `<input name="bar" />`. If
479 * the form control is set to 'joomla' however, the fields would be rendered like:
480 * `<input name="joomla[foo]" />` and `<input name="joomla[bar]" />`.
481 *
482 * @return string The form control string.
483 *
484 * @since 11.1
485 */
486 public function getFormControl()
487 {
488 return (string) $this->options['control'];
489 }
490
491 /**
492 * Method to get an array of JFormField objects in a given field group by name.
493 *
494 * @param string $group The dot-separated form group path for which to get the form fields.
495 * @param boolean $nested True to also include fields in nested groups that are inside of the
496 * group for which to find fields.
497 *
498 * @return array The array of JFormField objects in the field group.
499 *
500 * @since 11.1
501 */
502 public function getGroup($group, $nested = false)
503 {
504 $fields = array();
505
506 // Get all of the field elements in the field group.
507 $elements = $this->findFieldsByGroup($group, $nested);
508
509 // If no field elements were found return empty.
510 if (empty($elements))
511 {
512 return $fields;
513 }
514
515 // Build the result array from the found field elements.
516 foreach ($elements as $element)
517 {
518 // Get the field groups for the element.
519 $attrs = $element->xpath('ancestor::fields[@name]/@name');
520 $groups = array_map('strval', $attrs ? $attrs : array());
521 $group = implode('.', $groups);
522
523 // If the field is successfully loaded add it to the result array.
524 if ($field = $this->loadField($element, $group))
525 {
526 $fields[$field->id] = $field;
527 }
528 }
529
530 return $fields;
531 }
532
533 /**
534 * Method to get a form field markup for the field input.
535 *
536 * @param string $name The name of the form field.
537 * @param string $group The optional dot-separated form group path on which to find the field.
538 * @param mixed $value The optional value to use as the default for the field.
539 *
540 * @return string The form field markup.
541 *
542 * @since 11.1
543 */
544 public function getInput($name, $group = null, $value = null)
545 {
546 // Attempt to get the form field.
547 if ($field = $this->getField($name, $group, $value))
548 {
549 return $field->input;
550 }
551
552 return '';
553 }
554
555 /**
556 * Method to get the label for a field input.
557 *
558 * @param string $name The name of the form field.
559 * @param string $group The optional dot-separated form group path on which to find the field.
560 *
561 * @return string The form field label.
562 *
563 * @since 11.1
564 */
565 public function getLabel($name, $group = null)
566 {
567 // Attempt to get the form field.
568 if ($field = $this->getField($name, $group))
569 {
570 return $field->label;
571 }
572
573 return '';
574 }
575
576 /**
577 * Method to get the form name.
578 *
579 * @return string The name of the form.
580 *
581 * @since 11.1
582 */
583 public function getName()
584 {
585 return $this->name;
586 }
587
588 /**
589 * Method to get the value of a field.
590 *
591 * @param string $name The name of the field for which to get the value.
592 * @param string $group The optional dot-separated form group path on which to get the value.
593 * @param mixed $default The optional default value of the field value is empty.
594 *
595 * @return mixed The value of the field or the default value if empty.
596 *
597 * @since 11.1
598 */
599 public function getValue($name, $group = null, $default = null)
600 {
601 // If a group is set use it.
602 if ($group)
603 {
604 $return = $this->data->get($group . '.' . $name, $default);
605 }
606 else
607 {
608 $return = $this->data->get($name, $default);
609 }
610
611 return $return;
612 }
613
614 /**
615 * Method to get a control group with label and input.
616 *
617 * @param string $name The name of the field for which to get the value.
618 * @param string $group The optional dot-separated form group path on which to get the value.
619 * @param mixed $default The optional default value of the field value is empty.
620 *
621 * @return string A string containing the html for the control goup
622 *
623 * @since 3.2
624 * @deprecated 3.2.3 Use renderField() instead of getControlGroup
625 */
626 public function getControlGroup($name, $group = null, $default = null)
627 {
628 JLog::add('JForm->getControlGroup() is deprecated use JForm->renderField().', JLog::WARNING, 'deprecated');
629
630 return $this->renderField($name, $group, $default);
631 }
632
633 /**
634 * Method to get all control groups with label and input of a fieldset.
635 *
636 * @param string $name The name of the fieldset for which to get the values.
637 *
638 * @return string A string containing the html for the control goups
639 *
640 * @since 3.2
641 * @deprecated 3.2.3 Use renderFieldset() instead of getControlGroups
642 */
643 public function getControlGroups($name)
644 {
645 JLog::add('JForm->getControlGroups() is deprecated use JForm->renderFieldset().', JLog::WARNING, 'deprecated');
646
647 return $this->renderFieldset($name);
648 }
649
650 /**
651 * Method to get a control group with label and input.
652 *
653 * @param string $name The name of the field for which to get the value.
654 * @param string $group The optional dot-separated form group path on which to get the value.
655 * @param mixed $default The optional default value of the field value is empty.
656 * @param array $options Any options to be passed into the rendering of the field
657 *
658 * @return string A string containing the html for the control goup
659 *
660 * @since 3.2.3
661 */
662 public function renderField($name, $group = null, $default = null, $options = array())
663 {
664 $field = $this->getField($name, $group, $default);
665
666 if ($field)
667 {
668 return $field->renderField($options);
669 }
670
671 return '';
672 }
673
674 /**
675 * Method to get all control groups with label and input of a fieldset.
676 *
677 * @param string $name The name of the fieldset for which to get the values.
678 * @param array $options Any options to be passed into the rendering of the field
679 *
680 * @return string A string containing the html for the control goups
681 *
682 * @since 3.2.3
683 */
684 public function renderFieldset($name, $options = array())
685 {
686 $fields = $this->getFieldset($name);
687 $html = array();
688
689 foreach ($fields as $field)
690 {
691 $html[] = $field->renderField($options);
692 }
693
694 return implode('', $html);
695 }
696
697 /**
698 * Method to load the form description from an XML string or object.
699 *
700 * The replace option works per field. If a field being loaded already exists in the current
701 * form definition then the behavior or load will vary depending upon the replace flag. If it
702 * is set to true, then the existing field will be replaced in its exact location by the new
703 * field being loaded. If it is false, then the new field being loaded will be ignored and the
704 * method will move on to the next field to load.
705 *
706 * @param string $data The name of an XML string or object.
707 * @param string $replace Flag to toggle whether form fields should be replaced if a field
708 * already exists with the same group/name.
709 * @param string $xpath An optional xpath to search for the fields.
710 *
711 * @return boolean True on success, false otherwise.
712 *
713 * @since 11.1
714 */
715 public function load($data, $replace = true, $xpath = false)
716 {
717 // If the data to load isn't already an XML element or string return false.
718 if ((!($data instanceof SimpleXMLElement)) && (!is_string($data)))
719 {
720 return false;
721 }
722
723 // Attempt to load the XML if a string.
724 if (is_string($data))
725 {
726 try
727 {
728 $data = new SimpleXMLElement($data);
729 }
730 catch (Exception $e)
731 {
732 return false;
733 }
734
735 // Make sure the XML loaded correctly.
736 if (!$data)
737 {
738 return false;
739 }
740 }
741
742 // If we have no XML definition at this point let's make sure we get one.
743 if (empty($this->xml))
744 {
745 // If no XPath query is set to search for fields, and we have a <form />, set it and return.
746 if (!$xpath && ($data->getName() == 'form'))
747 {
748 $this->xml = $data;
749
750 // Synchronize any paths found in the load.
751 $this->syncPaths();
752
753 return true;
754 }
755
756 // Create a root element for the form.
757 else
758 {
759 $this->xml = new SimpleXMLElement('<form></form>');
760 }
761 }
762
763 // Get the XML elements to load.
764 $elements = array();
765
766 if ($xpath)
767 {
768 $elements = $data->xpath($xpath);
769 }
770 elseif ($data->getName() == 'form')
771 {
772 $elements = $data->children();
773 }
774
775 // If there is nothing to load return true.
776 if (empty($elements))
777 {
778 return true;
779 }
780
781 // Load the found form elements.
782 foreach ($elements as $element)
783 {
784 // Get an array of fields with the correct name.
785 $fields = $element->xpath('descendant-or-self::field');
786
787 foreach ($fields as $field)
788 {
789 // Get the group names as strings for ancestor fields elements.
790 $attrs = $field->xpath('ancestor::fields[@name]/@name');
791 $groups = array_map('strval', $attrs ? $attrs : array());
792
793 // Check to see if the field exists in the current form.
794 if ($current = $this->findField((string) $field['name'], implode('.', $groups)))
795 {
796 // If set to replace found fields, replace the data and remove the field so we don't add it twice.
797 if ($replace)
798 {
799 $olddom = dom_import_simplexml($current);
800 $loadeddom = dom_import_simplexml($field);
801 $addeddom = $olddom->ownerDocument->importNode($loadeddom, true);
802 $olddom->parentNode->replaceChild($addeddom, $olddom);
803 $loadeddom->parentNode->removeChild($loadeddom);
804 }
805 else
806 {
807 unset($field);
808 }
809 }
810 }
811
812 // Merge the new field data into the existing XML document.
813 self::addNode($this->xml, $element);
814 }
815
816 // Synchronize any paths found in the load.
817 $this->syncPaths();
818
819 return true;
820 }
821
822 /**
823 * Method to load the form description from an XML file.
824 *
825 * The reset option works on a group basis. If the XML file references
826 * groups that have already been created they will be replaced with the
827 * fields in the new XML file unless the $reset parameter has been set
828 * to false.
829 *
830 * @param string $file The filesystem path of an XML file.
831 * @param string $reset Flag to toggle whether form fields should be replaced if a field
832 * already exists with the same group/name.
833 * @param string $xpath An optional xpath to search for the fields.
834 *
835 * @return boolean True on success, false otherwise.
836 *
837 * @since 11.1
838 */
839 public function loadFile($file, $reset = true, $xpath = false)
840 {
841 // Check to see if the path is an absolute path.
842 if (!is_file($file))
843 {
844 // Not an absolute path so let's attempt to find one using JPath.
845 $file = JPath::find(self::addFormPath(), strtolower($file) . '.xml');
846
847 // If unable to find the file return false.
848 if (!$file)
849 {
850 return false;
851 }
852 }
853
854 // Attempt to load the XML file.
855 $xml = simplexml_load_file($file);
856
857 return $this->load($xml, $reset, $xpath);
858 }
859
860 /**
861 * Method to remove a field from the form definition.
862 *
863 * @param string $name The name of the form field for which remove.
864 * @param string $group The optional dot-separated form group path on which to find the field.
865 *
866 * @return boolean True on success, false otherwise.
867 *
868 * @since 11.1
869 * @throws UnexpectedValueException
870 */
871 public function removeField($name, $group = null)
872 {
873 // Make sure there is a valid JForm XML document.
874 if (!($this->xml instanceof SimpleXMLElement))
875 {
876 throw new UnexpectedValueException(sprintf('%s::removeField `xml` is not an instance of SimpleXMLElement', get_class($this)));
877 }
878
879 // Find the form field element from the definition.
880 $element = $this->findField($name, $group);
881
882 // If the element exists remove it from the form definition.
883 if ($element instanceof SimpleXMLElement)
884 {
885 $dom = dom_import_simplexml($element);
886 $dom->parentNode->removeChild($dom);
887
888 return true;
889 }
890
891 return false;
892 }
893
894 /**
895 * Method to remove a group from the form definition.
896 *
897 * @param string $group The dot-separated form group path for the group to remove.
898 *
899 * @return boolean True on success.
900 *
901 * @since 11.1
902 * @throws UnexpectedValueException
903 */
904 public function removeGroup($group)
905 {
906 // Make sure there is a valid JForm XML document.
907 if (!($this->xml instanceof SimpleXMLElement))
908 {
909 throw new UnexpectedValueException(sprintf('%s::removeGroup `xml` is not an instance of SimpleXMLElement', get_class($this)));
910 }
911
912 // Get the fields elements for a given group.
913 $elements = &$this->findGroup($group);
914
915 foreach ($elements as &$element)
916 {
917 $dom = dom_import_simplexml($element);
918 $dom->parentNode->removeChild($dom);
919 }
920
921 return true;
922 }
923
924 /**
925 * Method to reset the form data store and optionally the form XML definition.
926 *
927 * @param boolean $xml True to also reset the XML form definition.
928 *
929 * @return boolean True on success.
930 *
931 * @since 11.1
932 */
933 public function reset($xml = false)
934 {
935 unset($this->data);
936 $this->data = new Registry;
937
938 if ($xml)
939 {
940 unset($this->xml);
941 $this->xml = new SimpleXMLElement('<form></form>');
942 }
943
944 return true;
945 }
946
947 /**
948 * Method to set a field XML element to the form definition. If the replace flag is set then
949 * the field will be set whether it already exists or not. If it isn't set, then the field
950 * will not be replaced if it already exists.
951 *
952 * @param SimpleXMLElement $element The XML element object representation of the form field.
953 * @param string $group The optional dot-separated form group path on which to set the field.
954 * @param boolean $replace True to replace an existing field if one already exists.
955 * @param string $fieldset The name of the fieldset we are adding the field to.
956 *
957 * @return boolean True on success.
958 *
959 * @since 11.1
960 * @throws UnexpectedValueException
961 */
962 public function setField(SimpleXMLElement $element, $group = null, $replace = true, $fieldset = 'default')
963 {
964 // Make sure there is a valid JForm XML document.
965 if (!($this->xml instanceof SimpleXMLElement))
966 {
967 throw new UnexpectedValueException(sprintf('%s::setField `xml` is not an instance of SimpleXMLElement', get_class($this)));
968 }
969
970 // Find the form field element from the definition.
971 $old = $this->findField((string) $element['name'], $group);
972
973 // If an existing field is found and replace flag is false do nothing and return true.
974 if (!$replace && !empty($old))
975 {
976 return true;
977 }
978
979 // If an existing field is found and replace flag is true remove the old field.
980 if ($replace && !empty($old) && ($old instanceof SimpleXMLElement))
981 {
982 $dom = dom_import_simplexml($old);
983
984 // Get the parent element, this should be the fieldset
985 $parent = $dom->parentNode;
986 $fieldset = $parent->getAttribute('name');
987
988 $parent->removeChild($dom);
989 }
990
991 // Create the search path
992 $path = '//';
993
994 if (!empty($group))
995 {
996 $path .= 'fields[@name="' . $group . '"]/';
997 }
998
999 $path .= 'fieldset[@name="' . $fieldset . '"]';
1000
1001 $fs = $this->xml->xpath($path);
1002
1003 if (isset($fs[0]) && ($fs[0] instanceof SimpleXMLElement))
1004 {
1005 // Add field to the form.
1006 self::addNode($fs[0], $element);
1007
1008 // Synchronize any paths found in the load.
1009 $this->syncPaths();
1010
1011 return true;
1012 }
1013
1014 // We couldn't find a fieldset to add the field. Now we are checking, if we have set only a group
1015 if (!empty($group))
1016 {
1017 $fields = &$this->findGroup($group);
1018
1019 // If an appropriate fields element was found for the group, add the element.
1020 if (isset($fields[0]) && ($fields[0] instanceof SimpleXMLElement))
1021 {
1022 self::addNode($fields[0], $element);
1023 }
1024
1025 // Synchronize any paths found in the load.
1026 $this->syncPaths();
1027
1028 return true;
1029 }
1030
1031 // We couldn't find a parent so we are adding it at root level
1032
1033 // Add field to the form.
1034 self::addNode($this->xml, $element);
1035
1036 // Synchronize any paths found in the load.
1037 $this->syncPaths();
1038
1039 return true;
1040 }
1041
1042 /**
1043 * Method to set an attribute value for a field XML element.
1044 *
1045 * @param string $name The name of the form field for which to set the attribute value.
1046 * @param string $attribute The name of the attribute for which to set a value.
1047 * @param mixed $value The value to set for the attribute.
1048 * @param string $group The optional dot-separated form group path on which to find the field.
1049 *
1050 * @return boolean True on success.
1051 *
1052 * @since 11.1
1053 * @throws UnexpectedValueException
1054 */
1055 public function setFieldAttribute($name, $attribute, $value, $group = null)
1056 {
1057 // Make sure there is a valid JForm XML document.
1058 if (!($this->xml instanceof SimpleXMLElement))
1059 {
1060 throw new UnexpectedValueException(sprintf('%s::setFieldAttribute `xml` is not an instance of SimpleXMLElement', get_class($this)));
1061 }
1062
1063 // Find the form field element from the definition.
1064 $element = $this->findField($name, $group);
1065
1066 // If the element doesn't exist return false.
1067 if (!($element instanceof SimpleXMLElement))
1068 {
1069 return false;
1070 }
1071
1072 // Otherwise set the attribute and return true.
1073 else
1074 {
1075 $element[$attribute] = $value;
1076
1077 // Synchronize any paths found in the load.
1078 $this->syncPaths();
1079
1080 return true;
1081 }
1082 }
1083
1084 /**
1085 * Method to set some field XML elements to the form definition. If the replace flag is set then
1086 * the fields will be set whether they already exists or not. If it isn't set, then the fields
1087 * will not be replaced if they already exist.
1088 *
1089 * @param array &$elements The array of XML element object representations of the form fields.
1090 * @param string $group The optional dot-separated form group path on which to set the fields.
1091 * @param boolean $replace True to replace existing fields if they already exist.
1092 * @param string $fieldset The name of the fieldset we are adding the field to.
1093 *
1094 * @return boolean True on success.
1095 *
1096 * @since 11.1
1097 * @throws UnexpectedValueException
1098 */
1099 public function setFields(&$elements, $group = null, $replace = true, $fieldset = 'default')
1100 {
1101 // Make sure there is a valid JForm XML document.
1102 if (!($this->xml instanceof SimpleXMLElement))
1103 {
1104 throw new UnexpectedValueException(sprintf('%s::setFields `xml` is not an instance of SimpleXMLElement', get_class($this)));
1105 }
1106
1107 // Make sure the elements to set are valid.
1108 foreach ($elements as $element)
1109 {
1110 if (!($element instanceof SimpleXMLElement))
1111 {
1112 throw new UnexpectedValueException(sprintf('$element not SimpleXMLElement in %s::setFields', get_class($this)));
1113 }
1114 }
1115
1116 // Set the fields.
1117 $return = true;
1118
1119 foreach ($elements as $element)
1120 {
1121 if (!$this->setField($element, $group, $replace, $fieldset))
1122 {
1123 $return = false;
1124 }
1125 }
1126
1127 // Synchronize any paths found in the load.
1128 $this->syncPaths();
1129
1130 return $return;
1131 }
1132
1133 /**
1134 * Method to set the value of a field. If the field does not exist in the form then the method
1135 * will return false.
1136 *
1137 * @param string $name The name of the field for which to set the value.
1138 * @param string $group The optional dot-separated form group path on which to find the field.
1139 * @param mixed $value The value to set for the field.
1140 *
1141 * @return boolean True on success.
1142 *
1143 * @since 11.1
1144 */
1145 public function setValue($name, $group = null, $value = null)
1146 {
1147 // If the field does not exist return false.
1148 if (!$this->findField($name, $group))
1149 {
1150 return false;
1151 }
1152
1153 // If a group is set use it.
1154 if ($group)
1155 {
1156 $this->data->set($group . '.' . $name, $value);
1157 }
1158 else
1159 {
1160 $this->data->set($name, $value);
1161 }
1162
1163 return true;
1164 }
1165
1166 /**
1167 * Method to validate form data.
1168 *
1169 * Validation warnings will be pushed into JForm::errors and should be
1170 * retrieved with JForm::getErrors() when validate returns boolean false.
1171 *
1172 * @param array $data An array of field values to validate.
1173 * @param string $group The optional dot-separated form group path on which to filter the
1174 * fields to be validated.
1175 *
1176 * @return boolean True on success.
1177 *
1178 * @since 11.1
1179 */
1180 public function validate($data, $group = null)
1181 {
1182 // Make sure there is a valid JForm XML document.
1183 if (!($this->xml instanceof SimpleXMLElement))
1184 {
1185 return false;
1186 }
1187
1188 $return = true;
1189
1190 // Create an input registry object from the data to validate.
1191 $input = new Registry($data);
1192
1193 // Get the fields for which to validate the data.
1194 $fields = $this->findFieldsByGroup($group);
1195
1196 if (!$fields)
1197 {
1198 // PANIC!
1199 return false;
1200 }
1201
1202 // Validate the fields.
1203 foreach ($fields as $field)
1204 {
1205 $value = null;
1206 $name = (string) $field['name'];
1207
1208 // Get the group names as strings for ancestor fields elements.
1209 $attrs = $field->xpath('ancestor::fields[@name]/@name');
1210 $groups = array_map('strval', $attrs ? $attrs : array());
1211 $group = implode('.', $groups);
1212
1213 // Get the value from the input data.
1214 if ($group)
1215 {
1216 $value = $input->get($group . '.' . $name);
1217 }
1218 else
1219 {
1220 $value = $input->get($name);
1221 }
1222
1223 // Validate the field.
1224 $valid = $this->validateField($field, $group, $value, $input);
1225
1226 // Check for an error.
1227 if ($valid instanceof Exception)
1228 {
1229 $this->errors[] = $valid;
1230 $return = false;
1231 }
1232 }
1233
1234 return $return;
1235 }
1236
1237 /**
1238 * Method to apply an input filter to a value based on field data.
1239 *
1240 * @param string $element The XML element object representation of the form field.
1241 * @param mixed $value The value to filter for the field.
1242 *
1243 * @return mixed The filtered value.
1244 *
1245 * @since 11.1
1246 */
1247 protected function filterField($element, $value)
1248 {
1249 // Make sure there is a valid SimpleXMLElement.
1250 if (!($element instanceof SimpleXMLElement))
1251 {
1252 return false;
1253 }
1254
1255 // Get the field filter type.
1256 $filter = (string) $element['filter'];
1257
1258 // Process the input value based on the filter.
1259 $return = null;
1260
1261 switch (strtoupper($filter))
1262 {
1263 // Access Control Rules.
1264 case 'RULES':
1265 $return = array();
1266
1267 foreach ((array) $value as $action => $ids)
1268 {
1269 // Build the rules array.
1270 $return[$action] = array();
1271
1272 foreach ($ids as $id => $p)
1273 {
1274 if ($p !== '')
1275 {
1276 $return[$action][$id] = ($p == '1' || $p == 'true') ? true : false;
1277 }
1278 }
1279 }
1280 break;
1281
1282 // Do nothing, thus leaving the return value as null.
1283 case 'UNSET':
1284 break;
1285
1286 // No Filter.
1287 case 'RAW':
1288 $return = $value;
1289 break;
1290
1291 // Filter the input as an array of integers.
1292 case 'INT_ARRAY':
1293 // Make sure the input is an array.
1294 if (is_object($value))
1295 {
1296 $value = get_object_vars($value);
1297 }
1298
1299 $value = is_array($value) ? $value : array($value);
1300
1301 $value = ArrayHelper::toInteger($value);
1302 $return = $value;
1303 break;
1304
1305 // Filter safe HTML.
1306 case 'SAFEHTML':
1307 $return = JFilterInput::getInstance(null, null, 1, 1)->clean($value, 'html');
1308 break;
1309
1310 // Convert a date to UTC based on the server timezone offset.
1311 case 'SERVER_UTC':
1312 if ((int) $value > 0)
1313 {
1314 // Check if we have a localised date format
1315 $translateFormat = (string) $element['translateformat'];
1316
1317 if ($translateFormat && $translateFormat != 'false')
1318 {
1319 $showTime = (string) $element['showtime'];
1320 $showTime = ($showTime && $showTime != 'false');
1321 $format = ($showTime) ? JText::_('DATE_FORMAT_FILTER_DATETIME') : JText::_('DATE_FORMAT_FILTER_DATE');
1322 $date = date_parse_from_format($format, $value);
1323 $value = (int) $date['year'] . '-' . (int) $date['month'] . '-' . (int) $date['day'];
1324
1325 if ($showTime)
1326 {
1327 $value .= ' ' . (int) $date['hour'] . ':' . (int) $date['minute'] . ':' . (int) $date['second'];
1328 }
1329 }
1330
1331 // Get the server timezone setting.
1332 $offset = JFactory::getConfig()->get('offset');
1333
1334 // Return an SQL formatted datetime string in UTC.
1335 try
1336 {
1337 $return = JFactory::getDate($value, $offset)->toSql();
1338 }
1339 catch (Exception $e)
1340 {
1341 JFactory::getApplication()->enqueueMessage(JText::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', JText::_((string) $element['label'])), 'warning');
1342
1343 $return = '';
1344 }
1345 }
1346 else
1347 {
1348 $return = '';
1349 }
1350 break;
1351
1352 // Convert a date to UTC based on the user timezone offset.
1353 case 'USER_UTC':
1354 if ((int) $value > 0)
1355 {
1356 // Check if we have a localised date format
1357 $translateFormat = (string) $element['translateformat'];
1358
1359 if ($translateFormat && $translateFormat != 'false')
1360 {
1361 $showTime = (string) $element['showtime'];
1362 $showTime = ($showTime && $showTime != 'false');
1363 $format = ($showTime) ? JText::_('DATE_FORMAT_FILTER_DATETIME') : JText::_('DATE_FORMAT_FILTER_DATE');
1364 $date = date_parse_from_format($format, $value);
1365 $value = (int) $date['year'] . '-' . (int) $date['month'] . '-' . (int) $date['day'];
1366
1367 if ($showTime)
1368 {
1369 $value .= ' ' . (int) $date['hour'] . ':' . (int) $date['minute'] . ':' . (int) $date['second'];
1370 }
1371 }
1372
1373 // Get the user timezone setting defaulting to the server timezone setting.
1374 $offset = JFactory::getUser()->getTimezone();
1375
1376 // Return a MySQL formatted datetime string in UTC.
1377 try
1378 {
1379 $return = JFactory::getDate($value, $offset)->toSql();
1380 }
1381 catch (Exception $e)
1382 {
1383 JFactory::getApplication()->enqueueMessage(JText::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', JText::_((string) $element['label'])), 'warning');
1384
1385 $return = '';
1386 }
1387 }
1388 else
1389 {
1390 $return = '';
1391 }
1392 break;
1393
1394 /*
1395 * Ensures a protocol is present in the saved field unless the relative flag is set.
1396 * Only use when the only permitted protocols require '://'.
1397 * See JFormRuleUrl for list of these.
1398 */
1399
1400 case 'URL':
1401 if (empty($value))
1402 {
1403 return false;
1404 }
1405
1406 // This cleans some of the more dangerous characters but leaves special characters that are valid.
1407 $value = JFilterInput::getInstance()->clean($value, 'html');
1408 $value = trim($value);
1409
1410 // <>" are never valid in a uri see http://www.ietf.org/rfc/rfc1738.txt.
1411 $value = str_replace(array('<', '>', '"'), '', $value);
1412
1413 // Check for a protocol
1414 $protocol = parse_url($value, PHP_URL_SCHEME);
1415
1416 // If there is no protocol and the relative option is not specified,
1417 // we assume that it is an external URL and prepend http://.
1418 if (($element['type'] == 'url' && !$protocol && !$element['relative'])
1419 || (!$element['type'] == 'url' && !$protocol))
1420 {
1421 $protocol = 'http';
1422
1423 // If it looks like an internal link, then add the root.
1424 if (substr($value, 0, 9) == 'index.php')
1425 {
1426 $value = JUri::root() . $value;
1427 }
1428
1429 // Otherwise we treat it as an external link.
1430 else
1431 {
1432 // Put the url back together.
1433 $value = $protocol . '://' . $value;
1434 }
1435 }
1436
1437 // If relative URLS are allowed we assume that URLs without protocols are internal.
1438 elseif (!$protocol && $element['relative'])
1439 {
1440 $host = JUri::getInstance('SERVER')->gethost();
1441
1442 // If it starts with the host string, just prepend the protocol.
1443 if (substr($value, 0) == $host)
1444 {
1445 $value = 'http://' . $value;
1446 }
1447
1448 // Otherwise if it doesn't start with "/" prepend the prefix of the current site.
1449 elseif (substr($value, 0, 1) != '/')
1450 {
1451 $value = JUri::root(true) . '/' . $value;
1452 }
1453 }
1454
1455 $value = JStringPunycode::urlToPunycode($value);
1456 $return = $value;
1457 break;
1458
1459 case 'TEL':
1460 $value = trim($value);
1461
1462 // Does it match the NANP pattern?
1463 if (preg_match('/^(?:\+?1[-. ]?)?\(?([2-9][0-8][0-9])\)?[-. ]?([2-9][0-9]{2})[-. ]?([0-9]{4})$/', $value) == 1)
1464 {
1465 $number = (string) preg_replace('/[^\d]/', '', $value);
1466
1467 if (substr($number, 0, 1) == 1)
1468 {
1469 $number = substr($number, 1);
1470 }
1471
1472 if (substr($number, 0, 2) == '+1')
1473 {
1474 $number = substr($number, 2);
1475 }
1476
1477 $result = '1.' . $number;
1478 }
1479
1480 // If not, does it match ITU-T?
1481 elseif (preg_match('/^\+(?:[0-9] ?){6,14}[0-9]$/', $value) == 1)
1482 {
1483 $countrycode = substr($value, 0, strpos($value, ' '));
1484 $countrycode = (string) preg_replace('/[^\d]/', '', $countrycode);
1485 $number = strstr($value, ' ');
1486 $number = (string) preg_replace('/[^\d]/', '', $number);
1487 $result = $countrycode . '.' . $number;
1488 }
1489
1490 // If not, does it match EPP?
1491 elseif (preg_match('/^\+[0-9]{1,3}\.[0-9]{4,14}(?:x.+)?$/', $value) == 1)
1492 {
1493 if (strstr($value, 'x'))
1494 {
1495 $xpos = strpos($value, 'x');
1496 $value = substr($value, 0, $xpos);
1497 }
1498
1499 $result = str_replace('+', '', $value);
1500 }
1501
1502 // Maybe it is already ccc.nnnnnnn?
1503 elseif (preg_match('/[0-9]{1,3}\.[0-9]{4,14}$/', $value) == 1)
1504 {
1505 $result = $value;
1506 }
1507
1508 // If not, can we make it a string of digits?
1509 else
1510 {
1511 $value = (string) preg_replace('/[^\d]/', '', $value);
1512
1513 if ($value != null && strlen($value) <= 15)
1514 {
1515 $length = strlen($value);
1516
1517 // If it is fewer than 13 digits assume it is a local number
1518 if ($length <= 12)
1519 {
1520 $result = '.' . $value;
1521 }
1522 else
1523 {
1524 // If it has 13 or more digits let's make a country code.
1525 $cclen = $length - 12;
1526 $result = substr($value, 0, $cclen) . '.' . substr($value, $cclen);
1527 }
1528 }
1529
1530 // If not let's not save anything.
1531 else
1532 {
1533 $result = '';
1534 }
1535 }
1536
1537 $return = $result;
1538
1539 break;
1540 default:
1541 // Check for a callback filter.
1542 if (strpos($filter, '::') !== false && is_callable(explode('::', $filter)))
1543 {
1544 $return = call_user_func(explode('::', $filter), $value);
1545 }
1546
1547 // Filter using a callback function if specified.
1548 elseif (function_exists($filter))
1549 {
1550 $return = call_user_func($filter, $value);
1551 }
1552
1553 // Check for empty value and return empty string if no value is required,
1554 // otherwise filter using JFilterInput. All HTML code is filtered by default.
1555 else
1556 {
1557 $required = ((string) $element['required'] == 'true' || (string) $element['required'] == 'required');
1558
1559 if (($value === '' || $value === null) && ! $required)
1560 {
1561 $return = '';
1562 }
1563 else
1564 {
1565 $return = JFilterInput::getInstance()->clean($value, $filter);
1566 }
1567 }
1568 break;
1569 }
1570
1571 return $return;
1572 }
1573
1574 /**
1575 * Method to get a form field represented as an XML element object.
1576 *
1577 * @param string $name The name of the form field.
1578 * @param string $group The optional dot-separated form group path on which to find the field.
1579 *
1580 * @return SimpleXMLElement|boolean The XML element object for the field or boolean false on error.
1581 *
1582 * @since 11.1
1583 */
1584 protected function findField($name, $group = null)
1585 {
1586 $element = false;
1587 $fields = array();
1588
1589 // Make sure there is a valid JForm XML document.
1590 if (!($this->xml instanceof SimpleXMLElement))
1591 {
1592 return false;
1593 }
1594
1595 // Let's get the appropriate field element based on the method arguments.
1596 if ($group)
1597 {
1598 // Get the fields elements for a given group.
1599 $elements = &$this->findGroup($group);
1600
1601 // Get all of the field elements with the correct name for the fields elements.
1602 foreach ($elements as $el)
1603 {
1604 // If there are matching field elements add them to the fields array.
1605 if ($tmp = $el->xpath('descendant::field[@name="' . $name . '" and not(ancestor::field/form/*)]'))
1606 {
1607 $fields = array_merge($fields, $tmp);
1608 }
1609 }
1610
1611 // Make sure something was found.
1612 if (!$fields)
1613 {
1614 return false;
1615 }
1616
1617 // Use the first correct match in the given group.
1618 $groupNames = explode('.', $group);
1619
1620 foreach ($fields as &$field)
1621 {
1622 // Get the group names as strings for ancestor fields elements.
1623 $attrs = $field->xpath('ancestor::fields[@name]/@name');
1624 $names = array_map('strval', $attrs ? $attrs : array());
1625
1626 // If the field is in the exact group use it and break out of the loop.
1627 if ($names == (array) $groupNames)
1628 {
1629 $element = &$field;
1630 break;
1631 }
1632 }
1633 }
1634 else
1635 {
1636 // Get an array of fields with the correct name.
1637 $fields = $this->xml->xpath('//field[@name="' . $name . '" and not(ancestor::field/form/*)]');
1638
1639 // Make sure something was found.
1640 if (!$fields)
1641 {
1642 return false;
1643 }
1644
1645 // Search through the fields for the right one.
1646 foreach ($fields as &$field)
1647 {
1648 // If we find an ancestor fields element with a group name then it isn't what we want.
1649 if ($field->xpath('ancestor::fields[@name]'))
1650 {
1651 continue;
1652 }
1653
1654 // Found it!
1655 else
1656 {
1657 $element = &$field;
1658 break;
1659 }
1660 }
1661 }
1662
1663 return $element;
1664 }
1665
1666 /**
1667 * Method to get an array of `<field>` elements from the form XML document which are in a specified fieldset by name.
1668 *
1669 * @param string $name The name of the fieldset.
1670 *
1671 * @return SimpleXMLElement[]|boolean Boolean false on error or array of SimpleXMLElement objects.
1672 *
1673 * @since 11.1
1674 */
1675 protected function &findFieldsByFieldset($name)
1676 {
1677 $false = false;
1678
1679 // Make sure there is a valid JForm XML document.
1680 if (!($this->xml instanceof SimpleXMLElement))
1681 {
1682 return $false;
1683 }
1684
1685 /*
1686 * Get an array of <field /> elements that are underneath a <fieldset /> element
1687 * with the appropriate name attribute, and also any <field /> elements with
1688 * the appropriate fieldset attribute. To allow repeatable elements only fields
1689 * which are not descendants of other fields are selected.
1690 */
1691 $fields = $this->xml->xpath('(//fieldset[@name="' . $name . '"]//field | //field[@fieldset="' . $name . '"])[not(ancestor::field)]');
1692
1693 return $fields;
1694 }
1695
1696 /**
1697 * Method to get an array of `<field>` elements from the form XML document which are in a control group by name.
1698 *
1699 * @param mixed $group The optional dot-separated form group path on which to find the fields.
1700 * Null will return all fields. False will return fields not in a group.
1701 * @param boolean $nested True to also include fields in nested groups that are inside of the
1702 * group for which to find fields.
1703 *
1704 * @return SimpleXMLElement[]|boolean Boolean false on error or array of SimpleXMLElement objects.
1705 *
1706 * @since 11.1
1707 */
1708 protected function &findFieldsByGroup($group = null, $nested = false)
1709 {
1710 $false = false;
1711 $fields = array();
1712
1713 // Make sure there is a valid JForm XML document.
1714 if (!($this->xml instanceof SimpleXMLElement))
1715 {
1716 return $false;
1717 }
1718
1719 // Get only fields in a specific group?
1720 if ($group)
1721 {
1722 // Get the fields elements for a given group.
1723 $elements = &$this->findGroup($group);
1724
1725 // Get all of the field elements for the fields elements.
1726 foreach ($elements as $element)
1727 {
1728 // If there are field elements add them to the return result.
1729 if ($tmp = $element->xpath('descendant::field'))
1730 {
1731 // If we also want fields in nested groups then just merge the arrays.
1732 if ($nested)
1733 {
1734 $fields = array_merge($fields, $tmp);
1735 }
1736
1737 // If we want to exclude nested groups then we need to check each field.
1738 else
1739 {
1740 $groupNames = explode('.', $group);
1741
1742 foreach ($tmp as $field)
1743 {
1744 // Get the names of the groups that the field is in.
1745 $attrs = $field->xpath('ancestor::fields[@name]/@name');
1746 $names = array_map('strval', $attrs ? $attrs : array());
1747
1748 // If the field is in the specific group then add it to the return list.
1749 if ($names == (array) $groupNames)
1750 {
1751 $fields = array_merge($fields, array($field));
1752 }
1753 }
1754 }
1755 }
1756 }
1757 }
1758 elseif ($group === false)
1759 {
1760 // Get only field elements not in a group.
1761 $fields = $this->xml->xpath('descendant::fields[not(@name)]/field | descendant::fields[not(@name)]/fieldset/field ');
1762 }
1763 else
1764 {
1765 // Get an array of all the <field /> elements.
1766 $fields = $this->xml->xpath('//field[not(ancestor::field/form/*)]');
1767 }
1768
1769 return $fields;
1770 }
1771
1772 /**
1773 * Method to get a form field group represented as an XML element object.
1774 *
1775 * @param string $group The dot-separated form group path on which to find the group.
1776 *
1777 * @return SimpleXMLElement[]|boolean An array of XML element objects for the group or boolean false on error.
1778 *
1779 * @since 11.1
1780 */
1781 protected function &findGroup($group)
1782 {
1783 $false = false;
1784 $groups = array();
1785 $tmp = array();
1786
1787 // Make sure there is a valid JForm XML document.
1788 if (!($this->xml instanceof SimpleXMLElement))
1789 {
1790 return $false;
1791 }
1792
1793 // Make sure there is actually a group to find.
1794 $group = explode('.', $group);
1795
1796 if (!empty($group))
1797 {
1798 // Get any fields elements with the correct group name.
1799 $elements = $this->xml->xpath('//fields[@name="' . (string) $group[0] . '" and not(ancestor::field/form/*)]');
1800
1801 // Check to make sure that there are no parent groups for each element.
1802 foreach ($elements as $element)
1803 {
1804 if (!$element->xpath('ancestor::fields[@name]'))
1805 {
1806 $tmp[] = $element;
1807 }
1808 }
1809
1810 // Iterate through the nested groups to find any matching form field groups.
1811 for ($i = 1, $n = count($group); $i < $n; $i++)
1812 {
1813 // Initialise some loop variables.
1814 $validNames = array_slice($group, 0, $i + 1);
1815 $current = $tmp;
1816 $tmp = array();
1817
1818 // Check to make sure that there are no parent groups for each element.
1819 foreach ($current as $element)
1820 {
1821 // Get any fields elements with the correct group name.
1822 $children = $element->xpath('descendant::fields[@name="' . (string) $group[$i] . '"]');
1823
1824 // For the found fields elements validate that they are in the correct groups.
1825 foreach ($children as $fields)
1826 {
1827 // Get the group names as strings for ancestor fields elements.
1828 $attrs = $fields->xpath('ancestor-or-self::fields[@name]/@name');
1829 $names = array_map('strval', $attrs ? $attrs : array());
1830
1831 // If the group names for the fields element match the valid names at this
1832 // level add the fields element.
1833 if ($validNames == $names)
1834 {
1835 $tmp[] = $fields;
1836 }
1837 }
1838 }
1839 }
1840
1841 // Only include valid XML objects.
1842 foreach ($tmp as $element)
1843 {
1844 if ($element instanceof SimpleXMLElement)
1845 {
1846 $groups[] = $element;
1847 }
1848 }
1849 }
1850
1851 return $groups;
1852 }
1853
1854 /**
1855 * Method to load, setup and return a JFormField object based on field data.
1856 *
1857 * @param string $element The XML element object representation of the form field.
1858 * @param string $group The optional dot-separated form group path on which to find the field.
1859 * @param mixed $value The optional value to use as the default for the field.
1860 *
1861 * @return JFormField|boolean The JFormField object for the field or boolean false on error.
1862 *
1863 * @since 11.1
1864 */
1865 protected function loadField($element, $group = null, $value = null)
1866 {
1867 // Make sure there is a valid SimpleXMLElement.
1868 if (!($element instanceof SimpleXMLElement))
1869 {
1870 return false;
1871 }
1872
1873 // Get the field type.
1874 $type = $element['type'] ? (string) $element['type'] : 'text';
1875
1876 // Load the JFormField object for the field.
1877 $field = $this->loadFieldType($type);
1878
1879 // If the object could not be loaded, get a text field object.
1880 if ($field === false)
1881 {
1882 $field = $this->loadFieldType('text');
1883 }
1884
1885 /*
1886 * Get the value for the form field if not set.
1887 * Default to the translated version of the 'default' attribute
1888 * if 'translate_default' attribute if set to 'true' or '1'
1889 * else the value of the 'default' attribute for the field.
1890 */
1891 if ($value === null)
1892 {
1893 $default = (string) ($element['default'] ? $element['default'] : $element->default);
1894
1895 if (($translate = $element['translate_default']) && ((string) $translate == 'true' || (string) $translate == '1'))
1896 {
1897 $lang = JFactory::getLanguage();
1898
1899 if ($lang->hasKey($default))
1900 {
1901 $debug = $lang->setDebug(false);
1902 $default = JText::_($default);
1903 $lang->setDebug($debug);
1904 }
1905 else
1906 {
1907 $default = JText::_($default);
1908 }
1909 }
1910
1911 $value = $this->getValue((string) $element['name'], $group, $default);
1912 }
1913
1914 // Setup the JFormField object.
1915 $field->setForm($this);
1916
1917 if ($field->setup($element, $value, $group))
1918 {
1919 return $field;
1920 }
1921 else
1922 {
1923 return false;
1924 }
1925 }
1926
1927 /**
1928 * Proxy for {@link JFormHelper::loadFieldType()}.
1929 *
1930 * @param string $type The field type.
1931 * @param boolean $new Flag to toggle whether we should get a new instance of the object.
1932 *
1933 * @return JFormField|boolean JFormField object on success, false otherwise.
1934 *
1935 * @since 11.1
1936 */
1937 protected function loadFieldType($type, $new = true)
1938 {
1939 return JFormHelper::loadFieldType($type, $new);
1940 }
1941
1942 /**
1943 * Proxy for JFormHelper::loadRuleType().
1944 *
1945 * @param string $type The rule type.
1946 * @param boolean $new Flag to toggle whether we should get a new instance of the object.
1947 *
1948 * @return JFormRule|boolean JFormRule object on success, false otherwise.
1949 *
1950 * @see JFormHelper::loadRuleType()
1951 * @since 11.1
1952 */
1953 protected function loadRuleType($type, $new = true)
1954 {
1955 return JFormHelper::loadRuleType($type, $new);
1956 }
1957
1958 /**
1959 * Method to synchronize any field, form or rule paths contained in the XML document.
1960 *
1961 * @return boolean True on success.
1962 *
1963 * @since 11.1
1964 * @todo Maybe we should receive all addXXXpaths attributes at once?
1965 */
1966 protected function syncPaths()
1967 {
1968 // Make sure there is a valid JForm XML document.
1969 if (!($this->xml instanceof SimpleXMLElement))
1970 {
1971 return false;
1972 }
1973
1974 // Get any addfieldpath attributes from the form definition.
1975 $paths = $this->xml->xpath('//*[@addfieldpath]/@addfieldpath');
1976 $paths = array_map('strval', $paths ? $paths : array());
1977
1978 // Add the field paths.
1979 foreach ($paths as $path)
1980 {
1981 $path = JPATH_ROOT . '/' . ltrim($path, '/\\');
1982 self::addFieldPath($path);
1983 }
1984
1985 // Get any addformpath attributes from the form definition.
1986 $paths = $this->xml->xpath('//*[@addformpath]/@addformpath');
1987 $paths = array_map('strval', $paths ? $paths : array());
1988
1989 // Add the form paths.
1990 foreach ($paths as $path)
1991 {
1992 $path = JPATH_ROOT . '/' . ltrim($path, '/\\');
1993 self::addFormPath($path);
1994 }
1995
1996 // Get any addrulepath attributes from the form definition.
1997 $paths = $this->xml->xpath('//*[@addrulepath]/@addrulepath');
1998 $paths = array_map('strval', $paths ? $paths : array());
1999
2000 // Add the rule paths.
2001 foreach ($paths as $path)
2002 {
2003 $path = JPATH_ROOT . '/' . ltrim($path, '/\\');
2004 self::addRulePath($path);
2005 }
2006
2007 return true;
2008 }
2009
2010 /**
2011 * Method to validate a JFormField object based on field data.
2012 *
2013 * @param SimpleXMLElement $element The XML element object representation of the form field.
2014 * @param string $group The optional dot-separated form group path on which to find the field.
2015 * @param mixed $value The optional value to use as the default for the field.
2016 * @param Registry $input An optional Registry object with the entire data set to validate
2017 * against the entire form.
2018 *
2019 * @return boolean Boolean true if field value is valid, Exception on failure.
2020 *
2021 * @since 11.1
2022 * @throws InvalidArgumentException
2023 * @throws UnexpectedValueException
2024 */
2025 protected function validateField(SimpleXMLElement $element, $group = null, $value = null, Registry $input = null)
2026 {
2027 $valid = true;
2028
2029 // Check if the field is required.
2030 $required = ((string) $element['required'] == 'true' || (string) $element['required'] == 'required');
2031
2032 if ($required)
2033 {
2034 // If the field is required and the value is empty return an error message.
2035 if (($value === '') || ($value === null))
2036 {
2037 if ($element['label'])
2038 {
2039 $message = JText::_($element['label']);
2040 }
2041 else
2042 {
2043 $message = JText::_($element['name']);
2044 }
2045
2046 $message = JText::sprintf('JLIB_FORM_VALIDATE_FIELD_REQUIRED', $message);
2047
2048 return new RuntimeException($message);
2049 }
2050 }
2051
2052 // Get the field validation rule.
2053 if ($type = (string) $element['validate'])
2054 {
2055 // Load the JFormRule object for the field.
2056 $rule = $this->loadRuleType($type);
2057
2058 // If the object could not be loaded return an error message.
2059 if ($rule === false)
2060 {
2061 throw new UnexpectedValueException(sprintf('%s::validateField() rule `%s` missing.', get_class($this), $type));
2062 }
2063
2064 // Run the field validation rule test.
2065 $valid = $rule->test($element, $value, $group, $input, $this);
2066
2067 // Check for an error in the validation test.
2068 if ($valid instanceof Exception)
2069 {
2070 return $valid;
2071 }
2072 }
2073
2074 // Check if the field is valid.
2075 if ($valid === false)
2076 {
2077 // Does the field have a defined error message?
2078 $message = (string) $element['message'];
2079
2080 if ($message)
2081 {
2082 $message = JText::_($element['message']);
2083
2084 return new UnexpectedValueException($message);
2085 }
2086 else
2087 {
2088 $message = JText::_($element['label']);
2089 $message = JText::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', $message);
2090
2091 return new UnexpectedValueException($message);
2092 }
2093 }
2094
2095 return true;
2096 }
2097
2098 /**
2099 * Proxy for {@link JFormHelper::addFieldPath()}.
2100 *
2101 * @param mixed $new A path or array of paths to add.
2102 *
2103 * @return array The list of paths that have been added.
2104 *
2105 * @since 11.1
2106 */
2107 public static function addFieldPath($new = null)
2108 {
2109 return JFormHelper::addFieldPath($new);
2110 }
2111
2112 /**
2113 * Proxy for JFormHelper::addFormPath().
2114 *
2115 * @param mixed $new A path or array of paths to add.
2116 *
2117 * @return array The list of paths that have been added.
2118 *
2119 * @see JFormHelper::addFormPath()
2120 * @since 11.1
2121 */
2122 public static function addFormPath($new = null)
2123 {
2124 return JFormHelper::addFormPath($new);
2125 }
2126
2127 /**
2128 * Proxy for JFormHelper::addRulePath().
2129 *
2130 * @param mixed $new A path or array of paths to add.
2131 *
2132 * @return array The list of paths that have been added.
2133 *
2134 * @see JFormHelper::addRulePath()
2135 * @since 11.1
2136 */
2137 public static function addRulePath($new = null)
2138 {
2139 return JFormHelper::addRulePath($new);
2140 }
2141
2142 /**
2143 * Method to get an instance of a form.
2144 *
2145 * @param string $name The name of the form.
2146 * @param string $data The name of an XML file or string to load as the form definition.
2147 * @param array $options An array of form options.
2148 * @param boolean $replace Flag to toggle whether form fields should be replaced if a field
2149 * already exists with the same group/name.
2150 * @param string|boolean $xpath An optional xpath to search for the fields.
2151 *
2152 * @return JForm JForm instance.
2153 *
2154 * @since 11.1
2155 * @throws InvalidArgumentException if no data provided.
2156 * @throws RuntimeException if the form could not be loaded.
2157 */
2158 public static function getInstance($name, $data = null, $options = array(), $replace = true, $xpath = false)
2159 {
2160 // Reference to array with form instances
2161 $forms = &self::$forms;
2162
2163 // Only instantiate the form if it does not already exist.
2164 if (!isset($forms[$name]))
2165 {
2166 $data = trim($data);
2167
2168 if (empty($data))
2169 {
2170 throw new InvalidArgumentException(sprintf('JForm::getInstance(%s, *%s*)', $name, gettype($data)));
2171 }
2172
2173 // Instantiate the form.
2174 $forms[$name] = new JForm($name, $options);
2175
2176 // Load the data.
2177 if (substr($data, 0, 1) == '<')
2178 {
2179 if ($forms[$name]->load($data, $replace, $xpath) == false)
2180 {
2181 throw new RuntimeException('JForm::getInstance could not load form');
2182 }
2183 }
2184 else
2185 {
2186 if ($forms[$name]->loadFile($data, $replace, $xpath) == false)
2187 {
2188 throw new RuntimeException('JForm::getInstance could not load file');
2189 }
2190 }
2191 }
2192
2193 return $forms[$name];
2194 }
2195
2196 /**
2197 * Adds a new child SimpleXMLElement node to the source.
2198 *
2199 * @param SimpleXMLElement $source The source element on which to append.
2200 * @param SimpleXMLElement $new The new element to append.
2201 *
2202 * @return void
2203 *
2204 * @since 11.1
2205 */
2206 protected static function addNode(SimpleXMLElement $source, SimpleXMLElement $new)
2207 {
2208 // Add the new child node.
2209 $node = $source->addChild($new->getName(), htmlspecialchars(trim($new)));
2210
2211 // Add the attributes of the child node.
2212 foreach ($new->attributes() as $name => $value)
2213 {
2214 $node->addAttribute($name, $value);
2215 }
2216
2217 // Add any children of the new node.
2218 foreach ($new->children() as $child)
2219 {
2220 self::addNode($node, $child);
2221 }
2222 }
2223
2224 /**
2225 * Update the attributes of a child node
2226 *
2227 * @param SimpleXMLElement $source The source element on which to append the attributes
2228 * @param SimpleXMLElement $new The new element to append
2229 *
2230 * @return void
2231 *
2232 * @since 11.1
2233 */
2234 protected static function mergeNode(SimpleXMLElement $source, SimpleXMLElement $new)
2235 {
2236 // Update the attributes of the child node.
2237 foreach ($new->attributes() as $name => $value)
2238 {
2239 if (isset($source[$name]))
2240 {
2241 $source[$name] = (string) $value;
2242 }
2243 else
2244 {
2245 $source->addAttribute($name, $value);
2246 }
2247 }
2248 }
2249
2250 /**
2251 * Merges new elements into a source `<fields>` element.
2252 *
2253 * @param SimpleXMLElement $source The source element.
2254 * @param SimpleXMLElement $new The new element to merge.
2255 *
2256 * @return void
2257 *
2258 * @since 11.1
2259 */
2260 protected static function mergeNodes(SimpleXMLElement $source, SimpleXMLElement $new)
2261 {
2262 // The assumption is that the inputs are at the same relative level.
2263 // So we just have to scan the children and deal with them.
2264
2265 // Update the attributes of the child node.
2266 foreach ($new->attributes() as $name => $value)
2267 {
2268 if (isset($source[$name]))
2269 {
2270 $source[$name] = (string) $value;
2271 }
2272 else
2273 {
2274 $source->addAttribute($name, $value);
2275 }
2276 }
2277
2278 foreach ($new->children() as $child)
2279 {
2280 $type = $child->getName();
2281 $name = $child['name'];
2282
2283 // Does this node exist?
2284 $fields = $source->xpath($type . '[@name="' . $name . '"]');
2285
2286 if (empty($fields))
2287 {
2288 // This node does not exist, so add it.
2289 self::addNode($source, $child);
2290 }
2291 else
2292 {
2293 // This node does exist.
2294 switch ($type)
2295 {
2296 case 'field':
2297 self::mergeNode($fields[0], $child);
2298 break;
2299
2300 default:
2301 self::mergeNodes($fields[0], $child);
2302 break;
2303 }
2304 }
2305 }
2306 }
2307
2308 /**
2309 * Returns the value of an attribute of the form itself
2310 *
2311 * @param string $name Name of the attribute to get
2312 * @param mixed $default Optional value to return if attribute not found
2313 *
2314 * @return mixed Value of the attribute / default
2315 *
2316 * @since 3.2
2317 */
2318 public function getAttribute($name, $default = null)
2319 {
2320 if ($this->xml instanceof SimpleXMLElement)
2321 {
2322 $attributes = $this->xml->attributes();
2323
2324 // Ensure that the attribute exists
2325 if (property_exists($attributes, $name))
2326 {
2327 $value = $attributes->$name;
2328
2329 if ($value !== null)
2330 {
2331 return (string) $value;
2332 }
2333 }
2334 }
2335
2336 return $default;
2337 }
2338
2339 /**
2340 * Getter for the form data
2341 *
2342 * @return Registry Object with the data
2343 *
2344 * @since 3.2
2345 */
2346 public function getData()
2347 {
2348 return $this->data;
2349 }
2350
2351 /**
2352 * Method to get the XML form object
2353 *
2354 * @return SimpleXMLElement The form XML object
2355 *
2356 * @since 3.2
2357 */
2358 public function getXml()
2359 {
2360 return $this->xml;
2361 }
2362
2363 /**
2364 * Method to get a form field represented as an XML element object.
2365 *
2366 * @param string $name The name of the form field.
2367 * @param string $group The optional dot-separated form group path on which to find the field.
2368 *
2369 * @return SimpleXMLElement|boolean The XML element object for the field or boolean false on error.
2370 *
2371 * @since 3.7.0
2372 */
2373 public function getFieldXml($name, $group = null)
2374 {
2375 return $this->findField($name, $group);
2376 }
2377 }
2378