<?php

define('ZEBRA_FORM_UPLOAD_RANDOM_NAMES', false);

/**
 *  Zebra_Form, a jQuery augmented PHP library for creating and validating HTML forms
 *
 *  It provides an easy and intuitive way of creating template-driven, visually appealing forms, complex client-side and
 *  server-side validations and prevention against cross-site scripting (XSS) and cross-site request forgery (CSRF) attacks
 *  prevention.
 *
 *  For the form validation part you can use the built-in rules (i.e. required fields, emails, minimum/maximum length,
 *  etc) and you can also define custom rules, with extreme ease, depending on your specific needs.
 *
 *  All the basic controls that you would find in a form are available plus a few extra: text, textarea, submit, image,
 *  reset, button, file, password, radio buttons, checkboxes, hidden, captcha, date and time pickers.
 *
 *  One additional note: this class is not a drag and drop utility - it is intended for coders who are comfortable with
 *  PHP, HTML, CSS and JavaScript/jQuery - you will have to build your forms when using this class, but it saves a great
 *  deal of time when it comes to validation and assures that your forms are secure and have a consistent look and feel
 *  throughout your projects!
 *
 *  Requires PHP 5.3.0+ (compiled with the php_fileinfo extension), and jQuery 1.6.2+
 *
 *  Visit {@link http://stefangabos.ro/php-libraries/zebra-form/} for more information.
 *
 *  For more resources visit {@link http://stefangabos.ro/}
 *
 *  @author     Stefan Gabos <contact@stefangabos.ro>
 *  @version    2.9.8 (last revision: May 18, 2016)
 *  @copyright  (c) 2006 - 2016 Stefan Gabos
 *  @license    http://www.gnu.org/licenses/lgpl-3.0.txt GNU LESSER GENERAL PUBLIC LICENSE
 *  @package    Zebra_Form
 */

class Zebra_Form
{

    /**
     *  Array containing all the controls added to the form
     *
     *  @var    array
     *
     *  @access private
     */
    var $controls;

    /**
     *  Array containing all the error messages generated by the form
     *
     *  @var    array
     *
     *  @access private
     */
    var $errors;

    /**
     *  An associative array of items uploaded to the current script via the HTTP POST method.
     *  This property, available only if a file upload has occurred, will have the same values as
     *  {@link http://php.net/manual/en/reserved.variables.files.php $_FILES} plus some extra values:
     *
     *  - <b>path</b>       -   the path where the file was uploaded to
     *  - <b>file_name</b>  -   the name the file was uploaded with
     *  - <b>imageinfo</b>  -   <b>available only if the uploaded file is an image!</b><br>
     *                          an array of attributes specific to the uploaded image as returned by
     *                          {@link http://www.php.net/manual/en/function.getimagesize.php getimagesize()} but
     *                          with meaningful names:<br>
     *                          <b>bits</b><br>
     *                          <b>channels</b><br>
     *                          <b>mime</b><br>
     *                          <b>width</b><br>
     *                          <b>height</b><br>
     *                          <b>type</b> ({@link http://php.net/manual/en/function.exif-imagetype.php possible types})<br>
     *                          <b>html</b><br>
     *
     *  <b>Note that the file name can be different than the original name of the uploaded file!</b>
     *
     *  By design, the script will append
     *  a number to the end of a file's name if at the path where the file is uploaded to there is another file with the
     *  same name (for example, if at the path where a file named "example.txt" is uploaded to, a file with the same name
     *  exists, the file's new name will be "example1.txt").
     *
     *  The file names can also be random-generated. See the {@link Zebra_Form_Control::set_rule() set_rule()} method and
     *  the <b>upload</b> rule
     *
     *  @var    array
     */
    var $file_upload;

    /**
     *  Indicates the {@link http://en.wikipedia.org/wiki/Filesystem_permissions filesystem} permissions to be set for
     *  files uploaded through the {@link Zebra_Form_Control::set_rule() upload} rule.
     *
     *  <code>
     *  $form->file_upload_permissions = '0777';
     *  </code>
     *
     *  The permissions are set using PHP's {@link http://php.net/manual/en/function.chmod.php chmod} function which may
     *  or may not be available or be disabled on your environment. If so, this action will fail silently (no errors or
     *  notices will be shown by the library).
     *
     *  Better to leave this setting as it is.
     *
     *  If you know what you are doing, here is how you can calculate the permission levels:
     *
     *  - 400 Owner Read
     *  - 200 Owner Write
     *  - 100 Owner Execute
     *  - 40 Group Read
     *  - 20 Group Write
     *  - 10 Group Execute
     *  - 4 Global Read
     *  - 2 Global Write
     *  - 1 Global Execute
     *
     *  Default is '0755'
     *
     *  @var    string
     */
    var $file_upload_permissions;

    /**
     *  Array containing the variables to be made available in the template file (added through the {@link assign()}
     *  method)
     *
     *  @var    array
     *
     *  @access private
     */
    var $variables;

    /**
     *  Constructor of the class
     *
     *  Initializes the form.
     *
     *  <code>
     *  $form = new Zebra_Form('myform');
     *  </code>
     *
     *  @param  string  $name           Name of the form
     *
     *  @param  string  $method         (Optional) Specifies which HTTP method will be used to submit the form data set.
     *
     *                                  Possible (case-insensitive) values are <b>POST</b> an <b>GET</b>
     *
     *                                  Default is <b>POST</b>
     *
     *  @param  string  $action         (Optional) An URI to where to submit the form data set.
     *
     *                                  If left empty, the form will submit to itself.
     *
     *                                  <samp>You should *always* submit the form to itself, or server-side validation
     *                                  will not take place and you will have a great security risk. Submit the form
     *                                  to itself, let it do the server-side validation, and then redirect accordingly!</samp>
     *
     *  @param  array   $attributes     (Optional) An array of attributes valid for a <form> tag (i.e. style)
     *
     *                                  Note that the following attributes are automatically set when the control is
     *                                  created and should not be altered manually:
     *
     *                                  <b>action</b>, <b>method</b>, <b>enctype</b>, <b>name</b>
     *
     *  @return void
     */
    function __construct($name, $method = 'POST', $action = '', $attributes = '')
    {

        $this->controls = $this->variables = $this->errors = $this->master_labels = array();

        // default filesysyem permissions for uploaded files
        $this->file_upload_permissions = '0755';

        // default values for the form's properties
        $this->form_properties = array(

            'action'                    =>  ($action == '' ? $_SERVER['REQUEST_URI'] : $action),
            'assets_server_path'        =>  rtrim(dirname(__FILE__), '\\/') . DIRECTORY_SEPARATOR,
            'assets_url'                =>  rtrim(str_replace('\\', '/', 'http' . (isset($_SERVER['HTTPS']) || (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https') ? 's' : '') . '://' . rtrim($_SERVER['HTTP_HOST'], '\\/') . '/' . substr(rtrim(dirname(__FILE__), '\\/'), strlen($_SERVER['DOCUMENT_ROOT']))), '\\/') . '/',
            'attributes'                =>  $attributes,
            'auto_fill'                 =>  false,
            'captcha_storage'           =>  'cookie',
            'csrf_cookie_config'        =>  array('path' => '/', 'domain' => '', 'secure' => false, 'httponly' => true),
            'csrf_cookie_name'          =>  'zebra_csrf_token_' . $name,
            'csrf_storage_method'       =>  'auto',
            'csrf_token'                =>  '',
            'csrf_token_lifetime'       =>  0,
            'csrf_token_name'           =>  'zebra_csrf_token_' . $name,
            'doctype'                   =>  'html',
            'has_upload'                =>  false,
            'honeypot'                  =>  'zebra_honeypot_' . $name,
            'identifier'                =>  'name_' . $name,
            'language'                  =>  array(),
            'method'                    =>  strtoupper($method),
            'name'                      =>  $name,
            'other_suffix'              =>  '_other',
            'secret_key'                =>  '',
            'show_all_error_messages'   =>  false,

        );

        // set default client-side validation properties
        $this->clientside_validation(true);

        // get the maximum allowed file size for uploads
        $upload_max_filesize = ini_get('upload_max_filesize');

        // see what it is given in (G, M, K)
        $unit = strtolower(substr($upload_max_filesize, -1));

        // get the numeric value
        $value = substr($upload_max_filesize, 0, -1);

        // convert to bytes
        // notice that there is no break
        switch (strtolower(substr($upload_max_filesize, -1))) {

            case 'g':
                $value*=1024;

            case 'm':
                $value*=1024;

            case 'k':
                $value*=1024;

        }

        // set the form's respective property
        $this->form_properties['max_file_size'] = $value;

        // include the XSS filter class - the Zebra_Form_Control class extends this class
        require_once dirname(__FILE__) . '/includes/XSSClean.php';

        // include the Control.php file which contains the Zebra_Form_Control class which is
        // extended by all of the classes
        require_once dirname(__FILE__) . '/includes/Control.php';

        // load the default language file
        $this->language('english');

        // enable protection against CSRF attacks using the default values
        // note that this has no effect if this method was already called before
        $this->csrf();

    }

    /**
     *  Adds a control to the form.
     *
     *  <code>
     *  //  create a new form
     *  $form = new Zebra_Form('my_form');
     *
     *  // add a text control to the form
     *  $obj = $form->add('text', 'my_text');
     *
     *  // make the text field required
     *  $obj->set_rule(
     *       'required' => array(
     *          'error',            // variable to add the error message to
     *          'Field is required' // error message if value doesn't validate
     *       )
     *  );
     *
     *  // 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();
     *  </code>
     *
     *  @param  string  $type   Type of the control to add.
     *
     *                          Controls that can be added to a form:
     *
     *                          -   {@link Zebra_Form_Button buttons}
     *                          -   {@link Zebra_Form_Captcha CAPTCHAs}
     *                          -   {@link Zebra_Form_Checkbox checkboxes}
     *                          -   {@link Zebra_Form_Date date pickers}
     *                          -   {@link Zebra_Form_File file upload controls}
     *                          -   {@link Zebra_Form_Hidden hidden controls}
     *                          -   {@link Zebra_Form_Image image button controls}
     *                          -   {@link Zebra_Form_Label labels}
     *                          -   {@link Zebra_Form_Note notes}
     *                          -   {@link Zebra_Form_Password password controls}
     *                          -   {@link Zebra_Form_Radio radio buttons}
     *                          -   {@link Zebra_Form_Reset reset buttons}
     *                          -   {@link Zebra_Form_Select selects}
     *                          -   {@link Zebra_Form_Submit submit buttons}
     *                          -   {@link Zebra_Form_Text text box controls}
     *                          -   {@link Zebra_Form_Textarea textareas}
     *                          -   {@link Zebra_Form_Time time pickers}
     *
     *  @param  mixed   $arguments  A list of arguments as required by the control that is added.
     *
     *  @return reference           Returns a reference to the newly created object
     */
    function &add($type)
    {

        // if shortcut for multiple radio buttons or checkboxes
        if ($type == 'radios' || $type == 'checkboxes') {

            // if there are less than 3 arguments
            if (func_num_args() < 3)

                // trigger a warning
                _zebra_form_show_error('For <strong>' . $type . '</strong>, the <strong>add()</strong> method requires at least 3 arguments', E_USER_WARNING);

            // if third argument is not an array
            elseif (!is_array(func_get_arg(2)))

                // trigger a warning
                _zebra_form_show_error('For <strong>' . $type . '</strong>, the <strong>add()</strong> method requires the 3rd argument to be an array', E_USER_WARNING);

            // if everything is ok
            else {

                // controls' name
                $name = func_get_arg(1);

                // the values and labels
                $values = func_get_arg(2);

                // a 4th argument (the default option) was passed to the method
                if (func_num_args() >= 4) {

                    // save the default value
                    $default = func_get_arg(3);

                    // if default value is not given as an array
                    // (makes sense for checkboxes when there may be multiple preselected values)
                    // make it an array
                    if (!is_array($default)) $default = array($default);

                }

                if (func_num_args() >= 5) $additional = func_get_arg(4);

                $counter = 0;

                // iterate through values and their respective labels
                foreach ($values as $value => $caption) {

                    // sanitize controls' name (remove square brackets)
                    $sanitize_name = preg_replace('/\[\]$/', '', $name);

                    // santize the value
                    $value = preg_replace('/\_{1,}/', '_', preg_replace('/[^a-z0-9\_]/i', '_', $value));

                    // create control
                    $obj = & $this->add(
                        ($type == 'radios' ? 'radio' : 'checkbox'), $name, $value,
                        (isset($default) && in_array($value, $default) ?
                            (isset($additional) ? array_merge($additional, array('checked' => 'checked')) : array('checked' => 'checked')) :
                            (isset($additional) ? $additional :'')));

                    // if this is the first control in the array
                    // we will later need to return a reference to it
                    if ($counter++ == 0) $pointer = &$obj;

                    // add the label for the control
                    $this->add('label', 'label_' . $sanitize_name . '_' . $value, $sanitize_name . '_' . $value, $caption);

                }

                // if the array of values was not empty
                // return reference to the first control
                if (isset($pointer)) return $pointer;

            }

        // for all other controls
        } else {

            $file_name = ucfirst(strtolower($type));

            // the classes have the "Zebra_Form_" prefix
            $class_name = 'Zebra_Form_' . ucfirst(strtolower($type));

            // include the file containing the PHP class, if not already included
            require_once dirname(__FILE__) . '/includes/' . $file_name . '.php';

            // if included file contains such a class
            if (class_exists($class_name)) {

                // prepare arguments passed to the add() method
                // notice that first argument is ignored as it refers to the type of the control to add
                // and we don't have to pass that to the class
                $arguments = array_slice(func_get_args(), 1);

                // if name was not specified trigger an error
                if (strlen(trim($arguments[0])) == 0) trigger_error('Name is required for control of type ' . $class_name, E_USER_ERROR);

                // use this method to instantiate the object with dynamic arguments
                $obj = call_user_func_array(array(new ReflectionClass($class_name), 'newInstance'), $arguments);

                // make available the form's properties in the newly created object
                $obj->form_properties = & $this->form_properties;

                // get some attributes for the newly created control
                $attributes = $obj->get_attributes(array('id', 'name'));

                // perform some extra tasks for different types of controls
                switch ($class_name) {

                    // if the newly created control is a file upload control
                    case 'Zebra_Form_File':

                        // set a flag to be used at rendering
                        $this->form_properties['has_upload'] = true;

                        break;

                    // if the newly created control is a radio button or a checkbox
                    case 'Zebra_Form_Radio':
                    case 'Zebra_Form_Checkbox':

                        // radio buttons and checkboxes might have a "master label", a label that is tied to the radio buttons' or
                        // checkboxes' name rather than individual controls' IDs. (as grouped radio buttons and checkboxes share
                        // the same name but have different values)
                        // we use this so that, if the controls have the "required" rule set, the asterisk is attached to the master
                        // label rather than to one of the actual controls

                        // therefore, we generate a "lookup" array of "master" labels for each group of radio buttons or
                        // checkboxes. this does not means that there will be an actual master label - we use this lookup
                        // array to easily determine if a master label exists when rendering the form

                        // sanitize the control's name
                        $attributes['name'] = preg_replace('/\[\]$/', '', $attributes['name']);

                        // if there isn't a master label for the group the current control is part of
                        if (!isset($this->master_labels[$attributes['name']]))

                            // create the entry
                            // the "control" index will hold the actual label's name if a "master" label is added to the form
                            $this->master_labels[$attributes['name']] = array('control' => false);

                        break;

                }

                // put the reference to the newly created object in the 'controls' array
                $this->controls[$attributes['id']] = &$obj;

                // return the identifier to the newly created object
                return $obj;

            }

        }

    }

    /**
     *  Appends a message to an already existing {@link Zebra_Form_Control::set_rule() error block}
     *
     *  <code>
     *  //  create a new form
     *  $form = new Zebra_Form('my_form');
     *
     *  // add a text control to the form
     *  $obj = $form->add('text', 'my_text');
     *
     *  // make the text field required
     *  $obj->set_rule(
     *       'required' => array(
     *          'error',            // variable to add the error message to
     *          'Field is required' // error message if value doesn't validate
     *       )
     *  );
     *
     *  // don't forget to always call this method before rendering the form
     *  if ($form->validate()) {
     *
     *      // for the purpose of this example, we will do a custom validation
     *      // after calling the "validate" method.
     *      // for custom validations, using the "custom" rule is recommended instead
     *
     *      // check if value's is between 1 and 10
     *      if ((int)$_POST['my_text']) < 1 || (int)$_POST['my_text']) > 10) {
     *
     *          $form->add_error('error', 'Value must be an integer between 1 and 10!');
     *
     *      } else {
     *
     *          // put code here that is to be executed when the form values are ok
     *
     *      }
     *
     *  }
     *
     *  //  output the form using an automatically generated template
     *  $form->render();
     *  </code>
     *
     *  @param  string  $error_block    The name of the error block to append the error message to (also the name
     *                                  of the PHP variable that will be available in the template file).
     *
     *  @param  string  $error_message  The error message to append to the error block.
     *
     *  @return void
     */
    function add_error($error_block, $error_message)
    {

        // if the error block was not yet created, create the error block
        if (!isset($this->errors[$error_block])) $this->errors[$error_block] = array();

        // if the same exact message doesn't already exists
        if (!in_array(trim($error_message), $this->errors[$error_block]))

            // append the error message to the error block
            $this->errors[$error_block][] = trim($error_message);

    }

    /**
     *  Set the server path and URL to the "process.php" and "mimes.json" files.
     *
     *  These files are required for CAPTCHAs and uploads.
     *
     *  By default, the location of these files is in the same folder as Zebra_Form.php and the script will automatically
     *  try to determine both the server path and the URL to these files. However, when the script is run on a virtual
     *  host, or if these files were moved, the script may not correctly determine the paths to these files. In these
     *  instances, use this method to correctly set the server path - needed by the script to correctly include these
     *  files, and the URL - needed by the client-side validation to include these files.
     *
     *  Also, for security reasons, I recommend moving these two files by default to the root of your website (or another
     *  publicly accessible place) and manually set the paths, in order to prevent malicious users from finding out
     *  information about your directory structure.
     *
     *  <samp>If you move these files don't forget to also move the font file from the "includes" folder, and to manually
     *  adjust the path to the font file in "process.php"!</samp>
     *
     *  @param  string  $server_path    The server path (the one similar to what is in $_SERVER['DOCUMENT_ROOT']) to
     *                                  the folder where the "process.php" and "mimes.json" files can be found.
     *
     *                                  <i>With trailing slash!</i>
     *
     *  @param  string  $url            The URL to where the "process.php" and "mimes.json" files can be found.
     *
     *                                  <i>With trailing slash!</i>
     *
     *  @return void
     */
    function assets_path($server_path, $url)
    {

        // set values
        $this->form_properties['assets_server_path'] = $server_path;
        $this->form_properties['assets_url'] = $url;

    }

    /**
     *  Creates a PHP variable with the given value, available in the template file.
     *
     *  <code>
     *  //  create a new form
     *  $form = new Zebra_Form('my_form');
     *
     *  // make available the $my_value variable in the template file
     *  $form->assign('my_value', '100');
     *
     *  // don't forget to always call this method before rendering the form
     *  if ($form->validate()) {
     *      // put code here
     *  }
     *
     *  // output the form
     *  // notice that we are using a custom template
     *  // my_template.php file is expected to be found
     *  // and in this file, you may now use the $my_value variable
     *  $form->render('my_template.php');
     *  </code>
     *
     *  @param  string  $variable_name  Name by which the variable will be available in the template file.
     *
     *  @param  mixed   $value          The value to be assigned to the variable.
     *
     *  @return void
     */
    function assign($variable_name, $value)
    {

        // save the variable in an array that we will make available in the template file upon rendering
        $this->variables[$variable_name] = $value;

    }

    /**
     *  Call this method anytime *before* calling the {@link validate()} method (preferably, right after instantiating
     *  the class) to instruct the library to automatically fill out all of the form's fields with random content while
     *  obeying any rules that might be set for each control.
     *
     *  You can also use this method to set defaults for the form's elements by setting the method's second argument to TRUE.
     *
     *  <b>Notes:</b>
     *
     *  -   unless overridden, the value of {@link Zebra_Form_Password password} controls will always be "12345678";
     *  -   unless overridden, the value of controls having the "email" or "emails" {@link Zebra_Form_Control::set_rule() rule}
     *      set will be in the form of <i>random_text@random_text.com</i>;
     *  -   unless overridden, the value of controls having the "url" {@link Zebra_Form_Control::set_rule() rule} set will
     *      be in the form of <i>random_text.com</i>, prefixed or not with "http://", depending on the rule's attributes;
     *  -   {@link Zebra_Form_File file upload} controls and controls having the "captcha" or "regexp"
     *      {@link Zebra_Form_Control::set_rule() rule} set will *not* be autofilled;
     *
     *  <samp>This method will produce results *only* if the form has not yet been submitted! Also, this method will fill
     *  elements with random content *only* if the element does not already has a default value! And finally, this method
     *  will produce no results unless at some point the form's {@link validate()} method is called.</samp>
     *
     *  @param  array   $defaults           An associative array in the form of <i>$element => $value</i> used for filling
     *                                      out specific fields with specific values instead of random ones.
     *
     *                                      For elements that may have more than one value (checkboxes and selects with
     *                                      the "multiple" attribute set) you may set the value as an array.
     *
     *                                      <code>
     *                                      // auto-fill all fields with random values
     *                                      $form->auto_fill();
     *
     *                                      // auto-fill all fields with random values
     *                                      // except the one called "email" which should have a custom value
     *                                      $form->auto_fill(array(
     *                                          'email'     =>  'some@email.com',
     *                                      ));
     *
     *                                      // auto-fill all fields with random values
     *                                      // except a checkboxes group where we select multiple values
     *                                      // note that we use "my_checkboxes" insteas of "my_checkboxes[]"
     *                                      // (the same goes for "selects" with the "multiple" attribute set)
     *                                      $form->auto_fill(array(
     *                                          'my_checkboxes' =>  array('value_1', 'value_2'),
     *                                      ));
     *                                      </code>
     *
     *  @param  boolean $specifics_only     (Optional) If set to TRUE only the fields given in the $defaults argument
     *                                      will be filled with the given values and the other fields will be skipped.
     *
     *                                      Can be used as a handy shortcut for giving default values to elements instead
     *                                      of putting default values in the constructor or using the {@link set_attributes()}
     *                                      method.
     *
     *                                      Default is FALSE.
     *
     *
     *  @since 2.8.9
     *
     *  @return void
     */
    function auto_fill($defaults = array(), $specifics_only = false)
    {

        $this->form_properties['auto_fill'] = array($defaults, $specifics_only);

    }

    /**
     *  Sets the storage method for CAPTCHA values.
     *
     *  By default, captcha values are triple md5 hashed and stored in cookies, and when the user enters the captcha
     *  value the value is also triple md5 hashed and the two values are then compared.
     *
     *  Sometimes, your users may have a very restrictive cookie policy and so cookies will not be set, and therefore,
     *  they will never be able to get past the CAPTCHA control. If it's the case, call this method and set the storage
     *  method to "session".
     *
     *  In this case, call this method and set the the storage method to "session".
     *
     *  @param  string  $method     Storage method for CAPTCHA values.
     *
     *                              Valid values are "cookie" and "session".
     *
     *                              Default is "cookie".
     *
     *  @since  2.8.9
     *
     *  @return void
     */
    function captcha_storage($method)
    {

        // if storage method is "session"
        if ($method == 'session')

            // set the storage method
            $this->form_properties['captcha_storage'] = 'session';

        // "cookie" otherwise
        else $this->form_properties['captcha_storage'] = 'cookie';

    }

    /**
     *  Alias of {@link clientside_validation()} method.
     *
     *  Deprecated since 2.8.9
     */
    function client_side_validation($properties)
    {

        $this->clientside_validation($properties);

    }

    /**
     *  Sets properties for the client-side validation.
     *
     *  Client-side validation, when enabled, occurs on the "onsubmit" event of the form.
     *
     *  <code>
     *  //  create a new form
     *  $form = new Zebra_Form('my_form');
     *
     *  // disable client-side validation
     *  $form->clientside_validation(false);
     *
     *  // enable client-side validation using default properties
     *  $form->clientside_validation(true);
     *
     *  // enable client-side validation using customized properties
     *  $form->clientside_validation(array(
     *      'close_tips'                =>  false,      //  don't show a "close" button on tips with error messages
     *      'on_ready'                  =>  false,      //  no function to be executed when the form is ready
     *      'disable_upload_validation' =>  true,       //  using a custom plugin for managing file uploads
     *      'scroll_to_error'           =>  false,      //  don't scroll the browser window to the error message
     *      'tips_position'             =>  'right',    //  position tips with error messages to the right of the controls
     *      'validate_on_the_fly'       =>  false,      //  don't validate controls on the fly
     *      'validate_all'              =>  false,      //  show error messages one by one upon trying to submit an invalid form
     *  ));
     *  </code>
     *
     *  To access the JavaScript object and use the public methods provided by it, use $('#formname').data('Zebra_Form')
     *  where <i>formname</i> is the form's name <b>with any dashes turned into underscores!</b>
     *
     *  <i>Therefore, if a form's name is "my-form", the JavaScript object would be accessed like $('my_form').data('Zebra_Form').</i>
     *
     *  From JavaScript, these are the methods that can be called on this object:
     *
     *  -   <b>attach_tip(element, message)</b> -   displays a custom error message, attached to the specified jQuery
     *                                              element
     *  -   <b>clear_errors()</b>               -   hides all error messages;
     *  -   <b>submit()</b>                     -   submits the form;
     *  -   <b>validate()</b>                   -   checks if the form is valid; returns TRUE or FALSE;
     *                                              <i>if called with the "false" boolean argument, error messages will
     *                                              not be shown in case form does not validate</i>
     *
     *  Here's how you can use these methods, in JavaScript:
     *
     *  <code>
     *  //  let's submit the form when clicking on a random button
     *
     *  // get a reference to the Zebra_Form object
     *  var $form = $('#formname').data('Zebra_Form');
     *
     *  // handle the onclick event on a random button
     *  $('#somebutton').bind('click', function(e) {
     *
     *      // stop default action
     *      e.preventDefault();
     *
     *      // validate the form, and if the form validates
     *      if ($form.validate()) {
     *
     *          // do your thing here
     *
     *          // when you're done, submit the form
     *          $form.submit();
     *
     *      }
     *
     *      // if the form is not valid, everything is handled automatically by the library
     *
     *  });
     *  </code>
     *
     *  @param  mixed   $properties     Can have the following values:
     *
     *                                  -   FALSE, disabling the client-side validation;
     *                                      <i>Note that the {@link Zebra_Form_Control::set_rule() dependencies} rule will
     *                                      still be checked client-side so that callback functions get called, if it is
     *                                      the case!</i>
     *                                  -   TRUE, enabling the client-side validation with the default properties;
     *
     *                                  -   an associative array with customized properties for the client-side validation;
     *
     *                                  In this last case, the available properties are:
     *
     *                                  -   <b>close_tips</b>, boolean, TRUE or FALSE<br>
     *                                      Specifies whether the tips with error messages should have a "close" button
     *                                      or not<br>
     *                                      Default is <b>TRUE</b>.
     *
     *                                  -   <b>disable_upload_validation</b>, boolean, TRUE or FALSE<br>
     *                                      Useful for disabling all client-side processing of file upload controls, so
     *                                      that custom plugins may be used for it (like, for instance,
     *                                      {@link http://blueimp.github.io/jQuery-File-Upload/basic.html this one})
     *                                      Default is <b>FALSE</b>.
     *
     *                                  -   <b>on_ready</b>, JavaScript function to be executed when the form is loaded.
     *                                      Useful for getting a reference to the Zebra_Form object after everything is
     *                                      loaded.
     *
     *                                      <code>
     *                                      $form->clientside_validation(array(
     *                                          // where $form is a global variable and 'id' is the form's id
     *                                          'on_ready': 'function() { $form = $("#id").data('Zebra_Form'); }',
     *                                      ));
     *                                      </code>
     *
     *                                  -   <b>scroll_to_error</b>, boolean, TRUE or FALSE<br>
     *                                      Specifies whether the browser window should be scrolled to the error message
     *                                      or not.<br>
     *                                      Default is <b>TRUE</b>.
     *
     *                                  -   <b>tips_position</b>, string, <i>left</i>, <i>right</i> or <i>center</i><br>
     *                                      Specifies where the error message tip should be positioned relative to the
     *                                      control.<br>
     *                                      Default is <b>left</b>.
     *
     *                                  -   <b>validate_on_the_fly</b>, boolean, TRUE or FALSE<br>
     *                                      Specifies whether values should be validated as soon as the user leaves a
     *                                      field; if set to TRUE and the validation of the control fails, the error
     *                                      message will be shown right away<br>
     *                                      Default is <b>FALSE</b>.
     *
     *                                  -   <b>validate_all</b>, boolean, TRUE or FALSE<br>
     *                                      Specifies whether upon submitting the form, should all error messages be
     *                                      shown at once if there are any errors<br>
     *                                      Default is <b>FALSE</b>.
     *
     *  @return void
     */
    function clientside_validation($properties)
    {

        // default properties of the client-side validation
        $defaults = array(
            'clientside_disabled'       =>  false,
            'close_tips'                =>  true,
            'disable_upload_validation' =>  false,
            'on_ready'                  =>  false,
            'scroll_to_error'           =>  true,
            'tips_position'             =>  'left',
            'validate_on_the_fly'       =>  false,
            'validate_all'              =>  false,
        );

        // set the default properties for the client-side validation
        if (!isset($this->form_properties['clientside_validation'])) $this->form_properties['clientside_validation'] = $defaults;

        // if client-side validation needs to be disabled
        if ($properties == false) $this->form_properties['clientside_validation']['clientside_disabled'] = true;

        // if custom settings for client-side validation
        elseif (is_array($properties))

            // merge the new settings with the old ones
            $this->form_properties['clientside_validation'] = array_merge($this->form_properties['clientside_validation'], $properties);

    }

    /**
     *  By default, this class generates <b>HTML 4.01 Strict</b> markup.
     *
     *  Use this method if you want the generated HTML markup to validate as <b>XHTML 1.0 Strict</b>.
     *
     *  @param  string  $doctype    (Optional) The DOCTYPE of the generated HTML markup.
     *
     *                              Possible (case-insensitive) values are <b>HTML</b> or <b>XHTML</b>
     *
     *                              Default is HTML.
     *
     *  @return void
     */
    function doctype($doctype = 'html')
    {

        // set the doctype
        $this->form_properties['doctype'] = (strtolower($doctype) == 'xhtml' ? 'xhtml' : 'html');

    }

    /**
     *  Enables protection against {@link http://en.wikipedia.org/wiki/Cross-site_request_forgery Cross-site request
     *  forgery} (CSRF) attacks.
     *
     *  Read more about specifics and a simple implementation on
     *  {@link http://shiflett.org/articles/cross-site-request-forgeries Chris Shiflett's website}.
     *
     *  This method is automatically called by the library, so protection against CSRF attacks is enabled by default for
     *  all forms and the script will decide automatically on the method to use for storing the CSRF token: if a session
     *  is already started then the CSRF token will be stored in a session variable or, if a session is not started, the
     *  CSRF token will be stored in a session cookie (cookies that expire when the browser is closed) but, in this case,
     *  it offers a lower level of security.
     *
     *  If cookies are used for storage, you'll have to make sure that Zebra_Form is instantiated before any output from
     *  your script (this is a protocol restriction) including &lt;html> and &lt;head> tags as well as any whitespace!
     *  A workaround is to turn output buffering on in php.ini.
     *
     *  <i>You are encouraged to start a PHP session before instantiating this class in order to maximize the level of
     *  security of your forms.</i>
     *
     *  The CSRF token is automatically regenerated when the form is submitted regardless if the form validated or not.
     *  A notable exception is that the form doesn't validate but was submitted via AJAX the CSRF token will not be
     *  regenerated - useful if you submit forms by AJAX.
     *
     *  As an added benefit, protection against CSRF attacks prevents "double posts" by design.
     *
     *  <samp>You only need to call this method if you want to change CSRF related settings. If you do call this method
     *  then you should call it right after instantiating the class and *before* calling the form's {@link validate()}
     *  and {@link render()} methods!</samp>
     *
     *  <code>
     *  // recommended usage is:
     *
     *  // call session_start() somewhere in your code but before outputting anything to the browser
     *  session_start();
     *
     *  // include the Zebra_Form
     *  require 'path/to/Zebra_Form.php';
     *
     *  // instantiate the class
     *  // protection against CSRF attack will be automatically enabled
     *  // but will be less secure if a session is not started (as it will
     *  // rely on cookies)
     *  $form = new Zebra_Form('my_form');
     *  </code>
     *
     *  @param  string  $csrf_storage_method    (Optional) Sets whether the CSRF token should be stored in a cookie, in
     *                                          a session variable, or let the script to automatically decide and use
     *                                          sessions if available or a cookie otherwise.
     *
     *                                          Possible values are "auto", "cookie", "session" or boolean FALSE.
     *
     *                                          If value is "auto", the script will decide automatically on what to use:
     *                                          if a session is already started then the CSRF token will be stored in a
     *                                          session variable, or, if a session is not started, the CSRF token will be
     *                                          stored in a cookie with the parameters as specified by the
     *                                          <b>csrf_cookie_config</b> argument (read below).
     *
     *                                          If value is "cookie" the CSRF token will be stored in a cookie with the
     *                                          parameters as specified by the <b>csrf_cookie_config</b> argument (read
     *                                          below).
     *
     *                                          If value is "session" the CSRF token will be stored in a session variable
     *                                          and thus a session must be started before instantiating the library.
     *
     *                                          If value is boolean FALSE (not recommended), protection against CSRF
     *                                          attack will be disabled.
     *
     *                                          The stored value will be compared, upon for submission, with the value
     *                                          stored in the associated hidden field, and if the two values do not match
     *                                          the form will not validate.
     *
     *                                          Default is "auto".
     *
     *  @param  integer $csrf_token_lifetime    (Optional) The number of seconds after which the CSRF token is to be
     *                                          considered as expired.
     *
     *                                          If set to "0" the tokens will expire at the end of the session (when the
     *                                          browser closes or session expires).
     *
     *                                          <i>Note that if csrf_storage_method is set to "session" this value cannot
     *                                          be higher than the session's life time as, if idle, the session will time
     *                                          out regardless of this value!</i>
     *
     *                                          Default is 0.
     *
     *  @param  array   $csrf_cookie_config     (Optional) An associative array containing the properties to be used when
     *                                          setting the cookie with the CSRF token (if <b>csrf_storage_method</b> is
     *                                          set to "cookie").
     *
     *                                          The properties that can be set are "path", "domain", "secure" and "httponly".
     *                                          where:
     *
     *                                          -   <b>path</b>     -   the path on the server in which the cookie will
     *                                                                  be available on. If set to "/", the cookie will
     *                                                                  be available within the entire domain. If set to
     *                                                                  '/foo/', the cookie will only be available within
     *                                                                  the /foo/ directory and all subdirectories such
     *                                                                  as /foo/bar/ of domain.<br>
     *                                                                  Default is "/"
     *
     *                                          -   <b>domain</b>   -   The domain that the cookie will be available on.
     *                                                                  To make the cookie available on all subdomains of
     *                                                                  example.com, domain should be set to to
     *                                                                  ".example.com". The . (dot) is not required but
     *                                                                  makes it compatible with more browsers. Setting
     *                                                                  it to "www.example.com" will make the cookie
     *                                                                  available only in the www subdomain.
     *
     *                                          -   <b>secure</b>   -   Indicates whether cookie information should only
     *                                                                  be transmitted over a HTTPS connection.<br>
     *                                                                  Default is FALSE.
     *
     *                                          -   <b>httponly</b> -   When set to TRUE the cookie will be made accessible
     *                                                                  only through the HTTP protocol. This means that
     *                                                                  the cookie won't be accessible by scripting languages,
     *                                                                  such as JavaScript. It has been suggested that
     *                                                                  this setting can effectively help to reduce identity
     *                                                                  theft through XSS attacks (although it is not
     *                                                                  supported by all browsers), but that claim is often
     *                                                                  disputed. Available only in PHP 5.2.0+<br>
     *                                                                  Default is FALSE
     *
     *                                                                  Available only for PHP 5.2.0+ and will be ignored
     *                                                                  if not available.
     *
     *                                          Not all properties must be set - for the properties that are not set, the
     *                                          default values will be used instead.
     *
     *  @since  2.8.4
     *
     *  @return void
     */
    function csrf($csrf_storage_method = 'auto', $csrf_token_lifetime = 0, $csrf_cookie_config = array('path' => '/', 'domain' => '', 'secure' => false, 'httponly' => true))
    {

        // continue only if protection against CSRF attacks is not disabled and a token was not already generated
        if ($csrf_storage_method !== false && (func_num_args() > 0 || $this->form_properties['csrf_token'] == '')) {

            // set the storage method for the CSRF token
            $this->form_properties['csrf_storage_method'] = strtolower(trim($csrf_storage_method));

            // if the script should decide what method to use and a session is already started
            if ($this->form_properties['csrf_storage_method'] == 'auto') {

                // use sessions as storage method
                if (isset($_SESSION)) $this->form_properties['csrf_storage_method'] = 'session';

                // if a session is not already started, use cookies as storage method
                else $this->form_properties['csrf_storage_method'] = 'cookie';

            }

            // set the life time of the CSRF token
            $this->form_properties['csrf_token_lifetime'] = ($csrf_token_lifetime <= 0 ? 0 : $csrf_token_lifetime);

            // set the configuration options for cookies
            $this->form_properties['csrf_cookie_config'] = array_merge($this->form_properties['csrf_cookie_config'], $csrf_cookie_config);

            // generate a new CSRF token (if it is the case)
            // (if this method is called with any arguments it means it is called by the user and therefore the
            // token should be regenerated)
            $this->_csrf_generate_token(func_num_args() > 0 ? true : false);

        // if protection against CSRF attacks is disabled, save the option for later use
        } else $this->form_properties['csrf_storage_method'] = false;

    }

    /**
     *  Sets the language to be used by some of the form's controls (the date control, the select control, etc.)
     *
     *  The default language is English.
     *
     *  @param  string  $language   The name of the language file to be used, from the "languages" folder.
     *
     *                              Must be specified without extension ("german" for the german language
     *                              not "german.php")!
     *
     *  @var   string
     *
     *  @return void
     */
    function language($language)
    {

        // include the language file
        require rtrim(dirname(__FILE__), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'languages/' . strtolower(trim($language)) . '.php';

        // make the language available in the control
        $this->form_properties['language'] = &$this->language;

    }

    /**
     *  Renders the form.
     *
     *  @param  string  $template       The output of the form can be generated automatically, can be given from a template
     *                                  file or can be generated programmatically by a callback function.
     *
     *                                  For the automatically generated template there are two options:
     *
     *                                  -   when <i>$template</i> is an empty string or is "<i>*vertical</i>", the script
     *                                      will automatically generate an output where the labels are above the controls
     *                                      and controls come one under another (vertical view)
     *
     *                                  -   when <i>$template</i> is "<i>*horizontal</i>", the script will automatically
     *                                      generate an output where the labels are positioned to the left of the controls
     *                                      while the controls come one under another (horizontal view)
     *
     *                                  When templates are user-defined, <i>$template</i> needs to be a string representing
     *                                  the <i>path/to/the/template.php</i>.
     *
     *                                  The template file itself must be a plain PHP file where all the controls
     *                                  added to the form (except for the hidden controls, which are handled automatically)
     *                                  will be available as variables with the names as described in the documentation
     *                                  for each of the controls. Also, error messages will be available as described at
     *                                  {@link Zebra_Form_Control::set_rule() set_rule()}.
     *
     *                                  A special variable will also be available in the template file - a variable with
     *                                  the name of the form and being an associative array containing all the controls
     *                                  added to the form, as objects.
     *
     *                                  <i>The template file must not contain the <form> and </form> tags, nor any of the
     *                                  <hidden> controls added to the form as these are generated automatically!</i>
     *
     *                                  There is a third method of generating the output and that is programmatically,
     *                                  through a callback function. In this case <i>$template</i> needs to be the name
     *                                  of an existing function.
     *
     *                                  The function will be called with two arguments:
     *
     *                                  -   an associative array with the form's controls' ids and their respective
     *                                      generated HTML, ready for echo-ing (except for the hidden controls which will
     *                                      still be handled automatically);
     *
     *                                      <i>note that this array will also contain variables assigned through the
     *                                      {@link assign()} method as well as any server-side error messages, as you
     *                                      would in a custom template (see {@link Zebra_Form_Control::set_rule() set_rule()}
     *                                      method and read until the second highlighted box, inclusive)</i>
     *
     *                                  -   an associative array with all the controls added to the form, as objects
     *
     *                                  THE USER FUNCTION MUST RETURN THE GENERATED OUTPUT!
     *
     *  @param  boolean $return         (Optional) If set to TRUE, the output will be returned instead of being printed
     *                                  to the screen.
     *
     *                                  Default is FALSE.
     *
     *  @param  array   $variables      (Optional) An associative array in the form of "variable_name" => "value"
     *                                  representing variable names and their associated values, to be made available
     *                                  in custom template files.
     *
     *                                  This represents a quicker alternative for assigning many variables at once
     *                                  instead of calling the {@link assign()} method for each variable.
     *
     *  @return mixed                   Returns or displays the rendered form.
     */
    function render($template = '', $return = false, $variables = '')
    {

        // if
        if (

            // "process.php" file could not be found
            !file_exists($this->form_properties['assets_server_path'] . 'process.php') ||

            // or "mimes.json" file could not be found
            !file_exists($this->form_properties['assets_server_path'] . 'mimes.json')

        )

            // it means the most probably the script is run on a virtual host and that paths need to be set manually so
            // we inform the user about that
            _zebra_form_show_error('<strong>Zebra_Form</strong> could not automatically determine the correct path to the "process.php"
            and "mimes.json" files - this may happen if the script is run on a virtual host. To fix this, use the <u>assets_path()</u>
            method and manually set the correct <strong>server path</strong> and <strong>URL</strong> to these file!', E_USER_ERROR);

        // if variables is an array
        if (is_array($variables))

            // iterate through the values in the array
            foreach ($variables as $name => $value)

                // make each value available in the template
                $this->assign($name, $value);

        // start generating the output
        $output = '<form ' .
            ($this->form_properties['doctype'] == 'html' ? 'name="' . $this->form_properties['name'] . '" ' : '') .
            'id="' . $this->form_properties['name'] . '" ' .
            'action="' . htmlspecialchars($this->form_properties['action']) . '" ' .
            'method="' . strtolower($this->form_properties['method']) . '" ' .
            'novalidate="novalidate"';

        // if custom classes are to be set for the form
        if (isset($this->form_properties['attributes']['class']))

            // add the "Zebra_Form" required class
            $this->form_properties['attributes']['class'] .= ' Zebra_Form';

        // if no custom classes are set, set the required "Zebra_Form" class
        else $this->form_properties['attributes']['class'] = 'Zebra_Form';

        // if any form attributes have been specified
        if (is_array($this->form_properties['attributes']))

            // iterate through the form's attributes
            foreach ($this->form_properties['attributes'] as $attribute => $value)

                // write them
                $output .= ' ' . $attribute . '="' . $value . '"';

        // if the form has file upload controls
        if ($this->form_properties['has_upload'] === true) {

            // add the enctype to the attributes of the <form> tag
            $output .= ' enctype="multipart/form-data"';

            // and add this required hidden field containing the maximum allowed file size
            $this->add('hidden', 'MAX_FILE_SIZE', $this->form_properties['max_file_size']);

            // if client-side validation is not disabled
            if (!$this->form_properties['clientside_validation']['clientside_disabled'] && !$this->form_properties['clientside_validation']['disable_upload_validation'])

                // add a new property for the client-side validation
                $this->clientside_validation(array('assets_path' => rawurlencode($this->form_properties['assets_url'])));

        }

        $output .= '>';

        // iterate through the form's controls
        foreach ($this->controls as $key => $control) {

            // treat "email" and "number" types as "text"
            if (in_array($control->attributes['type'], array('email', 'number'))) $control->attributes['type'] = 'text';

            // get some attributes for each control
            $attributes = $control->get_attributes(array('type', 'for', 'name', 'id', 'multiple', 'other', 'class', 'default_other', 'disable_zebra_datepicker'));

            // sanitize the control's name
            $attributes['name'] = preg_replace('/\[\]/', '', $attributes['name']);

            // validate the control's name
            switch ($attributes['name']) {

                // if control has the same name as the form
                case $this->form_properties['name']:
                // if control has the same name as the name of the honeypot's name
                case $this->form_properties['honeypot']:
                // if control has the same name as the name of the field containing the CSRF token
                case $this->form_properties['csrf_token_name']:
                // if control has the name "submit"
                case 'submit':

                    // stop the execution of the script
                    _zebra_form_show_error('You are not allowed to have a control named "<strong>' .
                        $attributes['name'] . '</strong>" in form "<strong>' .
                        $this->form_properties['name'] . '</strong>"',
                    E_USER_ERROR);

                    break;

            }

            // if control name is not allowed because it looks like the automatically generated controls for <select> controls
            // with the "other" option attached
            if (preg_match('/' . preg_quote($this->form_properties['other_suffix']) . '$/', $attributes['name']) > 0)

                // stop the execution of the script
                _zebra_form_show_error('You are not allowed to have a control with the name ending in "<strong>' .
                    $this->form_properties['other_suffix'] . '</strong>" in form "<strong>' .
                    $this->form_properties['name'] . '</strong>"', E_USER_ERROR);

            // if control has any rules attached to it
            if (!empty($control->rules)) {

                // if client-side validation is not disabled and this variable not created yet,
                // create the variable holding client-side error messages
                if (!$this->form_properties['clientside_validation']['clientside_disabled'] && !isset($clientside_validation)) $clientside_validation = array();

                // if we applied the "age" rule to an element not being of "date" type
                if (isset($control->rules['age']) && !array_key_exists('pair', $control->attributes))

                    // trigger an error message
                    _zebra_form_show_error('The <strong>age</strong> rule can only be applied to a <strong>date</strong> element', E_USER_ERROR);

                // we need to make sure that rules are in propper order, the order of priority being "dependencies",
                // "required" and "upload"

                // if the upload rule exists
                if (isset($control->rules['upload'])) {

                    // remove it from wherever it is
                    $rule = array_splice($control->rules, array_search('upload', array_keys($control->rules)), 1, array());

                    // and make sure it's the first rule
                    $control->rules = array_merge($rule, $control->rules);

                }

                // if the "required" rule exists
                if (isset($control->rules['required'])) {

                    // remove it from wherever it is
                    $rule = array_splice($control->rules, array_search('required', array_keys($control->rules)), 1, array());

                    // and make sure it's the first rule (it has to be checked prior to the "upload" rule)
                    $control->rules = array_merge($rule, $control->rules);

                }

                // if the "dependencies" rule exists
                if (isset($control->rules['dependencies'])) {

                    // remove it from wherever it is
                    $rule = array_splice($control->rules, array_search('dependencies', array_keys($control->rules)), 1, array());

                    // and make sure it's the first rule (it has to be checked prior to the "required" and "upload" rules)
                    $control->rules = array_merge($rule, $control->rules);

                }

                // iterate through the rules attached to the control
                foreach ($control->rules as $rule => $properties) {

                    // these rules are not checked client side
                    if (

                        $rule == 'captcha' || $rule == 'convert' || $rule == 'resize' ||

                        // also, if we're not checking files clientside, also ignore these rules
                        (
                            !$this->form_properties['clientside_validation']['clientside_disabled'] &&
                            $this->form_properties['clientside_validation']['disable_upload_validation'] &&
                            in_array($rule, array('upload', 'filetype', 'filesize', 'image'))
                        )

                    ) continue;

                    // we need to remove the error_block part as it is not needed for client-side validation
                    switch ($rule) {

                        // for these rules
                        case 'age':
                        case 'alphabet':
                        case 'alphanumeric':
                        case 'compare':
                        case 'digits':
                        case 'filesize':
                        case 'filetype':
                        case 'float':
                        case 'number':
                        case 'range':
                        case 'regexp':
                        case 'url':

                            // the error block is the second argument; remove it
                            array_splice($properties, 1, 1);

                            break;

                        // for these rules
                        case 'date':
                        case 'email':
                        case 'emails':
                        case 'image':
                        case 'required':

                            // the error block is the first argument; remove it
                            array_splice($properties, 0, 1);

                            break;

                        // for these rules
                        case 'datecompare':
                        case 'length':
                        case 'upload':

                            // the error block is the third argument; remove it
                            array_splice($properties, 2, 1);

                            // for the "length" rule
                            if ($rule == 'length' && $properties[1] > 0)

                                // we also set the "maxlength" attribute of the control
                                $this->controls[$key]->set_attributes(array('maxlength' => $properties[1]));

                            // fot the "length" rule, for text and textarea elements
                            if ($rule == 'length' && in_array($attributes['type'], array('text', 'textarea')) && $properties[1] > 0)

                                // make sure the default value, if there is one, is not longer than the maximum allowed length
                                $control->set_attributes(array('value' => mb_substr($control->attributes['value'], 0, $properties[1])));

                            break;

                        // for the "custom" rule
                        case 'custom':

                            // custom rules are always given as an array of rules
                            // so, iterate over the custom rules
                            foreach ($properties as $index => $values)

                                // and remove the error block
                                array_splice($properties[$index], -2, 1);

                            break;

                    }

                    // if client-side validation is not disabled
                    if (!$this->form_properties['clientside_validation']['clientside_disabled'])

                        // this array will be fed to the JavaScript as a JSON
                        $clientside_validation[$attributes['name']][$rule] = $properties;

                }


            }

            // if control is a select control, doesn't have the "multiple" attribute set and has the "other" attribute set
            if (isset($attributes['type']) && $attributes['type'] == 'select' && !isset($attributes['multiple']) && isset($attributes['other'])) {

                // set a special class for the select control so that we know that it has a textbox attached to it

                // add a special class to the control
                $this->controls[$key]->set_attributes(array('class' => 'other'), false);

                // add a text control
                $obj = & $this->add('text', $attributes['id'] . $this->form_properties['other_suffix'], $attributes['default_other']);

                // set a special class for the control
                $obj->set_attributes(array('class' => 'other'), false);

                // if the select control was not submitted OR it was submitted but the selected option is other than
                // the "other" option
                if (!isset($control->submitted_value) || $control->submitted_value != 'other')

                    // hide the text box
                    $obj->set_attributes(array('class' => 'other-invisible'), false);

                // make sure the value in the control propagates
                $obj->get_submitted_value();

                // because we want this control to appear right beneath the select control when the form is auto-generated
                // we need to have it after the select control in the "controls" property

                // as is we just added the control, it means it is at the end of the array
                // we take it off the end of array
                $obj = array_pop($this->controls);

                // find the position of the parent control
                $parent_position = array_search($attributes['name'], array_keys($this->controls));

                // insert the control right after the parent control
                $this->controls =

                    array_slice($this->controls, 0, $parent_position + 1, true) +

                    array($attributes['id'] . $this->form_properties['other_suffix'] => $obj) +

                    array_slice($this->controls, $parent_position + 1, count($this->controls) - $parent_position, true);


            }

            // if control is a label and is a "master" label
            if (isset($attributes['type']) && $attributes['type'] == 'label' && array_key_exists($attributes['for'], $this->master_labels))

                // save the "master" label's name
                $this->master_labels[$attributes['for']]['control'] = $attributes['name'];

            // if control is a date control
            if (isset($attributes['type']) && $attributes['type'] == 'text' && preg_match('/\bdate\b/i', $attributes['class'])) {

                // if variable is not yet defined. define it
                if (!isset($datepicker_javascript)) $datepicker_javascript = '';

                // if Zebra_DatePicker is *not* disabled for this control
                if (!$attributes['disable_zebra_datepicker']) {

                    // append the new date picker object
                    $datepicker_javascript .= '$(\'#' . $attributes['id'] . '\').Zebra_DatePicker(';

                    // take day names from the language file
                    $control->attributes['days'] = $this->form_properties['language']['days'];

                    // if we have custom day name abbreviations, use them
                    if (is_array($this->form_properties['language']['days_abbr'])) $control->attributes['days_abbr'] = $this->form_properties['language']['days_abbr'];

                    // take month names from the language file
                    $control->attributes['months'] = $this->form_properties['language']['months'];

                    // if we have custom month abbreviations, use them
                    if (is_array($this->form_properties['language']['months_abbr'])) $control->attributes['months_abbr'] = $this->form_properties['language']['months_abbr'];

                    // use the caption from the language file for the "Clear date" button
                    $control->attributes['lang_clear_date'] = $this->form_properties['language']['clear_date'];

                    // if the "Today" button is not disabled use the caption from the language file
                    if ($control->attributes['show_select_today'] === null) $control->attributes['show_select_today'] = $this->form_properties['language']['today'];

                    $properties = '';

                    // iterate through control's attributes
                    foreach ($control->attributes as $attribute => $value) {

                        // if attribute is an attribute intended for the javascript object and is not null
                        if (in_array($attribute, $control->javascript_attributes) && ($control->attributes[$attribute] !== null || ($attribute == 'direction' && $value === false))) {

                            // append to the properties list (we change "inside_icon" to "inside" as "inside" is reserved)
                            $properties .= ($properties != '' ? ',' : '') . ($attribute == 'inside_icon' ? 'inside' : $attribute) . ':';

                            // if value is an array
                            if (is_array($value)) {

                                // assume we don't need to convert this to a JavaScript object
                                $is_object = false;

                                // iterate through all the keys/values
                                foreach ($value as $key => $val)

                                    // if at least one of the keys is not numeric
                                    if (preg_match('/[^0-9]/', $key) > 0) {

                                        // set this flag to true
                                        $is_object = true;

                                        // don't look further
                                        break;

                                    }

                                // format accordingly
                                $properties .= ($is_object ? '{' : '[');

                                // iterate through the values
                                foreach ($value as $key => $val)

                                    $properties .= (!is_numeric($key) ? '\'' . $key . '\':' : '') . ($val === true ? 'true' : ($val === false ? 'false' : (is_numeric($val) ? $val : (is_array($val) ? json_encode($val) : '\'' . $val . '\'')))) . ',';

                                $properties = rtrim($properties, ',') . ($is_object ? '}' : ']');

                            // if value is a jQuery selector
                            } elseif (preg_match('/^\$\((\'|\").*?\1\)/', trim($value)) > 0) {

                                // use it as it is
                                $properties .= $value;

                            // if value is a string but is not a javascript object
                            } elseif (is_string($value) && !preg_match('/^\{.*\}$/', trim($value)))

                                // format accordingly and escape single quotes or we'll kill JavaScript
                                $properties .= '\'' . addcslashes($value, '\'') . '\'';

                            // for any other case (javascript object, boolean)
                            else

                                // format accordingly
                                $properties .= ($value === true ? 'true' : ($value === false ? 'false' : (is_numeric($value) ? $value : '\'' . $value . '\'')));

                        }

                    }

                    $properties .= ',onSelect:function(){$("#' . $this->form_properties['name'] . '").data("Zebra_Form").hide_error("' . $attributes['name'] . '")}';

                    // wrap up the javascript object
                    $datepicker_javascript .= ($properties != '' ? '{' . $properties . '}' : '') . ');';

                // if Zebra_DatePicker is disabled
                } else

                    // in order to preserve client-side validation,
                    // we still need to pass some data to it
                    $datepicker_javascript .= '$(\'#' . $attributes['id'] . '\').data("Zebra_DatePicker", new Object({settings: {days: ["' . implode('","', $this->form_properties['language']['days']) . '"], months: ["' . implode('","', $this->form_properties['language']['months']) . '"], format: "' . $control->attributes['format'] . '"}}));';

            }

            // if control has the "filetype" rule set, load MIME types now
            if (isset($control->rules['filetype']) || isset($control->rules['image'])) $this->_load_mime_types();

        }

        // we add automatically this hidden control to the form, used to know that the form was submitted
        $this->add('hidden', $this->form_properties['identifier'], $this->form_properties['name']);

        // add a "honeypot" - a text field that we'll use to try and prevent spam-bots
        // this field will be hidden from users and we expect only spam-bots to fill it. if this field will not be empty
        // when submitting the form, we'll consider that the form was submitted by a spam-bot
        $this->add('text', $this->form_properties['honeypot'], '', array('autocomplete' => 'off'));

        // if CSRF protection is enabled (is not boolean FALSE)
        if ($this->form_properties['csrf_storage_method'] !== false)

            // add a hidden field to the form, containing the random token
            // (we will later compare the value in this field with the value in the associated session/cookie)
            $this->add('hidden', $this->form_properties['csrf_token_name'], $this->form_properties['csrf_token']);

        // start rendering the form's hidden controls
        $output .= '<div class="hidden">';

        // iterate through the controls assigned to the form, looking for hidden controls
        // also, use this opportunity to see which labels are attached to "required" controls
        foreach ($this->controls as $key => $control) {

            // get some attributes for each control
            $attributes = $control->get_attributes(array('type', 'for', 'name', 'label', 'inside'));

            // sanitize the control's name
            $attributes['name'] = preg_replace('/\[\]$/', '', $attributes['name']);

            // if control is a "hidden" control
            if ($attributes['type'] == 'hidden') {

                // append the hidden control to the hidden control's block
                $output .= $control->toHTML();

                // take the control out of the controls array because we don't have to bother with it anymore
                unset($this->controls[$key]);

            // if control is a text field and is the control intended for the "honeypot"
            } elseif ($attributes['type'] == 'text' && $attributes['name'] == $this->form_properties['honeypot']) {

                // because http://www.w3.org/WAI/GL/WCAG20/WD-WCAG20-TECHS/html.html#H44 requires it,
                // attach a label to the control
                $output .= '<label for="' . $this->form_properties['honeypot'] .  '" style="display:none">Leave this field blank</label>';

                // append the control to the hidden control's block (it will not be visible)
                $output .= $control->toHTML();

                // take the control out of the controls array so we don't have to bother with it anymore
                unset($this->controls[$key]);

            // if
            } elseif (

                // control is a label AND
                $attributes['type'] == 'label' &&

                // has the "for" attribute set
                isset($attributes['for']) &&

                (

                    // represents a label for a group of checkboxes or radio buttons
                    array_key_exists($attributes['for'], $this->master_labels) ||

                    // the label is attached to an existing control
                    array_key_exists($attributes['for'], $this->controls)

                )

            ) {

                // if the label is attached to an existing control
                if (array_key_exists($attributes['for'], $this->controls)) {

                    // get some attributes of the control the label is attached to
                    $ctrl_attributes = $this->controls[$attributes['for']]->get_attributes(array('name', 'id', 'type'));

                    // sanitize the control's name
                    $ctrl_attributes['name'] = preg_replace('/\[\]$/', '', $ctrl_attributes['name']);

                    // if
                    if (

                        // the label has the "inside" attribute set
                        isset($attributes['inside']) &&

                        // the label's "inside" attribute is set to TRUE AND
                        $attributes['inside'] === true &&

                        // the type of the control the label is attached to is either text, textarea or password
                        (isset($ctrl_attributes['type']) && (

                            $ctrl_attributes['type'] == 'text' ||

                            $ctrl_attributes['type'] == 'textarea' ||

                            $ctrl_attributes['type'] == 'password'

                        ))

                    ) {

                        // set some extra attributes for the control the label is attached to
                        $this->controls[$attributes['for']]->set_attributes(array(

                            // for textareas we set the "title" attribute while for the text and password
                            // controls we set the "alt" attribute
                            'title' => $attributes['label'],

                        ));

                        // set some extra attributes for the control the label is attached to
                        $this->controls[$attributes['for']]->set_attributes(array(

                            // set a class, used by the JavaScript to set some extra attributes at runtime
                            'class' => 'inside',

                        ), false);

                    // if the control the label is attached to a radio button or a checkbox
                    } elseif ($ctrl_attributes['type'] == 'radio' || $ctrl_attributes['type'] == 'checkbox')

                        // set a specific class for the label control
                        $control->set_attributes(array('class' => 'option'));

                }

                // if the control the label is attached to has the "disabled" attribute set
                if (isset($this->controls[$attributes['for']]->attributes['disabled']))

                    // set a special class for the label
                    $control->set_attributes(array('class' => 'disabled'), false);

                // if the control the label is attached to, has the "required" rule set
                if (isset($this->controls[$attributes['for']]->rules['required']))

                    // if
                    if (

                        // a "master" label could exist for the control the label is attached to
                        array_key_exists($ctrl_attributes['name'], $this->master_labels) &&

                        // and a control that could be the "master" label exists
                        isset($this->controls[$this->master_labels[$ctrl_attributes['name']]['control']])

                    ) {

                        // if asterisk is not already attached
                        if (strpos($this->controls[$this->master_labels[$ctrl_attributes['name']]['control']]->attributes['label'], '<span class="required">*</span>') === false)

                            // attach the asterisk to the "master" label instead rather than to the current label
                            $this->controls[$this->master_labels[$ctrl_attributes['name']]['control']]->set_attributes(array('label' => $this->controls[$this->master_labels[$ctrl_attributes['name']]['control']]->attributes['label'] . '<span class="required">*</span>'));

                    // otherwise
                    } else

                        // attach the asterisk to the current label
                        $this->controls[$key]->set_attributes(array('label' => $attributes['label'] . '<span class="required">*</span>'));

            // if
            } elseif (

                // control is a label AND
                $attributes['type'] == 'label' &&

                // has the "for" attribute set
                isset($attributes['for']) &&

                // is neither a "master" label for a group of checkboxes or radio buttons
                !array_key_exists($attributes['for'], $this->master_labels) &&

                // nor is attached to an existing control
                !array_key_exists($attributes['for'], $this->controls) &&

                // we're not on autopilot (if we are, we will remove the "for" attribute later on)
                ($template != '' && $template != '*horizontal' && $template != '*vertical')

            // remove the "for" attribute so that the form will pass the W3C validation
            ) unset($this->controls[$key]->attributes['for']);

        }

        // finish building the hidden controls block
        $output .= '</div>';

        // if there are any error messages
        if (!empty($this->errors))

            // iterate through each error block
            foreach ($this->errors as $error_block => $error_messages) {

                $content = '';

                // iterate through each message of the error block
                foreach ($error_messages as $error_message) {

                    // render each message in block
                    $content .= '<span>' . $error_message . '</span>';

                    // if only one error message is to be show
                    // break out from the foreach loop
                    if ($this->form_properties['show_all_error_messages'] === false) break;

                }

                // switch the array entry with it's rendered form
                $this->errors[$error_block] = '<div class="error"><div class="container">' . $content . '<div class="close"><a href="javascript:void(0)">close</a></div></div></div>';

            }

        // if there are any SPAM or CSRF errors
        if (isset($this->errors['zf_error_spam']) || isset($this->errors['zf_error_csrf'])) {

            // if there's a CSRF error leave only that
            if (isset($this->errors['zf_error_csrf'])) $this->errors['zf_error'] = $this->errors['zf_error_csrf'];

            // else, if there's a SPAM error, leave just that
            else $this->errors['zf_error'] = $this->errors['zf_error_spam'];

            // remove these two errors
            // as now there will be only one, under the name of "zf_error"
            unset($this->errors['zf_error_csrf']);
            unset($this->errors['zf_error_spam']);

        }

        // if output is to be auto-generated
        if ($template == '' || $template == '*horizontal' || $template == '*vertical') {

            $error_messages = '';

            // iterate through any existing error blocks
            // and render them at the top of the auto-generated output
            foreach ($this->errors as $errors) $error_messages .= $errors;

            // group controls in master-label/control/label/note
            // so, every block looks like
            // - master label (if any)
            // - label (if any)
            // - control
            // - (in case of radio buttons and checkboxes the above two may be repeated)
            // - note (if any)
            $blocks = array();

            // iterate through the form's controls
            foreach ($this->controls as $key => $control) {

                // get some attributes for the control
                $attributes = $control->get_attributes(array('type', 'name', 'id', 'for', 'inside'));

                // if control is a label that is to be placed inside another control, we skip it
                if ($attributes['type'] == 'label' && isset($attributes['inside'])) continue;

                // sanitize control's name
                $attributes['name'] = preg_replace('/\[\]$/', '', $attributes['name']);

                // if the control is a text box that is to be shown when user selects "other" in a select control
                if (preg_match('/(.*)' . preg_quote($this->form_properties['other_suffix']) . '$/', $attributes['name'], $matches) > 0)

                    // the parent is the control to which the control is attached to
                    $parent = $matches[1];

                // for other controls
                else {

                    // check the control's type
                    switch ($attributes['type']) {

                        // if control is captcha, label, or note
                        case 'captcha':
                        case 'label':
                        case 'note':

                            // save the control the current control is attached to
                            $parent = $attributes['for'];

                            // if
                            if (

                                // parent control exist AND
                                isset($this->controls[$parent]) &&

                                // control is a checkbox or radio button
                                ($this->controls[$parent]->attributes['type'] == 'checkbox' || $this->controls[$parent]->attributes['type'] == 'radio') &&

                                // the parent control's ID is different the parent control's name
                                // (as is the case for radio buttons and checkboxes)
                                $this->controls[$parent]->attributes['id'] != $this->controls[$parent]->attributes['name']

                            )

                                // save both the "master" parent and, separated by a dot, the actual parent
                                $parent = preg_replace('/\[\]$/', '', $this->controls[$parent]->attributes['name']) . '.' . $parent;

                            // if control is a label and the parent control doesn't exist (the label is most probably a "master" label)
                            elseif ($attributes['type'] == 'label' && !isset($this->controls[$parent]))

                                // remove the "for" attribute so that the form will pass the W3C validation
                                unset($this->controls[$key]->attributes['for']);

                            break;

                        // for any other controls
                        default:

                            // the parent is the control itself
                            $parent = $attributes['name'];

                    }

                }

                // as some controls (labels for checkboxes) can have multiple parent - the checkbox control and a master
                // label - and multiple parents are separated by a dot, explode by dot
                $parents = explode('.', $parent);

                // iterate through the control's parents
                foreach ($parents as $key => $parent) {

                    // if first entry
                    // make $array a pointer to the $blocks array
                    if ($key == 0) $array = & $blocks;

                    // if the parent control doesn't have its own key in the array
                    // (it may still be in the array but not as a "parent")
                    if (!isset($array[$parent])) {

                        // initialize the entry
                        $array[$parent] = array();

                        // if we already have the entry but not as a key
                        if (($pos = array_search($parent, $array)) !== false) {

                            // insert it in the newly created entry
                            $array[$parent][] = $array[$pos];

                            // and remove it from the old position
                            unset($array[$pos]);

                        }

                    }

                    // make $array a pointer
                    $array = & $array[$parent];

                    // if we're at the last parent
                    if ($key == count($parents) - 1)

                        // if control already exists in the parent's array as a key (remember that $array is a pointer!)
                        if (array_key_exists($attributes['id'], $array))

                            // add the control to the array
                            $array[$attributes['id']][] = $attributes['id'];

                        // if control doesn't exists in the parent's array (remember that $array is a pointer!)
                        else

                            // add the control to the array
                            $array[] = $attributes['id'];

                }

            }

            // if auto-generated output needs to be horizontal
            if ($template == '*horizontal') {

                // the output will be enclosed in a table
                $contents = '<table>';

                // if there are errors to be displayed
                if ($error_messages != '')

                    // show the error messages
                    $contents .= '<tr><td colspan="2">' . $error_messages . '</td></tr>';

                // keep track of odd/even rows
                $counter = 0;

                // total number of rows to be displayed
                $rows = count($blocks);

                // iterate through blocks
                foreach ($blocks as $controls) {

                    ++$counter;

                    // each block is in its own row
                    $contents .= '<tr class="row' . ($counter % 2 == 0 ? ' even' : '') . ($counter == $rows ? ' last' : '') . '">';

                    // the first cell will hold the label (if any)
                    $contents .= '<td>';

                    // as of PHP 5.3, array_shift required the argument to be a variable and not the result
                    // of a function so we need this intermediary step
                    $labels = array_values($controls);

                    // retrieve the first item in the block
                    $label = array_shift($labels);

                    // item is a label
                    if (!is_array($label) && $this->controls[$label]->attributes['type'] == 'label') {

                        // remove it from the block
                        array_shift($controls);

                        // render it
                        $contents .= $this->controls[$label]->toHTML();

                    }

                    // close the table cell
                    $contents .= '</td>';

                    // the second cell contains the actual controls
                    $contents .= '<td>';

                    // iterate through the controls to be rendered
                    foreach ($controls as $control) {

                        // if array of controls
                        // (radio buttons/checkboxes and their labels)
                        if (is_array($control)) {

                            // iterate through the array's items
                            foreach ($control as $ctrl)

                                // and display them on the same line
                                $contents .= '<div class="cell">' . $this->controls[$ctrl]->toHTML() . '</div>';

                            // clear floats
                            $contents .= '<div class="clear"></div>';

                        // if not an array of controls
                        } else

                            // if control is required but has the label as a tip inside the control
                            // we need to manually add the asterisk after the control
                            if (array_key_exists('required', $this->controls[$control]->rules) && preg_match('/\binside\b/', $this->controls[$control]->attributes['class'])) {

                                // first, make sure the control is inline so that the asterisk will be placed to the right of the control
                                $this->controls[$control]->set_attributes(array('class' => 'inline'), false);

                                // add the required symbol after the control
                                $contents .= $this->controls[$control]->toHTML() . '<span class="required">*</span>';

                            // else, render the control
                            } else $contents .= $this->controls[$control]->toHTML();

                    }

                    // close the cell
                    $contents .= '</td>';

                    // add a "separator" row
                    $contents .= '</tr>';

                }

                // finish rendering the table
                $contents .= '</table>';

            // if auto-generated output needs to be vertical
            } else {

                $contents = '';

                // if there are errors to be displayed, show the error messages
                if ($error_messages != '') $contents .= $error_messages;

                $counter = 0;

                // total number of rows to be displayed
                $rows = count($blocks);

                // iterate through blocks
                foreach ($blocks as $controls) {

                    // ...then block is contained in its own row
                    $contents .= '<div class="row' . (++$counter % 2 == 0 ? ' even' : '') . ($counter == $rows ? ' last' : '') . '">';

                    // iterate through the controls to be rendered
                    foreach ($controls as $control) {

                        // if array of controls
                        // (radio buttons/checkboxes and their labels)
                        if (is_array($control)) {

                            // iterate through the array's items
                            foreach ($control as $ctrl)

                                // and display them on the same line
                                $contents .= '<div class="cell">' . $this->controls[$ctrl]->toHTML() . '</div>';

                            // clear floats
                            $contents .= '<div class="clear"></div>';

                        // if not an array of controls
                        } else

                            // if control is required but has the label as a tip inside the control
                            // we need to manually add the asterisk after the control
                            if (array_key_exists('required', $this->controls[$control]->rules) && preg_match('/\binside\b/', $this->controls[$control]->attributes['class'])) {

                                // first, make sure the control is inline so that the asterisk will be placed to the right of the control
                                $this->controls[$control]->set_attributes(array('class' => 'inline'), false);

                                // add the required symbol after the control
                                $contents .= $this->controls[$control]->toHTML() . '<span class="required">*</span>';

                            // else, render the control
                            } else $contents .= $this->controls[$control]->toHTML();

                    }

                    // ...finish rendering
                    $contents .= '</div>';

                }

            }

        // if a function with the name given as $template, and the function exists
        } elseif (is_callable($template)) {

            // this variable will contain all the rendered controls
            $controls = array();

            // iterate through the controls assigned to the form
            foreach ($this->controls as $control) {

                // read some attributes of the control
                $attributes = $control->get_attributes(array('id', 'inside'));

                // render the control if the control is not a label that is to be displayed inside the control as it's
                // default value
                if (!isset($attributes['inside']))

                    // if control is required but has the label as a tip inside the control
                    // we need to manually add the asterisk after the control
                    if (array_key_exists('required', $control->rules) && preg_match('/\binside\b/', $control->attributes['class'])) {

                        // first, make sure the control is inline so that the asterisk will be placed to the right of the control
                        $control->set_attributes(array('class' => 'inline'), false);

                        // add the required symbol after the control
                        // and add generated HTML code to the $controls array
                        $controls[$attributes['id']] = $control->toHTML() . '<span class="required">*</span>';

                    // otherwise, add generated HTML code to the $controls array
                    } else $controls[$attributes['id']] = $control->toHTML();

            }

            // iterate through the variables assigned to the form
            foreach ($this->variables as $variable_name => $variable_value)

                // make available the assigned variables
                $controls[$variable_name] = $variable_value;

            // iterate through the error messages(if any)
            foreach ($this->errors as $error_block => $error_message)

                // make the error message available
                $controls[$error_block] = $error_message;

            // let the custom function generate the output
            // we're passing two arguments
            // an associative array with control ids and their respectively generated HTML
            // and an array with all the form's objects
            $contents = call_user_func_array($template, array($controls, &$this->controls));

        // if a template was specified
        } else {

            // this variable will contain all the rendered controls
            $controls = array();

            // iterate through the controls assigned to the form
            foreach ($this->controls as $control) {

                // read some attributes of the control
                $attributes = $control->get_attributes(array('id', 'inside'));

                // render the control if the control is not a label that is to be displayed inside the control as it's
                // default value
                if (!isset($attributes['inside']))

                    // if control is required but has the label as a tip inside the control
                    // we need to manually add the asterisk after the control
                    if (array_key_exists('required', $control->rules) && preg_match('/\binside\b/', $control->attributes['class'])) {

                        // first, make sure the control is inline so that the asterisk will be placed to the right of the control
                        $control->set_attributes(array('class' => 'inline'), false);

                        // add the required symbol after the control
                        // and add generated HTML code to the $controls array
                        $controls[$attributes['id']] = $control->toHTML() . '<span class="required">*</span>';

                    // otherwise, add generated HTML code to the $controls array
                    } else {

                        // if element name was given as an array (i.e. car[name])
                        if (preg_match('/^([^\[]+)\[([^\]]+)\]$/', $attributes['id'], $matches))

                            // assign the variable accordingly
                            $controls[$matches[1]][$matches[2]] = $control->toHTML();

                        // if element name was not given as an array, assign the variable accordingly
                        else $controls[$attributes['id']] = $control->toHTML();

                    }

            }

            //start output buffering
            ob_start();

            // make available in the template all the form's objects
            $controls[$this->form_properties['name']] = &$this->controls;

            // make the user-defined variables available in the template file as PHP variables
            extract($this->variables);

            // make the rendered controls available in the template file as PHP variables
            extract($controls);

            // make the error messages available in the template file as PHP variables
            extract($this->errors);

            // include the template file
            include $template;

            // put the parsed content in a variable
            $contents = ob_get_contents();

            // clean buffers
            ob_end_clean();

        }

        // finish building the output
        $output = $output . $contents . '</form>';

        // this will hold the properties to be set for the JavaScript object
        $javascript_object_properties = '';

        // if client-side validation is disabled
        if ($this->form_properties['clientside_validation']['clientside_disabled']) {

            // we still need to execute a function attached to the "on_ready" event (if any)
            $javascript_object_properties .= 'on_ready:' . ($this->form_properties['clientside_validation']['on_ready'] === false ? 'false' : $this->form_properties['clientside_validation']['on_ready']);

            // so we don't break the JavaScript code, we need this, too
            $javascript_object_properties .= ',validation_rules:{}';

        // if client-side validation is not disabled
        } else {

            // iterate through the properties
            foreach ($this->form_properties['clientside_validation'] as $key => $value)

                // save property
                $javascript_object_properties .=
                    ($javascript_object_properties != '' ? ',' : '') . $key . ':' .
                    ($value === true ? 'true' : ($value === false ? 'false' : ($key == 'on_ready' ? $value : '\'' . $value . '\'')));

            // save information about validation rules
            $javascript_object_properties .= ($javascript_object_properties != '' ? ',' : '') . 'validation_rules:' . (isset($clientside_validation) ? json_encode($clientside_validation) : '{}');

        }

        // function name for initializing client-side validation
        $function_name = 'init_' . md5(strtolower($this->form_properties['name'] . microtime()));

        $output .=
            '<script type="text/javascript">function ' . $function_name . '(){if(typeof jQuery=="undefined"||typeof jQuery.fn.Zebra_Form=="undefined"' .
            (isset($datepicker_javascript) ? '|| jQuery.fn.Zebra_DatePicker=="undefined"' : '') . '){setTimeout("' . $function_name . '()",100);return}else{' .
            '$(document).ready(function(){' .
            (isset($datepicker_javascript) ? $datepicker_javascript : '') .
            '$("#' . $this->form_properties['name'] . '").Zebra_Form(' . ($javascript_object_properties != '' ? '{' . $javascript_object_properties . '}' : '') . ')})}}' .
            $function_name . '()</script>';

        // if $return argument was TRUE, return the result
        if ($return) return $output;

        // if $return argument was FALSE, output the content
        else echo $output;

    }

    /**
     *  Resets the submitted values for all of the form's controls (also resets the POST/GET/FILES superglobals)
     *
     *  @return void
     */
    function reset()
    {

        // iterate through the form's controls
        foreach ($this->controls as $key => $control) {

            // reference to the control
            $obj = & $this->controls[$key];

            // reset
            $obj->reset();

        }

    }

    /**
     *  Sets how error messages generated upon server-side validation are displayed in an
     *  {@link Zebra_Form_Control::set_rule() error block}.
     *
     *  Client-side validation is done on the "onsubmit" event of the form. See {@link clientside_validation()} for
     *  more information on client-side validation.
     *
     *  <code>
     *  //  create a new form
     *  $form = new Zebra_Form('my_form');
     *
     *  //  display all error messages of error blocks
     *  $form->show_all_error_messages(true);
     *  </code>
     *
     *  @param  boolean $value  Setting this argument to TRUE will display <i>all</i> error messages of an error block,
     *                          while setting it to FALSE will display one error message at a time.
     *
     *                          Default is FALSE.
     *
     *
     *  @return                 void
     */
    function show_all_error_messages($value = false)
    {

        // set the property
        $this->form_properties['show_all_error_messages'] = $value;

    }

    /**
     *  This method performs the server-side validation of all the form's controls, making sure that all the values
     *  comply to the rules set for these controls through the {@link Zebra_Form_Control::set_rule() set_rule()} method.
     *
     *  Only by calling this method will the form's controls update their values. If this method is not called, all
     *  the controls will preserve their default values after submission even if these values were altered prior to
     *  submission.
     *
     *  This method must be called <b>before</b> the {@link render()} method or error messages will not be
     *  available.
     *
     *  After calling this method, if there are {@link Zebra_Form_File file} controls on the form, you might want to check
     *  for the existence of the {@link $file_upload} property to see the details of uploaded files and take actions
     *  accordingly.
     *
     *  Client-side validation is done on the "onsubmit" event of the form. See {@link clientside_validation()} for
     *  more information on client-side validation.
     *
     *  @return boolean     Returns TRUE if every rule was obeyed, FALSE if not.
     */
    function validate()
    {

        // reference to the form submission method
        global ${'_' . $this->form_properties['method']};

        $method = & ${'_' . $this->form_properties['method']};

        $this->proxies_cache = array();

        // we assume the form is not valid (or it was not submitted)
        $form_is_valid = false;

        // continue only if form was submitted
        if (

            isset($method[$this->form_properties['identifier']]) &&

            $method[$this->form_properties['identifier']] == $this->form_properties['name']

        ) {

            // if
            if (

                // the "honeypot" field was submitted AND
                isset($method[$this->form_properties['honeypot']]) &&

                // the "honeypot" field is empty
                $method[$this->form_properties['honeypot']] == '' &&

                // no possible CSRF attacks detected
                ($csrf_status = $this->_csrf_validate())

            ) {

                // remove the honeypot and csrf entries so that we don't polute the $_POST array
                unset($method[$this->form_properties['honeypot']]);
                unset($method[$this->form_properties['csrf_token_name']]);

                // by default, we assume that the form is valid
                $form_is_valid = true;

                // iterate through the controls
                foreach (array_keys($this->controls) as $key) {

                    // reference to control
                    $control = & $this->controls[$key];

                    // get some attributes of the control
                    $attribute = $control->get_attributes(array('type'));

                    // validate the control
                    $valid = $this->validate_control($key);

                    // do some extra checkings and cleanup
                    if (

                        //if type is password OR
                        $attribute['type'] == 'password' ||

                        //if type is text and has the "captcha" rule set
                        ($attribute['type'] == 'text' && isset($control->rules['captcha']))

                    // clear the value in the field
                    ) $control->set_attributes(array('value' => ''));

                    // if control is not valid, the form is not valid
                    if (!$valid) {

                        // unless the element is a file upload control, add the "error" class to it so we can apply
                        // custom styles to erroneous fields
                        if ($attribute['type'] != 'file') $control->set_attributes(array('class' => 'error'), false);

                        $form_is_valid = false;

                    }

                }

                // after iterating through all the controls,
                // check if the form is still valid
                if ($form_is_valid)

                    // if there are any actions to be performed when the form is valid
                    // (file upload, resize, convert)
                    if (isset($this->actions) && !empty($this->actions))

                        // iterate through the actions
                        foreach ($this->actions as $actions)

                            // if the respective action (method) exists
                            if (method_exists($this, $actions[0])) {

                                // if the method was erroneous
                                if (!call_user_func_array(array(&$this,$actions[0]), array_slice($actions, 1))) {

                                    // add error message to indicated error block
                                    $this->add_error($actions['block'], $actions['message']);

                                    // set the form as not being valid
                                    $form_is_valid = false;

                                }

                            // if the task (method) could not be found, trigger an error message
                            } else _zebra_form_show_error('Method ' . $actions[0] . ' does not exist!', E_USER_ERROR);

            // else if
            } elseif (

                // honeypot field was not submitted
                !isset($method[$this->form_properties['honeypot']]) ||

                // honeypot field is not empty
                $method[$this->form_properties['honeypot']] != ''

            // show the appropriate error message to the user
            ) $this->add_error('zf_error_spam', $this->form_properties['language']['spam_detected']);

            // else, if a possible CSRF attack was detected
            // show the appropriate error message to the user
            elseif (!$csrf_status) $this->add_error('zf_error_csrf', $this->form_properties['language']['csrf_detected']);

            // if
            if (

                // form is valid
                $form_is_valid ||

                // form is invalid and the from was not submitted via AJAX
                !isset($_SERVER['HTTP_X_REQUESTED_WITH'])

            // regenerate the CSRF token
            ) $this->_csrf_generate_token(true);

        // here's a special error check:
        // due to a bug (?) when the POST/GET data is larger than allowed by upload_max_filesize/post_max_size the
        // $_POST/$_GET/$_FILES superglobals are empty (see http://bugs.php.net/bug.php?id=49570)
        // but still, we need to present the user with some error message...
        } elseif (empty($method) && isset($_SERVER['CONTENT_LENGTH']) && (int)$_SERVER['CONTENT_LENGTH'] > 0)

            $form_is_valid = false;

        // if form was not submitted and fields are to be auto-filled
        elseif ($this->form_properties['auto_fill'] !== false) {

            // we'll use this variable to keep track of groups of radio buttons/checkboxes
            // where we've randomly selected a value
            $checked = array();

            // iterate through the form's controls
            foreach ($this->controls as $key => $control) {

                // get some attributes for each control
                $attributes = $control->get_attributes(array('type', 'name', 'value'));

                // sanitize controls' name (remove square brackets)
                $attributes['name'] = preg_replace('/\[\]$/', '', $attributes['name']);

                // if we need to select predefined values for specific controls
                if (isset($this->form_properties['auto_fill'][0][$attributes['name']])) {

                    // the value/values that is/are to be preselected
                    $value = $this->form_properties['auto_fill'][0][$attributes['name']];

                    // if control is a radio button or a checkbox
                    if ($attributes['type'] == 'checkbox' || $attributes['type'] == 'radio') {

                        // if
                        if (

                            // given value is not an array and the current element has that value OR
                            (!is_array($value) && $value == $attributes['value']) ||

                            // given value is an array and the current element's value is in that array
                            // also, make sure we don't select multiple values for radio buttons
                            (is_array($value) && $attributes['type'] != 'radio' && in_array($attributes['value'], $value))

                        // mark element as "checked"
                        ) $control->set_attributes(array('checked' => 'checked'));

                    // for the other controls, simply set the value
                    } else $control->set_attributes(array('value' => $value));

                // if no predefined value was given for the control and we don't auto-fill only specific controls
                } elseif (!$this->form_properties['auto_fill'][1]) {

                    // if control is a radio button or a checkbox
                    if ($attributes['type'] == 'checkbox' || $attributes['type'] == 'radio') {

                        // if we've not already selected a random value for the group of radio buttons/checkboxes
                        // the current element is part of
                        if (!isset($checked[$attributes['name']])) {

                            // this will hold the randomly selected elements
                            $tmp_checked = array();

                            // iterate through all the form's controls
                            foreach ($this->controls as $element)

                                // if control is of the same type and has the same name
                                if ($element->attributes['type'] == $attributes['type'] && $element->attributes['name'] == $attributes['name']) {

                                    // if element is checked by default, from when creating the form
                                    if (isset($element->attributes['checked'])) {

                                        // we will consider this group as checked
                                        $checked[$attributes['name']] = true;

                                        // ...and look no further
                                        break;

                                    // if element is not checked by default and we should select the current value,
                                    // save it for later as we first need to check if an element of the group is not
                                    // already checked
                                    // (also, for radio buttons make sure we select a single option)
                                    } elseif (rand(0, 1) == 1&& !($attributes['type'] == 'radio' && !empty($tmp_checked))) $tmp_checked[] = $element;

                                }

                            // if no element of the group was selected
                            if (!isset($checked[$attributes['name']])) {

                                // if there are any randomly selected elements
                                if (!empty($tmp_checked))

                                    // iterate through the selected elements and mark them as "checked"
                                    foreach ($tmp_checked as $element) $element->set_attributes(array('checked' => 'checked'));

                                // if there are no randomly selected elements then select the current (first) element
                                $control->set_attributes(array('checked' => 'checked'));

                            }

                            // flag this group of elements so we're only doing this once
                            $checked[$attributes['name']] = true;

                        }

                    // if control is a drop-down or a multiple select box and does not already has a value
                    } elseif ($attributes['type'] == 'select' && $attributes['value'] === '') {

                        // select a random value
                        // (if "multiple" attribute is set, select from all the values, or select starting from the second value otherwise)
                        $keys = array_slice(
                            array_keys($control->attributes['options']),
                            (isset($control->attributes['multiple']) && strtolower(trim($control->attributes['multiple'])) == 'multiple' ? 0 : 1)
                        );

                        // if the select has any values, set a random value
                        if (!empty($keys)) $control->set_attributes(array('value' => $keys[array_rand($keys)]));

                    // if control is "password"
                    } elseif ($attributes['type'] == 'password') {

                        // set the value to "12345678"
                        $control->set_attributes(array('value' => '12345678'));

                    // if control is "text" or "textarea" and does not already have a value
                    } elseif (in_array($attributes['type'], array('text', 'textarea', 'email', 'number')) && $attributes['value'] === '') {

                        // if this is a "date" control
                        if (strtolower(get_class($control)) == 'zebra_form_date') {

                            // get the date control's starting/ending date
                            $limits = $control->_init();

                            // set the value to the first selectabel date
                            $control->set_attributes(array('value' => date($control->attributes['format'], $limits[0] > 0 ? $limits[0] : time())));

                        // continue only if the control doesn't have the "regexp" or the "captcha" rule set
                        } elseif (!isset($control->rules['regexp']) && !isset($control->rules['captcha'])) {

                            unset($value);

                            // default character set to choose from
                            $characters = 'abcdefghijklmnopqrstuvwxyz';

                            // for controls having the "alphabet", "email" or "emails" rule set
                            if (isset($control->rules['alphabet']) || isset($control->rules['email']) || isset($control->rules['emails']))

                                // use these characters in the random string
                                $characters = 'abcdefghijklmnopqrstuvwxyz';

                            // for controls having the "alphanumeric" rule set
                            if (isset($control->rules['alphanumeric']))

                                // use these characters in the random string
                                $characters = 'abcdefghijklmnopqrstuvwxyz0123456789';

                            // for controls having the "digits" or "number" rule set
                            if (isset($control->rules['digits']) || isset($control->rules['number']))

                                // use these characters for the random value
                                $characters = '0123456789';

                            // for controls having the "float" rule set
                            if (isset($control->rules['float']))

                                // generate a random value
                                $value = number_format(mt_rand(0, 99999) / 100, 2);

                            // if a value was not yet generated
                            if (!isset($value)) {

                                $value = '';

                                // get a random length for our value
                                $length = rand(

                                    // if a length is specified and it has a lower limit, use that as rand()'s lower limit, or "10" otherwise
                                    (isset($control->rules['length']) && $control->rules['length'][0] != 0 ? $control->rules['length'][0] : 10),

                                    // if a length is specified and it has an upper limit, use that as rand()'s upper limit, or "10" otherwise
                                    // (for textareas not having an upper limit for length "100" will be used as rand()'s upper limit)
                                    (isset($control->rules['length']) && $control->rules['length'][1] != 0 ? $control->rules['length'][1] : ($attributes['type'] == 'textarea' ? 100 : 10))
                                );

                                // get a random character until we get to the length defined above
                                // for textareas include a space every once in a while
                                for ($i = 0; $i < $length; $i++) $value .= ($attributes['type'] == 'textarea' && in_array(rand(0, 10), array(4, 6)) ? ' ' : $characters[rand(0, strlen($characters) - 1)]);

                                // if control has the "email" or "emails" rule set
                                if (isset($control->rules['email']) || isset($control->rules['emails'])) {

                                    // append an "@" to the already generated value
                                    $value .= '@';

                                    // and then generate six more random characters
                                    for ($i = 0; $i < 6; $i++) $value .= $characters[rand(0, strlen($characters) - 1)];

                                    // finish up with a ".com"
                                    $value .= '.com';

                                // if control has the "url" rule set add the "http://" prefix (if required) and the ".com" suffix
                                } elseif (isset($control->rules['url'])) $value = ($control->rules['url'][0] ? 'http://' : '') . $value . '.com';

                            }

                            // finally, if we have a random value for the control, set it
                            if (isset($value)) $control->set_attributes(array('value' => trim($value)));

                        }

                    // if control is "time"
                    } elseif ($attributes['type'] == 'time' && $attributes['value'] === '')

                        // select random values, from the exiting ones
                        $control->set_attributes(array('value' => $control->attributes['hours'][array_rand($control->attributes['hours'])] . ':' . $control->attributes['minutes'][array_rand($control->attributes['minutes'])] . ':' . $control->attributes['seconds'][array_rand($control->attributes['seconds'])]));

                }

            }

        }

        // return the state of the form
        return $form_is_valid;

    }

    /**
     *  This method performs the server-side validation of a control, making sure that the value complies to the rules
     *  set for the control through the {@link Zebra_Form_Control::set_rule() set_rule()} method.
     *
     *  @param  string  $control    Unique name that identifies the control in the form.
     *
     *  @return boolean             Returns TRUE if every rule was obeyed, FALSE if not.
     */
    function validate_control($control)
    {

        // reference to the form submission method
        global ${'_' . $this->form_properties['method']};

        $method = & ${'_' . $this->form_properties['method']};

        // at this point, we assume that the control is not valid
        $valid = false;

        // continue only if form was submitted
        if (

            isset($method[$this->form_properties['identifier']]) &&

            $method[$this->form_properties['identifier']] == $this->form_properties['name']

        ) {

            // at this point, we assume that the control is valid
            $valid = true;

            // reference to control
            $control = & $this->controls[$control];

            // treat "email" and "number" types as "text"
            if (in_array($control->attributes['type'], array('email', 'number'))) $control->attributes['type'] = 'text';

            // manage submitted value
            $control->get_submitted_value();

            // get some attributes of the control
            $attribute = $control->get_attributes(array('name', 'id', 'type', 'value', 'multiple', 'format', 'disable_spam_filter', 'other'));

            // if control doesn't have the SPAM filter disabled
            if (!isset($attribute['disable_spam_filter']) || $attribute['disable_spam_filter'] !== true) {

                // check to see if there is SPAM/INJECTION attempt by checking if the values in select boxes, radio buttons
                // and checkboxes are in the list of allowable values, as set when initializing the controls

                // check controls by type
                switch ($attribute['type']) {

                    // if control is a select box
                    case 'select':

                        // if control was submitted
                        // (as there can also be no selections for a select box with the "multiple" attribute set, case in
                        // which there's no submission)
                        // (also, the isset() check is for when we "lock" controls)
                        if (isset($control->submitted_value) && $control->submitted_value) {

                            // flatten array (in case we have select groups)
                            $values = $this->_extract_values($control->attributes['options']);

                            // if the "other" attribute is set, then "other" is a valid option
                            if (isset($attribute['other'])) $values[] = 'other';

                            // we need to treat all values as strings
                            // or the in_array below will fail in strict mode
                            array_walk($values, create_function('&$value', '$value = (string)$value;'));

                            // if an array was submitted and there are values that are not in the list allowable values
                            if (is_array($control->submitted_value) && $control->submitted_value != array_intersect($control->submitted_value, $values))

                                // set a flag accordingly
                                $valid = false;

                            // if submitted value is not an array and submitted value is not in the list of allowable values
                            // we use strict mode or any string, when compared to 0, will be valid...
                            if (!is_array($control->submitted_value) && !in_array($control->submitted_value, $values, true))

                                // set a flag accordingly
                                $valid = false;

                        }

                        break;

                    // if control is a checkbox control or a radio button
                    case 'checkbox':
                    case 'radio':

                        // if control was submitted
                        if ($control->submitted_value) {

                            $values = array();

                            // iterate through all the form's controls
                            foreach ($this->controls as $element)

                                // if control is of the same type and has the same name
                                if ($element->attributes['type'] == $attribute['type'] && $element->attributes['name'] == $attribute['name'])

                                    // add the control's value to the list of valid values
                                    $values[] = $element->attributes['value'];

                            // if an array was submitted and there are values that are not in the list allowable values
                            if (is_array($control->submitted_value) && $control->submitted_value != array_intersect($control->submitted_value, $values))

                                // set a flag accordingly
                                $valid = false;

                            // if submitted value is not an array and submitted value is not in the list of allowable values
                            if (!is_array($control->submitted_value) && !in_array($control->submitted_value, $values))

                                // set a flag accordingly
                                $valid = false;

                        }

                        break;

                }

                // if spam attempt was detected
                if (!$valid) {

                    // set the error message
                    $this->add_error('zf_error_spam', $this->form_properties['language']['spam_detected']);

                    // don't look further
                    return false;

                }

            }

            // if
            if (

                // control was submitted and has rules assigned
                isset($control->submitted_value) && !empty($control->rules)

            ) {

                // we need to make sure that rules are in propper order, the order of priority being "dependencies",
                // "required" and "upload"

                // if the upload rule exists
                if (isset($control->rules['upload'])) {

                    // remove it from wherever it is
                    $rule = array_splice($control->rules, array_search('upload', array_keys($control->rules)), 1, array());

                    // and make sure it's the first rule
                    $control->rules = array_merge($rule, $control->rules);

                }

                // if the "required" rule exists
                if (isset($control->rules['required'])) {

                    // remove it from wherever it is
                    $rule = array_splice($control->rules, array_search('required', array_keys($control->rules)), 1, array());

                    // and make sure it's the first rule (it has to be checked prior to the "upload" rule)
                    $control->rules = array_merge($rule, $control->rules);

                }

                // if the "dependencies" rule exists
                if (isset($control->rules['dependencies'])) {

                    // remove it from wherever it is
                    $rule = array_splice($control->rules, array_search('dependencies', array_keys($control->rules)), 1, array());

                    // and make sure it's the first rule (it has to be checked prior to the "required" and "upload" rules)
                    $control->rules = array_merge($rule, $control->rules);

                }

                // iterate through rules assigned to the control
                foreach ($control->rules as $rule_name => $rule_attributes) {

                    // make sure the rule name is in lowercase
                    $rule_name = strtolower($rule_name);

                    // check the rule's name
                    switch ($rule_name) {

                        // if rule is "age"
                        case 'age':

                            if (

                                // control is 'text'
                                $attribute['type'] == 'text' &&

                                // a value was entered
                                $attribute['value'] != '' &&

                                // control was validated
                                isset($control->attributes['date']) &&

                                // control contains a valid date
                                date('Y-m-d', strtotime($control->attributes['date'])) == $control->attributes['date']

                            ) {

                                // the allowed age interval
                                $min_age = $rule_attributes[0][0];
                                $max_age = $rule_attributes[0][1];

                                // compute age
                                $datetime1 = new DateTime();
                                $datetime2 = new DateTime($control->attributes['date']);
                                $interval = $datetime1->diff($datetime2);
                                $age = $interval->format('%y');

                                // if age is invalid
                                if (!(($min_age == 0 || $age >= $min_age) && ($max_age == 0 || $age <= $max_age))) {

                                    // add error message to indicated error block
                                    $this->add_error($rule_attributes[1], $rule_attributes[2]);

                                    // the control does not validate
                                    $valid = false;

                                    // no further checking needs to be done for the control, making sure that only one
                                    // error message is displayed at a time for each erroneous control
                                    break 2;

                                }

                            }

                            break;

                        // if rule is 'alphabet'
                        case 'alphabet':

                            if (

                                (
                                    // control is 'password'
                                    $attribute['type'] == 'password' ||

                                    // control is 'text'
                                    $attribute['type'] == 'text' ||

                                    // control is 'textarea'
                                    $attribute['type'] == 'textarea'

                                ) &&

                                // a value was entered
                                $attribute['value'] != '' &&

                                // control does not contain only letters from the alphabet (and other allowed characters, if any)
                                // we're also fixing a bug where in PHP prior to 5.3, preg_quote was not escaping dashes (-)
                                !preg_match('/^[a-z' . preg_replace('/\//', '\/', preg_replace('/(?<!\\\)\-/', '\-', preg_quote($rule_attributes[0]))) . ']+$/i', $attribute['value'])

                            ) {

                                // add error message to indicated error block
                                $this->add_error($rule_attributes[1], $rule_attributes[2]);

                                // the control does not validate
                                $valid = false;

                                // no further checking needs to be done for the control, making sure that only one
                                // error message is displayed at a time for each erroneous control
                                break 2;

                            }

                            break;

                        // if rule is 'alphanumeric'
                        case 'alphanumeric':

                            if (

                                (
                                    // control is 'password'
                                    $attribute['type'] == 'password' ||

                                    // control is 'text'
                                    $attribute['type'] == 'text' ||

                                    // control is 'textarea'
                                    $attribute['type'] == 'textarea'

                                ) &&

                                // a value was entered
                                $attribute['value'] != '' &&

                                // control does not contain only allowed characters
                                // we're also fixing a bug where in PHP prior to 5.3, preg_quote was not escaping dashes (-)
                                !preg_match('/^[a-z0-9' . preg_replace('/\//', '\/', preg_replace('/(?<!\\\)\-/', '\-', preg_quote($rule_attributes[0]))) . ']+$/i', $attribute['value'])

                            ) {

                                // add error message to indicated error block
                                $this->add_error($rule_attributes[1], $rule_attributes[2]);

                                // the control does not validate
                                $valid = false;

                                // no further checking needs to be done for the control, making sure that only one
                                // error message is displayed at a time for each erroneous control
                                break 2;

                            }

                            break;

                        // if 'captcha'
                        case 'captcha':

                            if (

                                // control is 'text'
                                $attribute['type'] == 'text' &&

                                // control's value is not the one showed in the picture
                                md5(md5(md5(strtolower($control->submitted_value)))) !=  ($this->form_properties['captcha_storage'] == 'session' ? @$_SESSION['captcha'] : @$_COOKIE['captcha'])

                            ) {

                                // add error message to indicated error block
                                $this->add_error($rule_attributes[0], $rule_attributes[1]);

                                // the control does not validate
                                $valid = false;

                                // no further checking needs to be done for the control, making sure that only one
                                // error message is displayed at a time for each erroneous control
                                break 2;

                            }

                            break;

                        // if 'compare'
                        case 'compare':

                            if (

                                (
                                    // control is 'password'
                                    $attribute['type'] == 'password' ||

                                    // control is 'text'
                                    $attribute['type'] == 'text' ||

                                    // control is 'textarea'
                                    $attribute['type'] == 'textarea'

                                ) && (

                                    // the control to compare to was not submitted
                                    !isset($method[$rule_attributes[0]]) ||

                                    // OR
                                    (

                                        // the control to compare to was submitted
                                        isset($method[$rule_attributes[0]]) &&

                                        // and the values don't match
                                        $control->submitted_value != $method[$rule_attributes[0]]

                                    )

                                )

                            ) {

                                // add error message to indicated error block
                                $this->add_error($rule_attributes[1], $rule_attributes[2]);

                                // the control does not validate
                                $valid = false;

                                // no further checking needs to be done for the control, making sure that only one
                                // error message is displayed at a time for each erroneous control
                                break 2;

                            }

                            break;

                        // if 'dependencies'
                        case 'dependencies':

                            // if not all conditions are met, don't validate the control
                            if (!$this->_validate_dependencies($attribute['id'])) return true;

                            break;

                        // if 'convert'
                        case 'convert':

                            if (

                                // control is 'file'
                                $attribute['type'] == 'file' &&

                                // and a file was uploaded
                                isset($_FILES[$attribute['name']]) &&

                                // and file was uploaded without any errors
                                $_FILES[$attribute['name']]['error'] == 0

                            ) {

                                // as conversions are done only when the form is valid
                                // for now we only save some data that will be processed if the form is valid
                                // (we're adding keys so that we don't have duplicate actions if validate_control method is called repeatedly)
                                $this->actions[$attribute['name'] . '_convert'] = array(

                                    '_convert',                                             //  method to be called
                                    $attribute['name'],                                     //  the file upload control's name
                                    'extension'                 =>  $rule_attributes[0],    //  extension to convert to
                                    'quality'                   =>  $rule_attributes[1],    //  quality (available only for JPEG files)
                                    'preserve_original_file'    =>  $rule_attributes[2],    //  preserve original file?
                                    'overwrite'                 =>  $rule_attributes[3],    //  overwrite if file with new extension exists
                                    'block'                     =>  $rule_attributes[4],    //  error block
                                    'message'                   =>  $rule_attributes[5],    //  error message

                                );

                            }

                            break;

                        // if 'custom' rule
                        case 'custom':

                            // custom rules are stored as an array
                            // iterate through the custom rules
                            foreach ($rule_attributes as $custom_rule_attributes) {

                                // if custom function exists
                                if (is_callable($custom_rule_attributes[0])) {

                                    // the arguments that we are passing to the custom function are the control's
                                    // submitted value and all other arguments passed when setting the custom rule
                                    // except the first one which is the custom function's and the last two which are
                                    // the error block name and the error message respectively
                                    $arguments = array_merge(array($control->submitted_value), array_slice($custom_rule_attributes, 1, -2));

                                    // run the custom function
                                    // and if the function returns false
                                    if (!call_user_func_array($custom_rule_attributes[0], $arguments)) {

                                        // count the arguments passed when declaring the rules
                                        $attributes_count = count($custom_rule_attributes);

                                        // add error message to indicated error block
                                        $this->add_error($custom_rule_attributes[$attributes_count - 2], $custom_rule_attributes[$attributes_count - 1]);

                                        // the control does not validate
                                        $valid = false;

                                        // no further checking needs to be done for the control, making sure that only one
                                        // error message is displayed at a time for each erroneous control
                                        break 3;

                                    }

                                // if custom function doesn't exist, trigger an error message
                                } else _zebra_form_show_error('Function <strong>' . $custom_rule_attributes[0] . '()</strong> doesn\'t exist.', E_USER_ERROR);

                            }

                            break;

                        // if date
                        case 'date':

                            if (

                                // control is 'text'
                                $attribute['type'] == 'text' &&

                                // is a 'date' control
                                isset($attribute['format']) &&

                                // a value was entered
                                $attribute['value'] != ''

                            ) {

                                // if
                                if (

                                    // initialize the datepicker's data for further calculations
                                    $control->_init() &&

                                    // date has an invalid format
                                    !($timestamp = $control->_is_format_valid($attribute['value'])) ||

                                    // or date is disabled
                                    $control->_is_disabled(date('Y', $timestamp), date('n', $timestamp), date('d', $timestamp))

                                ) {

                                    // add error message to indicated error block
                                    $this->add_error($rule_attributes[0], $rule_attributes[1]);

                                    // the control does not validate
                                    $valid = false;

                                    // no further checking needs to be done for the control, making sure that only one
                                    // error message is displayed at a time for each erroneous control
                                    break 2;

                                }

                            }

                            break;

                        // if "datecompare"
                        case 'datecompare':

                            if (

                                // control is 'text'
                                $attribute['type'] == 'text' &&

                                // is a 'date' control
                                isset($attribute['format']) &&

                                // a value was entered
                                $attribute['value'] != '' &&

                                // control to compare with, exists
                                isset($this->controls[$rule_attributes[0]]) &&

                                // control to compare with, is a 'text' control
                                $this->controls[$rule_attributes[0]]->attributes['type'] == 'text' &&

                                // control to compare with, is a 'date' control
                                ($this->controls[$rule_attributes[0]]->attributes['format']) &&

                                // control validates
                                $this->validate_control($this->controls[$rule_attributes[0]]->attributes['id'])

                            ) {

                                // we assume the control is invalid
                                $valid = false;

                                // compare the controls according to the comparison operator
                                switch ($rule_attributes[1]) {
                                    case '>':
                                        $valid = ($control->attributes['date'] > $this->controls[$rule_attributes[0]]->attributes['date']);
                                        break;
                                    case '>=':
                                        $valid = ($control->attributes['date'] >= $this->controls[$rule_attributes[0]]->attributes['date']);
                                        break;
                                    case '<':
                                        $valid = ($control->attributes['date'] < $this->controls[$rule_attributes[0]]->attributes['date']);
                                        break;
                                    case '<=':
                                        $valid = ($control->attributes['date'] <= $this->controls[$rule_attributes[0]]->attributes['date']);
                                        break;
                                }

                                // if invalid
                                if (!$valid) {

                                    // add error message to indicated error block
                                    $this->add_error($rule_attributes[2], $rule_attributes[3]);

                                    // the control does not validate
                                    $valid = false;

                                    // no further checking needs to be done for the control, making sure that only one
                                    // error message is displayed at a time for each erroneous control
                                    break 2;

                                }

                            }

                            break;

                        // if rule is 'digits'
                        case 'digits':

                            if (

                                (
                                    // control is 'password'
                                    $attribute['type'] == 'password' ||

                                    // control is 'text'
                                    $attribute['type'] == 'text' ||

                                    // control is 'textarea'
                                    $attribute['type'] == 'textarea'

                                ) &&

                                // a value was entered
                                $attribute['value'] != '' &&

                                // but entered value does not contain digits only (and other allowed characters, if any)
                                // we're also fixing a bug where in PHP prior to 5.3, preg_quote was not escaping dashes (-)
                                !preg_match('/^[0-9' . preg_replace('/\//', '\/', preg_replace('/(?<!\\\)\-/', '\-', preg_quote($rule_attributes[0]))) . ']+$/', $attribute['value'])

                            ) {

                                // add error message to indicated error block
                                $this->add_error($rule_attributes[1], $rule_attributes[2]);

                                // the control does not validate
                                $valid = false;

                                // no further checking needs to be done for the control, making sure that only one
                                // error message is displayed at a time for each erroneous control
                                break 2;

                            }

                            break;

                        // if "email"
                        case 'email':

                            if (

                                (
                                    // control is 'password'
                                    $attribute['type'] == 'password' ||

                                    // control is 'text'
                                    $attribute['type'] == 'text' ||

                                    // control is 'textarea'
                                    $attribute['type'] == 'textarea'

                                ) &&

                                // a value was entered
                                $attribute['value'] != '' && (

                                    // email address contains consecutive dots
                                    preg_match('/\.{2,}/', $attribute['value']) ||

                                    // email address is longer than the maximum allowed length
                                    strlen($attribute['value']) > 254 ||

                                    // email address has an invalid format
                                    !preg_match('/^[^\.][a-z0-9_\-\+\~\^\{\}\.]{1,64}@[a-z0-9_\-\+\~\^\{\}\.]{1,255}\.[a-z0-9]{2,}$/i', $attribute['value'])

                                )

                            ) {

                                // add error message to indicated error block
                                $this->add_error($rule_attributes[0], $rule_attributes[1]);

                                // the control does not validate
                                $valid = false;

                                // no further checking needs to be done for the control, making sure that only one
                                // error message is displayed at a time for each erroneous control
                                break 2;

                            }

                            break;

                        // if "list of emails"
                        case 'emails':

                            if (

                                (
                                    // control is 'password'
                                    $attribute['type'] == 'password' ||

                                    // control is 'text'
                                    $attribute['type'] == 'text' ||

                                    // control is 'textarea'
                                    $attribute['type'] == 'textarea'

                                ) &&

                                // a value was entered
                                $attribute['value'] != ''

                            ) {

                                // convert string to an array of addresses
                                $addresses = explode(',', $attribute['value']);

                                // iterate through the addresses
                                foreach ($addresses as $address)

                                    // not a valid email address
                                    if (!preg_match('/^([a-zA-Z0-9_\-\+\~\^\{\}]+[\.]?)+@{1}([a-zA-Z0-9_\-\+\~\^\{\}]+[\.]?)+\.[A-Za-z0-9]{2,}$/', trim($address))) {

                                        // add error message to indicated error block
                                        $this->add_error($rule_attributes[0], $rule_attributes[1]);

                                        // the control does not validate
                                        $valid = false;

                                        // no further checking needs to be done for the control, making sure that only one
                                        // error message is displayed at a time for each erroneous control
                                        break 3;

                                    }

                            }

                            break;

                        // if "filesize"
                        case 'filesize':

                            if (

                                // control is 'file'
                                $attribute['type'] == 'file' &&

                                // and a file was uploaded
                                isset($_FILES[$attribute['name']]) &&

                                (

                                    // uploaded file size exceeds the size imposed when creating the form
                                    $_FILES[$attribute['name']]['size'] > $rule_attributes[0] ||

                                    // the uploaded file exceeds the upload_max_filesize directive in php.ini
                                    $_FILES[$attribute['name']]['error'] == 1 ||

                                    // the uploaded file exceeds the MAX_FILE_SIZE directive that was specified
                                    // in the HTML form
                                    $_FILES[$attribute['name']]['error'] == 2

                                )

                            ) {

                                // add error message to indicated error block
                                $this->add_error($rule_attributes[1], $rule_attributes[2]);

                                // the control does not validate
                                $valid = false;

                                // no further checking needs to be done for the control, making sure that only one
                                // error message is displayed at a time for each erroneous control
                                break 2;

                            }

                            break;

                        // if "filetype"
                        case 'filetype':

                            if (

                                // control is 'file'
                                $attribute['type'] == 'file' &&

                                // and a file was uploaded
                                isset($_FILES[$attribute['name']]) &&

                                // and file was uploaded without errors
                                $_FILES[$attribute['name']]['error'] == 0

                            ) {

                                // if "finfo_open" function exists (from PHP 5.3.0)
                                if (function_exists('finfo_open')) {

                                    // determine the "true" mime type of the uploaded file
                                    $finfo = finfo_open(FILEINFO_MIME_TYPE);
                                    $mime = finfo_file($finfo, $_FILES[$attribute['name']]['tmp_name']);
                                    finfo_close($finfo);

                                // otherwise, rely on the information returned by $_FILES which uses the file's
                                // extension to determine the uploaded file's mime type and is therefore unreliable
                                } else $mime = $_FILES[$attribute['name']]['type'];

                                // get the allowed file types
                                $allowed_file_types = array_map(create_function('$value', 'return trim($value);'), explode(',', $rule_attributes[0]));

                                // this will contain an array of file types that match for the currently uploaded file's
                                // mime type
                                $matching_file_types = array();

                                // load mime file types
                                $this->_load_mime_types();

                                // iterate through the known mime types
                                foreach ($this->form_properties['mimes'] as $extension => $type)

                                    // if
                                    if (

                                        // there are more mime types associated with the file extension and
                                        // the uploaded file's type is among them
                                        is_array($type) && in_array($mime, $type) ||

                                        // a single mime type is associated with the file extension and
                                        // the uploaded file's type matches the mime type
                                        !is_array($type) && $type == $mime

                                    // add file type to the list of file types that match for the currently uploaded
                                    // file's mime type
                                    ) $matching_file_types[] = $extension;

                                // is the file allowed?
                                $matches = array_intersect($matching_file_types, $allowed_file_types);

                                // if file is not allowed
                                if (empty($matches)) {

                                    // add error message to indicated error block
                                    $this->add_error($rule_attributes[1], $rule_attributes[2]);

                                    // the control does not validate
                                    $valid = false;

                                    // no further checking needs to be done for the control, making sure that only one
                                    // error message is displayed at a time for each erroneous control
                                    break 2;

                                }

                            }

                            break;

                        // if rule is 'float'
                        case 'float':

                            if (

                                (
                                    // control is 'password'
                                    $attribute['type'] == 'password' ||

                                    // control is 'text'
                                    $attribute['type'] == 'text' ||

                                    // control is 'textarea'
                                    $attribute['type'] == 'textarea'

                                ) &&

                                // a value was entered
                                $attribute['value'] != '' &&

                                (

                                    // only a dot given
                                    trim($attribute['value']) == '.' ||

                                    // only minus given
                                    trim($attribute['value']) == '-' ||

                                    // has too many minus sign
                                    preg_match_all('/\-/', $attribute['value'], $matches) > 1 ||

                                    // has too many dots in it
                                    preg_match_all('/\./', $attribute['value'], $matches) > 1 ||

                                    // not a floating point number
                                    // we're also fixing a bug where in PHP prior to 5.3, preg_quote was not escaping dashes (-)
                                    !preg_match('/^[0-9\-\.' . preg_replace('/\//', '\/', preg_replace('/(?<!\\\)\-/', '\-', preg_quote($rule_attributes[0]))) . ']+$/', $attribute['value']) ||

                                    // has a minus sign in it but is not at the very beginning
                                    (strpos($attribute['value'], '-') !== false && strpos($attribute['value'], '-') > 0)

                                )

                            ) {

                                // add error message to indicated error block
                                $this->add_error($rule_attributes[1], $rule_attributes[2]);

                                // the control does not validate
                                $valid = false;

                                // no further checking needs to be done for the control, making sure that only one
                                // error message is displayed at a time for each erroneous control
                                break 2;

                            }

                            break;

                        // if "image"
                        case 'image':

                            if (

                                // control is 'file'
                                $attribute['type'] == 'file' &&

                                // and a file was uploaded
                                isset($_FILES[$attribute['name']]) &&

                                // and file was uploaded without errors
                                $_FILES[$attribute['name']]['error'] == 0

                            ) {

                                // get some information about the file
                                list($width, $height, $type, $attr) = @getimagesize($_FILES[$attribute['name']]['tmp_name']);

                                // if file is not an image or image is not gif, png or jpeg
                                if ($type === false || $type < 1 || $type > 3) {

                                    // add error message to indicated error block
                                    $this->add_error($rule_attributes[0], $rule_attributes[1]);

                                    // the control does not validate
                                    $valid = false;

                                    // no further checking needs to be done for the control, making sure that only one
                                    // error message is displayed at a time for each erroneous control
                                    break 2;

                                }

                            }

                            break;

                        // if "length"
                        case 'length':

                            // the rule will be considered as not obeyed when
                            if (

                                (
                                    // control is 'password'
                                    $attribute['type'] == 'password' ||

                                    // control is 'text'
                                    $attribute['type'] == 'text' ||

                                    // control is 'textarea'
                                    $attribute['type'] == 'textarea'

                                ) &&

                                // a value was entered
                                $attribute['value'] != '' &&

                                (
                                    // the length of the value exceeds boundaries
                                    strlen(utf8_decode(html_entity_decode($attribute['value']))) < $rule_attributes[0] ||

                                    // we use the utf8_decode because some characters have 2 bytes and some 3 bytes
                                    // read more at http://globalizer.wordpress.com/2007/01/16/utf-8-and-string-length-limitations/
                                    ($rule_attributes[1] > 0 && strlen(utf8_decode(html_entity_decode($attribute['value']))) > $rule_attributes[1])

                                )

                            ) {

                                // add error message to indicated error block
                                $this->add_error($rule_attributes[2], $rule_attributes[3]);

                                // the control does not validate
                                $valid = false;

                                // no further checking needs to be done for the control, making sure that only one
                                // error message is displayed at a time for each erroneous control
                                break 2;

                            }

                            break;

                        // if rule is 'number'
                        case 'number':

                            if (

                                (
                                    // control is 'password'
                                    $attribute['type'] == 'password' ||

                                    // control is 'text'
                                    $attribute['type'] == 'text' ||

                                    // control is 'textarea'
                                    $attribute['type'] == 'textarea'

                                ) &&

                                // a value was entered
                                $attribute['value'] != '' &&

                                (

                                    // only minus given
                                    trim($attribute['value']) == '-' ||

                                    // has too many minus sign
                                    preg_match_all('/\-/', $attribute['value'], $matches) > 1 ||

                                    // not a number
                                    // we're also fixing a bug where in PHP prior to 5.3, preg_quote was not escaping dashes (-)
                                    !preg_match('/^[0-9\-' . preg_replace('/\//', '\/', preg_replace('/(?<!\\\)\-/', '\-', preg_quote($rule_attributes[0]))) . ']+$/', $attribute['value']) ||

                                    // has a minus sign in it but is not at the very beginning
                                    (strpos($attribute['value'], '-') !== false && strpos($attribute['value'], '-') > 0)

                                )

                            ) {

                                // add error message to indicated error block
                                $this->add_error($rule_attributes[1], $rule_attributes[2]);

                                // the control does not validate
                                $valid = false;

                                // no further checking needs to be done for the control, making sure that only one
                                // error message is displayed at a time for each erroneous control
                                break 2;

                            }

                            break;

                        // if rule is "range"
                        case 'range':

                            if (

                                // control is 'text'
                                $attribute['type'] == 'text' &&

                                // a value was entered
                                $attribute['value'] != ''

                            ) {

                                // get the allowed min and max
                                $min = $rule_attributes[0][0];
                                $max = $rule_attributes[0][1];

                                // make sure the value is a number
                                $value = (float)$attribute['value'];


                                // if
                                if (

                                    // parsed value is different than what the user entered
                                    $value != $attribute['value'] ||

                                    // or the value is not within range
                                    !(($min == 0 || $value >= $min) && ($max == 0 || $value <= $max))

                                ) {

                                    // add error message to indicated error block
                                    $this->add_error($rule_attributes[1], $rule_attributes[2]);

                                    // the control does not validate
                                    $valid = false;

                                    // no further checking needs to be done for the control, making sure that only one
                                    // error message is displayed at a time for each erroneous control
                                    break 2;

                                }

                            }

                            break;

                        // if "regexp"
                        case 'regexp':

                            if (

                                (
                                    // control is 'password'
                                    $attribute['type'] == 'password' ||

                                    // control is 'text'
                                    $attribute['type'] == 'text' ||

                                    // control is 'textarea'
                                    $attribute['type'] == 'textarea'

                                ) &&

                                // a value was entered
                                $attribute['value'] != '' &&

                                // value does not match regular expression
                                !preg_match('/' . $rule_attributes[0] . '/', $attribute['value'])

                            ) {

                                // add error message to indicated error block
                                $this->add_error($rule_attributes[1], $rule_attributes[2]);

                                // the control does not validate
                                $valid = false;

                                // no further checking needs to be done for the control, making sure that only one
                                // error message is displayed at a time for each erroneous control
                                break 2;

                            }

                            break;

                        // if "required"
                        case 'required':

                            // if it's a drop-down that is part of a time control
                            if ($attribute['type'] == 'time') {

                                // if invalid format specified, revert to the default "hm"
                                if (preg_match('/^[hmsg]+$/i', $attribute['format']) == 0 || strlen(preg_replace('/([a-z]{2,})/i', '$1', $attribute['format'])) != strlen($attribute['format'])) $attribute['format'] = 'hm';

                                $regexp = '';

                                // build the regular expression for validating the time
                                for ($i = 0; $i < strlen($attribute['format']); $i++) {

                                    // for each characher in the format we use a particular regular expression
                                    switch (strtolower(substr($attribute['format'], $i, 1))) {

                                        case 'h':

                                            // if 12 hour format is used use this expression...
                                            if (strpos(strtolower($attribute['format']), 'g')) $regexp .= '0[1-9]|1[012]';

                                            // ...and different expression for the 24 hour format
                                            else $regexp .= '([0-1][0-9]|2[0-3])';

                                            break;

                                        case 'm':
                                        case 's':

                                            // regular expression for validating minutes and seconds
                                            $regexp .= '[0-5][0-9]';

                                            break;

                                        case 'g':

                                            // validate am/pm
                                            $regexp .= '(am|pm)';

                                            break;

                                    }

                                }

                                // if time does not validate
                                if (preg_match('/' . $regexp . '/i', str_replace(array(':', ' '), '', $attribute['value'])) == 0) {

                                    // add error message to indicated error block
                                    $this->add_error($rule_attributes[0], $rule_attributes[1]);

                                    // the control does not validate
                                    $valid = false;

                                    // no further checking needs to be done for the control, making sure that only one
                                    // error message is displayed at a time for each erroneous control
                                    break 2;

                                }

                            // for other controls
                            } else {

                                // if control is 'select'
                                if ($attribute['type'] == 'select') {

                                    // as of PHP 5.3, array_shift required the argument to be a variable and not the result
                                    // of a function so we need this intermediary step
                                    $notSelectedIndex = array_keys($control->attributes['options']);

                                    // get the index which when selected indicated that 'nothing is selected'
                                    $notSelectedIndex = array_shift($notSelectedIndex);

                                }

                                // the rule will be considered as not obeyed when
                                if (

                                    // control is 'password' or 'text' or 'textarea' and the 'value' attribute is empty
                                    (($attribute['type'] == 'password' || $attribute['type'] == 'text' || $attribute['type'] == 'textarea') && trim($attribute['value']) == '') ||

                                    // control is 'file' and no file specified
                                    ($attribute['type'] == 'file' && isset($_FILES[$attribute['name']]) && trim($_FILES[$attribute['name']]['name']) == '') ||

                                    // control is 'checkbox' or 'radio' and the control was not submitted
                                    (($attribute['type'] == 'checkbox' || $attribute['type'] == 'radio') && $control->submitted_value === false) ||

                                    // control is 'select', the 'multiple' attribute is set and control was not submitted
                                    ($attribute['type'] == 'select' && isset($attribute['multiple']) && $control->submitted_value === false) ||

                                    // control is 'select', the 'multiple' attribute is not set and the select control's first value is selected
                                    ($attribute['type'] == 'select' && !isset($attribute['multiple']) && (is_array($control->submitted_value) || strcmp($control->submitted_value, $notSelectedIndex) == 0)) ||

                                    // control is 'select', the 'multiple' attribute is not set, the select control's value is "other" and the "other" control is empty
                                    ($attribute['type'] == 'select' && !isset($attribute['multiple']) && $control->submitted_value == 'other' && trim($method[$attribute['name'] . $this->form_properties['other_suffix']]) == '')

                                ) {

                                    // add error message to indicated error block
                                    $this->add_error($rule_attributes[0], $rule_attributes[1]);

                                    // the control does not validate
                                    $valid = false;

                                    // no further checking needs to be done for the control, making sure that only one
                                    // error message is displayed at a time for each erroneous control
                                    break 2;

                                }

                            }

                            break;

                        // if 'resize'
                        case 'resize':

                            if (

                                // control is 'file'
                                $attribute['type'] == 'file' &&

                                // and a file was uploaded
                                isset($_FILES[$attribute['name']]) &&

                                // and file was uploaded without any errors
                                $_FILES[$attribute['name']]['error'] == 0

                            ) {

                                // as of PHP 5.3, array_shift required the argument to be a variable and not the result
                                // of a function so we need this intermediary step
                                $tmp = array_values($rule_attributes);

                                // if not multiple resize calls
                                // make it look like multiple resize call
                                if (!is_array(array_shift($tmp)))

                                    $rule_attributes = array($rule_attributes);

                                // iterate through the resize calls
                                foreach ($rule_attributes as $index => $rule_attribute)

                                    // as resizes are done only when the form is valid and after the file has been
                                    // uploaded, for now we only save some data that will be processed if the form is valid
                                    // (we're adding keys so that we don't have duplicate actions if validate_control method is called repeatedly)
                                    $this->actions[$attribute['name'] . '_resize_' . $index] = array(

                                        '_resize',          //  method that needs to be called
                                        $attribute['name'], //  the file upload control's name
                                        $rule_attribute[0], //  prefix for the resized file
                                        $rule_attribute[1], //  width
                                        $rule_attribute[2], //  height
                                        $rule_attribute[3], //  preserve aspect ratio?
                                        $rule_attribute[4], //  method,
                                        $rule_attribute[5], //  background color
                                        $rule_attribute[6], //  enlarge smaller images?
                                        $rule_attribute[7], //  jpeg quality
                                        'block'     =>  $rule_attribute[8],  //  error block
                                        'message'   =>  $rule_attribute[9],  //  error message

                                    );

                            }

                            break;

                        // if 'upload'
                        case 'upload':

                            if (

                                // control is 'file'
                                $attribute['type'] == 'file' &&

                                // and a file was uploaded
                                isset($_FILES[$attribute['name']]) &&

                                // and file was uploaded without any errors
                                $_FILES[$attribute['name']]['error'] == 0

                            )

                                // as uploads are done only when the form is valid
                                // for now we only save some data that will be processed if the form is valid
                                // (we're adding keys so that we don't have duplicate actions if validate_control method is called repeatedly)
                                $this->actions[$attribute['name'] . '_upload'] = array(

                                    '_upload',                              //  method to be called
                                    $attribute['name'],                     //  the file upload control's name
                                    $rule_attributes[0],                    //  the folder where the file to be uploaded to
                                    $rule_attributes[1],                    //  should the original file name be preserved
                                    'block'     =>  $rule_attributes[2],    //  error block
                                    'message'   =>  $rule_attributes[3],    //  error message

                                );

                            break;

                        // if "url"
                        case 'url':

                            if (

                                (
                                    // control is 'password'
                                    $attribute['type'] == 'password' ||

                                    // control is 'text'
                                    $attribute['type'] == 'text' ||

                                    // control is 'textarea'
                                    $attribute['type'] == 'textarea'

                                ) &&

                                // a value was entered
                                $attribute['value'] != '' &&

                                // value does not match regular expression
                                !preg_match('/^(https?\:\/\/)' . ($rule_attributes[0] === true ? '' : '?') . '[^\s\.]+\..{2,}/i', $attribute['value'])

                            ) {

                                // add error message to indicated error block
                                $this->add_error($rule_attributes[1], $rule_attributes[2]);

                                // the control does not validate
                                $valid = false;

                                // no further checking needs to be done for the control, making sure that only one
                                // error message is displayed at a time for each erroneous control
                                break 2;

                            }

                            break;

                    }

                }

            }

        }

        return $valid;

    }

    /**
     *  Generates a CSRF token, unique to the current form.
     *
     *  Note that this will generate a new CSRF token only when the form is generated and not also when the form is
     *  submitted - unless the <b>$force</b> argument is set to TRUE.
     *
     *  @param  boolean $force                  (Optional) Instructs the method to forcefully generate a new CSRF token.
     *
     *                                          This parameter will be TRUE when the method is called after an unsuccessful
     *                                          CSRF token validation or after a successful form validation.
     *
     *                                          By default, this method will generate a new CSRF token *only* if the form
     *                                          is not being currently submitted (form information is not available in the $_POST
     *                                          superglobal).
     *
     *                                          Default is FALSE.
     *
     *  @return void
     *
     *  @access private
     */
    private function _csrf_generate_token($force = false)
    {

        // if CSRF protection is enabled (is not boolean FALSE) and CSRF token was not already generated
        if ($this->form_properties['csrf_storage_method'] !== false) {

            // reference to the form submission method
            global ${'_' . $this->form_properties['method']};

            $method = & ${'_' . $this->form_properties['method']};

            // if
            if (

                // form was submitted and we don't need to forcefully generate a new token
                isset($method[$this->form_properties['identifier']]) && $force === false &&
                // CSRF token is stored in a session variable
                $this->form_properties['csrf_storage_method'] == 'session' &&
                // the session variable exists
                isset($_SESSION[$this->form_properties['csrf_cookie_name']]) &&
                // the session variable holds an array
                is_array($_SESSION[$this->form_properties['csrf_cookie_name']]) &&
                // the array has 2 entries
                count($_SESSION[$this->form_properties['csrf_cookie_name']]) == 2

            // use the already existing CSRF token
            ) $this->form_properties['csrf_token'] = $_SESSION[$this->form_properties['csrf_cookie_name']][0];

            // else if
            elseif (

                // form was submitted and we don't need to forcefully generate a new token
                isset($method[$this->form_properties['identifier']]) && $force === false &&
                // CSRF token is stored in a cookie
                $this->form_properties['csrf_storage_method'] == 'cookie' &&
                // the cookie exists
                isset($_COOKIE[$this->form_properties['csrf_cookie_name']])

            // use the already existing CSRF token
            ) $this->form_properties['csrf_token'] = $_COOKIE[$this->form_properties['csrf_cookie_name']];

            // else, if form was not submitted, or we force new token generation
            elseif (!isset($method[$this->form_properties['identifier']])|| $force === true) {

                // generate a random token
                $this->form_properties['csrf_token'] = md5(uniqid(rand(), true));

                // compute token expiry timestamp
                $csrf_token_expiry = $this->form_properties['csrf_token_lifetime'] == 0 ? 0 : time() + $this->form_properties['csrf_token_lifetime'];

                // if storage method is "session"
                if ($this->form_properties['csrf_storage_method'] == 'session') {

                    // if no session is started, trigger an error message
                    if (!isset($_SESSION)) _zebra_form_show_error('You have chosen to enable protection against cross-site request forgery (CSRF) attacks and to use sessions for storing the CSRF token, but a session is not started! Start a session prior to calling the "csrf()" method', E_USER_ERROR);

                    // if sessions are on, store the CSRF token and the expiration data in session
                    $_SESSION[$this->form_properties['csrf_cookie_name']] = array($this->form_properties['csrf_token'], $csrf_token_expiry);

                // if storage method is "cookie"
                } else {

                    // if PHP version is 5.2.0+
                    if (version_compare(PHP_VERSION, '5.2.0', '>='))

                        // store the CSRF token in a cookie and use also the httponly argument
                    	if (!setcookie(
                            $this->form_properties['csrf_cookie_name'],
                            $this->form_properties['csrf_token'],
                            $csrf_token_expiry,
                            $this->form_properties['csrf_cookie_config']['path'],
                            $this->form_properties['csrf_cookie_config']['domain'],
                            $this->form_properties['csrf_cookie_config']['secure'],
                            $this->form_properties['csrf_cookie_config']['httponly']
                        )) trigger_error('The library tried to store the CSRF token in a cookie but was unable to do so because there was output already sent to the browser. You should either start a session prior to instantiating the library (recommended), have no output (including <html> and <head> tags, as well as any whitespace) sent to the browser prior to instantiating the library, or turn output buffering on in php.ini.', E_USER_ERROR);

                    // if PHP version is lower than 5.2.0
                    else

                        // store the CSRF token in a cookie without also using the httponly argument
                    	if (!setcookie(
                            $this->form_properties['csrf_cookie_name'],
                            $this->form_properties['csrf_token'],
                            $csrf_token_expiry,
                            $this->form_properties['csrf_cookie_config']['path'],
                            $this->form_properties['csrf_cookie_config']['domain'],
                            $this->form_properties['csrf_cookie_config']['secure']
                        )) trigger_error('The library tried to store the CSRF token in a cookie but was unable to do so because there was output already sent to the browser. You should either start a session prior to instantiating the library (recommended), have no output (including <html> and <head> tags, as well as any whitespace) sent to the browser prior to instantiating the library, or turn output buffering on in php.ini.', E_USER_ERROR);

                }

            }

        }

    }

    /**
     *  Validates CSRF token.
     *
     *  @return boolean     Returns TRUE if protection against CSRF attacks is disabled or it is enabled and the CSRF
     *                      token validates, or FALSE otherwise.
     *
     *  @access private
     */
    private function _csrf_validate()
    {

        // if CSRF protection is enabled (is not boolean FALSE)
        if ($this->form_properties['csrf_storage_method'] !== false) {

            // reference to the form submission method
            global ${'_' . $this->form_properties['method']};

            $method = & ${'_' . $this->form_properties['method']};

            // if
            if (

                // the hidden field with the CSRF token was submitted
                isset($method[$this->form_properties['csrf_token_name']]) && (

                    // CSRF token is stored in a session variable
                    ($this->form_properties['csrf_storage_method'] == 'session' &&
                    // the session variable exists
                    isset($_SESSION[$this->form_properties['csrf_cookie_name']]) &&
                    // the session variable holds an array
                    is_array($_SESSION[$this->form_properties['csrf_cookie_name']]) &&
                    // the array has 2 entries
                    count($_SESSION[$this->form_properties['csrf_cookie_name']]) == 2 &&
                    // the value of the hidden field and the value in the session match
                    $method[$this->form_properties['csrf_token_name']] == $_SESSION[$this->form_properties['csrf_cookie_name']][0] &&
                    // if CSRF token doesn't expire or it does but it didn't yet
                    ($_SESSION[$this->form_properties['csrf_cookie_name']][1] == 0 || $_SESSION[$this->form_properties['csrf_cookie_name']][1] > time()))

                    ||

                    // CSRF token is stored in a cookie
                    ($this->form_properties['csrf_storage_method'] == 'cookie' &&
                    // the cookie exists
                    isset($_COOKIE[$this->form_properties['csrf_cookie_name']]) &&
                    // the value of the hidden field and the value in the cookie match
                    $method[$this->form_properties['csrf_token_name']] == $_COOKIE[$this->form_properties['csrf_cookie_name']])

                )

            // everything seems in order, then
            ) return true;

            // if we get here something was fishy...
            return false;

        }

        // if protection against CSRF attacks is not enabled, pretend nothing happened
        return true;

    }

    /**
     *  Converts an image from one type to another.
     *
     *  Note that this method will update the entries in the {@link $file_upload} property as the converted file will
     *  become the "uploaded" file!
     *
     *  @param  string  $control                The file upload control's name
     *
     *  @param  string  $type                   Type to convert an image to.
     *
     *                                          Can be (case-insensitive) JPG, PNG or GIF
     *
     *  @param  integer $jpeg_quality           (Optional) Indicates the quality of the output image (better quality
     *                                          means bigger file size).
     *
     *                                          Range is 0 - 100
     *
     *                                          Available only if <b>type</b> is "jpg".
     *
     *                                          Default is 85.
     *
     *  @param  integer $preserve_original_file (Optional) Should the original file be preserved after the conversion
     *                                          is done?
     *
     *                                          Default is FALSE.
     *
     *  @param  boolean $overwrite              (Optional) 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.
     *
     *                                          Default is FALSE
     *
     *  @return boolean                         Returns TRUE on success or FALSE otherwise
     *
     *  @access private
     */
    private function _convert($control, $type, $jpeg_quality = 85, $preserve_original_file = false, $overwrite = false)
    {

        // make sure the new extension is in lowercase
        $type = strtolower($type);

        // if
        if (

            // file was uploaded
            isset($this->file_upload[$control]) &&

            // and file is indeed an image file
            isset($this->file_upload[$control]['imageinfo']) &&

            // we're trying to convert to a supported file type
            ($type == 'gif' || $type == 'png' || $type == 'jpg')

        ) {

            // get file's current name
            $current_file_name = substr($this->file_upload[$control]['file_name'], 0, strrpos($this->file_upload[$control]['file_name'], '.'));

            // get file's current extension
            $current_file_extension = strtolower(substr($this->file_upload[$control]['file_name'], strrpos($this->file_upload[$control]['file_name'], '.') + 1));

            // if extension is a variation of "jpeg", revert to default "jpg"
            if ($current_file_extension == 'jpeg') $current_file_extension = 'jpg';

            // if new extension is different than the file's current extension
            if ($type != $current_file_extension) {

                // if no overwrite and a file with the same name as the converted file already exists
                if (!$overwrite && is_file($this->file_upload[$control]['path'] . $current_file_name . '.' . $type)) {

                    $suffix = '';

                    // knowing the suffix...
                    // loop as long as
                    while (

                        // a file with the same name exists in the upload folder
                        // (file_exists returns also TRUE if a folder with that name exists)
                        is_file($this->file_upload[$control]['path'] . $current_file_name . $suffix . '.' . $type)

                    )

                        // if no suffix was yet set
                        if ($suffix === '')

                            // start the suffix like this
                            $suffix = '_1';

                        // if suffix was already initialized
                        else {

                            // drop the "_" from the suffix
                            $suffix = str_replace('_', '', $suffix);

                            // increment the suffix
                            $suffix = '_' . ++$suffix;

                        }

                    // the final file name
                    $current_file_name = $current_file_name . $suffix;

                }

                // if the image transformation class was not already instantiated
                if (!isset($this->Zebra_Image))

                    // create a new instance of the image transformation class
                    $this->Zebra_Image = new Zebra_Image();

                // set the source file
                $this->Zebra_Image->source_path = $this->file_upload[$control]['path'] . $this->file_upload[$control]['file_name'];

                // set the target file
                $this->Zebra_Image->target_path = $this->file_upload[$control]['path'] . $current_file_name . '.' . $type;

                // set the quality of the output image (better quality means bigger file size)
                // available only for jpeg files; ignored for other image types
                $this->Zebra_Image->jpeg_quality = $jpeg_quality;

                // if there was an error when resizing the image, return false
                if (!$this->Zebra_Image->resize(0, 0)) return false;

                // update entries in the file_upload property

                // get the size of the new file
                $this->file_upload[$control]['size'] = filesize($this->Zebra_Image->target_path);

                // update the file name (the file was converted and has a new extension)
                $this->file_upload[$control]['file_name'] = $current_file_name . '.' . $type;

                // get some info about the new file
                $imageinfo = @getimagesize($this->Zebra_Image->target_path);

                // rename some of the attributes returned by getimagesize
                $imageinfo['width'] = $imageinfo[0]; unset($imageinfo[0]);

                $imageinfo['height'] = $imageinfo[1]; unset($imageinfo[1]);

                $imageinfo['type'] = $imageinfo[2]; unset($imageinfo[2]);

                $imageinfo['html'] = $imageinfo[3]; unset($imageinfo[3]);

                // append image info to the file_upload property
                $this->file_upload[$control]['imageinfo'] = $imageinfo;

                // update the mime type as returned by getimagesize
                $this->file_upload[$control]['type'] = $imageinfo['mime'];

                // if original file is not to be preserved, delete original file
                if (!$preserve_original_file && (!$overwrite || $type != $current_file_extension)) @unlink($this->Zebra_Image->source_path);

            }

        }

        // if the script gets this far, it means that everything went as planned and we return true
        return true;

    }

    /**
     *  Helper method for validating select boxes. It extract all the values from an infinitely nested array and puts
     *  them in an uni-dimensional array so that we can check if the submitted value is allowed.
     *
     *  @param  array   $array  The array to transform.
     *
     *  @return array           Returns the flat array.
     *
     *  @access private
     */
    private function _extract_values($array)
    {

        $result = array();

        // iterate through the array's values
        foreach ($array as $index => $value)

            // if entry is an array, flatten array recursively
            if (is_array($value)) $result = array_merge($result, $this->_extract_values($value));

            // otherwise, add the index to the result array
            else $result[] = $index;

        // return found values
        return $result;

    }

    /**
     *  Load MIME types from mimes.json
     *
     *  @return void
     *
     *  @access private
     */
    private function _load_mime_types()
    {

        // if file with mime types was not already loaded
        if (!isset($this->form_properties['mimes'])) {

            // read file into an array
            $rows = file($this->form_properties['assets_server_path'] . 'mimes.json');

            // convert JSON to array
            // i'm aware that in PHP 5.2+ there is json_decode, but i want this library to be
            // as backward compatible as possible so, since the values in mimes.json has a
            // specific structure, i wrote my own decoder
            $this->form_properties['mimes'] = array();

            // iterate through all the rows
            foreach ($rows as $row) {

                // if valid row found
                if (strpos($row, ':') !== false) {

                    // explode the string by :
                    $items = explode(':', $row);

                    // the file type (extension)
                    $index = trim(str_replace('"', '', $items[0]));

                    // if there are more mime types attached
                    if (strpos($items[1], '[') !== false)

                        // convert to array
                        $value = array_diff(array_map(create_function('&$value', 'return trim($value);'), explode(',', str_replace(array('[', ']', '"', '\/'), array('', '', '', '/'), $items[1]))), array(''));

                    // if a single mime type is attached
                    else

                        // convert to string
                        $value = trim(str_replace(array('"', ',', '\/'), array('', '', '/'), $items[1]));

                    // save entry
                    $this->form_properties['mimes'][$index] = $value;

                }

            }

        }

    }

    /**
     *  Resize an uploaded image
     *
     *  This method will do nothing if the file is not a supported image file.
     *
     *  @param  string  $control                The file upload control's name
     *
     *  @param  string  $prefix                 If the resized image is to be saved as a new file and the originally
     *                                          uploaded file needs to be preserved, specify a prefix to be used for the
     *                                          new file. This way, the resized image will have the same name as the
     *                                          original file but prefixed with the given value (i.e. "thumb_").
     *
     *                                          Specifying an empty string as argument will instruct the script to apply
     *                                          the resizing to the uploaded image and therefore overwriting the
     *                                          originally uploaded file.
     *
     *  @param  integer $width                  The width to resize the image to.
     *
     *                                          If set to <b>0</b>, the width will be automatically adjusted, depending
     *                                          on the value of the <b>height</b> argument so that the image preserves
     *                                          its aspect ratio.
     *
     *                                          If <b>preserve_aspect_ratio</b> is set to TRUE and both this and the
     *                                          <b>height</b> arguments are values greater than <b>0</b>, the image will
     *                                          be resized to the exact required width and height and the aspect ratio
     *                                          will be preserved (see the description for the <b>method</b> argument
     *                                          below on how can this be done).
     *
     *                                          If <b>preserve_aspect_ratio</b> is set to FALSE, the image will be
     *                                          resized to the required width and the aspect ratio will be ignored.
     *
     *                                          If both <b>width</b> and <b>height</b> are set to <b>0</b>, a copy of
     *                                          the source image will be created (<b>jpeg_quality</b> will still apply).
     *
     *                                          If either <b>width</b> or <b>height</b> are set to <b>0</b>, the script
     *                                          will consider the value of the {@link preserve_aspect_ratio} to bet set
     *                                          to TRUE regardless of its actual value!
     *
     *  @param  integer $height                 The height to resize the image to.
     *
     *                                          If set to <b>0</b>, the height will be automatically adjusted, depending
     *                                          on the value of the <b>width</b> argument so that the image preserves
     *                                          its aspect ratio.
     *
     *                                          If <b>preserve_aspect_ratio</b> is set to TRUE and both this and the
     *                                          <b>width</b> arguments are values greater than <b>0</b>, the image will
     *                                          be resized to the exact required width and height and the aspect ratio
     *                                          will be preserved (see the description for the <b>method</b> argument
     *                                          below on how can this be done).
     *
     *                                          If <b>preserve_aspect_ratio</b> is set to FALSE, the image will be
     *                                          resized to the required height and the aspect ratio will be ignored.
     *
     *                                          If both <b>height</b> and <b>width</b> are set to <b>0</b>, a copy of
     *                                          the source image will be created (<b>jpeg_quality</b> will still apply).
     *
     *                                          If either <b>height</b> or <b>width</b> are set to <b>0</b>, the script
     *                                          will consider the value of the {@link preserve_aspect_ratio} to bet set
     *                                          to TRUE regardless of its actual value!
     *
     *  @param  boolean $preserve_aspect_ratio  (Optional) If set to TRUE, the image will be resized to the given width
     *                                          and height and the aspect ratio will be preserved.
     *
     *                                          Set this to FALSE if you want the image forcefully resized to the exact
     *                                          dimensions given by width and height ignoring the aspect ratio
     *
     *                                          Default is TRUE.
     *
     *  @param  int     $method                 (Optional) Method to use when resizing images to exact width and height
     *                                          while preserving aspect ratio.
     *
     *                                          If the $preserve_aspect_ratio property is set to TRUE and both the
     *                                          <b>width</b> and <b>height</b> arguments are values greater than <b>0</b>,
     *                                          the image will be resized to the exact given width and height and the
     *                                          aspect ratio will be preserved by using on of the following methods:
     *
     *                                          -   <b>ZEBRA_IMAGE_BOXED</b> - the image will be scalled so that it will
     *                                              fit in a box with the given width and height (both width/height will
     *                                              be smaller or equal to the required width/height) and then it will
     *                                              be centered both horizontally and vertically. The blank area will be
     *                                              filled with the color specified by the <b>$background_color</b>
     *                                              argument. (the blank area will be filled only if the image is not
     *                                              transparent!)
     *
     *                                          -   <b>ZEBRA_IMAGE_NOT_BOXED</b> - the image will be scalled so that it
     *                                              <i>could</i> fit in a box with the given width and height but will
     *                                              not be enclosed in a box with given width and height. The new width/
     *                                              height will be both smaller or equal to the required width/height
     *
     *                                          -   <b>ZEBRA_IMAGE_CROP_TOPLEFT</b>
     *                                          -   <b>ZEBRA_IMAGE_CROP_TOPCENTER</b>
     *                                          -   <b>ZEBRA_IMAGE_CROP_TOPRIGHT</b>
     *                                          -   <b>ZEBRA_IMAGE_CROP_MIDDLELEFT</b>
     *                                          -   <b>ZEBRA_IMAGE_CROP_CENTER</b>
     *                                          -   <b>ZEBRA_IMAGE_CROP_MIDDLERIGHT</b>
     *                                          -   <b>ZEBRA_IMAGE_CROP_BOTTOMLEFT</b>
     *                                          -   <b>ZEBRA_IMAGE_CROP_BOTTOMCENTER</b>
     *                                          -   <b>ZEBRA_IMAGE_CROP_BOTTOMRIGHT</b>
     *
     *                                          For the methods involving crop, first the image is scaled so that both
     *                                          its sides are equal or greater than the respective sizes of the bounding
     *                                          box; next, a region of required width and height will be cropped from
     *                                          indicated region of the resulted image.
     *
     *                                          Default is ZEBRA_IMAGE_BOXED
     *
     *  @param  boolean $background_color       (Optional) The hexadecimal color of the blank area (without the #).
     *                                          See the <b>method</b> argument.
     *
     *                                          Default is 'FFFFFF'
     *
     *  @param  boolean $enlarge_smaller_images (Optional) If set to FALSE, images having both width and height smaller
     *                                          than the required width and height, will be left untouched ({@link jpeg_quality}
     *                                          will still apply).
     *
     *                                          Default is TRUE
     *
     *  @param  boolean $quality                (Optional) Indicates the quality of the output image (better quality
     *                                          means bigger file size).
     *
     *                                          Range is 0 - 100
     *
     *                                          Available only for JPEG files.
     *
     *                                          Default is 85
     *
     *  @return boolean                         Returns TRUE on success or FALSE otherwise
     *
     *  @access private
     */
    private function _resize($control, $prefix, $width, $height, $preserve_aspect_ratio = true, $method = ZEBRA_IMAGE_BOXED, $background_color = 'FFFFFF', $enlarge_smaller_images = true, $jpeg_quality = 85)
    {

        // if
        if (

            // file was uploaded
            isset($this->file_upload[$control]) &&

            // and file is indeed an image file
            isset($this->file_upload[$control]['imageinfo'])

        ) {

            // if the image transformation class was not already instantiated
            if (!isset($this->Zebra_Image))

                // create a new instance of the image transformation class
                $this->Zebra_Image = new Zebra_Image();

            // set the file permissions as per Zebra_Form's settings
            $this->Zebra_Image->chmod_value = $this->file_upload_permissions;

            // set the source file
            $this->Zebra_Image->source_path = $this->file_upload[$control]['path'] . $this->file_upload[$control]['file_name'];

            // set the target file
            $this->Zebra_Image->target_path = $this->file_upload[$control]['path'] . trim($prefix) . $this->file_upload[$control]['file_name'];

            // set whether aspect ratio should be maintained or not
            $this->Zebra_Image->maintain_ratio = $preserve_aspect_ratio;

            // set the quality of the output image (better quality means bigger file size)
            // available only for jpeg files; ignored for other image types
            $this->Zebra_Image->jpeg_quality = $jpeg_quality;

            // should smaller images be enlarged?
            $this->Zebra_Image->enlarge_smaller_images = $enlarge_smaller_images;

            // if there was an error when resizing the image, return false
            if (!$this->Zebra_Image->resize($width, $height, $method, $background_color)) return false;

        }

        // if the script gets this far, it means that everything went as planned and we return true
        return true;

    }

    /**
     *  Checks if all the conditions set by the "dependencies" rule are met or not.
     *
     *  @param  string  $id         The ID of the element to check.
     *
     *  @param  array   $referer    (Private) Used by the library to prevent entering an infinite loop of dependencies.
     *
     *  @return boolean             Returns TRUE if all the conditions are met or FALSE otherwise.
     *
     *  @access private
     */
    private function _validate_dependencies($id, $referer = array())
    {

        // reference to the form submission method
        global ${'_' . $this->form_properties['method']};

        $method = & ${'_' . $this->form_properties['method']};

        // if the rule is applied to a radio button group or a checkbox group
        // there will be no entry with the given id as all group's elements will have their ID in the form of [name]_[value]
        if (!isset($this->controls[$id]))

            // ...therefore, we have to iterate over all the form's controls
            foreach ($this->controls as $control)

                // and if we find the control we're looking for
                if (preg_replace('/\[\]$/', '', $control->attributes['name']) == $id) {

                    // get the ID of the control
                    $id = $control->attributes['id'];

                    // don't look any further
                    break;

                }

        // if there are more than 2 entries in the referer array, remove the first one
        if (count($referer) > 2) array_shift($referer);

        // if current element is the referer array
        if (in_array($id, $referer))

            // we're having a recursion and we're stopping execution
            _zebra_form_show_error('Infinite recursion detected. The loop of dependencies is created by the following elements: "' . implode('", "', $referer) . '"', E_USER_ERROR);

        // add current element to the stack
        array_push($referer, $id);

        $result = true;

        // if the control exists
        if (isset($this->controls[$id])) {

            // if we're checking if a proxy depends on another proxy, but it doesn't, return TRUE now
            if (!isset($this->controls[$id]->rules['dependencies'])) return true;

            // get all the conditions needed to validate the element
            $conditions = $this->controls[$id]->rules['dependencies'];

            // if the name of a callback function is also given
            // the actual conditions are in the first entry of the array
            if (isset($conditions[1])) $conditions = $conditions[0];

            // iterate through the elements the validation of the current element depends on (proxies)
            foreach ($conditions as $proxy => $required_values) {

                // if we have a cached result of the result
                if (isset($this->proxies_cache[$proxy][serialize($required_values)]))

                    // get the result from cache
                    $result = $this->proxies_cache[$proxy][serialize($required_values)];

                // if we don't have a cached result of the result
                else {

                    $is_radio_or_checkbox = false;

                    // $this->controls[$proxy] will never be set for radio or checkbox controls (as $proxy is the ID, not the name)
                    if (!isset($this->controls[$proxy]))

                        // iterate through all the controls
                        foreach ($this->controls as $control_properties)

                            // if we found a control radio/checkbox element with the sought name
                            if ($control_properties->attributes['name'] == $proxy && ($control_properties->attributes['type'] == 'radio' || $control_properties->attributes['name'] == 'checkbox')) {

                                // set this flag
                                $is_radio_or_checkbox = true;

                                // don't look further
                                break;

                            }

                    $found = false;

                    // a proxy may also depend on the values of or or more other proxies
                    // therefore, continue only if those conditions are met
                    if (
                        (isset($this->controls[$proxy]) || $is_radio_or_checkbox) &&
                        ((!$is_radio_or_checkbox && $this->controls[$proxy]->attributes['type'] == 'image' && isset($method[$proxy . '_x']) && isset($method[$proxy . '_y'])) || isset($method[$proxy])) &&
                        $this->_validate_dependencies($proxy, $referer)
                    ) {

                        // if proxy is a submit or an image submit button
                        if (!$is_radio_or_checkbox && in_array($this->controls[$proxy]->attributes['type'], array('image', 'submit'))) $current_values = array('click');

                        // otherwise, get the proxy's current value/values
                        // (we'll treat the values as an array even if there's only a single value)
                        else $current_values = !is_array($method[$proxy]) ? array($method[$proxy]) : $method[$proxy];

                        // if condition is not an array
                        if (!is_array($required_values)) {

                            // iterate through the proxy's values
                            // (remember, we store it as an array even if there's a single value)
                            foreach ($current_values as $current_value)

                                // if the value of the condition is amongst the proxy's values, flag it
                                if ($current_value == $required_values) $found = true;

                        // if condition is given as an array
                        } else {

                            // iterate through all the conditions
                            foreach ($required_values as $required_value) {

                                $matches = 0;

                                // iterate through the values of the proxy element
                                // (remember, we store it as an array even if there's a single value)
                                foreach ($current_values as $current_value) {

                                    // if current entry in the conditions list is not an array
                                    // and its value is equal to the current value
                                    if (!is_array($required_value) && $current_value == $required_value) $found = true;

                                    // if current entry in the conditions list is an array
                                    // and the current value is part of that array
                                    else if (is_array($required_value) && in_array($current_value, $required_value)) $matches++;

                                }

                                // if all conditions are met
                                if (!$found && $matches == count($required_values)) $result = true;

                            }

                        }

                        // if not all conditions are met, don't check any further
                        if (!$found) { $result = false; break; }

                    // if proxy is not submitted, or proxy's dependendecies are not ok, don't check the other conditions
                    } else $result = false;

                }

                // cache the result
                if (!isset($this->proxies_cache[$proxy][serialize($required_values)]))

                    $this->proxies_cache[$proxy][serialize($required_values)] = $result;

            }

        }

        // if script gets this far, consider all the conditions are met, and validate the other rules
        return $result;

    }

    /**
     *  Uploads a file
     *
     *  @param  string  $control                The file upload control's name
     *
     *  @param  string  $upload_path            The path where the file to be uploaded to
     *
     *                                          The path is relative to the script containing the form, unless the path
     *                                          starts with "/" when it is relative to the
     *                                          {@link http://php.net/manual/en/reserved.variables.server.php DOCUMENT_ROOT}.
     *
     *                                          -   <b>uploads</b> would upload the files in the "upload" folder, at the
     *                                              path with the script
     *                                          -   <b>../uploads</b> would upload the files in the "upload" folder, one
     *                                              level up relative to the script's path
     *                                          -   <b>/uploads</b> would upload the files in the "upload" folder, in the
     *                                              DOCUMENT_ROOT
     *
     *  @param  boolean $filename               (Optional) Specifies whether the uploaded file's original name should be
     *                                          preserved, should it be prefixed with a string, or should it be randomly
     *                                          generated.
     *
     *                                          Possible values can be
     *
     *                                          -   TRUE - the uploaded file's original name will be preserved;
     *                                          -   FALSE (or, for better code readability, you should use the "ZEBRA_FORM_UPLOAD_RANDOM_NAMES"
     *                                              constant instead of "FALSE")- the uploaded file will have a randomly generated name;
     *                                          -   a string - the uploaded file's original name will be preserved but
     *                                              it will be prefixed with the given string (i.e. "original_", or "tmp_")
     *
     *                                          Note that when set to TRUE or a string, a suffix of "_n" (where n is an
     *                                          integer) will be appended to the file name if a file with the same name
     *                                          already exists at the given path.
     *
     *                                          Default is TRUE
     *
     *  @return boolean                         Returns TRUE on success or FALSE otherwise
     *
     *  @access private
     */
    private function _upload($control, $upload_path, $filename = true)
    {

        // trim trailing slash from folder
        $path = rtrim($upload_path, '\\/');

        // if upload folder does not have a trailing slash, add the trailing slash
        $path = $path . (substr($path, -1) != DIRECTORY_SEPARATOR ? DIRECTORY_SEPARATOR : '');

        // if
        if (

            // the file upload control with the given name exists
            isset($_FILES[$control]) &&

            // file is ready to be uploaded
            $_FILES[$control]['error'] == 0 &&

            // the upload folder exists
            is_dir($path)

        ) {

            // if file names should be random
            if ($filename === ZEBRA_FORM_UPLOAD_RANDOM_NAMES)

                // generate a random name for the file we're about to upload
                $file_name = md5(mt_rand() . microtime() . $_FILES[$control]['name']) . (strrpos($_FILES[$control]['name'], '.') !== false ? substr($_FILES[$control]['name'], strrpos($_FILES[$control]['name'], '.')) : '');

            // if file names are to be preserved
            else {

                // if the file we are about to upload does have an extension
                if (strrpos($_FILES[$control]['name'], '.') !== false) {

                    // split the file name into "file name"...
                    $file_name = substr($_FILES[$control]['name'], 0, strrpos($_FILES[$control]['name'], '.'));

                    // ...and "file extension"
                    $file_extension = substr($_FILES[$control]['name'], strrpos($_FILES[$control]['name'], '.'));

                // if the file we are about to upload does not have an extension
                } else {

                    // the file name will be the actual file name...
                    $file_name = $_FILES[$control]['name'];

                    // ...while the extension will be an empty string
                    $file_extension = '';

                }

                // prefix the file name if required
                $file_name = ($filename !== true ? $filename : '') . $file_name;

                $suffix = '';

                // knowing the suffix...
                // loop as long as
                while (

                    // a file with the same name exists in the upload folder
                    // (file_exists returns also TRUE if a folder with that name exists)
                    is_file($path . $file_name . $suffix . $file_extension)

                ) {

                    // if no suffix was yet set
                    if ($suffix === '')

                        // start the suffix like this
                        $suffix = '_1';

                    // if suffix was already initialized
                    else {

                        // drop the "_" from the suffix
                        $suffix = str_replace('_', '', $suffix);

                        // increment the suffix
                        $suffix = '_' . ++$suffix;

                    }

                }

                // the final file name
                $file_name = $file_name . $suffix . $file_extension;

            }

            // if file could be uploaded
            if (@move_uploaded_file($_FILES[$control]['tmp_name'], $path . $file_name)) {

                // get a list of functions disabled via configuration
                $disabled_functions = @ini_get('disable_functions');

                // if the 'chmod' function is not disabled via configuration
                if ($disabled_functions != '' && strpos('chmod', $disabled_functions) === false)

                    // chmod the file
                    chmod($path . $file_name, intval($this->file_upload_permissions, 8));

                // set a special property
                // the value of the property will be an array will information about the uploaded file
                $this->file_upload[$control] = $_FILES[$control];

                $this->file_upload[$control]['path'] = rtrim($upload_path, '/') . '/';

                $this->file_upload[$control]['file_name'] = $file_name;

                // if uploaded file is an image
                if ($imageinfo = @getimagesize($path . $this->file_upload[$control]['file_name'])) {

                    // rename some of the attributes returned by getimagesize
                    $imageinfo['width'] = $imageinfo[0]; unset($imageinfo[0]);

                    $imageinfo['height'] = $imageinfo[1]; unset($imageinfo[1]);

                    $imageinfo['type'] = $imageinfo[2]; unset($imageinfo[2]);

                    $imageinfo['html'] = $imageinfo[3]; unset($imageinfo[3]);

                    // append image info to the file_upload property
                    $this->file_upload[$control]['imageinfo'] = $imageinfo;

                }

                // return true, as everything went as planned
                return true;

            }

        }

        // if script gets this far, return false as something must've gone wrong
        return false;

    }

}

/**
 *  A custom function for showing error messages in the Zebra_Form's environment.
 *
 *  We need this so we show correct file/line number information when reporting errors as PHP's trigger_error() shows the
 *  file and the line number where the function is called and it is not what we need here (we always trigger the errors
 *  from one of the Zebra_Form's file but the errors come from user files).
 *
 *  I didn't use a custom error handler so I don't interfere with the one you might be using.
 *
 *  @param  string  $message    The message to be shown to the user.
 *
 *  @param  mixed   $type       Severity of the error message.
 *
 *                              Can be E_USER_ERROR, E_USER_NOTICE or E_USER_WARNING.
 *
 *                              When set to E_USER_ERROR the execution of the script will be halted after the error
 *                              message is displayed.
 *
 *  @return void
 *
 *  @access private
 */
function _zebra_form_show_error($message, $type)
{

    // if error reporting is on
    if (($type & error_reporting()) == $type) {

        // get backtrace information
        $backtraceInfo = debug_backtrace();

        // this is where the error actually occurred
        // (produces a "Strict Standards" warning unless muted)
        $errorInfo = @array_pop(array_slice($backtraceInfo, 2, 1));

        // show error message
        echo '<br><strong>' . ($type == E_USER_WARNING ? 'Warning' : ($type == E_USER_NOTICE ? 'Notice' : 'Fatal error')) . '</strong>:<br><br>' . $message . (isset($errorInfo['file']) ? '<br><br>in <strong>' . basename($errorInfo['file']) . '</strong> on line <strong>' . $errorInfo['line'] .  '</strong>' : '') . '<br>';

        // die if necessary
        if ($type == E_USER_ERROR) die();

    }

}

?>
