* @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 *