Current File : /home/pacjaorg/public_html/km/libraries/src/Form/Form.php |
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2009 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Form;
use Joomla\CMS\Factory;
use Joomla\CMS\Filesystem\Path;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Object\CMSObject;
use Joomla\Database\DatabaseAwareInterface;
use Joomla\Database\DatabaseAwareTrait;
use Joomla\Database\DatabaseInterface;
use Joomla\Database\Exception\DatabaseNotFoundException;
use Joomla\Registry\Registry;
use Joomla\Utilities\ArrayHelper;
// phpcs:disable PSR1.Files.SideEffects
\defined('JPATH_PLATFORM') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Form Class for the Joomla Platform.
*
* This class implements a robust API for constructing, populating, filtering, and validating forms.
* It uses XML definitions to construct form fields and a variety of field and rule classes to
* render and validate the form.
*
* @link https://www.w3.org/TR/html4/interact/forms.html
* @link https://html.spec.whatwg.org/multipage/forms.html
* @since 1.7.0
*/
class Form
{
use DatabaseAwareTrait;
/**
* The Registry data store for form fields during display.
*
* @var Registry
* @since 1.7.0
*/
protected $data;
/**
* The form object errors array.
*
* @var \Exception[]
* @since 1.7.0
*/
protected $errors = [];
/**
* The name of the form instance.
*
* @var string
* @since 1.7.0
*/
protected $name;
/**
* The form object options for use in rendering and validation.
*
* @var array
* @since 1.7.0
*/
protected $options = [];
/**
* The form XML definition.
*
* @var \SimpleXMLElement
* @since 1.7.0
*/
protected $xml;
/**
* Form instances.
*
* @var Form[]
* @since 1.7.0
*/
protected static $forms = [];
/**
* Allows extensions to implement repeating elements
*
* @var boolean
* @since 3.2
*/
public $repeat = false;
/**
* Method to instantiate the form object.
*
* @param string $name The name of the form.
* @param array $options An array of form options.
*
* @since 1.7.0
*/
public function __construct($name, array $options = [])
{
// Set the name for the form.
$this->name = $name;
// Initialise the Registry data.
$this->data = new Registry();
// Set the options if specified.
$this->options['control'] = $options['control'] ?? false;
}
/**
* Method to bind data to the form.
*
* @param mixed $data An array or object of data to bind to the form.
*
* @return boolean True on success.
*
* @since 1.7.0
*/
public function bind($data)
{
// Make sure there is a valid Form XML document.
if (!($this->xml instanceof \SimpleXMLElement)) {
throw new \UnexpectedValueException(sprintf('%s::%s `xml` is not an instance of SimpleXMLElement', \get_class($this), __METHOD__));
}
// The data must be an object or array.
if (!\is_object($data) && !\is_array($data)) {
return false;
}
$this->bindLevel(null, $data);
return true;
}
/**
* Method to bind data to the form for the group level.
*
* @param string $group The dot-separated form group path on which to bind the data.
* @param mixed $data An array or object of data to bind to the form for the group level.
*
* @return void
*
* @since 1.7.0
*/
protected function bindLevel($group, $data)
{
// Ensure the input data is an array.
if (\is_object($data)) {
if ($data instanceof Registry) {
// Handle a Registry.
$data = $data->toArray();
} elseif ($data instanceof CMSObject) {
// Handle a CMSObject.
$data = $data->getProperties();
} else {
// Handle other types of objects.
$data = (array) $data;
}
}
// Process the input data.
foreach ($data as $k => $v) {
$level = $group ? $group . '.' . $k : $k;
if ($this->findField($k, $group)) {
// If the field exists set the value.
$this->data->set($level, $v);
} elseif (\is_object($v) || ArrayHelper::isAssociative($v)) {
// If the value is an object or an associative array, hand it off to the recursive bind level method.
$this->bindLevel($level, $v);
}
}
}
/**
* Return Exceptions thrown during the form validation process.
*
* @return \Exception[]
*
* @since 1.7.0
*/
public function getErrors()
{
return $this->errors;
}
/**
* Method to get a form field represented as a FormField object.
*
* @param string $name The name of the form field.
* @param string $group The optional dot-separated form group path on which to find the field.
* @param mixed $value The optional value to use as the default for the field.
*
* @return FormField|boolean The FormField object for the field or boolean false on error.
*
* @since 1.7.0
*/
public function getField($name, $group = null, $value = null)
{
// Make sure there is a valid Form XML document.
if (!($this->xml instanceof \SimpleXMLElement)) {
throw new \UnexpectedValueException(sprintf('%s::%s `xml` is not an instance of SimpleXMLElement', \get_class($this), __METHOD__));
}
// Attempt to find the field by name and group.
$element = $this->findField($name, $group);
// If the field element was not found return false.
if (!$element) {
return false;
}
return $this->loadField($element, $group, $value);
}
/**
* Method to get an attribute value from a field XML element. If the attribute doesn't exist or
* is null then the optional default value will be used.
*
* @param string $name The name of the form field for which to get the attribute value.
* @param string $attribute The name of the attribute for which to get a value.
* @param mixed $default The optional default value to use if no attribute value exists.
* @param string $group The optional dot-separated form group path on which to find the field.
*
* @return mixed The attribute value for the field.
*
* @since 1.7.0
* @throws \UnexpectedValueException
*/
public function getFieldAttribute($name, $attribute, $default = null, $group = null)
{
// Make sure there is a valid Form XML document.
if (!($this->xml instanceof \SimpleXMLElement)) {
throw new \UnexpectedValueException(sprintf('%s::%s `xml` is not an instance of SimpleXMLElement', \get_class($this), __METHOD__));
}
// Find the form field element from the definition.
$element = $this->findField($name, $group);
// If the element exists and the attribute exists for the field return the attribute value.
if (($element instanceof \SimpleXMLElement) && \strlen((string) $element[$attribute])) {
return (string) $element[$attribute];
} else {
// Otherwise return the given default value.
return $default;
}
}
/**
* Method to get an array of FormField objects in a given fieldset by name. If no name is
* given then all fields are returned.
*
* @param string $set The optional name of the fieldset.
*
* @return FormField[] The array of FormField objects in the fieldset.
*
* @since 1.7.0
*/
public function getFieldset($set = null)
{
$fields = [];
// Get all of the field elements in the fieldset.
if ($set) {
$elements = $this->findFieldsByFieldset($set);
} else {
// Get all fields.
$elements = $this->findFieldsByGroup();
}
// If no field elements were found return empty.
if (empty($elements)) {
return $fields;
}
// Build the result array from the found field elements.
foreach ($elements as $element) {
// Get the field groups for the element.
$attrs = $element->xpath('ancestor::fields[@name]/@name');
$groups = array_map('strval', $attrs ?: []);
$group = implode('.', $groups);
// If the field is successfully loaded add it to the result array.
if ($field = $this->loadField($element, $group)) {
$fields[$field->id] = $field;
}
}
return $fields;
}
/**
* Method to get an array of fieldset objects optionally filtered over a given field group.
*
* @param string $group The dot-separated form group path on which to filter the fieldsets.
*
* @return object[] The array of fieldset objects.
*
* @since 1.7.0
*/
public function getFieldsets($group = null)
{
$fieldsets = [];
$sets = [];
// Make sure there is a valid Form XML document.
if (!($this->xml instanceof \SimpleXMLElement)) {
throw new \UnexpectedValueException(sprintf('%s::%s `xml` is not an instance of SimpleXMLElement', \get_class($this), __METHOD__));
}
if ($group) {
// Get the fields elements for a given group.
$elements = &$this->findGroup($group);
foreach ($elements as &$element) {
// Get an array of <fieldset /> elements and fieldset attributes within the fields element.
if ($tmp = $element->xpath('descendant::fieldset[@name] | descendant::field[@fieldset]/@fieldset')) {
$sets = array_merge($sets, (array) $tmp);
}
}
} else {
// Get an array of <fieldset /> elements and fieldset attributes.
$sets = $this->xml->xpath('//fieldset[@name and not(ancestor::field/form/*)] | //field[@fieldset and not(ancestor::field/form/*)]/@fieldset');
}
// If no fieldsets are found return empty.
if (empty($sets)) {
return $fieldsets;
}
// Process each found fieldset.
foreach ($sets as $set) {
if ((string) $set['hidden'] === 'true') {
continue;
}
// Are we dealing with a fieldset element?
if ((string) $set['name']) {
// Only create it if it doesn't already exist.
if (empty($fieldsets[(string) $set['name']])) {
// Build the fieldset object.
$fieldset = (object) ['name' => '', 'label' => '', 'description' => ''];
foreach ($set->attributes() as $name => $value) {
$fieldset->$name = (string) $value;
}
// Add the fieldset object to the list.
$fieldsets[$fieldset->name] = $fieldset;
}
} else {
// Must be dealing with a fieldset attribute.
// Only create it if it doesn't already exist.
if (empty($fieldsets[(string) $set])) {
// Attempt to get the fieldset element for data (throughout the entire form document).
$tmp = $this->xml->xpath('//fieldset[@name="' . (string) $set . '"]');
// If no element was found, build a very simple fieldset object.
if (empty($tmp)) {
$fieldset = (object) ['name' => (string) $set, 'label' => '', 'description' => ''];
} else {
// Build the fieldset object from the element.
$fieldset = (object) ['name' => '', 'label' => '', 'description' => ''];
foreach ($tmp[0]->attributes() as $name => $value) {
$fieldset->$name = (string) $value;
}
}
// Add the fieldset object to the list.
$fieldsets[$fieldset->name] = $fieldset;
}
}
}
return $fieldsets;
}
/**
* Method to get the form control. This string serves as a container for all form fields. For
* example, if there is a field named 'foo' and a field named 'bar' and the form control is
* empty the fields will be rendered like: `<input name="foo" />` and `<input name="bar" />`. If
* the form control is set to 'joomla' however, the fields would be rendered like:
* `<input name="joomla[foo]" />` and `<input name="joomla[bar]" />`.
*
* @return string The form control string.
*
* @since 1.7.0
*/
public function getFormControl()
{
return (string) $this->options['control'];
}
/**
* Method to get an array of FormField objects in a given field group by name.
*
* @param string $group The dot-separated form group path for which to get the form fields.
* @param boolean $nested True to also include fields in nested groups that are inside of the
* group for which to find fields.
*
* @return FormField[] The array of FormField objects in the field group.
*
* @since 1.7.0
*/
public function getGroup($group, $nested = false)
{
$fields = [];
// Get all of the field elements in the field group.
$elements = $this->findFieldsByGroup($group, $nested);
// If no field elements were found return empty.
if (empty($elements)) {
return $fields;
}
// Build the result array from the found field elements.
foreach ($elements as $element) {
// Get the field groups for the element.
$attrs = $element->xpath('ancestor::fields[@name]/@name');
$groups = array_map('strval', $attrs ?: []);
$group = implode('.', $groups);
// If the field is successfully loaded add it to the result array.
if ($field = $this->loadField($element, $group)) {
$fields[$field->id] = $field;
}
}
return $fields;
}
/**
* Method to get a form field markup for the field input.
*
* @param string $name The name of the form field.
* @param string $group The optional dot-separated form group path on which to find the field.
* @param mixed $value The optional value to use as the default for the field.
*
* @return string The form field markup.
*
* @since 1.7.0
*/
public function getInput($name, $group = null, $value = null)
{
// Attempt to get the form field.
if ($field = $this->getField($name, $group, $value)) {
return $field->input;
}
return '';
}
/**
* Method to get the label for a field input.
*
* @param string $name The name of the form field.
* @param string $group The optional dot-separated form group path on which to find the field.
*
* @return string The form field label.
*
* @since 1.7.0
*/
public function getLabel($name, $group = null)
{
// Attempt to get the form field.
if ($field = $this->getField($name, $group)) {
return $field->label;
}
return '';
}
/**
* Method to get the form name.
*
* @return string The name of the form.
*
* @since 1.7.0
*/
public function getName()
{
return $this->name;
}
/**
* Method to get the value of a field.
*
* @param string $name The name of the field for which to get the value.
* @param string $group The optional dot-separated form group path on which to get the value.
* @param mixed $default The optional default value of the field value is empty.
*
* @return mixed The value of the field or the default value if empty.
*
* @since 1.7.0
*/
public function getValue($name, $group = null, $default = null)
{
// If a group is set use it.
if ($group) {
$return = $this->data->get($group . '.' . $name, $default);
} else {
$return = $this->data->get($name, $default);
}
return $return;
}
/**
* Method to get a control group with label and input.
*
* @param string $name The name of the field for which to get the value.
* @param string $group The optional dot-separated form group path on which to get the value.
* @param mixed $default The optional default value of the field value is empty.
* @param array $options Any options to be passed into the rendering of the field
*
* @return string A string containing the html for the control group
*
* @since 3.2.3
*/
public function renderField($name, $group = null, $default = null, $options = [])
{
$field = $this->getField($name, $group, $default);
if ($field) {
return $field->renderField($options);
}
return '';
}
/**
* Method to get all control groups with label and input of a fieldset.
*
* @param string $name The name of the fieldset for which to get the values.
* @param array $options Any options to be passed into the rendering of the field
*
* @return string A string containing the html for the control groups
*
* @since 3.2.3
*/
public function renderFieldset($name, $options = [])
{
$fields = $this->getFieldset($name);
$html = [];
foreach ($fields as $field) {
$html[] = $field->renderField($options);
}
return implode('', $html);
}
/**
* Method to load the form description from an XML string or object.
*
* The replace option works per field. If a field being loaded already exists in the current
* form definition then the behavior or load will vary depending upon the replace flag. If it
* is set to true, then the existing field will be replaced in its exact location by the new
* field being loaded. If it is false, then the new field being loaded will be ignored and the
* method will move on to the next field to load.
*
* @param string|\SimpleXMLElement $data The name of an XML string or object.
* @param boolean $replace Flag to toggle whether form fields should be replaced if a field
* already exists with the same group/name.
* @param string $xpath An optional xpath to search for the fields.
*
* @return boolean True on success, false otherwise.
*
* @since 1.7.0
*/
public function load($data, $replace = true, $xpath = null)
{
// If the data to load isn't already an XML element or string return false.
if (!($data instanceof \SimpleXMLElement) && !\is_string($data)) {
return false;
}
// Attempt to load the XML if a string.
if (\is_string($data)) {
try {
$data = new \SimpleXMLElement($data);
} catch (\Exception $e) {
return false;
}
}
// If we have no XML definition at this point let's make sure we get one.
if (empty($this->xml)) {
// If no XPath query is set to search for fields, and we have a <form />, set it and return.
if (!$xpath && ($data->getName() === 'form')) {
$this->xml = $data;
// Synchronize any paths found in the load.
$this->syncPaths();
return true;
} else {
// Create a root element for the form.
$this->xml = new \SimpleXMLElement('<form></form>');
}
}
// Get the XML elements to load.
$elements = [];
if ($xpath) {
$elements = $data->xpath($xpath);
} elseif ($data->getName() === 'form') {
$elements = $data->children();
}
// If there is nothing to load return true.
if (empty($elements)) {
return true;
}
// Load the found form elements.
foreach ($elements as $element) {
// Get an array of fields with the correct name.
$fields = $element->xpath('descendant-or-self::field');
foreach ($fields as $field) {
// Get the group names as strings for ancestor fields elements.
$attrs = $field->xpath('ancestor::fields[@name]/@name');
$groups = array_map('strval', $attrs ?: []);
// Check to see if the field exists in the current form.
if ($current = $this->findField((string) $field['name'], implode('.', $groups))) {
// If set to replace found fields, replace the data and remove the field so we don't add it twice.
if ($replace) {
$olddom = dom_import_simplexml($current);
$loadeddom = dom_import_simplexml($field);
$addeddom = $olddom->ownerDocument->importNode($loadeddom, true);
$olddom->parentNode->replaceChild($addeddom, $olddom);
$loadeddom->parentNode->removeChild($loadeddom);
} else {
unset($field);
}
}
}
// Merge the new field data into the existing XML document.
self::addNode($this->xml, $element);
}
// Synchronize any paths found in the load.
$this->syncPaths();
return true;
}
/**
* Method to load the form description from an XML file.
*
* The reset option works on a group basis. If the XML file references
* groups that have already been created they will be replaced with the
* fields in the new XML file unless the $reset parameter has been set
* to false.
*
* @param string $file The filesystem path of an XML file.
* @param boolean $reset Flag to toggle whether form fields should be replaced if a field
* already exists with the same group/name.
* @param string $xpath An optional xpath to search for the fields.
*
* @return boolean True on success, false otherwise.
*
* @since 1.7.0
*/
public function loadFile($file, $reset = true, $xpath = null)
{
// Check to see if the path is an absolute path.
if (!is_file($file)) {
// Not an absolute path so let's attempt to find one using JPath.
$file = Path::find(self::addFormPath(), strtolower($file) . '.xml');
// If unable to find the file return false.
if (!$file) {
return false;
}
}
// Attempt to load the XML file.
$xml = simplexml_load_file($file);
return $this->load($xml, $reset, $xpath);
}
/**
* Method to remove a field from the form definition.
*
* @param string $name The name of the form field for which remove.
* @param string $group The optional dot-separated form group path on which to find the field.
*
* @return boolean True on success, false otherwise.
*
* @since 1.7.0
* @throws \UnexpectedValueException
*/
public function removeField($name, $group = null)
{
// Make sure there is a valid Form XML document.
if (!($this->xml instanceof \SimpleXMLElement)) {
throw new \UnexpectedValueException(sprintf('%s::%s `xml` is not an instance of SimpleXMLElement', \get_class($this), __METHOD__));
}
// Find the form field element from the definition.
$element = $this->findField($name, $group);
// If the element exists remove it from the form definition.
if ($element instanceof \SimpleXMLElement) {
$dom = dom_import_simplexml($element);
$dom->parentNode->removeChild($dom);
return true;
}
return false;
}
/**
* Method to remove a group from the form definition.
*
* @param string $group The dot-separated form group path for the group to remove.
*
* @return boolean True on success.
*
* @since 1.7.0
* @throws \UnexpectedValueException
*/
public function removeGroup($group)
{
// Make sure there is a valid Form XML document.
if (!($this->xml instanceof \SimpleXMLElement)) {
throw new \UnexpectedValueException(sprintf('%s::%s `xml` is not an instance of SimpleXMLElement', \get_class($this), __METHOD__));
}
// Get the fields elements for a given group.
$elements = &$this->findGroup($group);
foreach ($elements as &$element) {
$dom = dom_import_simplexml($element);
$dom->parentNode->removeChild($dom);
}
return true;
}
/**
* Method to reset the form data store and optionally the form XML definition.
*
* @param boolean $xml True to also reset the XML form definition.
*
* @return boolean True on success.
*
* @since 1.7.0
*/
public function reset($xml = false)
{
unset($this->data);
$this->data = new Registry();
if ($xml) {
unset($this->xml);
$this->xml = new \SimpleXMLElement('<form></form>');
}
return true;
}
/**
* Method to set a field XML element to the form definition. If the replace flag is set then
* the field will be set whether it already exists or not. If it isn't set, then the field
* will not be replaced if it already exists.
*
* @param \SimpleXMLElement $element The XML element object representation of the form field.
* @param string $group The optional dot-separated form group path on which to set the field.
* @param boolean $replace True to replace an existing field if one already exists.
* @param string $fieldset The name of the fieldset we are adding the field to.
*
* @return boolean True on success.
*
* @since 1.7.0
* @throws \UnexpectedValueException
*/
public function setField(\SimpleXMLElement $element, $group = null, $replace = true, $fieldset = 'default')
{
// Make sure there is a valid Form XML document.
if (!($this->xml instanceof \SimpleXMLElement)) {
throw new \UnexpectedValueException(sprintf('%s::%s `xml` is not an instance of SimpleXMLElement', \get_class($this), __METHOD__));
}
// Find the form field element from the definition.
$old = $this->findField((string) $element['name'], $group);
// If an existing field is found and replace flag is false do nothing and return true.
if (!$replace && !empty($old)) {
return true;
}
// If an existing field is found and replace flag is true remove the old field.
if ($replace && !empty($old) && ($old instanceof \SimpleXMLElement)) {
$dom = dom_import_simplexml($old);
// Get the parent element, this should be the fieldset
$parent = $dom->parentNode;
$fieldset = $parent->getAttribute('name');
$parent->removeChild($dom);
}
// Create the search path
$path = '//';
if (!empty($group)) {
$path .= 'fields[@name="' . $group . '"]/';
}
$path .= 'fieldset[@name="' . $fieldset . '"]';
$fs = $this->xml->xpath($path);
if (isset($fs[0]) && ($fs[0] instanceof \SimpleXMLElement)) {
// Add field to the form.
self::addNode($fs[0], $element);
// Synchronize any paths found in the load.
$this->syncPaths();
return true;
}
// We couldn't find a fieldset to add the field. Now we are checking, if we have set only a group
if (!empty($group)) {
$fields = &$this->findGroup($group);
// If an appropriate fields element was found for the group, add the element.
if (isset($fields[0]) && ($fields[0] instanceof \SimpleXMLElement)) {
self::addNode($fields[0], $element);
}
// Synchronize any paths found in the load.
$this->syncPaths();
return true;
}
// We couldn't find a parent so we are adding it at root level
// Add field to the form.
self::addNode($this->xml, $element);
// Synchronize any paths found in the load.
$this->syncPaths();
return true;
}
/**
* Method to set an attribute value for a field XML element.
*
* @param string $name The name of the form field for which to set the attribute value.
* @param string $attribute The name of the attribute for which to set a value.
* @param mixed $value The value to set for the attribute.
* @param string $group The optional dot-separated form group path on which to find the field.
*
* @return boolean True on success.
*
* @since 1.7.0
* @throws \UnexpectedValueException
*/
public function setFieldAttribute($name, $attribute, $value, $group = null)
{
// Make sure there is a valid Form XML document.
if (!($this->xml instanceof \SimpleXMLElement)) {
throw new \UnexpectedValueException(sprintf('%s::%s `xml` is not an instance of SimpleXMLElement', \get_class($this), __METHOD__));
}
// Find the form field element from the definition.
$element = $this->findField($name, $group);
// If the element doesn't exist return false.
if (!($element instanceof \SimpleXMLElement)) {
return false;
} else {
// Otherwise set the attribute and return true.
$element[$attribute] = $value;
// Synchronize any paths found in the load.
$this->syncPaths();
return true;
}
}
/**
* Method to set some field XML elements to the form definition. If the replace flag is set then
* the fields will be set whether they already exists or not. If it isn't set, then the fields
* will not be replaced if they already exist.
*
* @param \SimpleXMLElement[] &$elements The array of XML element object representations of the form fields.
* @param string $group The optional dot-separated form group path on which to set the fields.
* @param boolean $replace True to replace existing fields if they already exist.
* @param string $fieldset The name of the fieldset we are adding the field to.
*
* @return boolean True on success.
*
* @since 1.7.0
* @throws \UnexpectedValueException
*/
public function setFields(&$elements, $group = null, $replace = true, $fieldset = 'default')
{
// Make sure there is a valid Form XML document.
if (!($this->xml instanceof \SimpleXMLElement)) {
throw new \UnexpectedValueException(sprintf('%s::%s `xml` is not an instance of SimpleXMLElement', \get_class($this), __METHOD__));
}
// Make sure the elements to set are valid.
foreach ($elements as $element) {
if (!($element instanceof \SimpleXMLElement)) {
throw new \UnexpectedValueException(sprintf('%s::%s `xml` is not an instance of SimpleXMLElement', \get_class($this), __METHOD__));
}
}
// Set the fields.
$return = true;
foreach ($elements as $element) {
if (!$this->setField($element, $group, $replace, $fieldset)) {
$return = false;
}
}
// Synchronize any paths found in the load.
$this->syncPaths();
return $return;
}
/**
* Method to set the value of a field. If the field does not exist in the form then the method
* will return false.
*
* @param string $name The name of the field for which to set the value.
* @param string $group The optional dot-separated form group path on which to find the field.
* @param mixed $value The value to set for the field.
*
* @return boolean True on success.
*
* @since 1.7.0
*/
public function setValue($name, $group = null, $value = null)
{
// If the field does not exist return false.
if (!$this->findField($name, $group)) {
return false;
}
// If a group is set use it.
if ($group) {
$this->data->set($group . '.' . $name, $value);
} else {
$this->data->set($name, $value);
}
return true;
}
/**
* Method to process the form data.
*
* @param array $data An array of field values to filter.
* @param string $group The dot-separated form group path on which to filter the fields.
*
* @return mixed Array or false.
*
* @since 4.0.0
*/
public function process($data, $group = null)
{
$data = $this->filter($data, $group);
$valid = $this->validate($data, $group);
if (!$valid) {
return $valid;
}
return $this->postProcess($data, $group);
}
/**
* Method to filter the form data.
*
* @param array $data An array of field values to filter.
* @param string $group The dot-separated form group path on which to filter the fields.
*
* @return mixed Array or false.
*
* @since 4.0.0
*/
public function filter($data, $group = null)
{
// Make sure there is a valid Form XML document.
if (!($this->xml instanceof \SimpleXMLElement)) {
throw new \UnexpectedValueException(sprintf('%s::%s `xml` is not an instance of SimpleXMLElement', \get_class($this), __METHOD__));
}
$input = new Registry($data);
$output = new Registry();
// Get the fields for which to filter the data.
$fields = $this->findFieldsByGroup($group);
if (!$fields) {
// PANIC!
return false;
}
// Filter the fields.
foreach ($fields as $field) {
$name = (string) $field['name'];
// Get the field groups for the element.
$attrs = $field->xpath('ancestor::fields[@name]/@name');
$groups = array_map('strval', $attrs ?: []);
$attrGroup = implode('.', $groups);
$key = $attrGroup ? $attrGroup . '.' . $name : $name;
// Filter the value if it exists.
if ($input->exists($key)) {
$fieldObj = $this->loadField($field, $group);
// Only set into the output if the field was supposed to render on the page (i.e. setup returned true)
if ($fieldObj) {
$output->set($key, $fieldObj->filter($input->get($key, (string) $field['default']), $group, $input));
}
}
}
return $output->toArray();
}
/**
* Method to validate form data.
*
* Validation warnings will be pushed into Form::$errors and should be
* retrieved with Form::getErrors() when validate returns boolean false.
*
* @param array $data An array of field values to validate.
* @param string $group The optional dot-separated form group path on which to filter the
* fields to be validated.
*
* @return boolean True on success.
*
* @since 1.7.0
*/
public function validate($data, $group = null)
{
// Make sure there is a valid Form XML document.
if (!($this->xml instanceof \SimpleXMLElement)) {
throw new \UnexpectedValueException(sprintf('%s::%s `xml` is not an instance of SimpleXMLElement', \get_class($this), __METHOD__));
}
$return = true;
// Create an input registry object from the data to validate.
$input = new Registry($data);
// Get the fields for which to validate the data.
$fields = $this->findFieldsByGroup($group);
if (!$fields) {
// PANIC!
return false;
}
// Validate the fields.
foreach ($fields as $field) {
$name = (string) $field['name'];
// Define field name for messages
if ($field['label']) {
$fieldLabel = $field['label'];
// Try to translate label if not set to false
$translate = (string) $field['translateLabel'];
if (!($translate === 'false' || $translate === 'off' || $translate === '0')) {
$fieldLabel = Text::_($fieldLabel);
}
} else {
$fieldLabel = Text::_($name);
}
$disabled = ((string) $field['disabled'] === 'true' || (string) $field['disabled'] === 'disabled');
$fieldExistsInRequestData = $input->exists($name) || $input->exists($group . '.' . $name);
// If the field is disabled but it is passed in the request this is invalid as disabled fields are not added to the request
if ($disabled && $fieldExistsInRequestData) {
throw new \RuntimeException(Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', $fieldLabel));
}
// Get the field groups for the element.
$attrs = $field->xpath('ancestor::fields[@name]/@name');
$groups = array_map('strval', $attrs ?: []);
$attrGroup = implode('.', $groups);
$key = $attrGroup ? $attrGroup . '.' . $name : $name;
$fieldObj = $this->loadField($field, $attrGroup);
if ($fieldObj) {
$valid = $fieldObj->validate($input->get($key), $attrGroup, $input);
// Check for an error.
if ($valid instanceof \Exception) {
$this->errors[] = $valid;
$return = false;
}
} elseif ($input->exists($key)) {
// The field returned false from setup and shouldn't be included in the page body - yet we received
// a value for it. This is probably some sort of injection attack and should be rejected
$this->errors[] = new \RuntimeException(Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', $key));
$return = false;
}
}
return $return;
}
/**
* Method to post-process form data.
*
* @param array $data An array of field values to post-process.
* @param string $group The optional dot-separated form group path on which to filter the
* fields to be validated.
*
* @return mixed Array or false.
*
* @since 4.0.0
*/
public function postProcess($data, $group = null)
{
// Make sure there is a valid SimpleXMLElement
if (!($this->xml instanceof \SimpleXMLElement)) {
throw new \UnexpectedValueException(sprintf('%s::%s `xml` is not an instance of SimpleXMLElement', \get_class($this), __METHOD__));
}
$input = new Registry($data);
$output = new Registry();
// Get the fields for which to postProcess the data.
$fields = $this->findFieldsByGroup($group);
if (!$fields) {
// PANIC!
return false;
}
// Filter the fields.
foreach ($fields as $field) {
$name = (string) $field['name'];
// Get the field groups for the element.
$attrs = $field->xpath('ancestor::fields[@name]/@name');
$groups = array_map('strval', $attrs ?: []);
$attrGroup = implode('.', $groups);
$key = $attrGroup ? $attrGroup . '.' . $name : $name;
// Filter the value if it exists.
if ($input->exists($key)) {
$fieldobj = $this->loadField($field, $group);
$output->set($key, $fieldobj->postProcess($input->get($key, (string) $field['default']), $group, $input));
}
}
return $output->toArray();
}
/**
* Method to get a form field represented as an XML element object.
*
* @param string $name The name of the form field.
* @param string $group The optional dot-separated form group path on which to find the field.
*
* @return \SimpleXMLElement|boolean The XML element object for the field or boolean false on error.
*
* @since 1.7.0
*/
protected function findField($name, $group = null)
{
$element = false;
$fields = [];
// Make sure there is a valid Form XML document.
if (!($this->xml instanceof \SimpleXMLElement)) {
throw new \UnexpectedValueException(sprintf('%s::%s `xml` is not an instance of SimpleXMLElement', \get_class($this), __METHOD__));
}
// Let's get the appropriate field element based on the method arguments.
if ($group) {
// Get the fields elements for a given group.
$elements = &$this->findGroup($group);
// Get all of the field elements with the correct name for the fields elements.
foreach ($elements as $el) {
// If there are matching field elements add them to the fields array.
if ($tmp = $el->xpath('descendant::field[@name="' . $name . '" and not(ancestor::field/form/*)]')) {
$fields = array_merge($fields, $tmp);
}
}
// Make sure something was found.
if (!$fields) {
return false;
}
// Use the first correct match in the given group.
$groupNames = explode('.', $group);
foreach ($fields as &$field) {
// Get the group names as strings for ancestor fields elements.
$attrs = $field->xpath('ancestor::fields[@name]/@name');
$names = array_map('strval', $attrs ?: []);
// If the field is in the exact group use it and break out of the loop.
if ($names == (array) $groupNames) {
$element = &$field;
break;
}
}
} else {
// Get an array of fields with the correct name.
$fields = $this->xml->xpath('//field[@name="' . $name . '" and not(ancestor::field/form/*)]');
// Make sure something was found.
if (!$fields) {
return false;
}
// Search through the fields for the right one.
foreach ($fields as &$field) {
// If we find an ancestor fields element with a group name then it isn't what we want.
if ($field->xpath('ancestor::fields[@name]')) {
continue;
} else {
// Found it!
$element = &$field;
break;
}
}
}
return $element;
}
/**
* Method to get an array of `<field>` elements from the form XML document which are in a specified fieldset by name.
*
* @param string $name The name of the fieldset.
*
* @return \SimpleXMLElement[]|boolean Boolean false on error or array of SimpleXMLElement objects.
*
* @since 1.7.0
*/
protected function &findFieldsByFieldset($name)
{
// Make sure there is a valid Form XML document.
if (!($this->xml instanceof \SimpleXMLElement)) {
throw new \UnexpectedValueException(sprintf('%s::%s `xml` is not an instance of SimpleXMLElement', \get_class($this), __METHOD__));
}
/*
* Get an array of <field /> elements that are underneath a <fieldset /> element
* with the appropriate name attribute, and also any <field /> elements with
* the appropriate fieldset attribute. To allow repeatable elements only fields
* which are not descendants of other fields are selected.
*/
$fields = $this->xml->xpath('(//fieldset[@name="' . $name . '"]//field | //field[@fieldset="' . $name . '"])[not(ancestor::field)]');
return $fields;
}
/**
* Method to get an array of `<field>` elements from the form XML document which are in a control group by name.
*
* @param mixed $group The optional dot-separated form group path on which to find the fields.
* Null will return all fields. False will return fields not in a group.
* @param boolean $nested True to also include fields in nested groups that are inside of the
* group for which to find fields.
*
* @return \SimpleXMLElement[]|boolean Boolean false on error or array of SimpleXMLElement objects.
*
* @since 1.7.0
*/
protected function &findFieldsByGroup($group = null, $nested = false)
{
$fields = [];
// Make sure there is a valid Form XML document.
if (!($this->xml instanceof \SimpleXMLElement)) {
throw new \UnexpectedValueException(sprintf('%s::%s `xml` is not an instance of SimpleXMLElement', \get_class($this), __METHOD__));
}
// Get only fields in a specific group?
if ($group) {
// Get the fields elements for a given group.
$elements = &$this->findGroup($group);
// Get all of the field elements for the fields elements.
foreach ($elements as $element) {
// If there are field elements add them to the return result.
if ($tmp = $element->xpath('descendant::field')) {
// If we also want fields in nested groups then just merge the arrays.
if ($nested) {
$fields = array_merge($fields, $tmp);
} else {
// If we want to exclude nested groups then we need to check each field.
$groupNames = explode('.', $group);
foreach ($tmp as $field) {
// Get the names of the groups that the field is in.
$attrs = $field->xpath('ancestor::fields[@name]/@name');
$names = array_map('strval', $attrs ?: []);
// If the field is in the specific group then add it to the return list.
if ($names == (array) $groupNames) {
$fields = array_merge($fields, [$field]);
}
}
}
}
}
} elseif ($group === false) {
// Get only field elements not in a group.
$fields = $this->xml->xpath('descendant::fields[not(@name)]/field | descendant::fields[not(@name)]/fieldset/field ');
} else {
// Get an array of all the <field /> elements.
$fields = $this->xml->xpath('//field[not(ancestor::field/form/*)]');
}
return $fields;
}
/**
* Method to get a form field group represented as an XML element object.
*
* @param string $group The dot-separated form group path on which to find the group.
*
* @return \SimpleXMLElement[]|boolean An array of XML element objects for the group or boolean false on error.
*
* @since 1.7.0
*/
protected function &findGroup($group)
{
$groups = [];
$tmp = [];
// Make sure there is a valid Form XML document.
if (!($this->xml instanceof \SimpleXMLElement)) {
throw new \UnexpectedValueException(sprintf('%s::%s `xml` is not an instance of SimpleXMLElement', \get_class($this), __METHOD__));
}
// Make sure there is actually a group to find.
$group = explode('.', $group);
if (count($group)) {
// Get any fields elements with the correct group name.
$elements = $this->xml->xpath('//fields[@name="' . (string) $group[0] . '" and not(ancestor::field/form/*)]');
// Check to make sure that there are no parent groups for each element.
foreach ($elements as $element) {
if (!$element->xpath('ancestor::fields[@name]')) {
$tmp[] = $element;
}
}
// Iterate through the nested groups to find any matching form field groups.
for ($i = 1, $n = \count($group); $i < $n; $i++) {
// Initialise some loop variables.
$validNames = \array_slice($group, 0, $i + 1);
$current = $tmp;
$tmp = [];
// Check to make sure that there are no parent groups for each element.
foreach ($current as $element) {
// Get any fields elements with the correct group name.
$children = $element->xpath('descendant::fields[@name="' . (string) $group[$i] . '"]');
// For the found fields elements validate that they are in the correct groups.
foreach ($children as $fields) {
// Get the group names as strings for ancestor fields elements.
$attrs = $fields->xpath('ancestor-or-self::fields[@name]/@name');
$names = array_map('strval', $attrs ?: []);
// If the group names for the fields element match the valid names at this
// level add the fields element.
if ($validNames == $names) {
$tmp[] = $fields;
}
}
}
}
// Only include valid XML objects.
foreach ($tmp as $element) {
if ($element instanceof \SimpleXMLElement) {
$groups[] = $element;
}
}
}
return $groups;
}
/**
* Method to load, setup and return a FormField object based on field data.
*
* @param string|\SimpleXMLElement $element The XML element object representation of the form field.
* @param string $group The optional dot-separated form group path on which to find the field.
* @param mixed $value The optional value to use as the default for the field.
*
* @return FormField|boolean The FormField object for the field or boolean false on error.
*
* @since 1.7.0
*/
protected function loadField($element, $group = null, $value = null)
{
// Make sure there is a valid SimpleXMLElement.
if (!($element instanceof \SimpleXMLElement)) {
throw new \UnexpectedValueException(sprintf('%s::%s `xml` is not an instance of SimpleXMLElement', \get_class($this), __METHOD__));
}
// Get the field type.
$type = $element['type'] ? (string) $element['type'] : 'text';
// Load the FormField object for the field.
$field = FormHelper::loadFieldType($type);
if ($field instanceof DatabaseAwareInterface) {
try {
$field->setDatabase($this->getDatabase());
} catch (DatabaseNotFoundException $e) {
@trigger_error(sprintf('Database must be set, this will not be caught anymore in 5.0.'), E_USER_DEPRECATED);
$field->setDatabase(Factory::getContainer()->get(DatabaseInterface::class));
}
}
// If the object could not be loaded, get a text field object.
if ($field === false) {
$field = FormHelper::loadFieldType('text');
}
/*
* Get the value for the form field if not set.
* Default to the translated version of the 'default' attribute
* if 'translate_default' attribute if set to 'true' or '1'
* else the value of the 'default' attribute for the field.
*/
if ($value === null) {
$default = (string) ($element['default'] ? $element['default'] : $element->default);
if (($translate = $element['translate_default']) && ((string) $translate === 'true' || (string) $translate === '1')) {
$lang = Factory::getLanguage();
if ($lang->hasKey($default)) {
$debug = $lang->setDebug(false);
$default = Text::_($default);
$lang->setDebug($debug);
} else {
$default = Text::_($default);
}
}
$value = $this->getValue((string) $element['name'], $group, $default);
}
// Setup the FormField object.
$field->setForm($this);
if ($field->setup($element, $value, $group)) {
return $field;
} else {
return false;
}
}
/**
* Method to synchronize any field, form or rule paths contained in the XML document.
*
* @return boolean True on success.
*
* @since 1.7.0
* @todo Maybe we should receive all addXXXpaths attributes at once?
*/
protected function syncPaths()
{
// Make sure there is a valid Form XML document.
if (!($this->xml instanceof \SimpleXMLElement)) {
throw new \UnexpectedValueException(sprintf('%s::%s `xml` is not an instance of SimpleXMLElement', \get_class($this), __METHOD__));
}
// Get any addfieldpath attributes from the form definition.
$paths = $this->xml->xpath('//*[@addfieldpath]/@addfieldpath');
$paths = array_map('strval', $paths ?: []);
// Add the field paths.
foreach ($paths as $path) {
$path = JPATH_ROOT . '/' . ltrim($path, '/\\');
self::addFieldPath($path);
}
// Get any addformpath attributes from the form definition.
$paths = $this->xml->xpath('//*[@addformpath]/@addformpath');
$paths = array_map('strval', $paths ?: []);
// Add the form paths.
foreach ($paths as $path) {
$path = JPATH_ROOT . '/' . ltrim($path, '/\\');
self::addFormPath($path);
}
// Get any addrulepath attributes from the form definition.
$paths = $this->xml->xpath('//*[@addrulepath]/@addrulepath');
$paths = array_map('strval', $paths ?: []);
// Add the rule paths.
foreach ($paths as $path) {
$path = JPATH_ROOT . '/' . ltrim($path, '/\\');
self::addRulePath($path);
}
// Get any addrulepath attributes from the form definition.
$paths = $this->xml->xpath('//*[@addfilterpath]/@addfilterpath');
$paths = array_map('strval', $paths ?: []);
// Add the rule paths.
foreach ($paths as $path) {
$path = JPATH_ROOT . '/' . ltrim($path, '/\\');
self::addFilterPath($path);
}
// Get any addfieldprefix attributes from the form definition.
$prefixes = $this->xml->xpath('//*[@addfieldprefix]/@addfieldprefix');
$prefixes = array_map('strval', $prefixes ?: []);
// Add the field prefixes.
foreach ($prefixes as $prefix) {
FormHelper::addFieldPrefix($prefix);
}
// Get any addformprefix attributes from the form definition.
$prefixes = $this->xml->xpath('//*[@addformprefix]/@addformprefix');
$prefixes = array_map('strval', $prefixes ?: []);
// Add the field prefixes.
foreach ($prefixes as $prefix) {
FormHelper::addFormPrefix($prefix);
}
// Get any addruleprefix attributes from the form definition.
$prefixes = $this->xml->xpath('//*[@addruleprefix]/@addruleprefix');
$prefixes = array_map('strval', $prefixes ?: []);
// Add the field prefixes.
foreach ($prefixes as $prefix) {
FormHelper::addRulePrefix($prefix);
}
// Get any addruleprefix attributes from the form definition.
$prefixes = $this->xml->xpath('//*[@addfilterprefix]/@addfilterprefix');
$prefixes = array_map('strval', $prefixes ?: []);
// Add the field prefixes.
foreach ($prefixes as $prefix) {
FormHelper::addFilterPrefix($prefix);
}
return true;
}
/**
* Proxy for {@link FormHelper::addFieldPath()}.
*
* @param string|string[] $new A path or array of paths to add.
*
* @return string[] The list of paths that have been added.
*
* @since 1.7.0
*/
public static function addFieldPath($new = null)
{
return FormHelper::addFieldPath($new);
}
/**
* Proxy for FormHelper::addFormPath().
*
* @param string|string[] $new A path or array of paths to add.
*
* @return string[] The list of paths that have been added.
*
* @see FormHelper::addFormPath()
* @since 1.7.0
*/
public static function addFormPath($new = null)
{
return FormHelper::addFormPath($new);
}
/**
* Proxy for FormHelper::addRulePath().
*
* @param string|string[] $new A path or array of paths to add.
*
* @return string[] The list of paths that have been added.
*
* @see FormHelper::addRulePath()
* @since 1.7.0
*/
public static function addRulePath($new = null)
{
return FormHelper::addRulePath($new);
}
/**
* Proxy for FormHelper::addFilterPath().
*
* @param string|string[] $new A path or array of paths to add.
*
* @return string[] The list of paths that have been added.
*
* @see FormHelper::addFilterPath()
* @since 4.0.0
*/
public static function addFilterPath($new = null)
{
return FormHelper::addFilterPath($new);
}
/**
* Method to get an instance of a form.
*
* @param string $name The name of the form.
* @param string $data The name of an XML file or string to load as the form definition.
* @param array $options An array of form options.
* @param boolean $replace Flag to toggle whether form fields should be replaced if a field
* already exists with the same group/name.
* @param string|boolean $xpath An optional xpath to search for the fields.
*
* @return Form Form instance.
*
* @since 1.7.0
*
* @deprecated 4.3 will be removed in 6.0
* Use the FormFactory service from the container
* Example: Factory::getContainer()->get(FormFactoryInterface::class)->createForm($name, $options);
*
* @throws \InvalidArgumentException if no data provided.
* @throws \RuntimeException if the form could not be loaded.
*/
public static function getInstance($name, $data = null, $options = [], $replace = true, $xpath = false)
{
// Reference to array with form instances
$forms = &self::$forms;
// Only instantiate the form if it does not already exist.
if (!isset($forms[$name])) {
$data = trim($data);
if (empty($data)) {
throw new \InvalidArgumentException(sprintf('%1$s(%2$s, *%3$s*)', __METHOD__, $name, \gettype($data)));
}
// Instantiate the form.
$forms[$name] = Factory::getContainer()->get(FormFactoryInterface::class)->createForm($name, $options);
// Load the data.
if (substr($data, 0, 1) === '<') {
if ($forms[$name]->load($data, $replace, $xpath) == false) {
throw new \RuntimeException(sprintf('%s() could not load form', __METHOD__));
}
} else {
if ($forms[$name]->loadFile($data, $replace, $xpath) == false) {
throw new \RuntimeException(sprintf('%s() could not load file', __METHOD__));
}
}
}
return $forms[$name];
}
/**
* Adds a new child SimpleXMLElement node to the source.
*
* @param \SimpleXMLElement $source The source element on which to append.
* @param \SimpleXMLElement $new The new element to append.
*
* @return void
*
* @since 1.7.0
*/
protected static function addNode(\SimpleXMLElement $source, \SimpleXMLElement $new)
{
// Add the new child node.
$node = $source->addChild($new->getName(), htmlspecialchars(trim($new)));
// Add the attributes of the child node.
foreach ($new->attributes() as $name => $value) {
$node->addAttribute($name, $value);
}
// Add any children of the new node.
foreach ($new->children() as $child) {
self::addNode($node, $child);
}
}
/**
* Update the attributes of a child node
*
* @param \SimpleXMLElement $source The source element on which to append the attributes
* @param \SimpleXMLElement $new The new element to append
*
* @return void
*
* @since 1.7.0
*/
protected static function mergeNode(\SimpleXMLElement $source, \SimpleXMLElement $new)
{
// Update the attributes of the child node.
foreach ($new->attributes() as $name => $value) {
if (isset($source[$name])) {
$source[$name] = (string) $value;
} else {
$source->addAttribute($name, $value);
}
}
}
/**
* Merges new elements into a source `<fields>` element.
*
* @param \SimpleXMLElement $source The source element.
* @param \SimpleXMLElement $new The new element to merge.
*
* @return void
*
* @since 1.7.0
*/
protected static function mergeNodes(\SimpleXMLElement $source, \SimpleXMLElement $new)
{
// The assumption is that the inputs are at the same relative level.
// So we just have to scan the children and deal with them.
// Update the attributes of the child node.
foreach ($new->attributes() as $name => $value) {
if (isset($source[$name])) {
$source[$name] = (string) $value;
} else {
$source->addAttribute($name, $value);
}
}
foreach ($new->children() as $child) {
$type = $child->getName();
$name = $child['name'];
// Does this node exist?
$fields = $source->xpath($type . '[@name="' . $name . '"]');
if (empty($fields)) {
// This node does not exist, so add it.
self::addNode($source, $child);
} else {
// This node does exist.
switch ($type) {
case 'field':
self::mergeNode($fields[0], $child);
break;
default:
self::mergeNodes($fields[0], $child);
break;
}
}
}
}
/**
* Returns the value of an attribute of the form itself
*
* @param string $name Name of the attribute to get
* @param mixed $default Optional value to return if attribute not found
*
* @return mixed Value of the attribute / default
*
* @since 3.2
*/
public function getAttribute($name, $default = null)
{
if ($this->xml instanceof \SimpleXMLElement) {
$value = $this->xml->attributes()->$name;
if ($value !== null) {
return (string) $value;
}
}
return $default;
}
/**
* Getter for the form data
*
* @return Registry Object with the data
*
* @since 3.2
*/
public function getData()
{
return $this->data;
}
/**
* Method to get the XML form object
*
* @return \SimpleXMLElement The form XML object
*
* @since 3.2
*/
public function getXml()
{
return $this->xml;
}
/**
* Method to get a form field represented as an XML element object.
*
* @param string $name The name of the form field.
* @param string $group The optional dot-separated form group path on which to find the field.
*
* @return \SimpleXMLElement|boolean The XML element object for the field or boolean false on error.
*
* @since 3.7.0
*/
public function getFieldXml($name, $group = null)
{
return $this->findField($name, $group);
}
}