[ Index ]

PHP Cross Reference of Joomla 4.2.2 documentation

title

Body

[close]

/libraries/src/Form/ -> Form.php (source)

   1  <?php
   2  
   3  /**
   4   * Joomla! Content Management System
   5   *
   6   * @copyright  (C) 2009 Open Source Matters, Inc. <https://www.joomla.org>
   7   * @license    GNU General Public License version 2 or later; see LICENSE.txt
   8   */
   9  
  10  namespace Joomla\CMS\Form;
  11  
  12  use Joomla\CMS\Factory;
  13  use Joomla\CMS\Filesystem\Path;
  14  use Joomla\CMS\Language\Text;
  15  use Joomla\CMS\Object\CMSObject;
  16  use Joomla\Database\DatabaseAwareInterface;
  17  use Joomla\Database\DatabaseAwareTrait;
  18  use Joomla\Database\DatabaseInterface;
  19  use Joomla\Database\Exception\DatabaseNotFoundException;
  20  use Joomla\Registry\Registry;
  21  use Joomla\Utilities\ArrayHelper;
  22  
  23  // phpcs:disable PSR1.Files.SideEffects
  24  \defined('JPATH_PLATFORM') or die;
  25  // phpcs:enable PSR1.Files.SideEffects
  26  
  27  /**
  28   * Form Class for the Joomla Platform.
  29   *
  30   * This class implements a robust API for constructing, populating, filtering, and validating forms.
  31   * It uses XML definitions to construct form fields and a variety of field and rule classes to
  32   * render and validate the form.
  33   *
  34   * @link   https://www.w3.org/TR/html4/interact/forms.html
  35   * @link   https://html.spec.whatwg.org/multipage/forms.html
  36   * @since  1.7.0
  37   */
  38  class Form
  39  {
  40      use DatabaseAwareTrait;
  41  
  42      /**
  43       * The Registry data store for form fields during display.
  44       *
  45       * @var    Registry
  46       * @since  1.7.0
  47       */
  48      protected $data;
  49  
  50      /**
  51       * The form object errors array.
  52       *
  53       * @var    array
  54       * @since  1.7.0
  55       */
  56      protected $errors = [];
  57  
  58      /**
  59       * The name of the form instance.
  60       *
  61       * @var    string
  62       * @since  1.7.0
  63       */
  64      protected $name;
  65  
  66      /**
  67       * The form object options for use in rendering and validation.
  68       *
  69       * @var    array
  70       * @since  1.7.0
  71       */
  72      protected $options = [];
  73  
  74      /**
  75       * The form XML definition.
  76       *
  77       * @var    \SimpleXMLElement
  78       * @since  1.7.0
  79       */
  80      protected $xml;
  81  
  82      /**
  83       * Form instances.
  84       *
  85       * @var    Form[]
  86       * @since  1.7.0
  87       */
  88      protected static $forms = [];
  89  
  90      /**
  91       * Allows extensions to implement repeating elements
  92       *
  93       * @var    boolean
  94       * @since  3.2
  95       */
  96      public $repeat = false;
  97  
  98      /**
  99       * Method to instantiate the form object.
 100       *
 101       * @param   string  $name     The name of the form.
 102       * @param   array   $options  An array of form options.
 103       *
 104       * @since   1.7.0
 105       */
 106      public function __construct($name, array $options = [])
 107      {
 108          // Set the name for the form.
 109          $this->name = $name;
 110  
 111          // Initialise the Registry data.
 112          $this->data = new Registry();
 113  
 114          // Set the options if specified.
 115          $this->options['control'] = $options['control'] ?? false;
 116      }
 117  
 118      /**
 119       * Method to bind data to the form.
 120       *
 121       * @param   mixed  $data  An array or object of data to bind to the form.
 122       *
 123       * @return  boolean  True on success.
 124       *
 125       * @since   1.7.0
 126       */
 127      public function bind($data)
 128      {
 129          // Make sure there is a valid Form XML document.
 130          if (!($this->xml instanceof \SimpleXMLElement)) {
 131              throw new \UnexpectedValueException(sprintf('%s::%s `xml` is not an instance of SimpleXMLElement', \get_class($this), __METHOD__));
 132          }
 133  
 134          // The data must be an object or array.
 135          if (!\is_object($data) && !\is_array($data)) {
 136              return false;
 137          }
 138  
 139          $this->bindLevel(null, $data);
 140  
 141          return true;
 142      }
 143  
 144      /**
 145       * Method to bind data to the form for the group level.
 146       *
 147       * @param   string  $group  The dot-separated form group path on which to bind the data.
 148       * @param   mixed   $data   An array or object of data to bind to the form for the group level.
 149       *
 150       * @return  void
 151       *
 152       * @since   1.7.0
 153       */
 154      protected function bindLevel($group, $data)
 155      {
 156          // Ensure the input data is an array.
 157          if (\is_object($data)) {
 158              if ($data instanceof Registry) {
 159                  // Handle a Registry.
 160                  $data = $data->toArray();
 161              } elseif ($data instanceof CMSObject) {
 162                  // Handle a CMSObject.
 163                  $data = $data->getProperties();
 164              } else {
 165                  // Handle other types of objects.
 166                  $data = (array) $data;
 167              }
 168          }
 169  
 170          // Process the input data.
 171          foreach ($data as $k => $v) {
 172              $level = $group ? $group . '.' . $k : $k;
 173  
 174              if ($this->findField($k, $group)) {
 175                  // If the field exists set the value.
 176                  $this->data->set($level, $v);
 177              } elseif (\is_object($v) || ArrayHelper::isAssociative($v)) {
 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       * Return all errors, if any.
 186       *
 187       * @return  array  Array of error messages or RuntimeException objects.
 188       *
 189       * @since   1.7.0
 190       */
 191      public function getErrors()
 192      {
 193          return $this->errors;
 194      }
 195  
 196      /**
 197       * Method to get a form field represented as a FormField object.
 198       *
 199       * @param   string  $name   The name of the form field.
 200       * @param   string  $group  The optional dot-separated form group path on which to find the field.
 201       * @param   mixed   $value  The optional value to use as the default for the field.
 202       *
 203       * @return  FormField|boolean  The FormField object for the field or boolean false on error.
 204       *
 205       * @since   1.7.0
 206       */
 207      public function getField($name, $group = null, $value = null)
 208      {
 209          // Make sure there is a valid Form XML document.
 210          if (!($this->xml instanceof \SimpleXMLElement)) {
 211              throw new \UnexpectedValueException(sprintf('%s::%s `xml` is not an instance of SimpleXMLElement', \get_class($this), __METHOD__));
 212          }
 213  
 214          // Attempt to find the field by name and group.
 215          $element = $this->findField($name, $group);
 216  
 217          // If the field element was not found return false.
 218          if (!$element) {
 219              return false;
 220          }
 221  
 222          return $this->loadField($element, $group, $value);
 223      }
 224  
 225      /**
 226       * Method to get an attribute value from a field XML element.  If the attribute doesn't exist or
 227       * is null then the optional default value will be used.
 228       *
 229       * @param   string  $name       The name of the form field for which to get the attribute value.
 230       * @param   string  $attribute  The name of the attribute for which to get a value.
 231       * @param   mixed   $default    The optional default value to use if no attribute value exists.
 232       * @param   string  $group      The optional dot-separated form group path on which to find the field.
 233       *
 234       * @return  mixed  The attribute value for the field.
 235       *
 236       * @since   1.7.0
 237       * @throws  \UnexpectedValueException
 238       */
 239      public function getFieldAttribute($name, $attribute, $default = null, $group = null)
 240      {
 241          // Make sure there is a valid Form XML document.
 242          if (!($this->xml instanceof \SimpleXMLElement)) {
 243              throw new \UnexpectedValueException(sprintf('%s::%s `xml` is not an instance of SimpleXMLElement', \get_class($this), __METHOD__));
 244          }
 245  
 246          // Find the form field element from the definition.
 247          $element = $this->findField($name, $group);
 248  
 249          // If the element exists and the attribute exists for the field return the attribute value.
 250          if (($element instanceof \SimpleXMLElement) && \strlen((string) $element[$attribute])) {
 251              return (string) $element[$attribute];
 252          } else {
 253              // Otherwise return the given default value.
 254              return $default;
 255          }
 256      }
 257  
 258      /**
 259       * Method to get an array of FormField objects in a given fieldset by name.  If no name is
 260       * given then all fields are returned.
 261       *
 262       * @param   string  $set  The optional name of the fieldset.
 263       *
 264       * @return  FormField[]  The array of FormField objects in the fieldset.
 265       *
 266       * @since   1.7.0
 267       */
 268      public function getFieldset($set = null)
 269      {
 270          $fields = [];
 271  
 272          // Get all of the field elements in the fieldset.
 273          if ($set) {
 274              $elements = $this->findFieldsByFieldset($set);
 275          } else {
 276              // Get all fields.
 277              $elements = $this->findFieldsByGroup();
 278          }
 279  
 280          // If no field elements were found return empty.
 281          if (empty($elements)) {
 282              return $fields;
 283          }
 284  
 285          // Build the result array from the found field elements.
 286          foreach ($elements as $element) {
 287              // Get the field groups for the element.
 288              $attrs = $element->xpath('ancestor::fields[@name]/@name');
 289              $groups = array_map('strval', $attrs ?: []);
 290              $group = implode('.', $groups);
 291  
 292              // If the field is successfully loaded add it to the result array.
 293              if ($field = $this->loadField($element, $group)) {
 294                  $fields[$field->id] = $field;
 295              }
 296          }
 297  
 298          return $fields;
 299      }
 300  
 301      /**
 302       * Method to get an array of fieldset objects optionally filtered over a given field group.
 303       *
 304       * @param   string  $group  The dot-separated form group path on which to filter the fieldsets.
 305       *
 306       * @return  array  The array of fieldset objects.
 307       *
 308       * @since   1.7.0
 309       */
 310      public function getFieldsets($group = null)
 311      {
 312          $fieldsets = [];
 313          $sets = [];
 314  
 315          // Make sure there is a valid Form XML document.
 316          if (!($this->xml instanceof \SimpleXMLElement)) {
 317              throw new \UnexpectedValueException(sprintf('%s::%s `xml` is not an instance of SimpleXMLElement', \get_class($this), __METHOD__));
 318          }
 319  
 320          if ($group) {
 321              // Get the fields elements for a given group.
 322              $elements = &$this->findGroup($group);
 323  
 324              foreach ($elements as &$element) {
 325                  // Get an array of <fieldset /> elements and fieldset attributes within the fields element.
 326                  if ($tmp = $element->xpath('descendant::fieldset[@name] | descendant::field[@fieldset]/@fieldset')) {
 327                      $sets = array_merge($sets, (array) $tmp);
 328                  }
 329              }
 330          } else {
 331              // Get an array of <fieldset /> elements and fieldset attributes.
 332              $sets = $this->xml->xpath('//fieldset[@name and not(ancestor::field/form/*)] | //field[@fieldset and not(ancestor::field/form/*)]/@fieldset');
 333          }
 334  
 335          // If no fieldsets are found return empty.
 336          if (empty($sets)) {
 337              return $fieldsets;
 338          }
 339  
 340          // Process each found fieldset.
 341          foreach ($sets as $set) {
 342              if ((string) $set['hidden'] === 'true') {
 343                  continue;
 344              }
 345  
 346              // Are we dealing with a fieldset element?
 347              if ((string) $set['name']) {
 348                  // Only create it if it doesn't already exist.
 349                  if (empty($fieldsets[(string) $set['name']])) {
 350                      // Build the fieldset object.
 351                      $fieldset = (object) array('name' => '', 'label' => '', 'description' => '');
 352  
 353                      foreach ($set->attributes() as $name => $value) {
 354                          $fieldset->$name = (string) $value;
 355                      }
 356  
 357                      // Add the fieldset object to the list.
 358                      $fieldsets[$fieldset->name] = $fieldset;
 359                  }
 360              } else {
 361                  // Must be dealing with a fieldset attribute.
 362                  // Only create it if it doesn't already exist.
 363                  if (empty($fieldsets[(string) $set])) {
 364                      // Attempt to get the fieldset element for data (throughout the entire form document).
 365                      $tmp = $this->xml->xpath('//fieldset[@name="' . (string) $set . '"]');
 366  
 367                      // If no element was found, build a very simple fieldset object.
 368                      if (empty($tmp)) {
 369                          $fieldset = (object) array('name' => (string) $set, 'label' => '', 'description' => '');
 370                      } else {
 371                          // Build the fieldset object from the element.
 372                          $fieldset = (object) array('name' => '', 'label' => '', 'description' => '');
 373  
 374                          foreach ($tmp[0]->attributes() as $name => $value) {
 375                              $fieldset->$name = (string) $value;
 376                          }
 377                      }
 378  
 379                      // Add the fieldset object to the list.
 380                      $fieldsets[$fieldset->name] = $fieldset;
 381                  }
 382              }
 383          }
 384  
 385          return $fieldsets;
 386      }
 387  
 388      /**
 389       * Method to get the form control. This string serves as a container for all form fields. For
 390       * example, if there is a field named 'foo' and a field named 'bar' and the form control is
 391       * empty the fields will be rendered like: `<input name="foo" />` and `<input name="bar" />`.  If
 392       * the form control is set to 'joomla' however, the fields would be rendered like:
 393       * `<input name="joomla[foo]" />` and `<input name="joomla[bar]" />`.
 394       *
 395       * @return  string  The form control string.
 396       *
 397       * @since   1.7.0
 398       */
 399      public function getFormControl()
 400      {
 401          return (string) $this->options['control'];
 402      }
 403  
 404      /**
 405       * Method to get an array of FormField objects in a given field group by name.
 406       *
 407       * @param   string   $group   The dot-separated form group path for which to get the form fields.
 408       * @param   boolean  $nested  True to also include fields in nested groups that are inside of the
 409       *                            group for which to find fields.
 410       *
 411       * @return  FormField[]  The array of FormField objects in the field group.
 412       *
 413       * @since   1.7.0
 414       */
 415      public function getGroup($group, $nested = false)
 416      {
 417          $fields = [];
 418  
 419          // Get all of the field elements in the field group.
 420          $elements = $this->findFieldsByGroup($group, $nested);
 421  
 422          // If no field elements were found return empty.
 423          if (empty($elements)) {
 424              return $fields;
 425          }
 426  
 427          // Build the result array from the found field elements.
 428          foreach ($elements as $element) {
 429              // Get the field groups for the element.
 430              $attrs  = $element->xpath('ancestor::fields[@name]/@name');
 431              $groups = array_map('strval', $attrs ?: []);
 432              $group  = implode('.', $groups);
 433  
 434              // If the field is successfully loaded add it to the result array.
 435              if ($field = $this->loadField($element, $group)) {
 436                  $fields[$field->id] = $field;
 437              }
 438          }
 439  
 440          return $fields;
 441      }
 442  
 443      /**
 444       * Method to get a form field markup for the field input.
 445       *
 446       * @param   string  $name   The name of the form field.
 447       * @param   string  $group  The optional dot-separated form group path on which to find the field.
 448       * @param   mixed   $value  The optional value to use as the default for the field.
 449       *
 450       * @return  string  The form field markup.
 451       *
 452       * @since   1.7.0
 453       */
 454      public function getInput($name, $group = null, $value = null)
 455      {
 456          // Attempt to get the form field.
 457          if ($field = $this->getField($name, $group, $value)) {
 458              return $field->input;
 459          }
 460  
 461          return '';
 462      }
 463  
 464      /**
 465       * Method to get the label for a field input.
 466       *
 467       * @param   string  $name   The name of the form field.
 468       * @param   string  $group  The optional dot-separated form group path on which to find the field.
 469       *
 470       * @return  string  The form field label.
 471       *
 472       * @since   1.7.0
 473       */
 474      public function getLabel($name, $group = null)
 475      {
 476          // Attempt to get the form field.
 477          if ($field = $this->getField($name, $group)) {
 478              return $field->label;
 479          }
 480  
 481          return '';
 482      }
 483  
 484      /**
 485       * Method to get the form name.
 486       *
 487       * @return  string  The name of the form.
 488       *
 489       * @since   1.7.0
 490       */
 491      public function getName()
 492      {
 493          return $this->name;
 494      }
 495  
 496      /**
 497       * Method to get the value of a field.
 498       *
 499       * @param   string  $name     The name of the field for which to get the value.
 500       * @param   string  $group    The optional dot-separated form group path on which to get the value.
 501       * @param   mixed   $default  The optional default value of the field value is empty.
 502       *
 503       * @return  mixed  The value of the field or the default value if empty.
 504       *
 505       * @since   1.7.0
 506       */
 507      public function getValue($name, $group = null, $default = null)
 508      {
 509          // If a group is set use it.
 510          if ($group) {
 511              $return = $this->data->get($group . '.' . $name, $default);
 512          } else {
 513              $return = $this->data->get($name, $default);
 514          }
 515  
 516          return $return;
 517      }
 518  
 519      /**
 520       * Method to get a control group with label and input.
 521       *
 522       * @param   string  $name     The name of the field for which to get the value.
 523       * @param   string  $group    The optional dot-separated form group path on which to get the value.
 524       * @param   mixed   $default  The optional default value of the field value is empty.
 525       * @param   array   $options  Any options to be passed into the rendering of the field
 526       *
 527       * @return  string  A string containing the html for the control group
 528       *
 529       * @since   3.2.3
 530       */
 531      public function renderField($name, $group = null, $default = null, $options = [])
 532      {
 533          $field = $this->getField($name, $group, $default);
 534  
 535          if ($field) {
 536              return $field->renderField($options);
 537          }
 538  
 539          return '';
 540      }
 541  
 542      /**
 543       * Method to get all control groups with label and input of a fieldset.
 544       *
 545       * @param   string  $name     The name of the fieldset for which to get the values.
 546       * @param   array   $options  Any options to be passed into the rendering of the field
 547       *
 548       * @return  string  A string containing the html for the control groups
 549       *
 550       * @since   3.2.3
 551       */
 552      public function renderFieldset($name, $options = [])
 553      {
 554          $fields = $this->getFieldset($name);
 555          $html = [];
 556  
 557          foreach ($fields as $field) {
 558              $html[] = $field->renderField($options);
 559          }
 560  
 561          return implode('', $html);
 562      }
 563  
 564      /**
 565       * Method to load the form description from an XML string or object.
 566       *
 567       * The replace option works per field.  If a field being loaded already exists in the current
 568       * form definition then the behavior or load will vary depending upon the replace flag.  If it
 569       * is set to true, then the existing field will be replaced in its exact location by the new
 570       * field being loaded.  If it is false, then the new field being loaded will be ignored and the
 571       * method will move on to the next field to load.
 572       *
 573       * @param   string   $data     The name of an XML string or object.
 574       * @param   boolean  $replace  Flag to toggle whether form fields should be replaced if a field
 575       *                             already exists with the same group/name.
 576       * @param   string   $xpath    An optional xpath to search for the fields.
 577       *
 578       * @return  boolean  True on success, false otherwise.
 579       *
 580       * @since   1.7.0
 581       */
 582      public function load($data, $replace = true, $xpath = null)
 583      {
 584          // If the data to load isn't already an XML element or string return false.
 585          if ((!($data instanceof \SimpleXMLElement)) && (!\is_string($data))) {
 586              return false;
 587          }
 588  
 589          // Attempt to load the XML if a string.
 590          if (\is_string($data)) {
 591              try {
 592                  $data = new \SimpleXMLElement($data);
 593              } catch (\Exception $e) {
 594                  return false;
 595              }
 596          }
 597  
 598          // If we have no XML definition at this point let's make sure we get one.
 599          if (empty($this->xml)) {
 600              // If no XPath query is set to search for fields, and we have a <form />, set it and return.
 601              if (!$xpath && ($data->getName() === 'form')) {
 602                  $this->xml = $data;
 603  
 604                  // Synchronize any paths found in the load.
 605                  $this->syncPaths();
 606  
 607                  return true;
 608              } else {
 609                  // Create a root element for the form.
 610                  $this->xml = new \SimpleXMLElement('<form></form>');
 611              }
 612          }
 613  
 614          // Get the XML elements to load.
 615          $elements = [];
 616  
 617          if ($xpath) {
 618              $elements = $data->xpath($xpath);
 619          } elseif ($data->getName() === 'form') {
 620              $elements = $data->children();
 621          }
 622  
 623          // If there is nothing to load return true.
 624          if (empty($elements)) {
 625              return true;
 626          }
 627  
 628          // Load the found form elements.
 629          foreach ($elements as $element) {
 630              // Get an array of fields with the correct name.
 631              $fields = $element->xpath('descendant-or-self::field');
 632  
 633              foreach ($fields as $field) {
 634                  // Get the group names as strings for ancestor fields elements.
 635                  $attrs = $field->xpath('ancestor::fields[@name]/@name');
 636                  $groups = array_map('strval', $attrs ?: []);
 637  
 638                  // Check to see if the field exists in the current form.
 639                  if ($current = $this->findField((string) $field['name'], implode('.', $groups))) {
 640                      // If set to replace found fields, replace the data and remove the field so we don't add it twice.
 641                      if ($replace) {
 642                          $olddom = dom_import_simplexml($current);
 643                          $loadeddom = dom_import_simplexml($field);
 644                          $addeddom = $olddom->ownerDocument->importNode($loadeddom, true);
 645                          $olddom->parentNode->replaceChild($addeddom, $olddom);
 646                          $loadeddom->parentNode->removeChild($loadeddom);
 647                      } else {
 648                          unset($field);
 649                      }
 650                  }
 651              }
 652  
 653              // Merge the new field data into the existing XML document.
 654              self::addNode($this->xml, $element);
 655          }
 656  
 657          // Synchronize any paths found in the load.
 658          $this->syncPaths();
 659  
 660          return true;
 661      }
 662  
 663      /**
 664       * Method to load the form description from an XML file.
 665       *
 666       * The reset option works on a group basis. If the XML file references
 667       * groups that have already been created they will be replaced with the
 668       * fields in the new XML file unless the $reset parameter has been set
 669       * to false.
 670       *
 671       * @param   string   $file   The filesystem path of an XML file.
 672       * @param   boolean  $reset  Flag to toggle whether form fields should be replaced if a field
 673       *                           already exists with the same group/name.
 674       * @param   string   $xpath  An optional xpath to search for the fields.
 675       *
 676       * @return  boolean  True on success, false otherwise.
 677       *
 678       * @since   1.7.0
 679       */
 680      public function loadFile($file, $reset = true, $xpath = null)
 681      {
 682          // Check to see if the path is an absolute path.
 683          if (!is_file($file)) {
 684              // Not an absolute path so let's attempt to find one using JPath.
 685              $file = Path::find(self::addFormPath(), strtolower($file) . '.xml');
 686  
 687              // If unable to find the file return false.
 688              if (!$file) {
 689                  return false;
 690              }
 691          }
 692  
 693          // Attempt to load the XML file.
 694          $xml = simplexml_load_file($file);
 695  
 696          return $this->load($xml, $reset, $xpath);
 697      }
 698  
 699      /**
 700       * Method to remove a field from the form definition.
 701       *
 702       * @param   string  $name   The name of the form field for which remove.
 703       * @param   string  $group  The optional dot-separated form group path on which to find the field.
 704       *
 705       * @return  boolean  True on success, false otherwise.
 706       *
 707       * @since   1.7.0
 708       * @throws  \UnexpectedValueException
 709       */
 710      public function removeField($name, $group = null)
 711      {
 712          // Make sure there is a valid Form XML document.
 713          if (!($this->xml instanceof \SimpleXMLElement)) {
 714              throw new \UnexpectedValueException(sprintf('%s::%s `xml` is not an instance of SimpleXMLElement', \get_class($this), __METHOD__));
 715          }
 716  
 717          // Find the form field element from the definition.
 718          $element = $this->findField($name, $group);
 719  
 720          // If the element exists remove it from the form definition.
 721          if ($element instanceof \SimpleXMLElement) {
 722              $dom = dom_import_simplexml($element);
 723              $dom->parentNode->removeChild($dom);
 724  
 725              return true;
 726          }
 727  
 728          return false;
 729      }
 730  
 731      /**
 732       * Method to remove a group from the form definition.
 733       *
 734       * @param   string  $group  The dot-separated form group path for the group to remove.
 735       *
 736       * @return  boolean  True on success.
 737       *
 738       * @since   1.7.0
 739       * @throws  \UnexpectedValueException
 740       */
 741      public function removeGroup($group)
 742      {
 743          // Make sure there is a valid Form XML document.
 744          if (!($this->xml instanceof \SimpleXMLElement)) {
 745              throw new \UnexpectedValueException(sprintf('%s::%s `xml` is not an instance of SimpleXMLElement', \get_class($this), __METHOD__));
 746          }
 747  
 748          // Get the fields elements for a given group.
 749          $elements = &$this->findGroup($group);
 750  
 751          foreach ($elements as &$element) {
 752              $dom = dom_import_simplexml($element);
 753              $dom->parentNode->removeChild($dom);
 754          }
 755  
 756          return true;
 757      }
 758  
 759      /**
 760       * Method to reset the form data store and optionally the form XML definition.
 761       *
 762       * @param   boolean  $xml  True to also reset the XML form definition.
 763       *
 764       * @return  boolean  True on success.
 765       *
 766       * @since   1.7.0
 767       */
 768      public function reset($xml = false)
 769      {
 770          unset($this->data);
 771          $this->data = new Registry();
 772  
 773          if ($xml) {
 774              unset($this->xml);
 775              $this->xml = new \SimpleXMLElement('<form></form>');
 776          }
 777  
 778          return true;
 779      }
 780  
 781      /**
 782       * Method to set a field XML element to the form definition.  If the replace flag is set then
 783       * the field will be set whether it already exists or not.  If it isn't set, then the field
 784       * will not be replaced if it already exists.
 785       *
 786       * @param   \SimpleXMLElement  $element   The XML element object representation of the form field.
 787       * @param   string             $group     The optional dot-separated form group path on which to set the field.
 788       * @param   boolean            $replace   True to replace an existing field if one already exists.
 789       * @param   string             $fieldset  The name of the fieldset we are adding the field to.
 790       *
 791       * @return  boolean  True on success.
 792       *
 793       * @since   1.7.0
 794       * @throws  \UnexpectedValueException
 795       */
 796      public function setField(\SimpleXMLElement $element, $group = null, $replace = true, $fieldset = 'default')
 797      {
 798          // Make sure there is a valid Form XML document.
 799          if (!($this->xml instanceof \SimpleXMLElement)) {
 800              throw new \UnexpectedValueException(sprintf('%s::%s `xml` is not an instance of SimpleXMLElement', \get_class($this), __METHOD__));
 801          }
 802  
 803          // Find the form field element from the definition.
 804          $old = $this->findField((string) $element['name'], $group);
 805  
 806          // If an existing field is found and replace flag is false do nothing and return true.
 807          if (!$replace && !empty($old)) {
 808              return true;
 809          }
 810  
 811          // If an existing field is found and replace flag is true remove the old field.
 812          if ($replace && !empty($old) && ($old instanceof \SimpleXMLElement)) {
 813              $dom = dom_import_simplexml($old);
 814  
 815              // Get the parent element, this should be the fieldset
 816              $parent   = $dom->parentNode;
 817              $fieldset = $parent->getAttribute('name');
 818  
 819              $parent->removeChild($dom);
 820          }
 821  
 822          // Create the search path
 823          $path = '//';
 824  
 825          if (!empty($group)) {
 826              $path .= 'fields[@name="' . $group . '"]/';
 827          }
 828  
 829          $path .= 'fieldset[@name="' . $fieldset . '"]';
 830  
 831          $fs = $this->xml->xpath($path);
 832  
 833          if (isset($fs[0]) && ($fs[0] instanceof \SimpleXMLElement)) {
 834              // Add field to the form.
 835              self::addNode($fs[0], $element);
 836  
 837              // Synchronize any paths found in the load.
 838              $this->syncPaths();
 839  
 840              return true;
 841          }
 842  
 843          // We couldn't find a fieldset to add the field. Now we are checking, if we have set only a group
 844          if (!empty($group)) {
 845              $fields = &$this->findGroup($group);
 846  
 847              // If an appropriate fields element was found for the group, add the element.
 848              if (isset($fields[0]) && ($fields[0] instanceof \SimpleXMLElement)) {
 849                  self::addNode($fields[0], $element);
 850              }
 851  
 852              // Synchronize any paths found in the load.
 853              $this->syncPaths();
 854  
 855              return true;
 856          }
 857  
 858          // We couldn't find a parent so we are adding it at root level
 859  
 860          // Add field to the form.
 861          self::addNode($this->xml, $element);
 862  
 863          // Synchronize any paths found in the load.
 864          $this->syncPaths();
 865  
 866          return true;
 867      }
 868  
 869      /**
 870       * Method to set an attribute value for a field XML element.
 871       *
 872       * @param   string  $name       The name of the form field for which to set the attribute value.
 873       * @param   string  $attribute  The name of the attribute for which to set a value.
 874       * @param   mixed   $value      The value to set for the attribute.
 875       * @param   string  $group      The optional dot-separated form group path on which to find the field.
 876       *
 877       * @return  boolean  True on success.
 878       *
 879       * @since   1.7.0
 880       * @throws  \UnexpectedValueException
 881       */
 882      public function setFieldAttribute($name, $attribute, $value, $group = null)
 883      {
 884          // Make sure there is a valid Form XML document.
 885          if (!($this->xml instanceof \SimpleXMLElement)) {
 886              throw new \UnexpectedValueException(sprintf('%s::%s `xml` is not an instance of SimpleXMLElement', \get_class($this), __METHOD__));
 887          }
 888  
 889          // Find the form field element from the definition.
 890          $element = $this->findField($name, $group);
 891  
 892          // If the element doesn't exist return false.
 893          if (!($element instanceof \SimpleXMLElement)) {
 894              return false;
 895          } else {
 896              // Otherwise set the attribute and return true.
 897              $element[$attribute] = $value;
 898  
 899              // Synchronize any paths found in the load.
 900              $this->syncPaths();
 901  
 902              return true;
 903          }
 904      }
 905  
 906      /**
 907       * Method to set some field XML elements to the form definition.  If the replace flag is set then
 908       * the fields will be set whether they already exists or not.  If it isn't set, then the fields
 909       * will not be replaced if they already exist.
 910       *
 911       * @param   array    &$elements  The array of XML element object representations of the form fields.
 912       * @param   string   $group      The optional dot-separated form group path on which to set the fields.
 913       * @param   boolean  $replace    True to replace existing fields if they already exist.
 914       * @param   string   $fieldset   The name of the fieldset we are adding the field to.
 915       *
 916       * @return  boolean  True on success.
 917       *
 918       * @since   1.7.0
 919       * @throws  \UnexpectedValueException
 920       */
 921      public function setFields(&$elements, $group = null, $replace = true, $fieldset = 'default')
 922      {
 923          // Make sure there is a valid Form XML document.
 924          if (!($this->xml instanceof \SimpleXMLElement)) {
 925              throw new \UnexpectedValueException(sprintf('%s::%s `xml` is not an instance of SimpleXMLElement', \get_class($this), __METHOD__));
 926          }
 927  
 928          // Make sure the elements to set are valid.
 929          foreach ($elements as $element) {
 930              if (!($element instanceof \SimpleXMLElement)) {
 931                  throw new \UnexpectedValueException(sprintf('%s::%s `xml` is not an instance of SimpleXMLElement', \get_class($this), __METHOD__));
 932              }
 933          }
 934  
 935          // Set the fields.
 936          $return = true;
 937  
 938          foreach ($elements as $element) {
 939              if (!$this->setField($element, $group, $replace, $fieldset)) {
 940                  $return = false;
 941              }
 942          }
 943  
 944          // Synchronize any paths found in the load.
 945          $this->syncPaths();
 946  
 947          return $return;
 948      }
 949  
 950      /**
 951       * Method to set the value of a field. If the field does not exist in the form then the method
 952       * will return false.
 953       *
 954       * @param   string  $name   The name of the field for which to set the value.
 955       * @param   string  $group  The optional dot-separated form group path on which to find the field.
 956       * @param   mixed   $value  The value to set for the field.
 957       *
 958       * @return  boolean  True on success.
 959       *
 960       * @since   1.7.0
 961       */
 962      public function setValue($name, $group = null, $value = null)
 963      {
 964          // If the field does not exist return false.
 965          if (!$this->findField($name, $group)) {
 966              return false;
 967          }
 968  
 969          // If a group is set use it.
 970          if ($group) {
 971              $this->data->set($group . '.' . $name, $value);
 972          } else {
 973              $this->data->set($name, $value);
 974          }
 975  
 976          return true;
 977      }
 978  
 979      /**
 980       * Method to process the form data.
 981       *
 982       * @param   array   $data   An array of field values to filter.
 983       * @param   string  $group  The dot-separated form group path on which to filter the fields.
 984       *
 985       * @return  mixed  Array or false.
 986       *
 987       * @since   4.0.0
 988       */
 989      public function process($data, $group = null)
 990      {
 991          $data = $this->filter($data, $group);
 992  
 993          $valid = $this->validate($data, $group);
 994  
 995          if (!$valid) {
 996              return $valid;
 997          }
 998  
 999          return $this->postProcess($data, $group);
1000      }
1001  
1002      /**
1003       * Method to filter the form data.
1004       *
1005       * @param   array   $data   An array of field values to filter.
1006       * @param   string  $group  The dot-separated form group path on which to filter the fields.
1007       *
1008       * @return  mixed  Array or false.
1009       *
1010       * @since   4.0.0
1011       */
1012      public function filter($data, $group = null)
1013      {
1014          // Make sure there is a valid Form XML document.
1015          if (!($this->xml instanceof \SimpleXMLElement)) {
1016              throw new \UnexpectedValueException(sprintf('%s::%s `xml` is not an instance of SimpleXMLElement', \get_class($this), __METHOD__));
1017          }
1018  
1019          $input = new Registry($data);
1020          $output = new Registry();
1021  
1022          // Get the fields for which to filter the data.
1023          $fields = $this->findFieldsByGroup($group);
1024  
1025          if (!$fields) {
1026              // PANIC!
1027              return false;
1028          }
1029  
1030          // Filter the fields.
1031          foreach ($fields as $field) {
1032              $name = (string) $field['name'];
1033  
1034              // Get the field groups for the element.
1035              $attrs = $field->xpath('ancestor::fields[@name]/@name');
1036              $groups = array_map('strval', $attrs ?: []);
1037              $attrGroup = implode('.', $groups);
1038  
1039              $key = $attrGroup ? $attrGroup . '.' . $name : $name;
1040  
1041              // Filter the value if it exists.
1042              if ($input->exists($key)) {
1043                  $fieldObj = $this->loadField($field, $group);
1044  
1045                  // Only set into the output if the field was supposed to render on the page (i.e. setup returned true)
1046                  if ($fieldObj) {
1047                      $output->set($key, $fieldObj->filter($input->get($key, (string) $field['default']), $group, $input));
1048                  }
1049              }
1050          }
1051  
1052          return $output->toArray();
1053      }
1054  
1055      /**
1056       * Method to validate form data.
1057       *
1058       * Validation warnings will be pushed into JForm::errors and should be
1059       * retrieved with JForm::getErrors() when validate returns boolean false.
1060       *
1061       * @param   array   $data   An array of field values to validate.
1062       * @param   string  $group  The optional dot-separated form group path on which to filter the
1063       *                          fields to be validated.
1064       *
1065       * @return  boolean  True on success.
1066       *
1067       * @since   1.7.0
1068       */
1069      public function validate($data, $group = null)
1070      {
1071          // Make sure there is a valid Form XML document.
1072          if (!($this->xml instanceof \SimpleXMLElement)) {
1073              throw new \UnexpectedValueException(sprintf('%s::%s `xml` is not an instance of SimpleXMLElement', \get_class($this), __METHOD__));
1074          }
1075  
1076          $return = true;
1077  
1078          // Create an input registry object from the data to validate.
1079          $input = new Registry($data);
1080  
1081          // Get the fields for which to validate the data.
1082          $fields = $this->findFieldsByGroup($group);
1083  
1084          if (!$fields) {
1085              // PANIC!
1086              return false;
1087          }
1088  
1089          // Validate the fields.
1090          foreach ($fields as $field) {
1091              $name     = (string) $field['name'];
1092  
1093              // Define field name for messages
1094              if ($field['label']) {
1095                  $fieldLabel = $field['label'];
1096  
1097                  // Try to translate label if not set to false
1098                  $translate = (string) $field['translateLabel'];
1099  
1100                  if (!($translate === 'false' || $translate === 'off' || $translate === '0')) {
1101                      $fieldLabel = Text::_($fieldLabel);
1102                  }
1103              } else {
1104                  $fieldLabel = Text::_($name);
1105              }
1106  
1107              $disabled = ((string) $field['disabled'] === 'true' || (string) $field['disabled'] === 'disabled');
1108  
1109              $fieldExistsInRequestData = $input->exists($name) || $input->exists($group . '.' . $name);
1110  
1111              // If the field is disabled but it is passed in the request this is invalid as disabled fields are not added to the request
1112              if ($disabled && $fieldExistsInRequestData) {
1113                  throw new \RuntimeException(Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', $fieldLabel));
1114              }
1115  
1116              // Get the field groups for the element.
1117              $attrs = $field->xpath('ancestor::fields[@name]/@name');
1118              $groups = array_map('strval', $attrs ?: []);
1119              $attrGroup = implode('.', $groups);
1120  
1121              $key = $attrGroup ? $attrGroup . '.' . $name : $name;
1122  
1123              $fieldObj = $this->loadField($field, $attrGroup);
1124  
1125              if ($fieldObj) {
1126                  $valid = $fieldObj->validate($input->get($key), $attrGroup, $input);
1127  
1128                  // Check for an error.
1129                  if ($valid instanceof \Exception) {
1130                      $this->errors[] = $valid;
1131                      $return         = false;
1132                  }
1133              } elseif (!$fieldObj && $input->exists($key)) {
1134                  // The field returned false from setup and shouldn't be included in the page body - yet we received
1135                  // a value for it. This is probably some sort of injection attack and should be rejected
1136                  $this->errors[] = new \RuntimeException(Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', $key));
1137              }
1138          }
1139  
1140          return $return;
1141      }
1142  
1143      /**
1144       * Method to post-process form data.
1145       *
1146       * @param   array   $data   An array of field values to post-process.
1147       * @param   string  $group  The optional dot-separated form group path on which to filter the
1148       *                          fields to be validated.
1149       *
1150       * @return  mixed  Array or false.
1151       *
1152       * @since   4.0.0
1153       */
1154      public function postProcess($data, $group = null)
1155      {
1156          // Make sure there is a valid SimpleXMLElement
1157          if (!($this->xml instanceof \SimpleXMLElement)) {
1158              throw new \UnexpectedValueException(sprintf('%s::%s `xml` is not an instance of SimpleXMLElement', \get_class($this), __METHOD__));
1159          }
1160  
1161          $input = new Registry($data);
1162          $output = new Registry();
1163  
1164          // Get the fields for which to postProcess the data.
1165          $fields = $this->findFieldsByGroup($group);
1166  
1167          if (!$fields) {
1168              // PANIC!
1169              return false;
1170          }
1171  
1172          // Filter the fields.
1173          foreach ($fields as $field) {
1174              $name = (string) $field['name'];
1175  
1176              // Get the field groups for the element.
1177              $attrs = $field->xpath('ancestor::fields[@name]/@name');
1178              $groups = array_map('strval', $attrs ?: []);
1179              $attrGroup = implode('.', $groups);
1180  
1181              $key = $attrGroup ? $attrGroup . '.' . $name : $name;
1182  
1183              // Filter the value if it exists.
1184              if ($input->exists($key)) {
1185                  $fieldobj = $this->loadField($field, $group);
1186                  $output->set($key, $fieldobj->postProcess($input->get($key, (string) $field['default']), $group, $input));
1187              }
1188          }
1189  
1190          return $output->toArray();
1191      }
1192  
1193      /**
1194       * Method to get a form field represented as an XML element object.
1195       *
1196       * @param   string  $name   The name of the form field.
1197       * @param   string  $group  The optional dot-separated form group path on which to find the field.
1198       *
1199       * @return  \SimpleXMLElement|boolean  The XML element object for the field or boolean false on error.
1200       *
1201       * @since   1.7.0
1202       */
1203      protected function findField($name, $group = null)
1204      {
1205          $element = false;
1206          $fields = [];
1207  
1208          // Make sure there is a valid Form XML document.
1209          if (!($this->xml instanceof \SimpleXMLElement)) {
1210              throw new \UnexpectedValueException(sprintf('%s::%s `xml` is not an instance of SimpleXMLElement', \get_class($this), __METHOD__));
1211          }
1212  
1213          // Let's get the appropriate field element based on the method arguments.
1214          if ($group) {
1215              // Get the fields elements for a given group.
1216              $elements = &$this->findGroup($group);
1217  
1218              // Get all of the field elements with the correct name for the fields elements.
1219              foreach ($elements as $el) {
1220                  // If there are matching field elements add them to the fields array.
1221                  if ($tmp = $el->xpath('descendant::field[@name="' . $name . '" and not(ancestor::field/form/*)]')) {
1222                      $fields = array_merge($fields, $tmp);
1223                  }
1224              }
1225  
1226              // Make sure something was found.
1227              if (!$fields) {
1228                  return false;
1229              }
1230  
1231              // Use the first correct match in the given group.
1232              $groupNames = explode('.', $group);
1233  
1234              foreach ($fields as &$field) {
1235                  // Get the group names as strings for ancestor fields elements.
1236                  $attrs = $field->xpath('ancestor::fields[@name]/@name');
1237                  $names = array_map('strval', $attrs ?: []);
1238  
1239                  // If the field is in the exact group use it and break out of the loop.
1240                  if ($names == (array) $groupNames) {
1241                      $element = &$field;
1242                      break;
1243                  }
1244              }
1245          } else {
1246              // Get an array of fields with the correct name.
1247              $fields = $this->xml->xpath('//field[@name="' . $name . '" and not(ancestor::field/form/*)]');
1248  
1249              // Make sure something was found.
1250              if (!$fields) {
1251                  return false;
1252              }
1253  
1254              // Search through the fields for the right one.
1255              foreach ($fields as &$field) {
1256                  // If we find an ancestor fields element with a group name then it isn't what we want.
1257                  if ($field->xpath('ancestor::fields[@name]')) {
1258                      continue;
1259                  } else {
1260                      // Found it!
1261                      $element = &$field;
1262                      break;
1263                  }
1264              }
1265          }
1266  
1267          return $element;
1268      }
1269  
1270      /**
1271       * Method to get an array of `<field>` elements from the form XML document which are in a specified fieldset by name.
1272       *
1273       * @param   string  $name  The name of the fieldset.
1274       *
1275       * @return  \SimpleXMLElement[]|boolean  Boolean false on error or array of SimpleXMLElement objects.
1276       *
1277       * @since   1.7.0
1278       */
1279      protected function &findFieldsByFieldset($name)
1280      {
1281          // Make sure there is a valid Form XML document.
1282          if (!($this->xml instanceof \SimpleXMLElement)) {
1283              throw new \UnexpectedValueException(sprintf('%s::%s `xml` is not an instance of SimpleXMLElement', \get_class($this), __METHOD__));
1284          }
1285  
1286          /*
1287           * Get an array of <field /> elements that are underneath a <fieldset /> element
1288           * with the appropriate name attribute, and also any <field /> elements with
1289           * the appropriate fieldset attribute. To allow repeatable elements only fields
1290           * which are not descendants of other fields are selected.
1291           */
1292          $fields = $this->xml->xpath('(//fieldset[@name="' . $name . '"]//field | //field[@fieldset="' . $name . '"])[not(ancestor::field)]');
1293  
1294          return $fields;
1295      }
1296  
1297      /**
1298       * Method to get an array of `<field>` elements from the form XML document which are in a control group by name.
1299       *
1300       * @param   mixed    $group   The optional dot-separated form group path on which to find the fields.
1301       *                            Null will return all fields. False will return fields not in a group.
1302       * @param   boolean  $nested  True to also include fields in nested groups that are inside of the
1303       *                            group for which to find fields.
1304       *
1305       * @return  \SimpleXMLElement[]|boolean  Boolean false on error or array of SimpleXMLElement objects.
1306       *
1307       * @since   1.7.0
1308       */
1309      protected function &findFieldsByGroup($group = null, $nested = false)
1310      {
1311          $fields = [];
1312  
1313          // Make sure there is a valid Form XML document.
1314          if (!($this->xml instanceof \SimpleXMLElement)) {
1315              throw new \UnexpectedValueException(sprintf('%s::%s `xml` is not an instance of SimpleXMLElement', \get_class($this), __METHOD__));
1316          }
1317  
1318          // Get only fields in a specific group?
1319          if ($group) {
1320              // Get the fields elements for a given group.
1321              $elements = &$this->findGroup($group);
1322  
1323              // Get all of the field elements for the fields elements.
1324              foreach ($elements as $element) {
1325                  // If there are field elements add them to the return result.
1326                  if ($tmp = $element->xpath('descendant::field')) {
1327                      // If we also want fields in nested groups then just merge the arrays.
1328                      if ($nested) {
1329                          $fields = array_merge($fields, $tmp);
1330                      } else {
1331                          // If we want to exclude nested groups then we need to check each field.
1332                          $groupNames = explode('.', $group);
1333  
1334                          foreach ($tmp as $field) {
1335                              // Get the names of the groups that the field is in.
1336                              $attrs = $field->xpath('ancestor::fields[@name]/@name');
1337                              $names = array_map('strval', $attrs ?: []);
1338  
1339                              // If the field is in the specific group then add it to the return list.
1340                              if ($names == (array) $groupNames) {
1341                                  $fields = array_merge($fields, array($field));
1342                              }
1343                          }
1344                      }
1345                  }
1346              }
1347          } elseif ($group === false) {
1348              // Get only field elements not in a group.
1349              $fields = $this->xml->xpath('descendant::fields[not(@name)]/field | descendant::fields[not(@name)]/fieldset/field ');
1350          } else {
1351              // Get an array of all the <field /> elements.
1352              $fields = $this->xml->xpath('//field[not(ancestor::field/form/*)]');
1353          }
1354  
1355          return $fields;
1356      }
1357  
1358      /**
1359       * Method to get a form field group represented as an XML element object.
1360       *
1361       * @param   string  $group  The dot-separated form group path on which to find the group.
1362       *
1363       * @return  \SimpleXMLElement[]|boolean  An array of XML element objects for the group or boolean false on error.
1364       *
1365       * @since   1.7.0
1366       */
1367      protected function &findGroup($group)
1368      {
1369          $groups = [];
1370          $tmp = [];
1371  
1372          // Make sure there is a valid Form XML document.
1373          if (!($this->xml instanceof \SimpleXMLElement)) {
1374              throw new \UnexpectedValueException(sprintf('%s::%s `xml` is not an instance of SimpleXMLElement', \get_class($this), __METHOD__));
1375          }
1376  
1377          // Make sure there is actually a group to find.
1378          $group = explode('.', $group);
1379  
1380          if (!empty($group)) {
1381              // Get any fields elements with the correct group name.
1382              $elements = $this->xml->xpath('//fields[@name="' . (string) $group[0] . '" and not(ancestor::field/form/*)]');
1383  
1384              // Check to make sure that there are no parent groups for each element.
1385              foreach ($elements as $element) {
1386                  if (!$element->xpath('ancestor::fields[@name]')) {
1387                      $tmp[] = $element;
1388                  }
1389              }
1390  
1391              // Iterate through the nested groups to find any matching form field groups.
1392              for ($i = 1, $n = \count($group); $i < $n; $i++) {
1393                  // Initialise some loop variables.
1394                  $validNames = \array_slice($group, 0, $i + 1);
1395                  $current = $tmp;
1396                  $tmp = [];
1397  
1398                  // Check to make sure that there are no parent groups for each element.
1399                  foreach ($current as $element) {
1400                      // Get any fields elements with the correct group name.
1401                      $children = $element->xpath('descendant::fields[@name="' . (string) $group[$i] . '"]');
1402  
1403                      // For the found fields elements validate that they are in the correct groups.
1404                      foreach ($children as $fields) {
1405                          // Get the group names as strings for ancestor fields elements.
1406                          $attrs = $fields->xpath('ancestor-or-self::fields[@name]/@name');
1407                          $names = array_map('strval', $attrs ?: []);
1408  
1409                          // If the group names for the fields element match the valid names at this
1410                          // level add the fields element.
1411                          if ($validNames == $names) {
1412                              $tmp[] = $fields;
1413                          }
1414                      }
1415                  }
1416              }
1417  
1418              // Only include valid XML objects.
1419              foreach ($tmp as $element) {
1420                  if ($element instanceof \SimpleXMLElement) {
1421                      $groups[] = $element;
1422                  }
1423              }
1424          }
1425  
1426          return $groups;
1427      }
1428  
1429      /**
1430       * Method to load, setup and return a FormField object based on field data.
1431       *
1432       * @param   string  $element  The XML element object representation of the form field.
1433       * @param   string  $group    The optional dot-separated form group path on which to find the field.
1434       * @param   mixed   $value    The optional value to use as the default for the field.
1435       *
1436       * @return  FormField|boolean  The FormField object for the field or boolean false on error.
1437       *
1438       * @since   1.7.0
1439       */
1440      protected function loadField($element, $group = null, $value = null)
1441      {
1442          // Make sure there is a valid SimpleXMLElement.
1443          if (!($element instanceof \SimpleXMLElement)) {
1444              throw new \UnexpectedValueException(sprintf('%s::%s `xml` is not an instance of SimpleXMLElement', \get_class($this), __METHOD__));
1445          }
1446  
1447          // Get the field type.
1448          $type = $element['type'] ? (string) $element['type'] : 'text';
1449  
1450          // Load the FormField object for the field.
1451          $field = FormHelper::loadFieldType($type);
1452  
1453          if ($field instanceof DatabaseAwareInterface) {
1454              try {
1455                  $field->setDatabase($this->getDatabase());
1456              } catch (DatabaseNotFoundException $e) {
1457                  @trigger_error(sprintf('Database must be set, this will not be caught anymore in 5.0.'), E_USER_DEPRECATED);
1458                  $field->setDatabase(Factory::getContainer()->get(DatabaseInterface::class));
1459              }
1460          }
1461  
1462          // If the object could not be loaded, get a text field object.
1463          if ($field === false) {
1464              $field = FormHelper::loadFieldType('text');
1465          }
1466  
1467          /*
1468           * Get the value for the form field if not set.
1469           * Default to the translated version of the 'default' attribute
1470           * if 'translate_default' attribute if set to 'true' or '1'
1471           * else the value of the 'default' attribute for the field.
1472           */
1473          if ($value === null) {
1474              $default = (string) ($element['default'] ? $element['default'] : $element->default);
1475  
1476              if (($translate = $element['translate_default']) && ((string) $translate === 'true' || (string) $translate === '1')) {
1477                  $lang = Factory::getLanguage();
1478  
1479                  if ($lang->hasKey($default)) {
1480                      $debug = $lang->setDebug(false);
1481                      $default = Text::_($default);
1482                      $lang->setDebug($debug);
1483                  } else {
1484                      $default = Text::_($default);
1485                  }
1486              }
1487  
1488              $value = $this->getValue((string) $element['name'], $group, $default);
1489          }
1490  
1491          // Setup the FormField object.
1492          $field->setForm($this);
1493  
1494          if ($field->setup($element, $value, $group)) {
1495              return $field;
1496          } else {
1497              return false;
1498          }
1499      }
1500  
1501      /**
1502       * Method to synchronize any field, form or rule paths contained in the XML document.
1503       *
1504       * @return  boolean  True on success.
1505       *
1506       * @since   1.7.0
1507       * @todo    Maybe we should receive all addXXXpaths attributes at once?
1508       */
1509      protected function syncPaths()
1510      {
1511          // Make sure there is a valid Form XML document.
1512          if (!($this->xml instanceof \SimpleXMLElement)) {
1513              throw new \UnexpectedValueException(sprintf('%s::%s `xml` is not an instance of SimpleXMLElement', \get_class($this), __METHOD__));
1514          }
1515  
1516          // Get any addfieldpath attributes from the form definition.
1517          $paths = $this->xml->xpath('//*[@addfieldpath]/@addfieldpath');
1518          $paths = array_map('strval', $paths ?: []);
1519  
1520          // Add the field paths.
1521          foreach ($paths as $path) {
1522              $path = JPATH_ROOT . '/' . ltrim($path, '/\\');
1523              self::addFieldPath($path);
1524          }
1525  
1526          // Get any addformpath attributes from the form definition.
1527          $paths = $this->xml->xpath('//*[@addformpath]/@addformpath');
1528          $paths = array_map('strval', $paths ?: []);
1529  
1530          // Add the form paths.
1531          foreach ($paths as $path) {
1532              $path = JPATH_ROOT . '/' . ltrim($path, '/\\');
1533              self::addFormPath($path);
1534          }
1535  
1536          // Get any addrulepath attributes from the form definition.
1537          $paths = $this->xml->xpath('//*[@addrulepath]/@addrulepath');
1538          $paths = array_map('strval', $paths ?: []);
1539  
1540          // Add the rule paths.
1541          foreach ($paths as $path) {
1542              $path = JPATH_ROOT . '/' . ltrim($path, '/\\');
1543              self::addRulePath($path);
1544          }
1545  
1546          // Get any addrulepath attributes from the form definition.
1547          $paths = $this->xml->xpath('//*[@addfilterpath]/@addfilterpath');
1548          $paths = array_map('strval', $paths ?: []);
1549  
1550          // Add the rule paths.
1551          foreach ($paths as $path) {
1552              $path = JPATH_ROOT . '/' . ltrim($path, '/\\');
1553              self::addFilterPath($path);
1554          }
1555  
1556          // Get any addfieldprefix attributes from the form definition.
1557          $prefixes = $this->xml->xpath('//*[@addfieldprefix]/@addfieldprefix');
1558          $prefixes = array_map('strval', $prefixes ?: []);
1559  
1560          // Add the field prefixes.
1561          foreach ($prefixes as $prefix) {
1562              FormHelper::addFieldPrefix($prefix);
1563          }
1564  
1565          // Get any addformprefix attributes from the form definition.
1566          $prefixes = $this->xml->xpath('//*[@addformprefix]/@addformprefix');
1567          $prefixes = array_map('strval', $prefixes ?: []);
1568  
1569          // Add the field prefixes.
1570          foreach ($prefixes as $prefix) {
1571              FormHelper::addFormPrefix($prefix);
1572          }
1573  
1574          // Get any addruleprefix attributes from the form definition.
1575          $prefixes = $this->xml->xpath('//*[@addruleprefix]/@addruleprefix');
1576          $prefixes = array_map('strval', $prefixes ?: []);
1577  
1578          // Add the field prefixes.
1579          foreach ($prefixes as $prefix) {
1580              FormHelper::addRulePrefix($prefix);
1581          }
1582  
1583          // Get any addruleprefix attributes from the form definition.
1584          $prefixes = $this->xml->xpath('//*[@addfilterprefix]/@addfilterprefix');
1585          $prefixes = array_map('strval', $prefixes ?: []);
1586  
1587          // Add the field prefixes.
1588          foreach ($prefixes as $prefix) {
1589              FormHelper::addFilterPrefix($prefix);
1590          }
1591  
1592          return true;
1593      }
1594  
1595      /**
1596       * Proxy for {@link FormHelper::addFieldPath()}.
1597       *
1598       * @param   mixed  $new  A path or array of paths to add.
1599       *
1600       * @return  array  The list of paths that have been added.
1601       *
1602       * @since   1.7.0
1603       */
1604      public static function addFieldPath($new = null)
1605      {
1606          return FormHelper::addFieldPath($new);
1607      }
1608  
1609      /**
1610       * Proxy for FormHelper::addFormPath().
1611       *
1612       * @param   mixed  $new  A path or array of paths to add.
1613       *
1614       * @return  array  The list of paths that have been added.
1615       *
1616       * @see     FormHelper::addFormPath()
1617       * @since   1.7.0
1618       */
1619      public static function addFormPath($new = null)
1620      {
1621          return FormHelper::addFormPath($new);
1622      }
1623  
1624      /**
1625       * Proxy for FormHelper::addRulePath().
1626       *
1627       * @param   mixed  $new  A path or array of paths to add.
1628       *
1629       * @return  array  The list of paths that have been added.
1630       *
1631       * @see     FormHelper::addRulePath()
1632       * @since   1.7.0
1633       */
1634      public static function addRulePath($new = null)
1635      {
1636          return FormHelper::addRulePath($new);
1637      }
1638  
1639      /**
1640       * Proxy for FormHelper::addFilterPath().
1641       *
1642       * @param   mixed  $new  A path or array of paths to add.
1643       *
1644       * @return  array  The list of paths that have been added.
1645       *
1646       * @see     FormHelper::addFilterPath()
1647       * @since   4.0.0
1648       */
1649      public static function addFilterPath($new = null)
1650      {
1651          return FormHelper::addFilterPath($new);
1652      }
1653  
1654      /**
1655       * Method to get an instance of a form.
1656       *
1657       * @param   string          $name     The name of the form.
1658       * @param   string          $data     The name of an XML file or string to load as the form definition.
1659       * @param   array           $options  An array of form options.
1660       * @param   boolean         $replace  Flag to toggle whether form fields should be replaced if a field
1661       *                                    already exists with the same group/name.
1662       * @param   string|boolean  $xpath    An optional xpath to search for the fields.
1663       *
1664       * @return  Form  Form instance.
1665       *
1666       * @since   1.7.0
1667       * @deprecated  5.0 Use the FormFactory service from the container
1668       * @throws  \InvalidArgumentException if no data provided.
1669       * @throws  \RuntimeException if the form could not be loaded.
1670       */
1671      public static function getInstance($name, $data = null, $options = [], $replace = true, $xpath = false)
1672      {
1673          // Reference to array with form instances
1674          $forms = &self::$forms;
1675  
1676          // Only instantiate the form if it does not already exist.
1677          if (!isset($forms[$name])) {
1678              $data = trim($data);
1679  
1680              if (empty($data)) {
1681                  throw new \InvalidArgumentException(sprintf('%1$s(%2$s, *%3$s*)', __METHOD__, $name, \gettype($data)));
1682              }
1683  
1684              // Instantiate the form.
1685              $forms[$name] = Factory::getContainer()->get(FormFactoryInterface::class)->createForm($name, $options);
1686  
1687              // Load the data.
1688              if (substr($data, 0, 1) === '<') {
1689                  if ($forms[$name]->load($data, $replace, $xpath) == false) {
1690                      throw new \RuntimeException(sprintf('%s() could not load form', __METHOD__));
1691                  }
1692              } else {
1693                  if ($forms[$name]->loadFile($data, $replace, $xpath) == false) {
1694                      throw new \RuntimeException(sprintf('%s() could not load file', __METHOD__));
1695                  }
1696              }
1697          }
1698  
1699          return $forms[$name];
1700      }
1701  
1702      /**
1703       * Adds a new child SimpleXMLElement node to the source.
1704       *
1705       * @param   \SimpleXMLElement  $source  The source element on which to append.
1706       * @param   \SimpleXMLElement  $new     The new element to append.
1707       *
1708       * @return  void
1709       *
1710       * @since   1.7.0
1711       */
1712      protected static function addNode(\SimpleXMLElement $source, \SimpleXMLElement $new)
1713      {
1714          // Add the new child node.
1715          $node = $source->addChild($new->getName(), htmlspecialchars(trim($new)));
1716  
1717          // Add the attributes of the child node.
1718          foreach ($new->attributes() as $name => $value) {
1719              $node->addAttribute($name, $value);
1720          }
1721  
1722          // Add any children of the new node.
1723          foreach ($new->children() as $child) {
1724              self::addNode($node, $child);
1725          }
1726      }
1727  
1728      /**
1729       * Update the attributes of a child node
1730       *
1731       * @param   \SimpleXMLElement  $source  The source element on which to append the attributes
1732       * @param   \SimpleXMLElement  $new     The new element to append
1733       *
1734       * @return  void
1735       *
1736       * @since   1.7.0
1737       */
1738      protected static function mergeNode(\SimpleXMLElement $source, \SimpleXMLElement $new)
1739      {
1740          // Update the attributes of the child node.
1741          foreach ($new->attributes() as $name => $value) {
1742              if (isset($source[$name])) {
1743                  $source[$name] = (string) $value;
1744              } else {
1745                  $source->addAttribute($name, $value);
1746              }
1747          }
1748      }
1749  
1750      /**
1751       * Merges new elements into a source `<fields>` element.
1752       *
1753       * @param   \SimpleXMLElement  $source  The source element.
1754       * @param   \SimpleXMLElement  $new     The new element to merge.
1755       *
1756       * @return  void
1757       *
1758       * @since   1.7.0
1759       */
1760      protected static function mergeNodes(\SimpleXMLElement $source, \SimpleXMLElement $new)
1761      {
1762          // The assumption is that the inputs are at the same relative level.
1763          // So we just have to scan the children and deal with them.
1764  
1765          // Update the attributes of the child node.
1766          foreach ($new->attributes() as $name => $value) {
1767              if (isset($source[$name])) {
1768                  $source[$name] = (string) $value;
1769              } else {
1770                  $source->addAttribute($name, $value);
1771              }
1772          }
1773  
1774          foreach ($new->children() as $child) {
1775              $type = $child->getName();
1776              $name = $child['name'];
1777  
1778              // Does this node exist?
1779              $fields = $source->xpath($type . '[@name="' . $name . '"]');
1780  
1781              if (empty($fields)) {
1782                  // This node does not exist, so add it.
1783                  self::addNode($source, $child);
1784              } else {
1785                  // This node does exist.
1786                  switch ($type) {
1787                      case 'field':
1788                          self::mergeNode($fields[0], $child);
1789                          break;
1790  
1791                      default:
1792                          self::mergeNodes($fields[0], $child);
1793                          break;
1794                  }
1795              }
1796          }
1797      }
1798  
1799      /**
1800       * Returns the value of an attribute of the form itself
1801       *
1802       * @param   string  $name     Name of the attribute to get
1803       * @param   mixed   $default  Optional value to return if attribute not found
1804       *
1805       * @return  mixed             Value of the attribute / default
1806       *
1807       * @since   3.2
1808       */
1809      public function getAttribute($name, $default = null)
1810      {
1811          if ($this->xml instanceof \SimpleXMLElement) {
1812              $value = $this->xml->attributes()->$name;
1813  
1814              if ($value !== null) {
1815                  return (string) $value;
1816              }
1817          }
1818  
1819          return $default;
1820      }
1821  
1822      /**
1823       * Getter for the form data
1824       *
1825       * @return   Registry  Object with the data
1826       *
1827       * @since    3.2
1828       */
1829      public function getData()
1830      {
1831          return $this->data;
1832      }
1833  
1834      /**
1835       * Method to get the XML form object
1836       *
1837       * @return  \SimpleXMLElement  The form XML object
1838       *
1839       * @since   3.2
1840       */
1841      public function getXml()
1842      {
1843          return $this->xml;
1844      }
1845  
1846      /**
1847       * Method to get a form field represented as an XML element object.
1848       *
1849       * @param   string  $name   The name of the form field.
1850       * @param   string  $group  The optional dot-separated form group path on which to find the field.
1851       *
1852       * @return  \SimpleXMLElement|boolean  The XML element object for the field or boolean false on error.
1853       *
1854       * @since   3.7.0
1855       */
1856      public function getFieldXml($name, $group = null)
1857      {
1858          return $this->findField($name, $group);
1859      }
1860  }


Generated: Wed Sep 7 05:41:13 2022 Chilli.vc Blog - For Webmaster,Blog-Writer,System Admin and Domainer