PHP Classes
elePHPant
Icontem

File: Apeform.class.php

Recommend this page to a friend!
  Classes of Thiemo Kreuz  >  TM::Apeform  >  Apeform.class.php  >  Download  
File: Apeform.class.php
Role: Class source
Content type: text/plain
Description: Main class
Class: TM::Apeform
A very abstract web form builder and processor
Author: By
Last change: I updated the license a bit.
Date: 10 years ago
Size: 63,153 bytes
 

 

Contents

Class file image Download
<?php

/*
 * LICENSE
 * 1. If you like to use this class for personal purposes, it's free.
 * 2. For comercial purposes, please contact me (http://maettig.com/email).
 *    I'll send a license to you.
 * 3. When you copy the framework you must copy this notice with the source
 *    code. You may alter the source code, but you have to put the original
 *    with your altered version.
 * 4. The license is for all files included in this bundle.
 *
 * KNOWN BUGS/LIMITATIONS/TODO
 * - Element values will jump around when adding/removing elements on runtime.
 *   Same for multi page forms. Apeform class don't support both cases yet.
 * - <select multiple> is not supported yet (because it's highly useless for
 *   ordinary visitors).
 * - <input type=reset> is not supported because it's useless at all.
 * - Setting a <form target=...> didn't make sense in a self-calling form.
 * - Multiple elements can't handle multiple attributes.
 * - Add <fieldset> and <legend>? Did not work in Netscape 4.x.
 * - Add <button>? Did not work in Netscape 4.x.
 * - Add <optgroup>? Did not work in many browsers.
 * - A challenge/response could be used to avoid reloads and handle multi page
 *   forms. Critical, because this would need to start a session.
 */

/**
 * A very abstract web form builder.
 *
 * This class creates self repeating web forms - so called "Affenformulare" (ape
 * forms) - in a very useful, easy way. The whole process including creation of
 * the form, validation and usage of the form values that the user entered is
 * done in a single script.
 *
 * The class hides the access to POST and global variables and simply returns
 * the submitted values. It offers an easy way to handle input errors (checking
 * valid email adresses for example). It supports all usual form elements
 * including radio buttons, {@link select} boxes, {@link file} upload fields and
 * so on. It provides an own, tiny {@link templates templating} system, so you
 * don't have to deal with HTML at all. It creates labels and access keys
 * according to HTML 4 standard and returns XHTML compatible output. In addition
 * you can {@link addAttribute add} JavaScript handlers to any form element.
 *
 * The class is optimized to be used with the minimum amount of source code. See
 * {@link display()} for a tiny example.
 *
 * Don't hesitate to {@link http://bugs.maettig.com/ report bugs or feature
 * requests}.
 *
 * @author Thiemo Mttig (http://maettig.com/)
 * @version 2009-02-07
 * @package TM
 * @requires PHP 4.0.6 (array_map)
 */
class Apeform
{
    /**
     * Two-dimensional array containing all the web form elements.
     *
     * @var array
     * @access private
     */
    var $_rows = array();

    /**
     * Encryption type of the form. Switches to "multipart/form-data" when using
     * a file upload element.
     *
     * @var string
     * @access private
     */
    var $_encType = "";

    /**
     * Collection of attributes and JavaScript handlers to be added to <form>.
     * "onsubmit" is the only handler allowed here.
     *
     * @var array
     * @access private
     */
    var $_attributes = array();

    /**
     * @var bool Overrides the global magic_quotes_gpc configuration.
     * @access private
     */
    var $magicQuotes = null;

    /**
     * This will be true if one of the web form elements was submitted via POST.
     *
     * @var bool
     * @access private
     */
    var $_isSubmitted = false;

    /**
     * This will be true if error() was called at least once.
     *
     * @var bool
     * @access private
     */
    var $_hasErrors = false;

    /**
     * Defaults to "form". If set to something else, this will be used as prefix
     * for every element name and id. This setting can also be changed when
     * calling {@link Apeform()} or using an extended class.
     *
     * @var string Unique identifier for the form and its elements.
     */
    var $id = "form";

    /**
     * Defaults to the value of {@link id} which defaults to "form". This means,
     * the window scrolls to the top of the form after hitting the submit
     * button. For example <code>$form->anchor = "res";</code> produces
     * <code><form action="example.php#res"></code>. Set to "top" or
     * <code>false</code> to scroll to the top of the page.
     *
     * @var string Anchor name to be targeted when the form was submitted.
     */
    var $anchor = null;

    /**
     * Set to true to automatically add accesskey attributes to labels without
     * an <u>u</u>nderlined character. Defaults to false.
     *
     * @var bool Automatically creates accesskeys for all labels.
     */
    var $autoAccesskeys = false;

    /**
     * Sets the maximum number of characters the user is able to type in
     * {@link text()} and {@link password()} elements. Default is 255
     * characters. This setting can also be changed when calling
     * {@link Apeform()} or using an extended class. It's also possible to set
     * <i>maxLength</i> for single {@link text()} and {@link password()}
     * elements.
     *
     * @var int Default maximum length of the input values.
     */
    var $maxLength = 255;

    /**
     * Default width is 40 characters. This can also be changed when calling
     * {@link Apeform()} or using an extended class. It's also possible to set
     * <i>size</i> for single elements when calling {@link text()} and so on.
     *
     * @var int Default display size of the form elements.
     */
    var $size = 40;

    /**
     * It's important to store uploaded files when using a {@link file()}
     * element because PHP will remove any temporary file immediately. Set this
     * to a temporary directory nearby your scripts using a relative path, e.g.
     * "./temporary". Don't forget to enable writing access for this directory
     * (using chmod).
     *
     * Defaults to "/tmp" (default on Unix/Linux systems). If the directory
     * doesn't exists, one of the TMP/TMPDIR environment variables will be used.
     *
     * The tmpDir can also be changed using an extended class.
     *
     * @var string Directory where to store uploaded files temporary.
     */
    var $tmpDir = "/tmp";

    /**
     * The class uses it's own tiny HTML templating system. This associative
     * array contains all templates used to compile the form. It consists of up
     * to five parts:
     *
     * <code>'form'</code> will be used once as a container for the whole form.
     * It may contain a table header and footer for example.
     * <code>'input'</code> will be used for each form element. It may contain a
     * table row for example. <code>'label'</code>, <code>'error'</code> and
     * <code>'help'</code> are optional. They may contain some special
     * formating, line breaks etc. which should left out if help, error message
     * or label is empty.
     *
     * A basic example (default values are a little bit more complex):
     *
     * <pre>$form->templates = array(
     *     'form'   => "<table>{input}</table>",
     *     'header' => "<tr><th colspan=\"2\">{header}</th></tr>",
     *     'input'  => "<tr{class}><td>{label}</td><td>{input}</td></tr>",
     *     'label'  => "{label}:",
     *     'error'  => "<strong>{error}:</strong>",
     *     'help'   => "", //not used in this example
     *     'accesskey' => "<em class=\"key\">{accesskey}</em>");</pre>
     *
     * See {@link addClass()} for what <code>{class}</code> is for, see
     * {@link text()} for an example what <code>'accesskey'</code> is for.
     *
     * @var array The templates used to compile the form.
     */
    var $templates = array(
        'form' =>
            "<p><table border=\"0\" summary=\"\">\n{input}</table></p>",
            // This is valid (X)HTML but causes problems in Netscape 4.x:
            // "<div style=\"margin:1.12em 0\"><table border=\"0\" summary=\"\">\n{input}</table></div>",
        'header' =>
            "<tr{class}>\n<th colspan=\"2\">{header}</th>\n</tr>\n",
        'input' =>
            "<tr{class}>\n<td align=\"right\" valign=\"top\">{label}</td>\n<td valign=\"top\">{input}{help}</td>\n</tr>\n",
        'label' =>
            "{label}:",
        'error' =>
            '<strong class="error">{error}:</strong>',
        'help' =>
            '<br /><small>{help}</small>',
            // This is "semantical" better because <small> is a physical tag and
            // dropped in XHTML 2.0:
            // '<div style="font-size:smaller">{help}</div>',
        'accesskey' =>
            '<span style="text-decoration:underline">{accesskey}</span>'
            // This may be "semantical" better because <em> has a meaning:
            // '<em style="font-style:normal;text-decoration:underline">{accesskey}</em>'
    );

    /**
     * Creates a new web form builder.
     *
     * Class constructor of the web form builder. Returns a new Apeform object.
     * All parameters are optional. They can also be set using an extended
     * class. Default for <i>maxLength</i> is 255. Default for <i>size</i> is
     * 40. Default for <i>id</i> is "form". Setting <i>magicQuotes</i> the user
     * may disable or enable PHP's magic quotes behaviour manualy, independend
     * what's set in the <code>php.ini</code> (see get_magic_quotes_gpc()).
     * Defaults to the configurations default.
     *
     * @param maxLength int
     * @param size int
     * @param id string
     * @param magicQuotes bool
     * @return Apeform
     */
    function Apeform($maxLength = 0, $size = 0, $id = null, $magicQuotes = null)
    {
        // Set default maximum input length and width of the form elements.
        if ($maxLength > 0) $this->maxLength = (int)$maxLength;
        if ($size > 0) $this->size = (int)$size;

        // For backward compatibility cause parameter #3 moved to position #4.
        if (is_bool($id)) { $magicQuotes = $id; unset($id); }
        elseif (isset($id) && strlen($id)) $this->id = $id;

        if (isset($magicQuotes)) $this->magicQuotes = $magicQuotes;
        // Use default magic_quotes_gpc value if it's still not set.
        if (!isset($this->magicQuotes)) $this->magicQuotes = get_magic_quotes_gpc();
    }

    /**
     * Adds a header or subheading to the form.
     *
     * This is the only element that uses the {@link templates template}
     * <code>'header'</code>.
     *
     * @param header string
     * @return string
     */
    function &header($header)
    {
        $this->_rows[count($this->_rows)] = array('type' => "header",
            'header' => $header,
            'name' => false);
        return $header;
    }

    /**
     * Adds a static text to the form.
     *
     * For example, display a {@link text} element first. If the user entered a
     * valid value, replace the text with a staticText element. Example:
     *
     * <pre>$form = new Apeform();
     * $form->text("Text");
     * if (!$form->isValid()) $form->text("Static");
     * else $form->staticText("Static");
     * $form->submit();
     * $form->display();</pre>
     *
     * @param label string
     * @param help string
     * @param defaultValue string
     * @return string
     */
    function &staticText($label = "", $help = "", $defaultValue = "")
    {
        $id = count($this->_rows);
        $name = ($this->id == "form" ? "" : $this->id) . "element" . $id;
        $this->_rows[$id] = array('type' => "static",
            'label' => $label,
            'help' => $help,
            'value' => $defaultValue,
            'name' => $name);
        return $this->_fetchPostedValue();
    }

    /**
     * Adds a text input element to the form.
     *
     * Adds a single line input box to the form. All the arguments may be left
     * out in order from right to left.
     *
     * Use HTML tags for an <code>"<u>u</u>nderlined"</code> character in the
     * <i>label</i> to set an access key for this element. Pressing Alt +
     * character will focus this element later (Shift + Esc + character in
     * Opera). Keep this in mind, this works for almost all elements.
     *
     * If <i>help</i> is set to <code>"Help\t Unit"</code> for example, the text
     * "Unit" will be displayed behind the input field. This works for all
     * element types but makes sense only for a few of them.
     *
     * If one or all of <i>defaultValue</i>, <i>maxLength</i> or <i>size</i> is
     * an array instead of a single value, a multiple input field will be
     * created. Use more than one tab characters in <i>help</i> to add some text
     * between the elements. The following example creates a nice
     * [2004]-[12]-[31] date element made of three input fields:
     *
     * <pre>$date = $form->text("Date", "Year-Month-Day\t-\t-",
     *     array(2004, 12, 31), array(4, 2, 2));
     * echo $date[0]; //outputs 2004
     * //other possibilities to create multiple text fields:
     * $b = $form->text("Multi maxLength", "", "", array(9, 10));
     * $c = $form->text("Multi defaultValue", "", array("", ""));</pre>
     *
     * Returns <i>defaultValue</i> if the form is displayed the first time.
     * After this it returns the value(s) the user entered and submitted. Use
     * the <code>$ref = &$form->text();</code> syntax (note the ampersand) to
     * return the value by reference. This way you are able to change the value
     * displayed in the form (e.g. make it upper case).
     *
     * @param label string
     * @param help string
     * @param defaultValue mixed
     * @param maxLength mixed
     * @param size mixed
     * @return mixed
     */
    function &text($label = "", $help = "", $defaultValue = "", $maxLength = 0,
        $size = 0)
    {
        $count = max(count($defaultValue), count($maxLength), count($size));
        if (is_string($help)) $help = explode("\t", $help, 1 + $count);
        if ($count > 1)
        {
            $defaultValue = (array)$defaultValue;
            for ($i = 1; $i < $count; ++$i)
            {
                if (!isset($help[$i])) $help[$i] = "\n";
                if (!isset($defaultValue[$i])) $defaultValue[$i] = "";
            }
        }
        $id = count($this->_rows);
        $name = ($this->id == "form" ? "" : $this->id) . "element" . $id;
        $this->_rows[$id] = array('type' => "text",
            'label' => $this->_addAcceskey($label),
            'help' => $help,
            'value' => $defaultValue,
            'maxLength' => $maxLength,
            'size' => $size,
            'name' => $name);
        $value = &$this->_fetchPostedValue();
        if (is_array($value) && count($value) < 2) return $value[0];
        else return $value;
    }

    /**
     * Adds a password input element to the form.
     *
     * Adds a password input element that works the same way like
     * {@link text()}. The only difference is, if an error occurs the value will
     * be removed (because the user can't see what he typed before).
     *
     * @param label string
     * @param help string
     * @param defaultValue string
     * @param maxLength int
     * @param size int
     * @return string
     */
    function &password($label = "", $help = "", $defaultValue = "",
        $maxLength = 0, $size = 0)
    {
        $id = count($this->_rows);
        $name = ($this->id == "form" ? "" : $this->id) . "element" . $id;
        $this->_rows[$id] = array('type' => "password",
            'label' => $this->_addAcceskey($label),
            'help' => $help,
            'value' => $defaultValue,
            'maxLength' => $maxLength,
            'size' => $size,
            'name' => $name);
        return $this->_fetchPostedValue();
    }

    /**
     * Adds a text area to the form.
     *
     * Adds a multi line input box to the form. Works similar to {@link text()}.
     *
     * To change the height of the area set the number of <i>rows</i>. Default
     * is 3. Default for <i>cols</i> is 40 (see {@link size}). Default for
     * <i>wrap</i> is "virtual". Other possible values are "off" or
     * <code>false</code>.
     *
     * @param label string
     * @param help string
     * @param defaultValue string
     * @param rows int
     * @param cols int
     * @param wrap mixed
     * @return string
     */
    function &textarea($label = "", $help = "", $defaultValue = "", $rows = 0,
        $cols = 0, $wrap = "virtual")
    {
        $id = count($this->_rows);
        $name = ($this->id == "form" ? "" : $this->id) . "element" . $id;
        $this->_rows[$id] = array('type' => "textarea",
            'label' => $this->_addAcceskey($label),
            'help' => $help,
            'value' => $defaultValue,
            'rows' => $rows,
            'cols' => $cols,
            'wrap' => empty($wrap) ? "off" : $wrap,
            'name' => $name);
        return $this->_fetchPostedValue();
    }

    /**
     * Adds a hidden input element to the form.
     *
     * Adds a hidden element to the form. Returns the hidden value. If you need
     * the value before hidden() was called, set a <i>name</i>. This is the only
     * place you need to set a name for an element. This way you are able to
     * fetch the hidden value using <code>$_POST</code> or
     * <code>$_REQUEST['elementName']</code>.
     *
     * @param defaultValue string
     * @param name string
     * @return string
     */
    function &hidden($defaultValue = "", $name = "")
    {
        $id = count($this->_rows);
        $name = $name ? $name : ($this->id == "form" ? "" : $this->id) . "element" . $id;
        $this->_rows[$id] = array('type' => "hidden",
          'value' => $defaultValue,
          'name' => $name);
        return $this->_fetchPostedValue();
    }

    /**
     * Adds one or more checkbox elements to the form.
     *
     * Adds one or more checkbox elements to the form. If only one option is
     * given or <i>options</i> is empty a single checkbox will be displayed.
     * Returns a string in this case. If two or more options are given (see
     * {@link select()} for some examples) it will return an array. The
     * <i>defaultValue</i> also have to be an array in this case.
     *
     * @param label string
     * @param help string
     * @param options mixed
     * @param defaultValue mixed
     * @return mixed
     */
    function &checkbox($label, $help = "", $options = "", $defaultValue = "")
    {
        $id = count($this->_rows);
        $name = ($this->id == "form" ? "" : $this->id) . "element" . $id;
        // Use the label as the only checkbox value if no options are given.
        if (!$options) $options = array($label => "");
        $this->_rows[$id] = array('type' => "checkbox",
            'label' => $this->_addAcceskey($label),
            'help' => $help,
            'options' => $this->_explodeOptions($options),
            'name' => $name);
        // Default value is an array for multiple checkboxes, a string otherwise.
        if (count($this->_rows[$id]['options']) > 1)
        {
            // Don't use the default value if the form was already submitted.
            $this->_rows[$id]['value'] = $this->_isSubmitted ? array() :
                $this->_explodeOptions($defaultValue);
        }
        else
        {
            $this->_rows[$id]['value'] = $this->_isSubmitted ? "" :
                $defaultValue;
        }
        return $this->_fetchPostedValue();
    }

    /**
     * Adds some radio buttons to the form.
     *
     * Adds two or more radio buttons to the form. See {@link select()} for
     * further explanation.
     *
     * @param label string
     * @param help string
     * @param options mixed
     * @param defaultValue string
     * @return string
     */
    function &radio($label, $help, $options, $defaultValue = "")
    {
        $id = count($this->_rows);
        $name = ($this->id == "form" ? "" : $this->id) . "element" . $id;
        $this->_rows[$id] = array('type' => "radio",
            'label' => $this->_addAcceskey($label),
            'help' => $help,
            'options' => $this->_explodeOptions($options),
            'value' => $defaultValue,
            'name' => $name);
        // If a default array value instead of a array key was given, fix this.
        if (!isset($this->_rows[$id]['options'][$this->magicQuotes ?
            addslashes($defaultValue) : $defaultValue]))
        {
            $this->_rows[$id]['value'] =
                array_search($defaultValue, $this->_rows[$id]['options']);
            if ($this->magicQuotes)
            {
                $this->_rows[$id]['value'] = stripslashes($this->_rows[$id]['value']);
            }
        }
        return $this->_fetchPostedValue();
    }

    /**
     * Adds a select element to the form.
     *
     * Adds a box to the form to select a value out of two or more values. This
     * is almost similar to {@link radio()} except for the way it is rendered.
     *
     * The <i>options</i> may be an associative array, for example
     * <code>array("a" => "Option A", "b" => "Option B")</code>. The values of
     * this array will be displayed, the keys will be submitted. For example,
     * the user selects "Option B" so a "b" will be returned by select(). The
     * <i>options</i> can also be a string, for example
     * <code>"Option A|Option B"</code>. This way the displayed and submitted
     * values will be the same, for example "Option B".
     *
     * Set <i>defaultValue</i> to one of the array keys to select an option by
     * default. Leave it empty to select nothing by default.
     *
     * The <i>size</i> isn't the width but the number of rows of the element.
     * Default is one row.
     *
     * Returns <i>defaultValue</i> if the form is displayed the first time.
     * After this it returns the array key of the selected option or an empty
     * string if nothing was selected. Use the
     * <code>$ref = &$form->select();</code> syntax (note the &) to return the
     * value by reference.
     *
     * @param label string
     * @param help string
     * @param options mixed
     * @param defaultValue string
     * @param size int
     * @return string
     */
    function &select($label, $help, $options, $defaultValue = "", $size = 1)
    {
        $id = count($this->_rows);
        $name = ($this->id == "form" ? "" : $this->id) . "element" . $id;
        $this->_rows[$id] = array('type' => "select",
            'label' => $this->_addAcceskey($label),
            'help' => $help,
            'options' => $this->_explodeOptions($options),
            'value' => $defaultValue,
            'size' => $size,
            'name' => $name);
        // If a default array value instead of a array key was given, fix this.
        if (!isset($this->_rows[$id]['options'][$this->magicQuotes ?
            addslashes($defaultValue) : $defaultValue]))
        {
            $this->_rows[$id]['value'] =
                array_search($defaultValue, $this->_rows[$id]['options']);
            if ($this->magicQuotes)
            {
                $this->_rows[$id]['value'] = stripslashes($this->_rows[$id]['value']);
            }
        }
        return $this->_fetchPostedValue();
    }

    /**
     * Adds a file upload element to the form.
     *
     * Adds a single file upload element to the form. Encryption type of the
     * form will be set to "multipart/form-data" automaticaly. Returns all file
     * information in an associative array. For example
     * <code>$file = $form->file();</code> returns $file['name'], $file['type'],
     * $file['size'], $file['tmp_name'] and $file['error']. Additionaly,
     * $file['unixname'] provides a noncritical file name without any spaces and
     * special characters.
     *
     * Unlike regular upload forms the file is not lost when the script ends.
     * The file will be stored in the temporary directory specified by
     * {@link tmpDir} or in the systems default TMP/TMPDIR. Use copy() to move
     * the file anywhere, for example <code>copy($file['tmp_name'], "target/" .
     * $file['name']);</code>. Don't use move_uploaded_file()!
     *
     * If the file upload element got an {@link error()} the temporary file will
     * be removed immediately. The garbage collection will remove all out-dated
     * temporary files as soon as the next file is uploaded.
     *
     * @param label string
     * @param help string
     * @param size int
     * @return array
     */
    function &file($label = "", $help = "", $size = 0)
    {
        $id = count($this->_rows);
        $name = ($this->id == "form" ? "" : $this->id) . "element" . $id;
        $this->_rows[$id] = array('type' => "file",
            'label' => $this->_addAcceskey($label),
            'help' => $help,
            'value' => false,
            'size' => $size,
            'name' => $name);
        // File is the only element that requires another encryption type.
        $this->_encType = "multipart/form-data";

        if (isset($GLOBALS['HTTP_POST_FILES'][$name]) && !isset($_FILES[$name]))
            $_FILES[$name] = &$GLOBALS['HTTP_POST_FILES'][$name];
        // Accept uploaded files only if the file size is greater than zero.
        if (isset($_FILES[$name]) && $_FILES[$name]['size'])
        {
            $this->_rows[$id]['value'] = $_FILES[$name];
            $postedName = &$this->_rows[$id]['value']['name'];
            if (get_magic_quotes_gpc()) $postedName = stripslashes($postedName);
            if ($this->magicQuotes) $postedName = addslashes($postedName);
        }
        // Read meta data from the hidden element if present.
        elseif (isset($_POST[$name . "h"]))
        {
            $this->_rows[$id]['value'] =
                unserialize(stripslashes($_POST[$name . "h"]));
        }
        elseif (isset($GLOBALS['HTTP_POST_VARS'][$name . "h"]))
        {
            $this->_rows[$id]['value'] =
                unserialize(stripslashes($GLOBALS['HTTP_POST_VARS'][$name . "h"]));
        }
        if ($this->_rows[$id]['value'] || isset($_FILES[$name]))
            $this->_isSubmitted = true;

        // Handle the uploaded file and meta data if something is in 'value'.
        if ($this->_rows[$id]['value'])
        {
            $this->_rows[$id]['value']['unixname'] =
                $this->_getUnixName($this->_rows[$id]['value']['name']);
            // Fix for a bug in IE which returns "pjpeg" for progressive JPEGs.
            $this->_rows[$id]['value']['type'] =
                preg_replace('{^image\W\wjpe?g$}is', 'image/jpeg',
                $this->_rows[$id]['value']['type']);

            // Store the file to avoid it to be deleted when the script ends.
            if (is_uploaded_file($this->_rows[$id]['value']['tmp_name']))
            {
                // tempnam() needs an absolute path so use realpath() to be sure.
                $tempnam = tempnam($realpath = realpath($this->tmpDir), "tmp");
                if (!$this->_doGarbageCollection($tempnam))
                {
                    user_error("Apeform::file() failed, tmpDir is not set properly",
                        E_USER_WARNING);
                    return $this->_rows[$id]['value'] = false;
                }
                // Preserve extension to be sure the real content type is used.
                $extension = strrchr($this->_rows[$id]['value']['name'], '.');
                rename($tempnam, $tempnam . $extension);
                $tempnam .= $extension;
                if (!move_uploaded_file($this->_rows[$id]['value']['tmp_name'],
                    $tempnam))
                {
                    return $this->_rows[$id]['value'] = false;
                }
                // Make the temporary path relative again if tmpDir exists.
                if (dirname($tempnam) == $realpath)
                {
                    $tempnam = $this->tmpDir . "/" . basename($tempnam);
                }
                $this->_rows[$id]['value']['tmp_name'] = $tempnam;
            }
            // Force an error if the file was lost for whatever reason.
            if (!is_file($this->_rows[$id]['value']['tmp_name']))
                $this->error();
        }
        // Returns false if there was no file submitted.
        return $this->_rows[$id]['value'];
    }

    /**
     * Generates an usefull Unix filename besides the original one.
     *
     * @param name string The filename to be processed.
     * @param maxLength int Maximum filename length, defaults to 64 (Joliet).
     * @return string
     * @access private
     */
    function _getUnixName($name, $maxLength = 64)
    {
        // Replace German umlauts and other special characters.
        $name = str_replace("", "ss", $name);
        // Diphton  becomes e etc.
        $name = preg_replace('/[\x8C\x9C]/', '\0e', $name);
        // Replace Windows-1252 (CP1252) and ISO-8859-1 with ASCII characters.
        $name = strtr($name,
            "\x80\x83\x8A\x8C\x8E\x96\x97\x9A\x9C\x9E\x9F" .
            "",
            "EfSOZ--sozYcLYca-r23u1o" .
            "AAAAAAACEEEEIIIIDNOOOOOxOUUUUyTaaaaaaaceeeeiiiidnoooooouuuuyty");
        // Replace all remaining special characters with an underscore.
        $name = preg_replace('/[^a-z0-9.-]+/i', '_', $name);
        // Remove all useless underscores, e.g. "_a_a_._a_" becomes "a_a.a".
        $name = preg_replace('/_*\b_*/', '', $name);
        // Crop the filename if it's too long.
        while (strlen($name) > $maxLength)
            $name = preg_replace('/.\b/', '', $name, 1);
        return $name;
    }

    /**
     * Remove some old temporary files which exceeded the timeout. Default for
     * timeout is 1440 seconds (24 minutes, see session.gc_maxlifetime).
     *
     * @param filename string
     * @return bool
     * @access private
     */
    function _doGarbageCollection($filename, $timeout = 1440)
    {
        $dir = dirname($filename) . "/";
        if (!($fp = opendir($dir))) return false;
        while (($file = readdir($fp)) !== false)
        {
            if (strpos($file, "tmp") === 0 && filemtime($dir . $file) < time() - $timeout)
            {
                @unlink($dir . $file);
            }
        }
        closedir($fp);
        return true;
    }

    /**
     * Adds one or more submit buttons to the form.
     *
     * Adds some submit buttons to the form. The <i>value</i> may be a simple
     * string to create a single button. It may be an array to create multiple
     * buttons. It's also possible to use a string for multiple buttons, for
     * example <code>"Button A|Button B"</code>. Returns the value of the button
     * the user pressed.
     *
     * @param value mixed
     * @param help string
     * @return string
     */
    function submit($value = "", $help = "")
    {
        $id = count($this->_rows);
        $name = ($this->id == "form" ? "" : $this->id) . "element" . $id;
        $this->_rows[$id] = array('type' => 'submit',
            'value' => empty($value) ? array("") : $this->_explodeOptions($value),
            'help' => $help,
            'name' => $name);
        if (isset($_POST[$name]))
        {
            $postedValue = $_POST[$name];
        }
        elseif (isset($GLOBALS['HTTP_POST_VARS'][$name]))
        {
            $postedValue = $GLOBALS['HTTP_POST_VARS'][$name];
        }
        if (isset($postedValue))
        {
            if (get_magic_quotes_gpc()) $postedValue = stripslashes($postedValue);
            if (empty($value)) $postedValue = true;
            // Deny unknown values.
            elseif (!in_array($postedValue, $this->_rows[$id]['value'])) return false;
            elseif ($this->magicQuotes) $postedValue = addslashes($postedValue);
            $this->_isSubmitted = true;
            // Return the buttons value if it was clicked before.
            return $postedValue;
        }
        // Return nothing otherwise. Needed to handle multiple buttons.
        return false;
    }

    /**
     * Adds one or more image buttons to the form.
     *
     * <i>Warning! This method is EXPERIMENTAL. The behaviour of this method may
     * change in a future release of this class.</i>
     *
     * @param src string
     * @param help string
     * @return string
     */
    function image($src, $help = "")
    {
        $id = count($this->_rows);
        $name = ($this->id == "form" ? "" : $this->id) . "element" . $id;
        $this->_rows[$id] = array('type' => "image",
            'value' => $this->_explodeOptions($src),
            'help' => $help,
            'name' => $name);
        if (isset($_POST[$name . '_x']))
        {
            $x = $_POST[$name . '_x'];
            $y = $_POST[$name . '_y'];
        }
        elseif (isset($GLOBALS['HTTP_POST_VARS'][$name. '_x']))
        {
            $x = $GLOBALS['HTTP_POST_VARS'][$name . '_x'];
            $y = $GLOBALS['HTTP_POST_VARS'][$name . '_y'];
        }
        if (isset($x))
        {
            $this->_isSubmitted = true;
            return array(0 => $x, 1 => $y, 'x' => $x, 'y' => $y);
        }
        return false;
    }

    /**
     * Add an acceskey attribute if missing and if autoAccesskeys is set.
     *
     * @param label string
     * @return string
     * @access private
     */
    function _addAcceskey($label)
    {
        if (!$this->autoAccesskeys || stristr($label, "<u>")) return $label;
        $c = strtolower(preg_replace('/[^a-z]+/isS', '', $label));
        for ($i = 0, $len = strlen($c); $i < $len; ++$i)
        {
            if (empty($this->_accesskeys[$c{$i}]))
            {
                $this->_accesskeys[$c{$i}] = true;
                return preg_replace('/' . $c{$i} . '/i', '<u>\0</u>', $label, 1);
            }
        }
        return $label;
    }

    /**
     * Explodes a string at "\t" or at "|".
     *
     * @param options mixed
     * @return array
     * @access private
     */
    function _explodeOptions($options)
    {
        // Options can be an associative array or a string, e.g. "a|b".
        if (!is_array($options))
        {
            if (strlen($options) < 1) return array();
            if (strpos($options, "\t") === false)
            {
                $options = strtr($options, array("\\\\" => "\\", "\|" => "|", "|" => "\t"));
            }
            $exploded = explode("\t", $options);
            $options = array();
            // Copy all options to an array, e.g. "a" => "a", "b" => "b".
            foreach ($exploded as $value) $options[$value] = $value;
        }
        if ($this->magicQuotes)
        {
            $array = $options;
            $options = array();
            foreach ($array as $key => $value) $options[addslashes($key)] = $value;
        }
        return $options;
    }

    /**
     * @return mixed
     * @access private
     */
    function &_fetchPostedValue()
    {
        // Create a shortcut reference for use in the following lines.
        $element = &$this->_rows[count($this->_rows) - 1];
        if (isset($_POST[$element['name']]))
        {
            $postedValue = $_POST[$element['name']];
        }
        elseif (isset($GLOBALS['HTTP_POST_VARS'][$element['name']]))
        {
            $postedValue = $GLOBALS['HTTP_POST_VARS'][$element['name']];
        }
        if (isset($postedValue))
        {
            if (get_magic_quotes_gpc())
            {
                $postedValue = is_array($postedValue)
                    ? array_map('stripslashes', $postedValue)
                    : stripslashes($postedValue);
            }
            if (strpos(implode("", (array)$postedValue), "\0"))
                $this->error();
            // Strip all null characters, they can't be entered anywhere.
            $postedValue = str_replace("\0", "", $postedValue);
            switch ($element['type'])
            {
                case "text":
                case "password":
                    if (strpos(implode("", (array)$postedValue), "\n"))
                        $this->error();
                    $postedValue = preg_replace('/[\r\n]+/s', ' ', $postedValue);
                    foreach ((array)$postedValue as $i => $v)
                    {
                        if (!is_array($element['maxLength']) && !empty($element['maxLength']))
                            $l = $element['maxLength'];
                        elseif (!empty($element['maxLength'][$i]))
                            $l = $element['maxLength'][$i];
                        else $l = $this->maxLength;
                        if (strlen($v) > $l)
                        {
                            $this->error();
                            if (is_array($postedValue)) $postedValue[$i] = substr($v, 0, $l);
                            else $postedValue = substr($v, 0, $l);
                        }
                    }
                    break;
                case "checkbox":
                case "radio":
                case "select":
                    foreach ((array)$postedValue as $i => $v)
                    {
                        if (!isset($element['options'][$v]))
                        {
                            $this->error();
                            if (is_array($postedValue)) unset($postedValue[$i]);
                            else $postedValue = "";
                        }
                    }
                    break;
            }
            $element['value'] = $postedValue;
            $this->_isSubmitted = true;
        }
        if ($this->magicQuotes)
        {
            $element['value'] = is_array($element['value'])
                ? array_map('addslashes', $element['value'])
                : addslashes($element['value']);
        }
        return $element['value'];
    }

    /**
     * Gets the name of the element added last.
     *
     * <i>Note: Normally, you don't need this. One of the main purposes of the
     * class is to hide these element names.</i>
     *
     * This returns the internal element names "element0", "element1" and so on.
     * Exception: {@link hidden()} may have a custom name. This may be useful to
     * access $_POST variables or to create custom JavaScript handlers.
     *
     * Use an absolute (positive values starting with 0 for the first element)
     * or relative (negative values starting with -1 for the element added last)
     * <i>offset</i> to get the name of any previously added element. Defaults
     * to -1. Returns false on error.
     *
     * @param offset int
     * @return string
     */
    function getName($offset = -1)
    {
        $id = ($offset < 0) ? (count($this->_rows) + $offset) : $offset;
        return isset($this->_rows[$id]) ? $this->_rows[$id]['name'] : false;
    }

    /**
     * Adds an additional attribute to the form or any inner input element.
     *
     * Puts the attribute or JavaScript event handler to the form element added
     * last. If no element was created yet or <i>attribute</i> is "onsubmit" the
     * attribute will be added to the form itself.
     *
     * Some useful examples:
     *
     * <pre>$form->addAttribute("onfocus", "if (this.value == '...') this.value = '';");
     * $form->addAttribute("onblur", "if (this.value == '') this.value = '...';");
     * $form->addAttribute("onclick", "this.form.submit();");
     * $form->addAttribute(
     *     "onsubmit",
     *     "if (this.elements['element0'].value == '') {
     *          alert('Please enter something!');
     *          return false;
     *      }");</pre>
     *
     * @param attribute string
     * @param value string
     * @return string
     */
    function addAttribute($attribute, $value = null)
    {
        if (!isset($value)) $value = $attribute;
        // If there are still no elements, add the attribute or handler to the form.
        if (empty($this->_rows) || strcasecmp($attribute, "onsubmit") == 0)
        {
            $a = &$this->_attributes[$attribute];
        }
        else
        {
            // Add the attribute or event handler to the form element added last.
            $a = &$this->_rows[count($this->_rows) - 1]['attributes'][$attribute];
        }
        if (empty($a)) $a = ""; else $a .= " ";
        return $a .= $value;
    }

    /**
     * Adds a class to any outer input element template.
     *
     * Use <code>{class}</code> somewhere in one of the {@link templates}.
     *
     * @param class string
     * @return string
     */
    function addClass($class)
    {
        if (empty($this->_rows)) return false;
        $a = &$this->_rows[count($this->_rows) - 1]['class'];
        if (empty($a)) $a = ""; else $a .= " ";
        return $a .= $class;
    }

    /**
     * Adds an error message to an element.
     *
     * Sets an error to the element added last by {@link checkbox()},
     * {@link file()}, {@link password()}, {@link radio()}, {@link select()},
     * {@link submit()}, {@link text()} or {@link textarea()}. Use a negative or
     * positive (starting with 0 for the first element) <i>offset</i> to add an
     * error message to any previous element. Defaults to -1.
     *
     * The label of the faulty element will be replaced by the error message (if
     * present) and displayed using another {@link templates template} (if
     * present). If error() was called one or more times, {@link isValid()} will
     * return false.
     *
     * @param message string
     * @param offset int
     * @return void
     */
    function error($message = "", $offset = -1)
    {
        $id = ($offset < 0) ? (count($this->_rows) + $offset) : $offset;
        if (!isset($this->_rows[$id])) return false;
        // If there is already an error message don't replace it.
        if ($this->_isSubmitted && empty($this->_rows[$id]['error']))
        {
            $this->_rows[$id]['error'] = $message;

            // Always reset a password element if it got an error.
            if ($this->_rows[$id]['type'] == "password")
            {
                $this->_rows[$id]['value'] = "";
            }
            // Always reset a file upload element if it got an error.
            elseif ($this->_rows[$id]['type'] == "file")
            {
                @unlink($this->_rows[$id]['value']['tmp_name']);
                $this->_rows[$id]['value'] = false;
            }
            $this->_hasErrors = true;
        }
    }

    /**
     * Checks if the form is submitted error-free.
     *
     * Returns true if the form was submitted already and no error has been set.
     * If the form is displayed the first time, isValid() always returns false.
     * If {@link error()} was called at least once it returns false too.
     *
     * @return bool
     */
    function isValid()
    {
        return $this->_isSubmitted && !$this->_hasErrors;
    }

    /**
     * Compiles the form and returns the HTML output.
     *
     * Compiles the whole form using default or user defined {@link templates}
     * and returns the resulting HTML code as a string. Example:
     *
     * <code>echo $form->toHTML();</code>
     *
     * @param bool $setFocus
     * @return string
     */
    function toHTML($setFocus = null)
    {
        $form = '<form action="';
        $form .= isset($_SERVER['PHP_SELF']) ? $_SERVER['PHP_SELF'] :
            $GLOBALS['HTTP_SERVER_VARS']['PHP_SELF'];
        if (!empty($_SERVER['QUERY_STRING']))
        {
            $form .= "?" . htmlspecialchars($_SERVER['QUERY_STRING']);
        }
        elseif (!empty($GLOBALS['HTTP_SERVER_VARS']['QUERY_STRING']))
        {
            $form .= "?" . htmlspecialchars($GLOBALS['HTTP_SERVER_VARS']['QUERY_STRING']);
        }
        if (!isset($this->anchor)) $form .= "#" . $this->id;
        elseif ($this->anchor)      $form .= "#" . $this->anchor;
        $form .= '"';
        if (!empty($this->_encType)) $form .= ' enctype="' . $this->_encType . '"';
        $form .= $this->_implodeAttributes($this->_attributes);
        $form .= ' id="' . $this->id . '" method="post">';
        $elements = "";

        reset($this->_rows);
        while (list($id, $row) = each($this->_rows))
        {
            $nameAttr = ' name="' . $row['name'] . '"';
            $idAttr = ' id="' . $row['name'] . 'i"';
            $genericAttr = $this->_implodeAttributes($row['attributes']);
            if (isset($row['help']) && is_string($row['help']))
            {
                $row['help'] = explode("\t", $row['help'], 2);
            }
            if (!empty($row['help'][0]))
            {
                $genericAttr .= ' title="' . str_replace('"', '&quot;',
                    strip_tags($row['help'][0])) . '"';
            }
            $input = '';
            switch ($row['type'])
            {
                case "header":
                    $elements .= str_replace(array("{header}", "{class}"),
                        array($row['header'], empty($row['class']) ? "" : ' class="' . $row['class'] . '"'),
                        $this->getTemplate('header', $row['name']));
                    continue 2;
                case "hidden":
                    // Dont use any templates for hidden elements; skip and continue.
                    $form .= '<input type="hidden"' . $nameAttr . ' value="' .
                        htmlspecialchars($this->magicQuotes ?
                        stripslashes($row['value']) : $row['value']) . '" />';
                    continue 2;
                case "static":
                    $input = nl2br(htmlspecialchars($row['value'])) .
                        '<input type="hidden"' . $nameAttr . ' value="' .
                        htmlspecialchars($this->magicQuotes ?
                        stripslashes($row['value']) : $row['value']) . '" />';
                    break;
                case "textarea":
                    $input = '<textarea' . $nameAttr;
                    $input .= ' cols="' . (empty($row['cols']) ? $this->size : $row['cols']) . '"';
                    $input .= ' rows="' . (empty($row['rows']) ? 3 : $row['rows']) . '"';
                    $input .= ' wrap="' . (empty($row['wrap']) ? "virtual" : $row['wrap']) . '"';
                    // Add an id for use in the label (added later).
                    $input .= $idAttr . $genericAttr . '>';
                    $input .= htmlspecialchars($this->magicQuotes ?
                        stripslashes($row['value']) : $row['value']);
                    $input .= '</textarea>';
                    break;
                case "select":
                    $input = '<select' . $nameAttr . ' size="' . $row['size'] . '"';
                    // Add an id for use in the label (added later).
                    $input .= $idAttr . $genericAttr . '>';
                    while (list($key, $value) = each($row['options']))
                    {
                        $input .= '<option value="';
                        $input .= htmlspecialchars($this->magicQuotes ?
                            stripslashes($key) : $key);
                        $input .= '"';
                        if (strcmp($key, $row['value']) == 0) $input .= ' selected="selected"';
                        $input .= '>' . str_replace("<", "&lt;", $value) . "</option>\n";
                    }
                    $input .= '</select>';
                    break;
                case "radio":
                case "checkbox":
                    $i = 0;
                    while (list($key, $value) = each($row['options']))
                    {
                        $input .= '<input type="' . $row['type'] . '" name="' . $row['name'];
                        // Name have to be an array when using multiple checkboxes.
                        if ($row['type'] == 'checkbox' && count($row['options']) > 1)
                        {
                            $input .= '[]';
                        }
                        $input .= '" value="' . htmlspecialchars(
                            $this->magicQuotes ? stripslashes($key) : $key);
                        $input .= '"';
                        // This works for a single value too; it will be casted to an array.
                        if (in_array($key, (array)$row['value']))
                        {
                            $input .= ' checked="checked"';
                        }
                        // Id attribute will be used in the label only.
                        $input .= ' id="' . $row['name'] . 'i' . $i . '"';
                        $input .= $genericAttr . ' />';
                        if ($value)
                        {
                            $input .= '<label for="' . $row['name'] . 'i' . $i . '"';
                            // Add an access key handler if there is an underlined character.
                            if (preg_match('/<u>(\w)/i', $value, $match))
                            {
                                $input .= ' accesskey="' . strtolower($match[1]) . '"';
                            }
                            $input .= ">" . $value . "</label>\n";
                        }
                        $i++;
                    }
                    break;
                case "submit":
                    while (list($i, $value) = each($row['value']))
                    {
                        $input .= '<input type="submit"' . $nameAttr;
                        // An input button may have a default value.
                        if (!empty($value))
                        {
                            $input .= ' value="' . strtr($this->magicQuotes ?
                                stripslashes($value) : $value, array('"' =>
                                '&quot;', '<' => '&lt;', '>' => '&gt;')) . '"';
                        }
                        if (count($row['value']) == 1) $input .= ' accesskey="s"';
                        $input .= $genericAttr . " />\n";
                    }
                    break;
                case "image":
                    while (list($i, $value) = each($row['value']))
                    {
                        $input .= '<input type="image"' . $nameAttr . ' src="' .
                            htmlspecialchars($this->magicQuotes ?
                            stripslashes($value) : $value) . '"' . $genericAttr
                            . " />\n";
                    }
                    break;
                case 'text':
                    $c = count($row['value']);
                    $maxes = (array)$row['maxLength'];
                    $sizes = (array)$row['size'];
                    foreach ((array)$row['value'] as $i => $value)
                    {
                        if (!isset($maxes[$i])) $maxes[$i] = $maxes[$i - 1];
                        if (!isset($sizes[$i])) $sizes[$i] = $sizes[$i - 1];
                        if ($input && isset($row['help'][1]))
                        {
                            $input .= current(array_splice($row['help'], 1, 1));
                        }
                        $input .= '<input type="' . $row['type'] . '" name="' . $row['name'];
                        // Name have to be an array on multiple text fields.
                        if ($c > 1) $input .= '[]';
                        $input .= '" value="';
                        $input .= htmlspecialchars($this->magicQuotes ?
                            stripslashes($value) : $value);
                        $input .= '" maxlength="' . ($maxes[$i] ? $maxes[$i] :
                            $this->maxLength) . '"';
                        $size = (int)round($this->size / $c);
                        // Don't make the element larger than needed, if no size is given.
                        if (!empty($maxes[$i]) && $maxes[$i] < $size)
                        {
                            $size = $maxes[$i];
                        }
                        if ($sizes[$i]) $size = $sizes[$i];
                        $input .= ' size="' . $size . '"';
                        // Add an id for use in the label (added later).
                        $input .= ' id="' . $row['name'] . 'i' . ($c > 1 ? $i : '') . '"';
                        $input .= $genericAttr . ' />';
                    }
                    break;
                default:
                    $input = '<input type="' . $row['type'] . '"' . $nameAttr;
                    // Needed because some elements did not have or allow a value.
                    if (isset($row['value']) && $row['type'] != "file")
                    {
                        $input .= ' value="';
                        $input .= htmlspecialchars($this->magicQuotes ?
                            stripslashes($row['value']) : $row['value']);
                        $input .= '"';
                    }
                    // A file upload element did not have a maximum length at all.
                    if (isset($row['maxLength']))
                    {
                        $input .= ' maxlength="' . ($row['maxLength'] ?
                            $row['maxLength'] : $this->maxLength) . '"';
                    }
                    // Use default width if it is set but zero.
                    if (isset($row['size']))
                    {
                        // The file element have its own button so make it smaller.
                        $size = $this->size - ($row['type'] == "file" ? 22 : 0);
                        // Don't make the element larger than needed, if no size is given.
                        if (!empty($row['maxLength']) && $row['maxLength'] < $size)
                        {
                            $size = $row['maxLength'];
                        }
                        if ($row['size']) $size = $row['size'];
                        // Make the field a lot smaller if it already contains a file.
                        if ($row['type'] == "file" && !empty($row['value'])) $size = 5;
                        $input .= ' size="' . $size . '"';
                    }
                    // Add an id for use in the label (added later).
                    $input .= $idAttr . $genericAttr . ' />';

                    // Hidden element with meta data of files already uploaded.
                    if ($row['type'] == "file" && !empty($row['value']))
                    {
                        // Display the file name already submitted before.
                        $name = $row['value']['name'];
                        $length = max($this->size - 32, 12);
                        if (strlen($name) > $length)
                        {
                            $name = substr($name, 0, $length) . '...';
                        }
                        $input = $name . ' ' . $input;
                        $input .= '<input type="hidden" name="' . $row['name'] . 'h"';
                        // Serialized array contains: tmp_name, name, type, size.
                        $input .= ' value="' .
                            htmlspecialchars(serialize($row['value'])) . '" />';
                    }
            }

            if (!empty($row['help'][1])) $input .= $row['help'][1];
            $element = str_replace("{input}", $input, $this->getTemplate('input', $row['name']));

            // If there is no {error} tag in the template but an error.
            if (isset($row['error']) && strpos($element, "{error}") === false)
            {
                // Save the labels access key, if the error message have no own.
                if (!stristr($row['error'], "<u>") &&
                    preg_match('/<u>(\w)/i', @$row['label'], $match))
                {
                    $row['error'] = preg_replace('/' . $match[1] . '/i',
                        '<u>\0</u>', $row['error'], 1);
                }
                // Make the label an error if no other error message was given.
                if (empty($row['error'])) $row['error'] = @$row['label'];
                $row['label'] = "";
            }

            // If there is no <label> tag in this element but an id, add one.
            if (!(empty($row['label']) && empty($row['error'])) &&
                strpos($input, "<label") === false &&
                preg_match('/id="([^"]+)/', $input, $mId))
            {
                $labelTag = '<label';
                // Add an accesskey handler if there is an underlined character.
                if (preg_match('/<u>(\w)/i', @$row['label'] . @$row['error'], $mKey))
                {
                    $labelTag .= ' accesskey="' . strtolower($mKey[1]) . '"';
                }
                $labelTag .= ' for="' . $mId[1] . '">';
                $element = str_replace("{label}", $labelTag . "{label}</label>",
                    $element);
            }

            if (isset($row['error']) && strpos($element, "{error}") === false)
            {
                $element = str_replace("{label}", "{error}", $element);
            }

            // Load all the tiny sub templates if needed and if exists.
            if (!empty($row['label']) && $this->getTemplate('label', $row['name']))
            {
                $element = str_replace("{label}", $this->getTemplate('label', $row['name']),
                    $element);
            }
            if (!empty($row['error']) && $this->getTemplate('error', $row['name']))
            {
                $element = str_replace("{error}", $this->getTemplate('error', $row['name']),
                    $element);
            }
            if (!empty($row['help'][0]) && $this->getTemplate('help', $row['name']))
            {
                $element = str_replace("{help}", $this->getTemplate('help', $row['name']),
                    $element);
            }

            $elements .= str_replace(
                array("{label}", "{error}", "{help}", "{class}"),
                array(@$row['label'], @$row['error'], @$row['help'][0],
                empty($row['class']) ? "" : ' class="' . $row['class'] . '"'),
                $element);
        }

        // Do a global search and replace on underlined accesskey characters.
        if ($this->getTemplate('accesskey'))
        {
            $replacement = str_replace("{accesskey}", '\1', $this->getTemplate('accesskey'));
            $elements = preg_replace('/<u>(\w)<\/u>/i', $replacement, $elements);
        }

        $form .= str_replace("{input}", $elements, $this->getTemplate('form'));
        $form .= '</form>';
        if ((!isset($setFocus) && $this->id == "form") || !empty($setFocus))
        {
            $form .= $this->_getFocusHandler();
        }
        // Returns the whole compiled web form.
        return $form;
    }

    /**
     * Internal method which returns one of the templates.
     *
     * This is an interface you can replace using an extended class to return
     * more than one different <code>'input'</code> {@link templates template}
     * for example.
     *
     * @param template string One of the templates names.
     * @param name string Element name currently rendered.
     * @return string
     */
    function getTemplate($template, $name = null)
    {
        return empty($this->templates[$template]) ? false : $this->templates[$template];
    }

    /**
     * @param attributes array
     * @return string
     * @access private
     */
    function _implodeAttributes(&$attributes)
    {
        if (empty($attributes)) return "";
        $html = "";
        foreach ($attributes as $attribute => $value)
        {
            // Compile the HTML output of this attribute or JavaScript handler.
            $html .= ' ' . $attribute . '="' . str_replace('"', '&quot;', $value) . '"';
        }
        return $html;
    }

    /**
     * @return string
     * @access private
     */
    function _getFocusHandler()
    {
        $form = "";
        $id = null;
        $count = count($this->_rows);
        for ($i = 0; $i < $count; $i++)
        {
            // Search for the first text element. Focus makes only sense there.
            if ($this->_rows[$i]['type'] != 'text' &&
                $this->_rows[$i]['type'] != 'password' &&
                $this->_rows[$i]['type'] != 'textarea') continue;
            if (!isset($id)) $id = $i;
            // Search for the first text element containing an error.
            if (!empty($this->_rows[$i]['error'])) { $id = $i; break; }
        }
        if (isset($id))
        {
            // Set the focus on the first element using JavaScript.
            $form = "<script type=\"text/javascript\">\nself.onload=";
            $form .= "function(){var f=document.forms['" . $this->id . "'];";
            $form .= "if(f){var e=f.elements['" . $this->_rows[$id]['name'];
            // Handle multiple text elements too.
            $form .= count($this->_rows[$id]['value']) > 1 ? "[]'][0" : "'";
            $form .= "];if(e&&e.focus)e.focus();}}\n</script>";
        }
        return $form;
    }

    /**
     * Outputs the form.
     *
     * Displays the whole compiled form. Set <i>setFocus</i> to disable or
     * enable the auto-focus feature (enabled by default if no id was set for
     * the form, disabled otherwise). A slightly complex example:
     *
     * <pre><?php
     * require_once("Apeform.class.php");
     * $form = new Apeform();
     * $text = $form->text("Something");
     * if (!$text) $form->error("Please enter something");
     * $form->submit();
     * if ($form->isValid()) echo "Thank you!";
     * else $form->display();
     * ?></pre>
     *
     * @param bool $setFocus
     * @return void
     */
    function display($setFocus = null)
    {
        echo $this->toHTML($setFocus);
    }
}