Location: PHPKode > projects > Pieforms > pieforms-php5-0.2.2/src/pieform.php
<?php
/**
 * This program is part of Pieforms
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
 *
 * @package    pieform
 * @subpackage core
 * @author     Nigel McNie <hide@address.com>
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL
 * @copyright  (C) 2006 Catalyst IT Ltd http://catalyst.net.nz
 *
 */

$GLOBALS['_PIEFORM_REGISTRY'] = array();

/** The form was processed successfully */
define('PIEFORM_OK', 0);
/** The form failed processing/validating */
define('PIEFORM_ERR', -1);
/** A cancel button was pressed */
define('PIEFORM_CANCEL', -2);

/**
 * Builds, validates and processes a form.
 *
 * Given a form definition, and as long as one or two functions are implemented
 * by the caller, this function will handle everything else.
 *
 * USAGE:
 *
 * <pre>
 * $form = array(
 *     'name' => 'myform',
 *     'action' => 'myscript.php',
 *     'method' => 'post',
 *     'elements' => array(
 *         // definition of elements in the form
 *     )
 * );
 *
 * $smarty->assign('myform', pieform($form));
 *
 * function myform_validate(Pieform $form, $values) {
 *     // perform validation agains form elements here
 *     // some types of validation are conveniently available already as
 *     // as part of the form definition hash
 * }
 *
 * function myform_submit(Pieform $form, $values) {
 *     // perform action knowing that the values are valid, e.g. DB insert.
 * }
 * </pre>
 *
 * Please see https://eduforge.org/wiki/wiki/mahara/wiki?pagename=FormAPI for
 * more information on creating and using forms.
 *
 */
function pieform($data) {
    return Pieform::process($data);
    //
    // @todo stuff to do for forms:
    // 
    //  - more form element types (inc. types like autocomplete and wyswiyg)
    //  - support processing of data before validation occurs (e.g. trim(), strtoupper())
    //  - Basic validation is possible as there's a callback function for checking,
    //    but some helper functions could be written to make people's job validating
    //    stuff much easier (form_validate_email, form_validate_date etc).
    //  - Collapsible js for fieldsets
    //  - Grippie for textareas
    //  - handle multipage forms?
    //  - handle a tabbed interface type of form?
    //  
}

if (!function_exists('json_encode')) {
    require_once 'JSON/JSON.php';
    function json_encode($data) {
        $json = new Services_JSON();
        return $json->encode($data);
    }
}

/**
 * Pieform throws PieformExceptions, so you can catch them specifically
 */
class PieformException extends Exception {}

/**
 * Represents an HTML form. Forms created using this class have a lot of the
 * legwork for forms abstracted away.
 *
 * Pieforms makes it really easy to build complex HTML forms, simply by
 * building a hash describing your form, and defining one or two callback
 * functions.
 *
 * For more information on how Pieforms works, please see the documentation
 * at https://eduforge.org/wiki/wiki/mahara/wiki?pagename=FormAPI
 */
class Pieform {

    /**
     * Maintains a tab index across all created forms, to make it easy for
     * people to forget about it and have it just work for all of their forms.
     *
     * @var int
     */
    public static $formtabindex = 1;

    /**
     * The form name. This is required.
     *
     * @var string
     */
    private $name = '';

    /**
     * Data for the form
     *
     * @var array
     */
    private $data = array();

    /**
     * Whether this form includes a file element. If so, the enctype attribute
     * for the form will be specified as "multipart/mixed" as required. This
     * is auto-detected by the Pieform class.
     *
     * @var bool
     */
    private $fileupload = false;

    /**
     * Whether the form has been submitted. Available through the
     * {@link is_submitted} method.
     *
     * @var bool
     */
    private $submitted = false;

    /**
     * Processes the form. Called by the {@link pieform} function. It simply
     * builds the form (processing it if it has been submitted), and returns
     * the HTML to display the form
     *
     * @param array $data The form description hash
     * @return string     The HTML representing the form
     */
    public static function process($data) {
        $form = new Pieform($data);
        return $form->build();
    }

    /**
     * Sets the attributes of the form according to the passed data, performing
     * validation on the way. If the form is submitted, this checks and processes
     * the form.
     *
     * @param array $data The form description hash
     */
    public function __construct($data) {
        $GLOBALS['_PIEFORM_REGISTRY'][] = $this;

        if (!isset($data['name']) || !preg_match('/^[a-z_][a-z0-9_]*$/', $data['name'])) {
            throw new PieformException('Forms must have a name, and that name must be valid (validity test: could you give a PHP function the name?)');
        }
        $this->name = $data['name'];

        // If the form has global configuration, get it now
        if (function_exists('pieform_configure')) {
            $formconfig = pieform_configure();
            $defaultelements = (isset($formconfig['elements'])) ? $formconfig['elements'] : array();
            foreach ($defaultelements as $name => $element) {
                if (!isset($data['elements'][$name])) {
                    $data['elements'][$name] = $element;
                }
            }
        }
        else {
            $formconfig = array();
        }

        // Assign defaults for the form
        $formdefaults = array(
            // The method used to submit the form, should always be 'get' or 'post'
            'method' => 'get',

            // The form target. The vast majority of the time this should be blank,
            // as the functions that handle the submit should be in the same script
            // as the form definition
            'action' => '',

            // The form elements
            'elements' => array(),

            // The form renderer (see the pieform/renderers directory)
            'renderer' => 'table',

            // Whether to validate the form. Non validated forms have none of the
            // validate, success or error callbacks called on them
            'validate' => true,

            // Whether to process the submission of this form. The form will still
            // be validated. Handy if the code handling the submission is elsewhere
            'submit' => true,

            // The PHP callback called to validate the form. Optional
            'validatecallback' => '',

            // The PHP callback called to process the submission of the form.
            // Required, unless a success function is provided for each submit
            // button in the form
            'successcallback' => '',

            // The PHP callback called if there is any validation error. Optional
            'errorcallback' => '',

            // Whether this form should submit to a hidden iframe and use DOM
            // manipulation to insert error messages (faster than a normal submit,
            // supported in less browsers. Most modern browsers should be fine)
            'jsform' => false,

            // The javascript function called before submission of a form
            // (regardless of whether the form is a jsform)
            'presubmitcallback' => '',

            // The javascript function called after submission of a form. As non-js
            // forms will trigger a page load on submit, this has no effect for them. 
            'postsubmitcallback' => '',

            // The javascript function called if the form submission was successful
            'jssuccesscallback' => '',

            // The javascript function called if the form submission was unsuccessful
            'jserrorcallback' => '',

            // The javascript function called if the form submission returned an
            // unknown error code
            'globaljserrorcallback' => '',

            // The message to pass back as a reason for the form submission failing
            // if the form is a jsform. This can be used by your application however
            // you choose.
            'jserrormessage' => '',

            // Whether this form can be cancelled, regardless of the presence of
            // 'cancel' buttons or form inputs mischeviously named as to behave
            // like cancel buttons
            'iscancellable' => true,

            // Whether Pieforms should die after calling a submit function. Generally
            // this is a good idea, as it forces the user to reply to the form
            // submission. However, there are occasions where you might want to let
            // it continue, so this behaviour can be turned off
            'dieaftersubmit' => true,

            // Whether to auto-focus either the first field (if the value is true,
            // or the named field (if the value is a string) when the form is
            // displayed. If this has any value other than false and the form has
            // been submitted with an error, the first field with an error will
            // be focussed.
            'autofocus'  => false,

            // The directories to search for additional elements, renderers and
            // rules
            'configdirs' => array(),

            // The language to use for any form strings, such as those found in
            // rules.
            'language'   => 'en.utf8',

            // Any overriding language strings for rules
            'rulei18n'   => array(),

            // The tabindex for the form (managed automatically by Pieforms)
            'tabindex'   => false,

            // Whether to add a class of the type of the element to each element
            'elementclasses' => false
        );
        $data = array_merge($formdefaults, $formconfig, $data);
        $this->data = $data;

        // Set the method - only get/post allowed
        $this->data['method'] = strtolower($data['method']);
        if ($this->data['method'] != 'post') {
            $this->data['method'] = 'get';
        }

        // Make sure that the javascript callbacks are valid
        if ($this->data['jsform']) {
            $this->validate_js_callbacks();
        }

        if (!$this->data['validatecallback']) {
            $this->data['validatecallback'] = $this->name . '_validate';
        }

        if (!$this->data['successcallback']) {
            $this->data['successcallback'] = $this->name . '_submit';
        }

        $this->data['configdirs'] = array_map(
            create_function('$a', 'return substr($a, -1) == "/" ? substr($a, 0, -1) : $a;'),
            (array) $this->data['configdirs']);


        if (empty($this->data['tabindex'])) {
            $this->data['tabindex'] = self::$formtabindex++;
        }

        if (!is_array($this->data['elements']) || count($this->data['elements']) == 0) {
            throw new PieformException('Forms must have a list of elements');
        }

        // Rename all the keys to have nice compliant names
        // @todo:
        //   - This isn't done for elements inside fieldsets
        //   - There's no easy way for other things do do all this preprocessing if they
        //     need. It should be a method so that other things (like multirecord)
        //     can use it.
        $elements = array();
        foreach ($this->data['elements'] as $name => $element) {
            $newname = preg_replace('/[^a-zA-Z0-9_]/', '_', $name);
            if (isset($elements[$name])) {
                throw new PieformException('Element "' . $name . '" has a dangerous name that interferes with another element');
            }
            $elements[$newname] = $element;
        }
        $this->data['elements'] = $elements;

        // Remove elements to ignore
        foreach ($this->data['elements'] as $name => $element) {
            if (isset($element['type']) && $element['type'] == 'fieldset') {
                foreach ($element['elements'] as $subname => $subelement) {
                    if (!empty($subelement['ignore'])) {
                        unset ($this->data['elements'][$name]['elements'][$subname]);
                    }
                }
            }
            else {
                if (!empty($element['ignore'])) {
                    unset($this->data['elements'][$name]);
                }
            }
        }

        // Set some attributes for all elements
        $autofocusadded = false;
        foreach ($this->data['elements'] as $name => &$element) {
            // @todo re-check ordering of this section
            // The name can be in the element itself. This is compatibility for the perl version
            if (isset($element['name'])) {
                $name = $element['name'];
            }
            if (count($element) == 0) {
                throw new PieformException('An element in form "' . $this->name . '" has no data (' . $name . ')');
            }
            if (!isset($element['type'])) {
                $element['type'] = 'markup';
                if (!isset($element['value'])) {
                    throw new PieformException('The markup element "'
                        . $name . '" has no value');
                }
            }
            if (!isset($element['title'])) {
                $element['title'] = '';
            }
            if ($element['type'] == 'file') {
                $this->fileupload = true;
                if ($this->data['method'] == 'get') {
                    $this->data['method'] = 'post';
                    self::info("Your form '$this->name' had the method 'get' and also a file element - it has been converted to 'post'");
                }
            }
            if ($element['type'] == 'fieldset') {
                $this->include_plugin('element', 'fieldset');
                foreach ($element['elements'] as $subname => &$subelement) {
                    // The name can be in the element itself. This is compatibility for the perl version
                    if (isset($subelement['name'])) {
                        $subname = $subelement['name'];
                    }
                    if (count($subelement) == 0) {
                        throw new PieformException('An element in form "' . $this->name . '" has no data (' . $subname . ')');
                    }
                    if (!isset($subelement['type'])) {
                        $subelement['type'] = 'markup';
                        if (!isset($subelement['value'])) {
                            throw new PieformException('The markup element "'
                                . $name . '" has no value');
                        }
                    }

                    // Configure some basics for real elements
                    if ($subelement['type'] != 'markup') {
                        // This function can be defined by the application using Pieforms,
                        // and applies to all elements of this type
                        $function = 'pieform_element_' . $subelement['type'] . '_configure';
                        if (function_exists($function)) {
                            $subelement = $function($subelement);
                        }

                        // This function is defined by the plugin itself, to set fields on
                        // the element that need to be set but should not be set by the
                        // application
                        $function = 'pieform_element_' . $subelement['type'] . '_set_attributes';
                        $this->include_plugin('element',  $subelement['type']);
                        if (function_exists($function)) {
                            $subelement = $function($subelement);
                        }

                        // Add the autofocus flag to the element if required
                        if (!$autofocusadded && $this->data['autofocus'] === true && empty($element['nofocus'])) {
                            $subelement['autofocus'] = true;
                            $autofocusadded = true;
                        }
                        else if (!empty($this->data['autofocus']) && $this->data['autofocus'] !== true
                            && $subname == $this->data['autofocus']) {
                            $subelement['autofocus'] = true;
                        }

                        // All elements should have some kind of title
                        if (!isset($subelement['title'])) {
                            $subelement['title'] = '';
                        }

                        // Force the form method to post if there is a file to upload.
                        if ($subelement['type'] == 'file') {
                            $this->fileupload = true;
                            if ($this->data['method'] == 'get') {
                                $this->data['method'] = 'post';
                                self::info("Your form '$this->name' had the method 'get' and also a file element - it has been converted to 'post'");
                            }
                        }

                        // All elements inherit the form tabindex
                        $subelement['tabindex'] = $this->data['tabindex'];
                    }
                    $subelement['name'] = $subname;

                }
            }
            else {
                // Let each element set and override attributes if necessary
                if ($element['type'] != 'markup') {
                    $function = 'pieform_element_' . $element['type'] . '_configure';
                    if (function_exists($function)) {
                        $element = $function($element);
                    }

                    $function = 'pieform_element_' . $element['type'] . '_set_attributes';
                    $this->include_plugin('element',  $element['type']);
                    if (function_exists($function)) {
                        $element = $function($element);
                    }

                    // Add the autofocus flag to the element if required
                    if (!$autofocusadded && $this->data['autofocus'] === true && empty($element['nofocus'])) {
                        $element['autofocus'] = true;
                        $autofocusadded = true;
                    }
                    elseif (!empty($this->data['autofocus']) && $this->data['autofocus'] !== true
                        && $name == $this->data['autofocus']) {
                        $element['autofocus'] = true;
                    }

                    $element['tabindex'] = $this->data['tabindex'];
                }
                $element['name'] = $name;
            }

        }

        // Check if the form was submitted, and if so, validate and process it
        $global = ($this->data['method'] == 'get') ? $_GET: $_POST;
        if ($this->data['validate'] && isset($global['pieform_' . $this->name] )) {
            if ($this->data['submit']) {
                $this->submitted = true;
                // Check if the form has been cancelled
                if ($this->data['iscancellable']) {
                    foreach ($global as $key => $value) {
                        if (substr($key, 0, 7) == 'cancel_') {
                            // Check for and call the cancel function handler, if defined
                            $function = $this->name . '_' . $key;
                            if (function_exists($function)) {
                                $function($this);
                            }

                            // Redirect the user to where they should go, if the cancel handler didn't already
                            $element = $this->get_element(substr($key, 7));
                            if (!isset($element['goto'])) {
                                throw new PieformException('Cancel element "' . $element['name'] . '" has no page to go to');
                            }
                            if ($this->data['jsform']) {
                                $this->json_reply(PIEFORM_CANCEL, $element['goto']);
                            }
                            header('HTTP/1.1 303 See Other');
                            header('Location:' . $element['goto']);
                            exit;
                        }
                    }
                }
            }

            // Get the values that were submitted
            $values = $this->get_submitted_values();
            // Perform general validation first
            $this->validate($values);

            // Submit the form if things went OK
            if ($this->data['submit'] && !$this->has_errors()) {
                $submitted = false;
                foreach ($this->get_elements() as $element) {
                    if (!empty($element['submitelement']) && isset($global[$element['name']])) {
                        $function = "{$this->data['successcallback']}_{$element['name']}";
                        if (function_exists($function)) {
                            $function($this, $values);
                            $submitted = true;
                            break;
                        }
                    }
                }
                $function = $this->data['successcallback'];
                if (!$submitted && is_callable($function)) {
                    // Call the user defined function for processing a submit
                    // This function should really redirect/exit after it has
                    // finished processing the form.
                    // @todo maybe it should do just that...
                    call_user_func_array($function, array($this, $values));
                    if ($this->data['dieaftersubmit']) {
                        // This will only work if I can make the login_submit function stuff work in login_validate
                        if ($this->data['jsform']) {
                            $message = 'Your ' . $this->name . '_submit function should use $form->json_reply to send a response';
                        }
                        else {
                            $message = 'Your ' . $this->name . '_submit function should redirect or exit when it is done';
                        }
                        throw new PieformException($message);
                    }
                    else {
                        // Successful submission, and the user doesn't care about replying, so...
                        return;
                    }
                }
                else if (!$submitted) {
                    throw new PieformException('No function registered to handle form submission for form "' . $this->name . '"');
                }
            }

            // Auto focus the first element with an error if required
            if ($this->data['autofocus'] !== false) {
                $this->auto_focus_first_error();
            }

            // Call the user-defined PHP error function, if it exists
            $function = $this->data['errorcallback'];
            if (is_callable($function)) {
                call_user_func_array($function, array($this));
            }
            
            // If the form has been submitted by javascript, return json
            if ($this->data['jsform']) {
                $errors = $this->get_errors();
                $json = array();
                foreach ($errors as $element) {
                    $json[$element['name']] = $element['error'];
                }
                $message = $this->get_property('jserrormessage');
                $this->json_reply(PIEFORM_ERR, array('message' => $message, 'errors' => $json));
            }
        }
    }

    /**
     * Returns a generic property. This can be used to retrieve any property
     * set in the form data array, so developers can pass in random stuff and
     * get access to it.
     *
     * @param string The key of the property to return
     * @return mixed
     */
    public function get_property($key) {
        return $this->data[$key];
    }

    /**
     * Returns the form name
     *
     * @return string
     */
    public function get_name() {
        return $this->name;
    }

    /**
     * Returns whether the form has been submitted
     *
     * @return bool
     */
    public function is_submitted() {
        return $this->submitted;
    }

    /**
     * Returns the HTML for the <form...> tag
     *
     * @return string
     */
    public function get_form_tag() {
        $result = '<form class="pieform"';
        foreach (array('name', 'method', 'action') as $attribute) {
            $result .= ' ' . $attribute . '="' . $this->data[$attribute] . '"';
        }
        $result .= ' id="' . $this->name . '"';
        if ($this->fileupload) {
            $result .= ' enctype="multipart/form-data"';
        }
        $result .= '>';
        return $result;
    }

    /**
     * Builds and returns the HTML for the form, respecting the chosen renderer.
     *
     * Note that the "action" attribute for the form tag are NOT HTML escaped
     * for you. This allows you to build your own URLs, should you require. On
     * the other hand, this means you must be careful about escaping the URL,
     * especially if it has data from an external source in it.
     *
     * @param boolean Whether to include the <form...></form> tags in the output
     * @return string The form as HTML
     */
    public function build($outputformtags=true) {
        $result = '';
        if ($outputformtags) {
            $result = $this->get_form_tag() . "\n";
        }

        $this->include_plugin('renderer',  $this->data['renderer']);
        
        // Form header
        $function = 'pieform_renderer_' . $this->data['renderer'] . '_header';
        if (function_exists($function)) {
            $result .= $function();
        }

        // Render each element
        foreach ($this->data['elements'] as $name => $elem) {
            if ($elem['type'] != 'hidden') {
                $result .= pieform_render_element($this, $elem);
            }
        }

        // Form footer
        $function = 'pieform_renderer_' . $this->data['renderer'] . '_footer';
        if (function_exists($function)) {
            $result .= $function();
        }

        // Hidden elements
        $this->include_plugin('element', 'hidden');
        foreach ($this->get_elements() as $element) {
            if ($element['type'] == 'hidden') {
                $result .= pieform_element_hidden($element, $this);
            }
        }
        $element = array(
            'type'  => 'hidden',
            'name'  => 'pieform_' . $this->name,
            'value' => ''
        );
        $result .= pieform_element_hidden($element, $this);
        if ($outputformtags) {
            $result .= "</form>\n";
        }

        if ($this->data['jsform'] || $this->data['presubmitcallback']) {
            $result .= '<script type="text/javascript">';
            $result .= "\n" . $this->whichbutton_js();
        }
        if ($this->data['jsform']) {
            $result .= $this->submit_js();
        }
        else if ($this->data['presubmitcallback']) {
            $result .= 'connect(\'' . $this->name . '\', \'onsubmit\', '
                . 'function() { ' . $this->data['presubmitcallback'] . "('{$this->name}', {$this->name}_btn); });";
        }
        if ($this->data['jsform'] || $this->data['presubmitcallback']) {
            $result .=  "\n</script>\n";
        }

        return $result;
    }

    /**
     * Given an element, gets the value for it from this form
     *
     * @param  array $element The element to get the value for
     * @return mixed          The element's value. <kbd>null</kbd> if no value
     *                        is available for the element.
     */
    public function get_value($element) {
        $function = 'pieform_element_' . $element['type'] . '_get_value';
        if (function_exists($function)) {
            return $function($this, $element);
        }
        $global = ($this->data['method'] == 'get') ? $_GET : $_POST;
        // If the element is a submit element and has its value in the request, return it
        // Otherwise, we don't return the value if the form has been submitted, as they
        // aren't normally returned using a standard form.
        if (isset($element['value'])) {
            return $element['value'];
        }
        else if (isset($global[$element['name']]) && $element['type'] != 'submit') {
            return $global[$element['name']];
        }
        else if (isset($element['defaultvalue'])) {
            return $element['defaultvalue'];
        }
        return null;
    }

    /**
     * Retrieves a list of elements in the form.
     *
     * This flattens fieldsets, and ignores the actual fieldset elements
     *
     * @return array The elements of the form
     */ 
    public function get_elements() {
        $elements = array();
        foreach ($this->data['elements'] as $name => $element) {
            if ($element['type'] == 'fieldset') {
                foreach ($element['elements'] as $subelement) {
                    $elements[] = $subelement;
                }
            }
            else {
                $elements[] = $element;
            }
        }
        return $elements;
    }
    
    /**
     * Returns the element with the given name. Throws a PieformException if the
     * element cannot be found.
     *
     * Fieldset elements are ignored. This might change if a valid case for
     * needing them is found.
     *
     * @param  string $name     The name of the element to find
     * @return array            The element
     * @throws PieformException If the element could not be found
     */
    public function get_element($name) {
        foreach ($this->get_elements() as $element) {
            if ($element['name'] == $name) {
                return $element;
            }
        }
        throw new PieformException('Element "' . $name . '" cannot be found');
    }

    /**
     * Retrieves submitted values from the request for the elements of this form.
     *
     * This takes into account that some elements may not even have been set,
     * for example if they were check boxes that were not checked upon
     * submission.
     *
     * A value is returned for every element (except fieldsets of course). If
     * an element was not set, the value set is <kbd>null</kbd>.
     *
     * @return array The submitted values
     */
    private function get_submitted_values() {
        $result = array();
        $global = ($this->data['method'] == 'get') ? $_GET : $_POST;
        foreach ($this->get_elements() as $element) {
            if ($element['type'] != 'markup') {
                if (
                    (empty($element['submitelement']) && empty($element['cancelelement'])) ||
                    (
                        (!empty($element['submitelement']) || !empty($element['cancelelement']))
                        && isset($global[$element['name']])
                    )
                ) {
                    $result[$element['name']] = $this->get_value($element);
                }
            }
        }
        return $result;
    }

    /**
     * Performs simple validation based off the definition array.
     *
     * Rules can be added to <kbd>pieform/rules/</kbd> directory, and then
     * re-used in the 'rules' index of each element in the form definition
     * hash.
     *
     * More complicated validation is possible by defining an optional
     * callback with the name {$form->name}_validate. See the documentation for
     * more information.
     *
     * @param array $values The submitted values from the form
     */
    private function validate($values) {
        // Call the overall validation function if it is available
        if (function_exists('pieform_validate')) {
            pieform_validate($this, $values);
        }

        // Perform rule validation
        foreach ($this->get_elements() as $element) {
            if (isset($element['rules']) && is_array($element['rules'])) {
                foreach ($element['rules'] as $rule => $data) {
                    if (!$this->get_error($element['name'])) {
                        // See if this element has a function that describes
                        // how this rule should apply to it
                        $function = 'pieform_element_' . $element['type'] . '_rule_' . $rule;
                        if (!function_exists($function)) {
                            // Try instead the default rule function
                            $function = 'pieform_rule_' . $rule;
                            if (!function_exists($function)) {
                                $this->include_plugin('rule', $rule);
                                if (!function_exists($function)) {
                                    throw new PieformException('No such form rule "' . $rule . '"');
                                }
                            }
                        }
                        if ($error = $function($this, $values[$element['name']], $element, $data)) {
                            $this->set_error($element['name'], $error);
                        }
                    }
                }
            }
        }

        // Then user specific validation if a function is available for that
        $function = $this->data['validatecallback'];
        if (is_callable($function)) {
            call_user_func_array($function, array($this, $values));
        }
    }

    private function whichbutton_js() {
        $result = "var {$this->name}_btn = null;\n";

        $connecteventadded = false;
        foreach ($this->get_elements() as $element) {
            if (!empty($element['submitelement'])) {
                if (!$connecteventadded) {
                    $result .= "addLoadEvent(function() {\n";
                    $connecteventadded = true;
                }
                if (!empty($element['cancelelement'])) {
                    $cancelstr = 'cancel_';
                }
                else {
                    $cancelstr = '';
                }
                $result .= "    connect($('{$cancelstr}{$this->name}_{$element['name']}'), 'onclick', function() { {$this->name}_btn = '{$cancelstr}{$this->name}_{$element['name']}'; });\n";
            }
        }
        if ($connecteventadded) {
            $result .= "});\n";
        }

        return $result;
    }

    /**
     * Builds the javascript for submitting the form. Note that the iframe is
     * not hidden with display: none, as safari/konqueror/ns6 ignore things with
     * display: none. Positioning it absolute and 'hidden' has the same effect
     * without the breakage.
     */
    private function submit_js() {
        $result = <<<EOF
connect($('{$this->name}'), 'onsubmit', function(e) {
    if (typeof(tinyMCE) != 'undefined') { tinyMCE.triggerSave(); }

EOF;
        if (!empty($this->data['presubmitcallback'])) {
            $result .= "    {$this->data['presubmitcallback']}('{$this->name}', {$this->name}_btn);\n";
        }
        $result .= <<<EOF

    var iframe = $('{$this->name}_iframe');
    $('{$this->name}').target = '{$this->name}_iframe';
    if (!iframe) {
        iframe = createDOM('iframe', {
            'name': '{$this->name}_iframe',
            'id'  : '{$this->name}_iframe',
            'style': 'position: absolute; visibility: hidden;'
        });
        insertSiblingNodesAfter($('{$this->name}'), iframe);

        window.pieformHandler_{$this->name} = function(data) {

EOF;
        if (isset($this->data['processingstopcallback'])) {
            $result .= "            {$this->data['processingstopcallback']}('{$this->name}', {$this->name}_btn);\n";
        }

        $result .= <<<EOF
            evalJSONRequest(data);
            if (data.returnCode == 0) {
                {$this->name}_remove_all_errors();
                // The request completed successfully

EOF;
        if (!empty($this->data['jssuccesscallback'])) {
            $result .= "                {$this->data['jssuccesscallback']}('{$this->name}', data);\n";
        }

        $result .= <<<EOF
            }
            else {
                if (data.returnCode == -2) {
                    window.location = data.message;
                    return;
                }
                    
                {$this->name}_remove_all_errors();
                if (data.message.errors) {
                    for (error in data.message.errors) {
                        {$this->name}_set_error(data.message.errors[error], error);
                    }
                    // @todo only output when fieldsets are present
                    forEach(getElementsByTagAndClassName('fieldset', 'collapsed', '{$this->name}'), function(fieldset) {
                        if (getFirstElementByTagAndClassName(null, 'error', fieldset)) {
                            removeElementClass(fieldset, 'collapsed');
                        }
                    });
                }

                if (data.returnCode == -1) {

EOF;
        if (!empty($this->data['jserrorcallback'])) {
            $result .= "                    {$this->data['jserrorcallback']}('{$this->name}', data);\n";
        }
        $result .= <<<EOF
                }
                else {

EOF;
        if (!empty($this->data['globaljserrorcallback'])) {
            $result .= "                    {$this->data['globaljserrorcallback']}('{$this->name}', data);\n";
        }
        else {
            $result .= "                    alert('Developer: got error code ' + data.returnCode
                    + ', either fix your form to not use this code or define a global js error handler');\n";
        }
        $result .= <<<EOF
                }
            }

EOF;
        if (!empty($this->data['postsubmitcallback'])) {
            $result .= "            {$this->data['postsubmitcallback']}('{$this->name}', {$this->name}_btn);\n";
        }

        $result .= <<<EOF
            {$this->name}_btn = null;
        }
    }

EOF;
        $result .= "});\n\n";
        $function = 'pieform_renderer_' . $this->data['renderer'] . '_get_js';
        if (!function_exists($function)) {
            throw new PieformException('No renderer message function "' . $function . '"');
        }

        return $result . $function($this->name);
    }
    
    public function json_reply($returncode, $message=null) {
        $data = array(
            'returnCode' => intval($returncode),
            'message'    => $message
        );
        $result = json_encode($data);

        echo <<<EOF
<html><head><script type="text/javascript">function sendResult() { parent.pieformHandler_{$this->name}($result); }</script></head><body onload="sendResult(); "></body></html>
EOF;
        exit;
    }

    /**
     * Returns whether a field has an error marked on it.
     *
     * This method should be used in the custom validation functions, to see if
     * there is an error on an element before checking for any more validation.
     *
     * Example:
     *
     * <code>
     * if (!$form->get_error('name') && /* condition {@*}) {
     *     $form->set_error('name', 'error message');
     * }
     * </code>
     *
     * @param  string $name  The name of the element to check
     * @return bool          Whether the element has an error
     * @throws PieformException If the element could not be found
     */
    public function get_error($name) {
        $element = $this->get_element($name);
        return isset($element['error']);
    }

    /**
     * Marks a field has having an error.
     *
     * This method should be used to set an error on an element in a custom
     * validation function, if one has occured.
     *
     * Note that for the Mahara project, your error messages must be passed
     * through {@link get_string} to internationalise them.
     *
     * @param string $name    The name of the element to set an error on
     * @param string $message The error message
     * @throws PieformException  If the element could not be found
     */
    public function set_error($name, $message) {
        foreach ($this->data['elements'] as &$element) {
            if ($element['type'] == 'fieldset') {
                foreach ($element['elements'] as &$subelement) {
                    if ($subelement['name'] == $name) {
                        $subelement['error'] = $message;
                        return;
                    }
                }
            }
            else {
                if ($element['name'] == $name) {
                    $element['error'] = $message;
                    return;
                }
            }
        }
        throw new PieformException('Element "' . $name . '" could not be found');
    }

    /**
     * Makes an ID for an element.
     *
     * Element IDs are used for <label>s, so use this method to ensure that
     * an element gets an ID.
     *
     * The element's existing 'id' and 'name' attributes are checked first. If
     * they are not specified, a random ID is synthesised
     *
     * @param array $element The element to make an ID for
     * @return string        The ID for the element
     */
    public function make_id($element) {
        if (isset($element['id'])) {
            return self::hsc($element['id']);
        }
        if (isset($element['name'])) {
            return self::hsc($element['name']);
        }
        return substr(md5(mt_rand()), 0, 4);
    }

    /**
     * Makes a class for an element.
     *
     * Elements can have several classes set on them depending on their state.
     * The classes are useful for (among other things), styling elements
     * differently if they are in these states.
     *
     * Currently, the states an element can be in are 'required' and 'error'.
     *
     * @param array $element The element to make a class for
     * @return string        The class for an element
     */
    public function make_class($element) {
        $classes = array();
        if (isset($element['class'])) {
            $classes[] = $element['class'];
        }
        if (!empty($element['rules']['required'])) {
            $classes[] = 'required';
        }
        if (!empty($element['error'])) {
            $classes[] = 'error';
        }
        if ($this->data['elementclasses']) {
            $classes[] = $element['type'];
        }
        // Please make sure that 'autofocus' is the last class added in this
        // method. Otherwise, improve the logic for removing 'autofocus' from
        // the elemnt class string in pieform_render_element
        if (!empty($element['autofocus'])) {
            $classes[] = 'autofocus';
        }
        return implode(' ', $classes);
    }

    /**
     * Given an element, returns a string representing the basic attribute
     * list for the element.
     *
     * This EXCLUDES the "value" attribute, as various form elements set
     * their value in different ways.
     *
     * This allows each element to have most of the standard HTML attributes
     * that you can normally set on a form element.
     *
     * The attributes generated by this method will include (if set for the
     * element itself), are <kbd>accesskey, class, dir, id, lang, maxlength,
     * name, size, style</kbd> and <kbd>tabindex</kbd>.
     *
     * The <kbd>class</kbd> and <kbd>id</kbd> attributes are typically built
     * beforehand with {@link make_class} and {@link make_id} respectively.
     * The <kbd>maxlength</kbd> attribute is only set if the element has a
     * "maxlength" rule on it.
     *
     * @param array $element The element to make attributes for
     * @param array $exclude Any attributes to explicitly exclude from adding
     * @return string        The attributes for the element
     */
    public function element_attributes($element, $exclude=array()) {
        static $attributes = array('accesskey', 'class', 'dir', 'id', 'lang', 'name', 'onclick', 'size', 'style', 'tabindex');
        $elementattributes = array_diff($attributes, $exclude);
        $result = '';
        foreach ($elementattributes as $attribute) {
            if (isset($element[$attribute]) && $element[$attribute] !== '') {
                if ($attribute == 'id') {
                    $element[$attribute] = $this->name . '_' . $element[$attribute];
                }
                $result .= ' ' . $attribute . '="' . self::hsc($element[$attribute]) . '"';
            }
        }

        if (!in_array('maxlength', $exclude) && isset($element['rules']['maxlength'])) {
            $result .= ' maxlength="' . intval($element['rules']['maxlength']) . '"';
        }

        foreach (array_diff(array('disabled', 'readonly'), $exclude) as $attribute) {
            if (!empty($element[$attribute])) {
                $result .= " $attribute=\"$attribute\"";
            }
        }
        
        return $result;
    }

    /**
     * Checks if there are errors on any of the form elements.
     *
     * @return bool Whether there are errors with the form
     */
    public function has_errors() {
        foreach ($this->get_elements() as $element) {
            if (isset($element['error'])) {
                return true;
            }
        }
        return false;
    }

    /**
     * Includes a plugin file, checking any configured plugin directories.
     *
     * @param string $type The type of plugin to include: 'element', 'renderer' or 'rule'
     * @param string $name The name of the plugin to include
     * @throws PieformException If the given type or plugin could not be found
     */
    public function include_plugin($type, $name) {
        if (!in_array($type, array('element', 'renderer', 'rule'))) {
            throw new PieformException("The type \"$type\" is not allowed for an include plugin");
        }

        // Check the configured include paths if they are specified
        foreach ($this->data['configdirs'] as $directory) {
            $file = "$directory/{$type}s/$name.php";
            if (is_readable($file)) {
                include_once($file);
                return;
            }
        }

        // Check the default include path
        $file = dirname(__FILE__) . "/pieform/{$type}s/{$name}.php";
        if (is_readable($file)) {
            include_once($file);
            return;
        }

        throw new PieformException("Could not find $type \"$name\"");
    }

    /**
     * Return an internationalised string based on the passed input key
     *
     * Returns english by default.
     *
     * @param string $plugin     The type of plugin (element, renderer, rule)
     * @param string $pluginname The name of the plugin to get the language
     *                           strings for
     * @param string $key        The language key to look up
     * @param array  $element    The element to get the string for. Elements
     *                           can specify there own i18n strings for rules
     * @return string            The internationalised string
     */
    public function i18n($plugin, $pluginname, $key, $element) {
        if (!in_array($plugin, array('element', 'renderer', 'rule'))) {
            throw new PieformException("Invalid plugin name '$plugin'");
        }

        // Check the element itself for the language string
        if ($plugin == 'rule' && isset($element['rulei18n'][$key])) {
            return $element['rulei18n'][$key];
        }

        // Check to see if a default was configured for the form
        if ($plugin == 'rule' && isset($this->data['rulei18n'][$key])) {
            return $this->data['rulei18n'][$key];
        }

        // Fall back to the default string
        $function = 'pieform_' . $plugin . '_' . $pluginname . '_i18n';
        if (function_exists($function)) {
            $strings = $function();
            return $strings[$this->data['language']][$key];
        }

        // We don't recognise this string
        return '[[' . $key . ']]';
    }

    /**
     * HTML-escapes the given value
     *
     * @param string $text The text to escape
     * @return string      The text, HTML escaped
     */
    public static function hsc($text) {
        return htmlspecialchars($text, ENT_COMPAT, 'UTF-8');
    }

    /**
     * Hook for giving information back to the developer
     *
     * @param string $message The message to give to the developer
     */
    public static function info($message) {
        $function = 'pieform_info';
        if (function_exists($function)) {
            $function($message);
        }
        else {
            trigger_error($message, E_USER_NOTICE);
        }
    }

    /**
     * Makes sure that the javascript callbacks for this form are valid javascript
     * function names.
     */
    private function validate_js_callbacks() {
        foreach (array('presubmitcallback', 'postsubmitcallback', 'jssuccesscallback',
            'jserrorcallback', 'globaljserrorcallback') as $callback) {
            if ($this->data[$callback] != '' && !preg_match('/^[a-zA-Z][a-zA-Z0-9_]*$/', $this->data[$callback])) {
                throw new PieformException("'{$this->data[$callback]}' is not a valid javascript callback name for callback '$callback'");
            }
        }
    }

    /**
     * Returns elements with errors on them
     *
     * @return array An array of elements with errors on them, the empty array
     *               in the result of no errors.
     */
    public function get_errors() {
        $result = array();
        foreach ($this->get_elements() as $element) {
            if (isset($element['error'])) {
                $result[] = $element;
            }
        }
        return $result;
    }

    /**
     * Sets the 'autofocus' property on the first element encountered that has
     * an error on it
     */
    private function auto_focus_first_error() {
        foreach ($this->data['elements'] as &$element) {
            if ($element['type'] == 'fieldset') {
                foreach ($element['elements'] as &$subelement) {
                    if (isset($subelement['error'])) {
                        $subelement['autofocus'] = true;
                        return;
                    }
                    unset($subelement['autofocus']);
                }
            }
            else {
                if (isset($element['error'])) {
                    $element['autofocus'] = true;
                    return;
                }
                unset($element['autofocus']);
            }
        }
    }
}


/**
 * Renders an element, and returns the result.
 *
 * This function looks in <kbd>pieform/renderers</kbd> for available overall form
 * renderers, and in <kbd>pieform/elements</kbd> for renderers for each form
 * element.
 *
 * If any of the renderers are not available, this function will throw a
 * PieformException.
 *
 * {@internal This is separate so that child element types can nest other
 * elements inside them (like the fieldset element does for example).}}
 *
 * @param Pieform  $form    The form to render the element for
 * @param array    $element The element to render
 * @return string           The rendered element
 */
function pieform_render_element(Pieform $form, $element) {
    // If the element is pure markup, don't pass it to the renderer
    if ($element['type'] == 'markup') {
        return $element['value'] . "\n";
    }

    // Make sure that the function to render the element type is available
    $function = 'pieform_element_' . $element['type'];

    $rendererfunction = 'pieform_renderer_' . $form->get_property('renderer');
    if (!function_exists($rendererfunction)) {
        throw new PieformException('No such form renderer function: "' . $rendererfunction . '"');
    }

    $element['id']    = $form->make_id($element);
    $element['class'] = $form->make_class($element);
    $builtelement = $function($form, $element);

    // Remove the 'autofocus' class, because we only want it on the form input
    // itself, not the wrapping HTML
    $element['class'] = preg_replace('/\s?autofocus/', '', $element['class']);

    return $rendererfunction($form, $builtelement, $element);
}

function pieform_get_headdata() {
    $htmlelements = array();
    foreach ($GLOBALS['_PIEFORM_REGISTRY'] as $form) {
        foreach ($form->get_elements() as $element) {
            $function = 'pieform_element_' . $element['type'] . '_get_headdata';
            if (function_exists($function)) {
                $elems = $function($element);
                $htmlelements = array_merge($htmlelements, $elems);
            }
        }
    }

    return array_unique($htmlelements);
}

?>
Return current item: Pieforms