diff --git a/libraries/src/Form/Field/CalendarField.php b/libraries/src/Form/Field/CalendarField.php index 6e68b01b964e6..294e8b0a9a321 100644 --- a/libraries/src/Form/Field/CalendarField.php +++ b/libraries/src/Form/Field/CalendarField.php @@ -13,6 +13,7 @@ use Joomla\CMS\Factory; use Joomla\CMS\Form\FormField; use Joomla\CMS\Language\Text; +use Joomla\Registry\Registry; /** * Form Field class for the Joomla Platform. @@ -317,4 +318,67 @@ protected function getLayoutData() return array_merge($data, $extraData); } + + /** + * Method to filter a field value. + * + * @param mixed $value The optional value to use as the default for the field. + * @param string $group The optional dot-separated form group path on which to find the field. + * @param Registry $input An optional Registry object with the entire data set to filter + * against the entire form. + * + * @return mixed The filtered value. + * + * @since __DEPLOY_VERSION__ + */ + public function filter($value, $group = null, Registry $input = null) + { + // Make sure there is a valid SimpleXMLElement. + if (!($this->element instanceof \SimpleXMLElement)) + { + throw new \UnexpectedValueException(sprintf('%s::filter `element` is not an instance of SimpleXMLElement', get_class($this))); + } + + // Get the field filter type. + $filter = (string) $this->element['filter']; + + $return = $value; + + switch (strtoupper($filter)) + { + // Convert a date to UTC based on the server timezone offset. + case 'SERVER_UTC': + if ((int) $value > 0) + { + // Get the server timezone setting. + $offset = Factory::getConfig()->get('offset'); + + // Return an SQL formatted datetime string in UTC. + $return = Factory::getDate($value, $offset)->toSql(); + } + else + { + $return = ''; + } + break; + + // Convert a date to UTC based on the user timezone offset. + case 'USER_UTC': + if ((int) $value > 0) + { + // Get the user timezone setting defaulting to the server timezone setting. + $offset = Factory::getUser()->getParam('timezone', Factory::getConfig()->get('offset')); + + // Return an SQL formatted datetime string in UTC. + $return = Factory::getDate($value, $offset)->toSql(); + } + else + { + $return = ''; + } + break; + } + + return $return; + } } diff --git a/libraries/src/Form/Filter/IntarrayFilter.php b/libraries/src/Form/Filter/IntarrayFilter.php new file mode 100644 index 0000000000000..649f6d6452447 --- /dev/null +++ b/libraries/src/Form/Filter/IntarrayFilter.php @@ -0,0 +1,53 @@ +` tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * @param Registry $input An optional Registry object with the entire data set to validate against the entire form. + * @param Form $form The form object for which the field is being tested. + * + * @return mixed The filtered value. + * + * @since __DEPLOY_VERSION__ + */ + public function filter(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null) + { + if (is_object($value)) + { + $value = get_object_vars($value); + } + + $value = is_array($value) ? $value : array($value); + + $value = ArrayHelper::toInteger($value); + + return $value; + } +} diff --git a/libraries/src/Form/Filter/RawFilter.php b/libraries/src/Form/Filter/RawFilter.php new file mode 100644 index 0000000000000..8f47fc3d0db24 --- /dev/null +++ b/libraries/src/Form/Filter/RawFilter.php @@ -0,0 +1,43 @@ +` tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * @param Registry $input An optional Registry object with the entire data set to validate against the entire form. + * @param Form $form The form object for which the field is being tested. + * + * @return mixed The filtered value. + * + * @since __DEPLOY_VERSION__ + */ + public function filter(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null) + { + return $value; + } +} diff --git a/libraries/src/Form/Filter/RulesFilter.php b/libraries/src/Form/Filter/RulesFilter.php new file mode 100644 index 0000000000000..0c7f2f3613443 --- /dev/null +++ b/libraries/src/Form/Filter/RulesFilter.php @@ -0,0 +1,59 @@ +` tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * @param Registry $input An optional Registry object with the entire data set to validate against the entire form. + * @param Form $form The form object for which the field is being tested. + * + * @return mixed The filtered value. + * + * @since __DEPLOY_VERSION__ + */ + public function filter(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null) + { + $return = array(); + + foreach ((array) $value as $action => $ids) + { + // Build the rules array. + $return[$action] = array(); + + foreach ($ids as $id => $p) + { + if ($p !== '') + { + $return[$action][$id] = ($p == '1' || $p == 'true') ? true : false; + } + } + } + + return $return; + } +} diff --git a/libraries/src/Form/Filter/SafehtmlFilter.php b/libraries/src/Form/Filter/SafehtmlFilter.php new file mode 100644 index 0000000000000..4a594d9da5428 --- /dev/null +++ b/libraries/src/Form/Filter/SafehtmlFilter.php @@ -0,0 +1,44 @@ +` tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * @param Registry $input An optional Registry object with the entire data set to validate against the entire form. + * @param Form $form The form object for which the field is being tested. + * + * @return mixed The filtered value. + * + * @since __DEPLOY_VERSION__ + */ + public function filter(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null) + { + return InputFilter::getInstance(null, null, 1, 1)->clean($value, 'html'); + } +} diff --git a/libraries/src/Form/Filter/TelFilter.php b/libraries/src/Form/Filter/TelFilter.php new file mode 100644 index 0000000000000..198cf4be56a1b --- /dev/null +++ b/libraries/src/Form/Filter/TelFilter.php @@ -0,0 +1,120 @@ +` tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * @param Registry $input An optional Registry object with the entire data set to validate against the entire form. + * @param Form $form The form object for which the field is being tested. + * + * @return mixed The filtered value. + * + * @since __DEPLOY_VERSION__ + */ + public function filter(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null) + { + $value = trim($value); + + // Does it match the NANP pattern? + if (preg_match('/^(?:\+?1[-. ]?)?\(?([2-9][0-8][0-9])\)?[-. ]?([2-9][0-9]{2})[-. ]?([0-9]{4})$/', $value) == 1) + { + $number = (string) preg_replace('/[^\d]/', '', $value); + + if (substr($number, 0, 1) == 1) + { + $number = substr($number, 1); + } + + if (substr($number, 0, 2) == '+1') + { + $number = substr($number, 2); + } + + $result = '1.' . $number; + } + + // If not, does it match ITU-T? + elseif (preg_match('/^\+(?:[0-9] ?){6,14}[0-9]$/', $value) == 1) + { + $countrycode = substr($value, 0, strpos($value, ' ')); + $countrycode = (string) preg_replace('/[^\d]/', '', $countrycode); + $number = strstr($value, ' '); + $number = (string) preg_replace('/[^\d]/', '', $number); + $result = $countrycode . '.' . $number; + } + + // If not, does it match EPP? + elseif (preg_match('/^\+[0-9]{1,3}\.[0-9]{4,14}(?:x.+)?$/', $value) == 1) + { + if (strstr($value, 'x')) + { + $xpos = strpos($value, 'x'); + $value = substr($value, 0, $xpos); + } + + $result = str_replace('+', '', $value); + } + + // Maybe it is already ccc.nnnnnnn? + elseif (preg_match('/[0-9]{1,3}\.[0-9]{4,14}$/', $value) == 1) + { + $result = $value; + } + + // If not, can we make it a string of digits? + else + { + $value = (string) preg_replace('/[^\d]/', '', $value); + + if ($value != null && strlen($value) <= 15) + { + $length = strlen($value); + + // If it is fewer than 13 digits assume it is a local number + if ($length <= 12) + { + $result = '.' . $value; + } + else + { + // If it has 13 or more digits let's make a country code. + $cclen = $length - 12; + $result = substr($value, 0, $cclen) . '.' . substr($value, $cclen); + } + } + + // If not let's not save anything. + else + { + $result = ''; + } + } + + return $result; + } +} diff --git a/libraries/src/Form/Filter/UnsetFilter.php b/libraries/src/Form/Filter/UnsetFilter.php new file mode 100644 index 0000000000000..df2a14d5156b3 --- /dev/null +++ b/libraries/src/Form/Filter/UnsetFilter.php @@ -0,0 +1,43 @@ +` tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * @param Registry $input An optional Registry object with the entire data set to validate against the entire form. + * @param Form $form The form object for which the field is being tested. + * + * @return mixed The filtered value. + * + * @since __DEPLOY_VERSION__ + */ + public function filter(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null) + { + return null; + } +} diff --git a/libraries/src/Form/Filter/UrlFilter.php b/libraries/src/Form/Filter/UrlFilter.php new file mode 100644 index 0000000000000..a6381a927743c --- /dev/null +++ b/libraries/src/Form/Filter/UrlFilter.php @@ -0,0 +1,102 @@ +` tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * @param Registry $input An optional Registry object with the entire data set to validate against the entire form. + * @param Form $form The form object for which the field is being tested. + * + * @return mixed The filtered value. + * + * @since __DEPLOY_VERSION__ + */ + public function filter(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null) + { + if (empty($value)) + { + return false; + } + + // This cleans some of the more dangerous characters but leaves special characters that are valid. + $value = InputFilter::getInstance()->clean($value, 'html'); + $value = trim($value); + + // <>" are never valid in a uri see http://www.ietf.org/rfc/rfc1738.txt. + $value = str_replace(array('<', '>', '"'), '', $value); + + // Check for a protocol + $protocol = parse_url($value, PHP_URL_SCHEME); + + // If there is no protocol and the relative option is not specified, + // we assume that it is an external URL and prepend http://. + if (($element['type'] == 'url' && !$protocol && !$element['relative']) + || (!$element['type'] == 'url' && !$protocol)) + { + $protocol = 'http'; + + // If it looks like an internal link, then add the root. + if (substr($value, 0, 9) == 'index.php') + { + $value = Uri::root() . $value; + } + + // Otherwise we treat it as an external link. + else + { + // Put the url back together. + $value = $protocol . '://' . $value; + } + } + + // If relative URLS are allowed we assume that URLs without protocols are internal. + elseif (!$protocol && $element['relative']) + { + $host = Uri::getInstance('SERVER')->gethost(); + + // If it starts with the host string, just prepend the protocol. + if (substr($value, 0) == $host) + { + $value = 'http://' . $value; + } + + // Otherwise if it doesn't start with "/" prepend the prefix of the current site. + elseif (substr($value, 0, 1) != '/') + { + $value = Uri::root(true) . '/' . $value; + } + } + + $value = PunycodeHelper::urlToPunycode($value); + + return $value; + } +} diff --git a/libraries/src/Form/Form.php b/libraries/src/Form/Form.php index 81ae9d3e0e6a6..6e6c8d2a842fc 100644 --- a/libraries/src/Form/Form.php +++ b/libraries/src/Form/Form.php @@ -12,11 +12,8 @@ use Joomla\CMS\Factory; use Joomla\CMS\Filesystem\Path; -use Joomla\CMS\Filter\InputFilter; use Joomla\CMS\Language\Text; use Joomla\CMS\Object\CMSObject; -use Joomla\CMS\String\PunycodeHelper; -use Joomla\CMS\Uri\Uri; use Joomla\Registry\Registry; use Joomla\Utilities\ArrayHelper; @@ -123,7 +120,7 @@ public function bind($data) // Make sure there is a valid JForm XML document. if (!($this->xml instanceof \SimpleXMLElement)) { - return false; + 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. @@ -187,58 +184,6 @@ protected function bindLevel($group, $data) } } - /** - * 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 1.7.0 - */ - public function filter($data, $group = null) - { - // Make sure there is a valid JForm XML document. - if (!($this->xml instanceof \SimpleXMLElement)) - { - return false; - } - - $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 ? $attrs : array()); - $group = implode('.', $groups); - - $key = $group ? $group . '.' . $name : $name; - - // Filter the value if it exists. - if ($input->exists($key)) - { - $output->set($key, $this->filterField($field, $input->get($key, (string) $field['default']))); - } - } - - return $output->toArray(); - } - /** * Return all errors, if any. * @@ -267,7 +212,7 @@ public function getField($name, $group = null, $value = null) // Make sure there is a valid JForm XML document. if (!($this->xml instanceof \SimpleXMLElement)) { - return false; + 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. @@ -301,7 +246,7 @@ public function getFieldAttribute($name, $attribute, $default = null, $group = n // Make sure there is a valid JForm XML document. if (!($this->xml instanceof \SimpleXMLElement)) { - throw new \UnexpectedValueException(sprintf('%s::getFieldAttribute `xml` is not an instance of SimpleXMLElement', get_class($this))); + 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. @@ -387,7 +332,7 @@ public function getFieldsets($group = null) // Make sure there is a valid JForm XML document. if (!($this->xml instanceof \SimpleXMLElement)) { - return $fieldsets; + throw new \UnexpectedValueException(sprintf('%s::%s `xml` is not an instance of SimpleXMLElement', get_class($this), __METHOD__)); } if ($group) @@ -701,12 +646,6 @@ public function load($data, $replace = true, $xpath = false) { return false; } - - // Make sure the XML loaded correctly. - if (!$data) - { - return false; - } } // If we have no XML definition at this point let's make sure we get one. @@ -843,7 +782,7 @@ public function removeField($name, $group = null) // Make sure there is a valid JForm XML document. if (!($this->xml instanceof \SimpleXMLElement)) { - throw new \UnexpectedValueException(sprintf('%s::removeField `xml` is not an instance of SimpleXMLElement', get_class($this))); + 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. @@ -876,7 +815,7 @@ public function removeGroup($group) // Make sure there is a valid JForm XML document. if (!($this->xml instanceof \SimpleXMLElement)) { - throw new \UnexpectedValueException(sprintf('%s::removeGroup `xml` is not an instance of SimpleXMLElement', get_class($this))); + 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. @@ -934,7 +873,7 @@ public function setField(\SimpleXMLElement $element, $group = null, $replace = t // Make sure there is a valid JForm XML document. if (!($this->xml instanceof \SimpleXMLElement)) { - throw new \UnexpectedValueException(sprintf('%s::setField `xml` is not an instance of SimpleXMLElement', get_class($this))); + 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. @@ -1027,7 +966,7 @@ public function setFieldAttribute($name, $attribute, $value, $group = null) // Make sure there is a valid JForm XML document. if (!($this->xml instanceof \SimpleXMLElement)) { - throw new \UnexpectedValueException(sprintf('%s::setFieldAttribute `xml` is not an instance of SimpleXMLElement', get_class($this))); + 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. @@ -1071,7 +1010,7 @@ public function setFields(&$elements, $group = null, $replace = true, $fieldset // Make sure there is a valid JForm XML document. if (!($this->xml instanceof \SimpleXMLElement)) { - throw new \UnexpectedValueException(sprintf('%s::setFields `xml` is not an instance of SimpleXMLElement', get_class($this))); + 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. @@ -1079,7 +1018,7 @@ public function setFields(&$elements, $group = null, $replace = true, $fieldset { if (!($element instanceof \SimpleXMLElement)) { - throw new \UnexpectedValueException(sprintf('$element not SimpleXMLElement in %s::setFields', get_class($this))); + throw new \UnexpectedValueException(sprintf('%s::%s `xml` is not an instance of SimpleXMLElement', get_class($this), __METHOD__)); } } @@ -1133,6 +1072,83 @@ public function setValue($name, $group = null, $value = null) 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 __DEPLOY_VERSION__ + */ + 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 __DEPLOY_VERSION__ + */ + public function filter($data, $group = null) + { + // Make sure there is a valid JForm 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 ? $attrs : array()); + $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->filter($input->get($key, (string) $field['default']), $group, $input)); + } + } + + return $output->toArray(); + } + /** * Method to validate form data. * @@ -1152,7 +1168,7 @@ public function validate($data, $group = null) // Make sure there is a valid JForm XML document. if (!($this->xml instanceof \SimpleXMLElement)) { - return false; + throw new \UnexpectedValueException(sprintf('%s::%s `xml` is not an instance of SimpleXMLElement', get_class($this), __METHOD__)); } $return = true; @@ -1172,26 +1188,18 @@ public function validate($data, $group = null) // Validate the fields. foreach ($fields as $field) { - $value = null; $name = (string) $field['name']; - // Get the group names as strings for ancestor fields elements. + // Get the field groups for the element. $attrs = $field->xpath('ancestor::fields[@name]/@name'); $groups = array_map('strval', $attrs ? $attrs : array()); - $group = implode('.', $groups); + $attrGroup = implode('.', $groups); - // Get the value from the input data. - if ($group) - { - $value = $input->get($group . '.' . $name); - } - else - { - $value = $input->get($name); - } + $key = $attrGroup ? $attrGroup . '.' . $name : $name; + + $fieldObj = $this->loadField($field, $group); - // Validate the field. - $valid = $this->validateField($field, $group, $value, $input); + $valid = $fieldObj->validate($input->get($key), $group, $input); // Check for an error. if ($valid instanceof \Exception) @@ -1205,346 +1213,57 @@ public function validate($data, $group = null) } /** - * Method to apply an input filter to a value based on field data. + * Method to post-process form data. * - * @param string $element The XML element object representation of the form field. - * @param mixed $value The value to filter for the field. + * @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 The filtered value. + * @return mixed Array or false. * - * @since 1.7.0 + * @since 4.0 */ - protected function filterField($element, $value) + public function postProcess($data, $group = null) { - // Make sure there is a valid SimpleXMLElement. - if (!($element instanceof \SimpleXMLElement)) + // Make sure there is a valid SimpleXMLElement + if (!($this->xml instanceof \SimpleXMLElement)) { - return false; + throw new \UnexpectedValueException(sprintf('%s::%s `xml` is not an instance of SimpleXMLElement', get_class($this), __METHOD__)); } - // Get the field filter type. - $filter = (string) $element['filter']; + $input = new Registry($data); + $output = new Registry; - // Process the input value based on the filter. - $return = null; + // Get the fields for which to postProcess the data. + $fields = $this->findFieldsByGroup($group); - switch (strtoupper($filter)) + if (!$fields) { - // Access Control Rules. - case 'RULES': - $return = array(); - - foreach ((array) $value as $action => $ids) - { - // Build the rules array. - $return[$action] = array(); - - foreach ($ids as $id => $p) - { - if ($p !== '') - { - $return[$action][$id] = ($p == '1' || $p == 'true') ? true : false; - } - } - } - break; - - // Do nothing, thus leaving the return value as null. - case 'UNSET': - break; - - // No Filter. - case 'RAW': - $return = $value; - break; - - // Filter the input as an array of integers. - case 'INT_ARRAY': - // Make sure the input is an array. - if (is_object($value)) - { - $value = get_object_vars($value); - } - - $value = is_array($value) ? $value : array($value); - - $value = ArrayHelper::toInteger($value); - $return = $value; - break; - - // Filter safe HTML. - case 'SAFEHTML': - $return = InputFilter::getInstance(null, null, 1, 1)->clean($value, 'html'); - break; - - // Convert a date to UTC based on the server timezone offset. - case 'SERVER_UTC': - if ((int) $value > 0) - { - // Check if we have a localised date format - $translateFormat = (string) $element['translateformat']; - - if ($translateFormat && $translateFormat != 'false') - { - $showTime = (string) $element['showtime']; - $showTime = ($showTime && $showTime != 'false'); - $format = ($showTime) ? Text::_('DATE_FORMAT_FILTER_DATETIME') : Text::_('DATE_FORMAT_FILTER_DATE'); - $date = date_parse_from_format($format, $value); - $value = (int) $date['year'] . '-' . (int) $date['month'] . '-' . (int) $date['day']; - - if ($showTime) - { - $value .= ' ' . (int) $date['hour'] . ':' . (int) $date['minute'] . ':' . (int) $date['second']; - } - } - - // Get the server timezone setting. - $offset = Factory::getApplication()->get('offset'); - - // Return an SQL formatted datetime string in UTC. - try - { - $return = Factory::getDate($value, $offset)->toSql(); - } - catch (\Exception $e) - { - Factory::getApplication()->enqueueMessage( - Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', Text::_((string) $element['label'])), - 'warning' - ); - - $return = ''; - } - } - else - { - $return = ''; - } - break; - - // Convert a date to UTC based on the user timezone offset. - case 'USER_UTC': - if ((int) $value > 0) - { - // Check if we have a localised date format - $translateFormat = (string) $element['translateformat']; - - if ($translateFormat && $translateFormat != 'false') - { - $showTime = (string) $element['showtime']; - $showTime = ($showTime && $showTime != 'false'); - $format = ($showTime) ? Text::_('DATE_FORMAT_FILTER_DATETIME') : Text::_('DATE_FORMAT_FILTER_DATE'); - $date = date_parse_from_format($format, $value); - $value = (int) $date['year'] . '-' . (int) $date['month'] . '-' . (int) $date['day']; - - if ($showTime) - { - $value .= ' ' . (int) $date['hour'] . ':' . (int) $date['minute'] . ':' . (int) $date['second']; - } - } - - // Get the user timezone setting defaulting to the server timezone setting. - $offset = Factory::getUser()->getTimezone(); - - // Return a MySQL formatted datetime string in UTC. - try - { - $return = Factory::getDate($value, $offset)->toSql(); - } - catch (\Exception $e) - { - Factory::getApplication()->enqueueMessage( - Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', Text::_((string) $element['label'])), - 'warning' - ); - - $return = ''; - } - } - else - { - $return = ''; - } - break; - - /* - * Ensures a protocol is present in the saved field unless the relative flag is set. - * Only use when the only permitted protocols require '://'. - * See JFormRuleUrl for list of these. - */ - - case 'URL': - if (empty($value)) - { - return false; - } - - // This cleans some of the more dangerous characters but leaves special characters that are valid. - $value = InputFilter::getInstance()->clean($value, 'html'); - $value = trim($value); - - // <>" are never valid in a uri see http://www.ietf.org/rfc/rfc1738.txt. - $value = str_replace(array('<', '>', '"'), '', $value); - - // Check for a protocol - $protocol = parse_url($value, PHP_URL_SCHEME); - - // If there is no protocol and the relative option is not specified, - // we assume that it is an external URL and prepend http://. - if (($element['type'] == 'url' && !$protocol && !$element['relative']) - || (!$element['type'] == 'url' && !$protocol)) - { - $protocol = 'http'; - - // If it looks like an internal link, then add the root. - if (substr($value, 0, 9) == 'index.php') - { - $value = Uri::root() . $value; - } - - // Otherwise we treat it as an external link. - else - { - // Put the url back together. - $value = $protocol . '://' . $value; - } - } - - // If relative URLS are allowed we assume that URLs without protocols are internal. - elseif (!$protocol && $element['relative']) - { - $host = Uri::getInstance('SERVER')->gethost(); - - // If it starts with the host string, just prepend the protocol. - if (substr($value, 0) == $host) - { - $value = 'http://' . $value; - } - - // Otherwise if it doesn't start with "/" prepend the prefix of the current site. - elseif (substr($value, 0, 1) != '/') - { - $value = Uri::root(true) . '/' . $value; - } - } - - $value = PunycodeHelper::urlToPunycode($value); - $return = $value; - break; - - case 'TEL': - $value = trim($value); - - // Does it match the NANP pattern? - if (preg_match('/^(?:\+?1[-. ]?)?\(?([2-9][0-8][0-9])\)?[-. ]?([2-9][0-9]{2})[-. ]?([0-9]{4})$/', $value) == 1) - { - $number = (string) preg_replace('/[^\d]/', '', $value); - - if (substr($number, 0, 1) == 1) - { - $number = substr($number, 1); - } - - if (substr($number, 0, 2) == '+1') - { - $number = substr($number, 2); - } - - $result = '1.' . $number; - } - - // If not, does it match ITU-T? - elseif (preg_match('/^\+(?:[0-9] ?){6,14}[0-9]$/', $value) == 1) - { - $countrycode = substr($value, 0, strpos($value, ' ')); - $countrycode = (string) preg_replace('/[^\d]/', '', $countrycode); - $number = strstr($value, ' '); - $number = (string) preg_replace('/[^\d]/', '', $number); - $result = $countrycode . '.' . $number; - } - - // If not, does it match EPP? - elseif (preg_match('/^\+[0-9]{1,3}\.[0-9]{4,14}(?:x.+)?$/', $value) == 1) - { - if (strstr($value, 'x')) - { - $xpos = strpos($value, 'x'); - $value = substr($value, 0, $xpos); - } - - $result = str_replace('+', '', $value); - } - - // Maybe it is already ccc.nnnnnnn? - elseif (preg_match('/[0-9]{1,3}\.[0-9]{4,14}$/', $value) == 1) - { - $result = $value; - } - - // If not, can we make it a string of digits? - else - { - $value = (string) preg_replace('/[^\d]/', '', $value); - - if ($value != null && strlen($value) <= 15) - { - $length = strlen($value); - - // If it is fewer than 13 digits assume it is a local number - if ($length <= 12) - { - $result = '.' . $value; - } - else - { - // If it has 13 or more digits let's make a country code. - $cclen = $length - 12; - $result = substr($value, 0, $cclen) . '.' . substr($value, $cclen); - } - } - - // If not let's not save anything. - else - { - $result = ''; - } - } - - $return = $result; + // PANIC! + return false; + } - break; - default: - // Check for a callback filter. - if (strpos($filter, '::') !== false && is_callable(explode('::', $filter))) - { - $return = call_user_func(explode('::', $filter), $value); - } + // Filter the fields. + foreach ($fields as $field) + { + $name = (string) $field['name']; - // Filter using a callback function if specified. - elseif (function_exists($filter)) - { - $return = call_user_func($filter, $value); - } + // Get the field groups for the element. + $attrs = $field->xpath('ancestor::fields[@name]/@name'); + $groups = array_map('strval', $attrs ? $attrs : array()); + $attrGroup = implode('.', $groups); - // Check for empty value and return empty string if no value is required, - // otherwise filter using InputFilter. All HTML code is filtered by default. - else - { - $required = ((string) $element['required'] == 'true' || (string) $element['required'] == 'required'); + $key = $attrGroup ? $attrGroup . '.' . $name : $name; - if (($value === '' || $value === null) && ! $required) - { - $return = ''; - } - else - { - $return = InputFilter::getInstance()->clean($value, $filter); - } - } - break; + // 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 $return; + return $output->toArray(); } /** @@ -1565,7 +1284,7 @@ protected function findField($name, $group = null) // Make sure there is a valid JForm XML document. if (!($this->xml instanceof \SimpleXMLElement)) { - return false; + 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. @@ -1655,7 +1374,7 @@ protected function &findFieldsByFieldset($name) // Make sure there is a valid JForm XML document. if (!($this->xml instanceof \SimpleXMLElement)) { - return $false; + throw new \UnexpectedValueException(sprintf('%s::%s `xml` is not an instance of SimpleXMLElement', get_class($this), __METHOD__)); } /* @@ -1689,7 +1408,7 @@ protected function &findFieldsByGroup($group = null, $nested = false) // Make sure there is a valid JForm XML document. if (!($this->xml instanceof \SimpleXMLElement)) { - return $false; + throw new \UnexpectedValueException(sprintf('%s::%s `xml` is not an instance of SimpleXMLElement', get_class($this), __METHOD__)); } // Get only fields in a specific group? @@ -1763,7 +1482,7 @@ protected function &findGroup($group) // Make sure there is a valid JForm XML document. if (!($this->xml instanceof \SimpleXMLElement)) { - return $false; + 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. @@ -1843,19 +1562,19 @@ protected function loadField($element, $group = null, $value = null) // Make sure there is a valid SimpleXMLElement. if (!($element instanceof \SimpleXMLElement)) { - return false; + 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 JFormField object for the field. - $field = $this->loadFieldType($type); + $field = FormHelper::loadFieldType($type); // If the object could not be loaded, get a text field object. if ($field === false) { - $field = $this->loadFieldType('text'); + $field = FormHelper::loadFieldType('text'); } /* @@ -1900,37 +1619,6 @@ protected function loadField($element, $group = null, $value = null) } } - /** - * Proxy for {@link FormHelper::loadFieldType()}. - * - * @param string $type The field type. - * @param boolean $new Flag to toggle whether we should get a new instance of the object. - * - * @return FormField|boolean FormField object on success, false otherwise. - * - * @since 1.7.0 - */ - protected function loadFieldType($type, $new = true) - { - return FormHelper::loadFieldType($type, $new); - } - - /** - * Proxy for FormHelper::loadRuleType(). - * - * @param string $type The rule type. - * @param boolean $new Flag to toggle whether we should get a new instance of the object. - * - * @return FormRule|boolean FormRule object on success, false otherwise. - * - * @see FormHelper::loadRuleType() - * @since 1.7.0 - */ - protected function loadRuleType($type, $new = true) - { - return FormHelper::loadRuleType($type, $new); - } - /** * Method to synchronize any field, form or rule paths contained in the XML document. * @@ -1944,7 +1632,7 @@ protected function syncPaths() // Make sure there is a valid JForm XML document. if (!($this->xml instanceof \SimpleXMLElement)) { - return false; + 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. @@ -1980,6 +1668,17 @@ protected function syncPaths() self::addRulePath($path); } + // Get any addrulepath attributes from the form definition. + $paths = $this->xml->xpath('//*[@addfilterpath]/@addfilterpath'); + $paths = array_map('strval', $paths ? $paths : array()); + + // 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 ? $prefixes : array()); @@ -2010,92 +1709,14 @@ protected function syncPaths() FormHelper::addRulePrefix($prefix); } - return true; - } - - /** - * Method to validate a JFormField object based on field data. - * - * @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 find the field. - * @param mixed $value The optional value to use as the default for the field. - * @param Registry $input An optional Registry object with the entire data set to validate - * against the entire form. - * - * @return boolean Boolean true if field value is valid, Exception on failure. - * - * @since 1.7.0 - * @throws \InvalidArgumentException - * @throws \UnexpectedValueException - */ - protected function validateField(\SimpleXMLElement $element, $group = null, $value = null, Registry $input = null) - { - $valid = true; - - // Check if the field is required. - $required = ((string) $element['required'] == 'true' || (string) $element['required'] == 'required'); - - if ($required) - { - // If the field is required and the value is empty return an error message. - if (($value === '') || ($value === null)) - { - if ($element['label']) - { - $message = Text::_($element['label']); - } - else - { - $message = Text::_($element['name']); - } - - $message = Text::sprintf('JLIB_FORM_VALIDATE_FIELD_REQUIRED', $message); - - return new \RuntimeException($message); - } - } - - // Get the field validation rule. - if ($type = (string) $element['validate']) - { - // Load the JFormRule object for the field. - $rule = $this->loadRuleType($type); - - // If the object could not be loaded return an error message. - if ($rule === false) - { - throw new \UnexpectedValueException(sprintf('%s::validateField() rule `%s` missing.', get_class($this), $type)); - } - - // Run the field validation rule test. - $valid = $rule->test($element, $value, $group, $input, $this); - - // Check for an error in the validation test. - if ($valid instanceof \Exception) - { - return $valid; - } - } + // Get any addruleprefix attributes from the form definition. + $prefixes = $this->xml->xpath('//*[@addfilterprefix]/@addfilterprefix'); + $prefixes = array_map('strval', $prefixes ? $prefixes : array()); - // Check if the field is valid. - if ($valid === false) + // Add the field prefixes. + foreach ($prefixes as $prefix) { - // Does the field have a defined error message? - $message = (string) $element['message']; - - if ($message) - { - $message = Text::_($element['message']); - - return new \UnexpectedValueException($message); - } - else - { - $message = Text::_($element['label']); - $message = Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', $message); - - return new \UnexpectedValueException($message); - } + FormHelper::addFilterPrefix($prefix); } return true; @@ -2145,6 +1766,21 @@ public static function addRulePath($new = null) return FormHelper::addRulePath($new); } + /** + * Proxy for FormHelper::addFilterPath(). + * + * @param mixed $new A path or array of paths to add. + * + * @return array The list of paths that have been added. + * + * @see FormHelper::addFilterPath() + * @since __DEPLOY_VERSION__ + */ + public static function addFilterPath($new = null) + { + return FormHelper::addFilterPath($new); + } + /** * Method to get an instance of a form. * @@ -2326,17 +1962,11 @@ public function getAttribute($name, $default = null) { if ($this->xml instanceof \SimpleXMLElement) { - $attributes = $this->xml->attributes(); + $value = $this->xml->attributes()->$name; - // Ensure that the attribute exists - if (property_exists($attributes, $name)) + if ($value !== null) { - $value = $attributes->$name; - - if ($value !== null) - { - return (string) $value; - } + return (string) $value; } } diff --git a/libraries/src/Form/FormField.php b/libraries/src/Form/FormField.php index 88373d8ad4423..69d4550b3771d 100644 --- a/libraries/src/Form/FormField.php +++ b/libraries/src/Form/FormField.php @@ -10,9 +10,11 @@ defined('JPATH_PLATFORM') or die; +use Joomla\CMS\Filter\InputFilter; use Joomla\CMS\Language\Text; use Joomla\CMS\Layout\FileLayout; use Joomla\CMS\Log\Log; +use Joomla\Registry\Registry; use Joomla\String\Normalise; use Joomla\String\StringHelper; @@ -962,6 +964,167 @@ public function renderField($options = array()) return $this->getRenderer($this->renderLayout)->render($data); } + /** + * Method to filter a field value. + * + * @param mixed $value The optional value to use as the default for the field. + * @param string $group The optional dot-separated form group path on which to find the field. + * @param Registry $input An optional Registry object with the entire data set to filter + * against the entire form. + * + * @return mixed The filtered value. + * + * @since __DEPLOY_VERSION__ + */ + public function filter($value, $group = null, Registry $input = null) + { + // Make sure there is a valid SimpleXMLElement. + if (!($this->element instanceof \SimpleXMLElement)) + { + throw new \UnexpectedValueException(sprintf('%s::filter `element` is not an instance of SimpleXMLElement', get_class($this))); + } + + // Get the field filter type. + $filter = (string) $this->element['filter']; + + if ($filter != '') + { + $required = ((string) $this->element['required'] == 'true' || (string) $this->element['required'] == 'required'); + + if (($value === '' || $value === null) && !$required) + { + return ''; + } + + if (strpos($filter, '::') !== false && is_callable(explode('::', $filter))) + { + return call_user_func(explode('::', $filter), $value); + } + + // Load the JFormRule object for the field. JFormRule objects take precedence over PHP functions + $obj = FormHelper::loadFilterType($filter); + + // Run the filter rule. + if ($obj) + { + return $obj->filter($this->element, $value, $group, $input, $this->form); + } + + if (function_exists($filter)) + { + return call_user_func($filter, $value); + } + } + + return InputFilter::getInstance()->clean($value, $filter); + } + + /** + * Method to validate a JFormField object based on field data. + * + * @param mixed $value The optional value to use as the default for the field. + * @param string $group The optional dot-separated form group path on which to find the field. + * @param Registry $input An optional Registry object with the entire data set to validate + * against the entire form. + * + * @return boolean Boolean true if field value is valid, Exception on failure. + * + * @since __DEPLOY_VERSION__ + * @throws \InvalidArgumentException + * @throws \UnexpectedValueException + */ + public function validate($value, $group = null, \Joomla\Registry\Registry $input = null) + { + // Make sure there is a valid SimpleXMLElement. + if (!($this->element instanceof \SimpleXMLElement)) + { + throw new \UnexpectedValueException(sprintf('%s::validate `element` is not an instance of SimpleXMLElement', get_class($this))); + } + + $valid = true; + + // Check if the field is required. + $required = ((string) $this->element['required'] == 'true' || (string) $this->element['required'] == 'required'); + + // If the field is required and the value is empty return an error message. + if ($required && (($value === '') || ($value === null))) + { + if ($this->element['label']) + { + $message = Text::_($this->element['label']); + } + else + { + $message = Text::_($this->element['name']); + } + + $message = Text::sprintf('JLIB_FORM_VALIDATE_FIELD_REQUIRED', $message); + + return new \RuntimeException($message); + } + + // Get the field validation rule. + if ($type = (string) $this->element['validate']) + { + // Load the JFormRule object for the field. + $rule = FormHelper::loadRuleType($type); + + // If the object could not be loaded return an error message. + if ($rule === false) + { + throw new \UnexpectedValueException(sprintf('%s::validate() rule `%s` missing.', get_class($this), $type)); + } + + try + { + // Run the field validation rule test. + $valid = $rule->test($this->element, $value, $group, $input, $this->form); + } + catch (\Exception $e) + { + return $e; + } + } + + // Check if the field is valid. + if ($valid === false) + { + // Does the field have a defined error message? + $message = (string) $this->element['message']; + + if ($message) + { + $message = Text::_($this->element['message']); + } + else + { + $message = Text::_($this->element['label']); + $message = Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', $message); + } + + return new \UnexpectedValueException($message); + } + + return true; + } + + /** + * Method to post-process a field value. + * + * @param mixed $value The optional value to use as the default for the field. + * @param string $group The optional dot-separated form group path on which to find the field. + * @param Registry $input An optional Registry object with the entire data set to filter + * against the entire form. + * + * @return mixed The processed value. + * + * @since __DEPLOY_VERSION__ + */ + public function postProcess($value, $group = null, Registry $input = null) + { + return $value; + } + /** * Method to get the data to be passed to the layout for rendering. * diff --git a/libraries/src/Form/FormFilterInterface.php b/libraries/src/Form/FormFilterInterface.php new file mode 100644 index 0000000000000..9cc9d91e09203 --- /dev/null +++ b/libraries/src/Form/FormFilterInterface.php @@ -0,0 +1,38 @@ +` tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * @param Registry $input An optional Registry object with the entire data set to validate against the entire form. + * @param JForm $form The form object for which the field is being tested. + * + * @return mixed The filtered value. + * + * @since __DEPLOY_VERSION__ + */ + public function filter(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null); +} diff --git a/libraries/src/Form/FormHelper.php b/libraries/src/Form/FormHelper.php index 79e30eb1c422a..509847d9bd310 100644 --- a/libraries/src/Form/FormHelper.php +++ b/libraries/src/Form/FormHelper.php @@ -44,7 +44,7 @@ class FormHelper * @var string * @since 3.8.0 */ - protected static $prefixes = array('field' => array(), 'form' => array(), 'rule' => array()); + protected static $prefixes = array('field' => array(), 'form' => array(), 'rule' => array(), 'filter' => array()); /** * Static array of Form's entity objects for re-use. @@ -58,7 +58,7 @@ class FormHelper * @var array * @since 1.7.0 */ - protected static $entities = array('field' => array(), 'form' => array(), 'rule' => array()); + protected static $entities = array('field' => array(), 'form' => array(), 'rule' => array(), 'filter' => array()); /** * Method to load a form field object given a type. @@ -90,6 +90,21 @@ public static function loadRuleType($type, $new = true) return self::loadType('rule', $type, $new); } + /** + * Method to load a form filter object given a type. + * + * @param string $type The rule type. + * @param boolean $new Flag to toggle whether we should get a new instance of the object. + * + * @return FormFilter|boolean FormRule object on success, false otherwise. + * + * @since __DEPLOY_VERSION__ + */ + public static function loadFilterType($type, $new = true) + { + return self::loadType('filter', $type, $new); + } + /** * Method to load a form entity object given a type. * Each type is loaded only once and then used as a prototype for other objects of same type. @@ -159,6 +174,21 @@ public static function loadRuleClass($type) return self::loadClass('rule', $type); } + /** + * Attempt to import the FormFilter class file if it isn't already imported. + * You can use this method outside of Form for loading a filter for inheritance or composition. + * + * @param string $type Type of a filter whose class should be loaded. + * + * @return string|boolean Class name on success or false otherwise. + * + * @since __DEPLOY_VERSION__ + */ + public static function loadFilterClass($type) + { + return self::loadClass('filter', $type); + } + /** * Load a class for one of the form's entities of a particular type. * Currently, it makes sense to use this method for the "field" and "rule" entities @@ -301,6 +331,20 @@ public static function addRulePath($new = null) return self::addPath('rule', $new); } + /** + * Method to add a path to the list of filter include paths. + * + * @param mixed $new A path or array of paths to add. + * + * @return array The list of paths that have been added. + * + * @since __DEPLOY_VERSION__ + */ + public static function addFilterPath($new = null) + { + return self::addPath('filter', $new); + } + /** * Method to add a path to the list of include paths for one of the form's entities. * Currently supported entities: field, rule and form. You are free to support your own in a subclass. @@ -384,6 +428,20 @@ public static function addRulePrefix($new = null) return self::addPrefix('rule', $new); } + /** + * Method to add a namespace to the list of filter lookups. + * + * @param mixed $new A namespace or array of namespaces to add. + * + * @return array The list of namespaces that have been added. + * + * @since __DEPLOY_VERSION__ + */ + public static function addFilterPrefix($new = null) + { + return self::addPrefix('filter', $new); + } + /** * Method to add a namespace to the list of namespaces for one of the form's entities. * Currently supported entities: field, rule and form. You are free to support your own in a subclass.