* @copyright (c) 2006 - 2016 Stefan Gabos
* @package Generic
*/
class Zebra_Form_Control extends XSS_Clean
{
/**
* Array of HTML attributes of the element
*
* @var array
*
* @access private
*/
public $attributes;
/**
* Array of HTML attributes that the control's {@link render_attributes()} method should skip
*
* @var array
*
* @access private
*/
public $private_attributes;
/**
* Array of validation rules set for the control
*
* @var array
*
* @access private
*/
public $rules;
/**
* Constructor of the class
*
* @return void
*
* @access private
*/
function __construct()
{
$this->attributes = array(
'locked' => false,
'disable_xss_filters' => false,
);
$this->private_attributes = array();
$this->rules = array();
}
/**
* Call this method to instruct the script to force all letters typed by the user, to either uppercase or lowercase,
* in real-time.
*
* Works only on {@link Zebra_Form_Text text}, {@link Zebra_Form_Textarea textarea} and
* {@link Zebra_Form_Password password} controls.
*
*
* // create a new form
* $form = new Zebra_Form('my_form');
*
* // add a text control to the form
* $obj = $form->add('text', 'my_text');
*
* // entered characters will be upper-case
* $obj->change_case('upper');
*
* // don't forget to always call this method before rendering the form
* if ($form->validate()) {
* // put code here
* }
*
* // output the form using an automatically generated template
* $form->render();
*
*
* @param string $case The case to convert all entered characters to.
*
* Can be (case-insensitive) "upper" or "lower".
*
* @since 2.8
*
* @return void
*/
function change_case($case)
{
// make sure the argument is lowercase
$case = strtolower($case);
// if valid case specified
if ($case == 'upper' || $case == 'lower')
// add an extra class to the element
$this->set_attributes(array('class' => 'modifier-' . $case . 'case'), false);
}
/**
* Disables the SPAM filter for the control.
*
* By default, for checkboxes, radio buttons and select boxes, the library will prevent the submission of other
* values than those declared when creating the form, by triggering the error: "SPAM attempt detected!". Therefore,
* if you plan on adding/removing values dynamically, from JavaScript, you will have to call this method to prevent
* that from happening.
*
* Works only for {@link Zebra_Form_Checkbox checkbox}, {@link Zebra_Form_Radio radio} and
* {@link Zebra_Form_Select select} controls.
*
* @return void
*/
function disable_spam_filter()
{
// set the "disable_xss_filters" private attribute of the control
$this->set_attributes(array('disable_spam_filter' => true));
}
/**
* Disables XSS filtering of the control's submitted value.
*
* By default, all submitted values are filtered for XSS (Cross Site Scripting) injections. The script will
* automatically remove possibly malicious content (event handlers, javascript code, etc). While in general this is
* the right thing to do, there may be the case where this behaviour is not wanted: for example, for a CMS where
* the WYSIWYG editor inserts JavaScript code.
*
*
* // $obj is a reference to a control
* $obj->disable_xss_filters();
*
*
* @return void
*/
function disable_xss_filters()
{
// set the "disable_xss_filters" private attribute of the control
$this->set_attributes(array('disable_xss_filters' => true));
}
/**
* Returns the values of requested attributes.
*
*
* // create a new form
* $form = new Zebra_Form('my_form');
*
* // add a text field to the form
* $obj = $form->add('text', 'my_text');
*
* // set some attributes for the text field
* $obj->set_attributes(array(
* 'readonly' => 'readonly',
* 'style' => 'font-size:20px',
* ));
*
* // retrieve the attributes
* $attributes = $obj->get_attributes(array('readonly', 'style'));
*
* // the result will be an associative array
* //
* // $attributes = Array(
* // [readonly] => "readonly",
* // [style] => "font-size:20px"
* // )
*
*
* @param mixed $attributes A single or an array of attributes for which the values to be returned.
*
* @return array Returns an associative array where keys are the attributes and the values are
* each attribute's value, respectively.
*/
function get_attributes($attributes)
{
// initialize the array that will be returned
$result = array();
// if the request was for a single attribute,
// treat it as an array of attributes
if (!is_array($attributes)) $attributes = array($attributes);
// iterate through the array of attributes to look for
foreach ($attributes as $attribute)
// if attribute exists
if (array_key_exists($attribute, $this->attributes))
// populate the $result array
$result[$attribute] = $this->attributes[$attribute];
// return the results
return $result;
}
/**
* Returns the control's value after the form is submitted.
*
* This method is automatically called by the form's {@link Zebra_Form::validate() validate()} method!
*
*
* // $obj is a reference to a control
* $obj->get_submitted_value();
*
*
* @return void
*
* @access private
*/
function get_submitted_value()
{
// get some attributes of the control
$attribute = $this->get_attributes(array('name', 'type', 'value', 'disable_xss_filters', 'locked'));
// if control's value is not locked to the default value
if ($attribute['locked'] !== true) {
// strip any [] from the control's name (usually used in conjunction with multi-select select boxes and
// checkboxes)
$attribute['name'] = preg_replace('/\[\]/', '', $attribute['name']);
// reference to the form submission method
global ${'_' . $this->form_properties['method']};
$method = & ${'_' . $this->form_properties['method']};
// if form was submitted
if (
isset($method[$this->form_properties['identifier']]) &&
$method[$this->form_properties['identifier']] == $this->form_properties['name']
) {
// if control is a time picker control
if ($attribute['type'] == 'time') {
// combine hour, minutes and seconds into one single string (values separated by :)
// hours
$combined = (isset($method[$attribute['name'] . '_hours']) ? $method[$attribute['name'] . '_hours'] : '');
// minutes
$combined .= (isset($method[$attribute['name'] . '_minutes']) ? ($combined != '' ? ':' : '') . $method[$attribute['name'] . '_minutes'] : '');
// seconds
$combined .= (isset($method[$attribute['name'] . '_seconds']) ? ($combined != '' ? ':' : '') . $method[$attribute['name'] . '_seconds'] : '');
// AM/PM
$combined .= (isset($method[$attribute['name'] . '_ampm']) ? ($combined != '' ? ' ' : '') . $method[$attribute['name'] . '_ampm'] : '');
// create a super global having the name of our time picker control
// (remember, we don't have a control with the time picker's control name but three other controls
// having the time picker's control name as prefix and _hours, _minutes and _seconds respectively
// as suffix)
// we need to do this so that the values will also be filtered for XSS injection
$method[$attribute['name']] = $combined;
// unset the three temporary fields as we want to return to the user the result in a single field
// having the name he supplied
unset($method[$attribute['name'] . '_hours']);
unset($method[$attribute['name'] . '_minutes']);
unset($method[$attribute['name'] . '_seconds']);
unset($method[$attribute['name'] . '_ampm']);
}
// if control was submitted
if (isset($method[$attribute['name']])) {
// create the submitted_value property for the control and
// assign to it the submitted value of the control
$this->submitted_value = $method[$attribute['name']];
// if submitted value is an array
if (is_array($this->submitted_value)) {
// iterate through the submitted values
foreach ($this->submitted_value as $key => $value)
// and also, if magic_quotes_gpc is on (meaning that
// both single and double quotes are escaped)
// strip those slashes
if (get_magic_quotes_gpc()) $this->submitted_value[$key] = stripslashes($value);
// if submitted value is not an array
} else
// and also, if magic_quotes_gpc is on (meaning that both
// single and double quotes are escaped)
// strip those slashes
if (get_magic_quotes_gpc()) $this->submitted_value = stripslashes($this->submitted_value);
// if submitted value is an array
if (is_array($this->submitted_value))
// iterate through the submitted values
foreach ($this->submitted_value as $key => $value)
// filter the control's value for XSS injection and/or convert applicable characters to their equivalent HTML entities
$this->submitted_value[$key] = htmlspecialchars(!$attribute['disable_xss_filters'] ? $this->sanitize($value) : $value);
// if submitted value is not an array, filter the control's value for XSS injection and/or convert applicable characters to their equivalent HTML entities
else {
// don't apply htmlspecialchars to URLs and don't use rawurldecode neither
if (isset($this->rules['url'])) $this->submitted_value = (!$attribute['disable_xss_filters'] ? $this->sanitize($this->submitted_value, false) : $this->submitted_value);
// for all other values
else $this->submitted_value = htmlspecialchars(!$attribute['disable_xss_filters'] ? $this->sanitize($this->submitted_value) : $this->submitted_value);
}
// set the respective $_POST/$_GET value to the filtered value
$method[$attribute['name']] = $this->submitted_value;
// if control is a file upload control and a file was indeed uploaded
} elseif ($attribute['type'] == 'file' && isset($_FILES[$attribute['name']]))
$this->submitted_value = true;
// if control was not submitted
// we set this for those controls that are not submitted even
// when the form they reside in is (i.e. unchecked checkboxes)
// so that we know that they were indeed submitted but they
// just don't have a value
else $this->submitted_value = false;
if (
//if type is password, textarea or text OR
($attribute['type'] == 'password' || $attribute['type'] == 'textarea' || $attribute['type'] == 'text') &&
// control has the "uppercase" or "lowercase" modifier set
preg_match('/\bmodifier\-uppercase\b|\bmodifier\-lowercase\b/i', $this->attributes['class'], $modifiers)
) {
// if string must be uppercase, update the value accordingly
if ($modifiers[0] == 'modifier-uppercase') $this->submitted_value = strtoupper($this->submitted_value);
// otherwise, string needs to be lowercase
else $this->submitted_value = strtolower($this->submitted_value);
// set the respective $_POST/$_GET value to the updated value
$method[$attribute['name']] = $this->submitted_value;
}
}
// if control was submitted
if (isset($this->submitted_value)) {
// the assignment of the submitted value is type dependant
switch ($attribute['type']) {
// if control is a checkbox
case 'checkbox':
if (
(
// if is submitted value is an array
is_array($this->submitted_value) &&
// and the checkbox's value is in the array
in_array($attribute['value'], $this->submitted_value)
// OR
) ||
// assume submitted value is not an array and the
// checkbox's value is the same as the submitted value
$attribute['value'] == $this->submitted_value
// set the "checked" attribute of the control
) $this->set_attributes(array('checked' => 'checked'));
// if checkbox was "submitted" as not checked
// and if control's default state is checked, uncheck it
elseif (isset($this->attributes['checked'])) unset($this->attributes['checked']);
break;
// if control is a radio button
case 'radio':
if (
// if the radio button's value is the same as the
// submitted value
($attribute['value'] == $this->submitted_value)
// set the "checked" attribute of the control
) $this->set_attributes(array('checked' => 'checked'));
break;
// if control is a select box
case 'select':
// set the "value" private attribute of the control
// the attribute will be handled by the
// Zebra_Form_Select::_render_attributes() method
$this->set_attributes(array('value' => $this->submitted_value));
break;
// if control is a file upload control, a hidden control, a password field, a text field or a textarea control
case 'file':
case 'hidden':
case 'password':
case 'text':
case 'textarea':
case 'time':
// set the "value" standard HTML attribute of the control
$this->set_attributes(array('value' => $this->submitted_value));
break;
}
}
}
}
/**
* Locks the control's value. A locked control will preserve its default value after the form is submitted
* even if the user altered it.
*
* This doesn't mean that the submitted value will be the default one! It will still be the one selected by the
* user, but when and if the form is repainted, the value shown in the control will be the locked one, not the one
* selected by the user
*
*
* // $obj is a reference to a control
* $obj->lock();
*
*
* @return void
*/
function lock() {
// set the "locked" private attribute of the control
$this->set_attributes(array('locked' => true));
}
/**
* Resets the control's submitted value (empties text fields, unchecks radio buttons/checkboxes, etc).
*
* This method also resets the associated POST/GET/FILES superglobals!
*
*
* // $obj is a reference to a control
* $obj->reset();
*
*
* @return void
*/
function reset()
{
// reference to the form submission method
global ${'_' . $this->form_properties['method']};
$method = & ${'_' . $this->form_properties['method']};
// get some attributes of the control
$attributes = $this->get_attributes(array('type', 'name', 'other'));
// sanitize the control's name
$attributes['name'] = preg_replace('/\[\]/', '', $attributes['name']);
// see of what type is the current control
switch ($attributes['type']) {
// control is any of the types below
case 'checkbox':
case 'radio':
// unset the "checked" attribute
unset($this->attributes['checked']);
// unset the associated superglobal
unset($method[$attributes['name']]);
break;
// control is any of the types below
case 'date':
case 'hidden':
case 'password':
case 'select':
case 'text':
case 'textarea':
// simply empty the "value" attribute
$this->attributes['value'] = '';
// unset the associated superglobal
unset($method[$attributes['name']]);
// if control has the "other" attribute set
if (isset($attributes['other']))
// clear the associated superglobal's value
unset($method[$attributes['name'] . '_other']);
break;
// control is a file upload control
case 'file':
// unset the related superglobal
unset($_FILES[$attributes['name']]);
break;
// for any other control types
default:
// as long as control is not label, note nor captcha
if (
$attributes['type'] != 'label' &&
$attributes['type'] != 'note' &&
$attributes['type'] != 'captcha'
// unset the associated superglobal
) unset($method[$attributes['name']]);
}
}
/**
* Sets one or more of the control's attributes.
*
*
* // create a new form
* $form = new Zebra_Form('my_form');
*
* // add a text field to the form
* $obj = $form->add('text', 'my_text');
*
* // set some attributes for the text field
* $obj->set_attributes(array(
* 'readonly' => 'readonly',
* 'style' => 'font-size:20px',
* ));
*
* // retrieve the attributes
* $attributes = $obj->get_attributes(array('readonly', 'style'));
*
* // the result will be an associative array
* //
* // $attributes = Array(
* // [readonly] => "readonly",
* // [style] => "font-size:20px"
* // )
*
*
* @param array $attributes An associative array, in the form of attribute => value.
*
* @param boolean $overwrite Setting this argument to FALSE will instruct the script to append the values
* of the attributes to the already existing ones (if any) rather then overwriting
* them.
*
* Useful, for adding an extra CSS class to the already existing ones.
*
* For example, the {@link Zebra_Form_Text text} control has, by default, the
* class attribute set and already containing some classes needed both
* for styling and for JavaScript functionality. If there's the need to add one
* more class to the existing ones, without breaking styles nor functionality,
* one would use:
*
*
* // obj is a reference to a control
* $obj->set_attributes(array('class'=>'my_class'), false);
*
*
* Default is TRUE
*
* @return void
*/
function set_attributes($attributes, $overwrite = true)
{
// check if $attributes is given as an array
if (is_array($attributes))
// iterate through the given attributes array
foreach ($attributes as $attribute => $value) {
// we need to url encode the prefix as it may contain HTML entities which would produce validation errors
if ($attribute == 'data-prefix') $value = urlencode($value);
// if the value is to be appended to the already existing one
// and there is a value set for the specified attribute
// and the values do not represent an array
if (!$overwrite && isset($this->attributes[$attribute]) && !is_array($this->attributes[$attribute]))
// append the value
$this->attributes[$attribute] = $this->attributes[$attribute] . ' ' . $value;
// otherwise, add attribute to attributes array
else $this->attributes[$attribute] = $value;
}
}
/**
* Sets a single or an array of validation rules for the control.
*
*
* // $obj is a reference to a control
* $obj->set_rule(array(
* 'rule #1' => array($arg1, $arg2, ... $argn),
* 'rule #2' => array($arg1, $arg2, ... $argn),
* ...
* ...
* 'rule #n' => array($arg1, $arg2, ... $argn),
* ));
* // where 'rule #1', 'rule #2', 'rule #n' are any of the rules listed below
* // and $arg1, $arg2, $argn are arguments specific to each rule
*
*
* When a validation rule is not passed, a variable becomes available in the template file, having the name
* as specified by the rule's error_block argument and having the value as specified by the rule's
* error_message argument.
*
* Validation rules are checked in the given order, the exceptions being the "dependencies", "required" and
* "upload" rules, which are *always* checked in the order of priority: "dependencies" has priority over "required"
* which in turn has priority over "upload".
*
* I usually have at the top of my custom templates something like (assuming all errors are sent to an error block
* named "error"):
*
* echo (isset($zf_error) ? $zf_error : (isset($error) ? $error : ''));
*
* The above code nees to be used only for custom templates, or when the output is generated via callback
* functions! For automatically generated templates it is all taken care for you automatically by the library! Notice
* the $zf_error variable which is automatically created by the library if there is a SPAM or a CSRF error! Unless
* you use it, these errors will not be visible for the user. Again, remember, we're talking about custom templates,
* or output generated via callback functions.
*
* One or all error messages can be displayed in an error block.
* See the {@link Zebra_Form::show_all_error_messages() show_all_error_messages()} method.
*
* Everything related to error blocks applies only for server-side validation.
* See the {@link Zebra_Form::client_side_validation() client_side_validation()} method for configuring how errors
* are to be displayed to the user upon client-side validation.
*
* Available rules are
* - age
* - alphabet
* - alphanumeric
* - captcha
* - compare
* - convert
* - custom
* - date
* - datecompare
* - dependencies
* - digits
* - email
* - emails
* - filesize
* - filetype
* - float
* - image
* - length
* - number
* - range
* - regexp
* - required
* - resize
* - upload
* - url
*
* Rules description:
*
* - age
*
* 'age' => array($range, $error_block, $error_message)
*
* where
*
* - range is an array with 2 values, representing the minimum and maximum age allowed. 0 (zero) means
* "any age". Therefore, a range of array(21, 0) would validate ages above 20, array(6, 12) would
* validate ages between 6 and 12 (inclusive), while (0, 12) would validate ages below 13
*
* - error_block is the PHP variable to append the error message to, in case the rule does not validate
*
* - error_message is the error message to be shown when rule is not obeyed
*
* Validates if the difference in years between the current date and the date entered in the control is whitin the
* allowed range
*
* Available for the following controls: {@link Zebra_Form_Text text}
*
*
* // $obj is a reference to a control
* $obj->set_rule(
* 'age' => array(
* array(21, 0) // allow ages above 20
* 'error', // variable to add the error message to
* 'Age must be above 20' // error message if value doesn't validate
* )
* );
*
*
* - alphabet
*
* 'alphabet' => array($additional_characters, $error_block, $error_message)
*
* where
*
* - additional_characters is a list of additionally allowed characters besides the alphabet (provide
* an empty string if none); note that if you want to use / (backslash) you need to specify it as three (3)
* backslashes ("///")!
*
* - error_block is the PHP variable to append the error message to, in case the rule does not validate
*
* - error_message is the error message to be shown when rule is not obeyed
*
* Validates if the value contains only characters from the alphabet (case-insensitive a to z) plus characters
* given as additional characters (if any).
*
* Available for the following controls: {@link Zebra_Form_Password password}, {@link Zebra_Form_Text text},
* {@link Zebra_Form_Textarea textarea}
*
*
* // $obj is a reference to a control
* $obj->set_rule(
* 'alphabet' => array(
* '-' // allow alphabet plus dash
* 'error', // variable to add the error message to
* 'Only alphabetic characters allowed!' // error message if value doesn't validate
* )
* );
*
*
* - alphanumeric
*
* 'alphanumeric' => array($additional_characters, $error_block, $error_message)
*
* where
*
* - additional_characters is a list of additionally allowed characters besides the alphabet and
* digits 0 to 9 (provide an empty string if none); note that if you want to use / (backslash) you need to
* specify it as three (3) backslashes ("///")!
*
* - error_block is the PHP variable to append the error message to, in case the rule does not validate
*
* - error_message is the error message to be shown when rule is not obeyed
*
* Validates if the value contains only characters from the alphabet (case-insensitive a to z) and digits (0 to 9)
* plus characters given as additional characters (if any).
*
* Available for the following controls: {@link Zebra_Form_Password password}, {@link Zebra_Form_Text text},
* {@link Zebra_Form_Textarea textarea}
*
*
* // $obj is a reference to a control
* $obj->set_rule(
* 'alphanumeric' => array(
* '-', // allow alphabet, digits and dash
* 'error', // variable to add the error message to
* 'Only alphanumeric characters allowed!' // error message if value doesn't validate
* )
* );
*
*
* - captcha
*
* 'captcha' => array($error_block, $error_message)
*
* where
*
* - error_block is the PHP variable to append the error message to, in case the rule does not validate
*
* - error_message is the error message to be shown when rule is not obeyed
*
* Validates if the value matches the characters seen in the {@link Zebra_Form_Captcha captcha} image
* (therefore, there must be a {@link Zebra_Form_Captcha captcha} image on the form)
*
* Available only for the {@link Zebra_Form_Text text} control
*
* This rule is not available client-side!
*
*
* // $obj is a reference to a control
* $obj->set_rule(
* 'captcha' => array(
* 'error', // variable to add the error message to
* 'Characters not entered correctly!' // error message if value doesn't validate
* )
* );
*
*
* - compare
*
* 'compare' => array($control, $error_block, $error_message)
*
* where
*
* - control is the name of a control on the form to compare values with
*
* - error_block is the PHP variable to append the error message to, in case the rule does not validate
*
* - error_message is the error message to be shown when rule is not obeyed
*
* Validates if the value is the same as the value of the control indicated by control.
*
* Useful for password confirmation.
*
* Available for the following controls: {@link Zebra_Form_Password password}, {@link Zebra_Form_Text text},
* {@link Zebra_Form_Textarea textarea}
*
*
* // $obj is a reference to a control
* $obj->set_rule(
* 'compare' => array(
* 'password' // name of the control to compare values with
* 'error', // variable to add the error message to
* 'Password not confirmed correctly!' // error message if value doesn't validate
* )
* );
*
*
* - convert
*
* This rule requires the prior inclusion of the {@link http://stefangabos.ro/php-libraries/zebra-image Zebra_Image}
* library!
*
* 'convert' => array($type, $jpeg_quality, $preserve_original_file, $overwrite, $error_block, $error_message)
*
* where
*
* - type the type to convert the image to; can be (case-insensitive) JPG, PNG or GIF
*
* - jpeg_quality: Indicates the quality of the output image (better quality means bigger file size).
*
* Range is 0 - 100
*
* Available only if type is "jpg".
*
* - preserve_original_file: Should the original file be preserved after the conversion is done?
*
* - $overwrite: If a file with the same name as the converted file already exists, should it be
* overwritten or should the name be automatically computed.
*
* If a file with the same name as the converted file already exists and this argument is FALSE, a suffix of
* "_n" (where n is an integer) will be appended to the file name.
*
* - error_block is the PHP variable to append the error message to, in case the rule does not validate
*
* - error_message is the error message to be shown when rule is not obeyed
*
* This rule will convert an image file uploaded using the upload rule from whatever its type (as long as is one
* of the supported types) to the type indicated by type.
*
* Validates if the uploaded file is an image file and type is valid.
*
* This is not actually a "rule", but because it can generate an error message it is included here
*
* You should use this rule in conjunction with the upload and image rules.
*
* If you are also using the resize rule, make sure you are using it AFTER the convert rule!
*
* Available only for the {@link Zebra_Form_File file} control
*
* This rule is not available client-side!
*
*
* // $obj is a reference to a file upload control
* $obj->set_rule(
* 'convert' => array(
* 'jpg', // type to convert to
* 85, // converted file quality
* false, // preserve original file?
* false, // overwrite if converted file already exists?
* 'error', // variable to add the error message to
* 'File could not be uploaded!' // error message if value doesn't validate
* )
* );
*
*
* - custom
*
* Using this rule, custom rules can be applied to the submitted values.
*
* 'custom'=>array($callback_function_name, [optional arguments to be passed to the function], $error_block, $error_message)
*
* where
*
* - callback_function_name is the name of the callback function
*
* - error_block is the PHP variable to append the error message to, in case the rule does not validate
*
* - error_message is the error message to be shown when rule is not obeyed
*
* The callback function's first argument must ALWAYS be the control's submitted value. The optional arguments to
* be passed to the callback function will start as of the second argument!
*
* The callback function MUST return TRUE on success or FALSE on failure!
*
* Multiple custom rules can also be set through an array of callback functions:
*
*
* 'custom' => array(
*
* array($callback_function_name1, [optional arguments to be passed to the function], $error_block, $error_message),
* array($callback_function_name1, [optional arguments to be passed to the function], $error_block, $error_message)
*
* )
*
*
* If {@link Zebra_Form::client_side_validation() client-side validation} is enabled (enabled by default), the
* custom function needs to also be available in JavaScript, with the exact same name as the function in PHP!
*
* For example, here's a custom rule for checking that an entered value is an integer, greater than 21:
*
*
* // the custom function in JavaScript
*