<?php
/**
 * HTML_FormPersister: in-place "human-expectable" form tags post-processing.
 * (C) 2005 Dmitry Koterov, http://forum.dklab.ru/users/DmitryKoterov/
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * See http://www.gnu.org/copyleft/lesser.html

 * Modify HTML-forms adding "value=..." fields to <input> tags according 
 * to STANDARD PHP $_GET and $_POST variable. Also supported <select> and 
 * <textarea>.
 *
 * The simplest example:
 *
 * <?
 *   require_once 'HTML/FormPersister.php'; 
 *   ob_start(array('HTML_FormPersister', 'ob_formpersisterhandler'));
 * ? >  <!-- please remove space after "?" while testing -->
 * <form>
 *   <input type="text" name="simple" default="Enter your name">
 *   <input type="text" name="second[a][b]" default="Something">
 *   <select name="sel">
 *     <option value="1">first</option>
 *     <option value="2">second</option>
 *   </select>
 *   <input type="submit">
 * </form>
 *
 * Clicking the submit button, you see that values of text fields and 
 * selected element in list remain unchanged - the same as you entered before 
 * submitting the form! 
 *
 * The same method also works with <select multiple>, checkboxes etc. You do 
 * not need anymore to write "value=..." or "if (...) echo "selected" 
 * manually in your scripts, nor use dynamic form-field generators confusing
 * your HTML designer. Everything is done automatically based on $_GET and 
 * $_POST arrays.
 *
 * Form fields parser is based on fast HTML_SemiParser library, which 
 * performes incomplete HTML parsing searching for only needed tags. On most 
 * sites (especially XHTML) it is fully acceptable. Parser is fast: if
 * there are no one form elements in the page, it returns immediately, don't
 * ever think about overhead costs of parsing.
 *
 * @author Dmitry Koterov 
 * @version 1.112
 * @package HTML 
 */
require_once 'HTML/SemiParser.php';
 
class 
HTML_FormPersister extends HTML_SemiParser 
{
    
/**
     * Constructor. Create new FormPersister instance.
     */
    
function HTML_FormPersister()
    {
        
$this->HTML_SemiParser();
    }

    
/**
     * Process HTML text.
     *
     * @param string $st  Input HTML text.
     * @return HTML text with all substitutions.
     */
    
function process($st)
    {
        
$this->fp_autoindexes = array();
        return 
HTML_SemiParser::process($st);
    } 

    
/**
     * Static handler for ob_start().
     *
     * Usage:
     *   ob_start(array('HTML_FormPersister', 'ob_formpersisterhandler'));
     *
     * Of course you may not use OB handling but call process() manually
     * in your scripts.
     *
     * @param string $html  Input HTML text.
     * @return processed output with all form fields modified.
     */
    
function ob_formPersisterHandler($st)
    {
        
$fp = new HTML_FormPersister();
        
$r $fp->process($st);
        return 
$r;
    } 


    
/**
     * Tag and container callback handlers.
     * See usage of HTML_SemiParser.
     */

    /**
     * <FORM> tag handler (add default action attribute).
     * See HTML_SemiParser.
     */
    
function tag_form($attr)
    {
        if (isset(
$attr['action'])) return;
        if (isset(
$_SERVER['REQUEST_URI'])) {
            if (
strtolower(@$attr['method']) == 'get') {
                
$attr['action'] = preg_replace('/\?.*/s'''$_SERVER['REQUEST_URI']);
            } else { 
                
$attr['action'] = $_SERVER['REQUEST_URI'];
            }
        } else {
            
$attr['action'] = "";
        }
        return 
$attr;
    }
    
    
/**
     * <INPUT> tag handler.
     * See HTML_SemiParser.
     */
    
function tag_input($attr)
    {
        static 
$uid 0;
        switch (@
strtolower($attr['type'])) {
            case 
'radio':
                if (!isset(
$attr['name'])) return;
                if (isset(
$attr['checked']) || !isset($attr['value'])) return;
                if (
$attr['value'] == $this->getCurValue($attr)) $attr['checked'] = 'checked';
                else unSet(
$attr['checked']);
                break;
            case 
'checkbox':
                if (!isset(
$attr['name'])) return;
                if (isset(
$attr['checked'])) return;
                if (!isset(
$attr['value'])) $attr['value'] = 'on';
                if (
$this->getCurValue($attrtrue)) $attr['checked'] = 'checked';
                break;
            case 
'image':
            case 
'submit':
                if (isset(
$attr['confirm'])) {
                    
$attr['onclick'] = 'return confirm("' $attr['confirm'] . '")';
                    unSet(
$attr['confirm']);
                } 
                break;
            case 
'text': case 'password': case 'hidden': case '':
            default:
                if (!isset(
$attr['name'])) return;
                if (!isset(
$attr['value']))
                    
$attr['value'] = $this->getCurValue($attr);
                break;
        }
        
// Handle label pseudo-attribute. Button is placed RIGHTER
        // than the text if label text ends with "^". Example:
        // <input type=checkbox label="hello">   ==>  [x]hello
        // <input type=checkbox label="hello^">  ==>  hello[x]
        
if (isset($attr['label'])) {
            
$text $attr['label'];
            if (!isset(
$attr['id'])) $attr['id'] = 'FPlab' . ($uid++);
            
$right 1;
            if (
$text[strlen($text)-1] == '^') {
                
$right 0;
                
$text substr($text0, -1);
            } 
            unSet(
$attr['label']);
            
$attr[$right'_right' '_left'] = '<label for="'.$this->quoteHandler($attr['id']).'">' $text '</label>';
        }
        
// We CANNOT return $orig_attr['_orig'] if attributes are not modified,
        // because we know nothing about following handlers. They may need
        // the parsed attributes, not a plain text.
        
unset($attr['default']);
        return 
$attr;
    } 

    
/**
     * <TEXTAREA> tag handler.
     * See HTML_SemiParser.
     */
    
function container_textarea($attr)
    {
        if (
trim($attr['_text']) == '') {
            
$attr['_text'] = $this->quoteHandler($this->getCurValue($attr));
        }
        unset(
$attr['default']);
        return 
$attr;
    } 

    
/**
     * <SELECT> tag handler.
     * See HTML_SemiParser.
     */
    
function container_select($attr)
    { 
        if (!isset(
$attr['name'])) return;
        
        
// Multiple lists MUST contain [] in the name.
        
if (isset($attr['multiple']) && strpos($attr['name'], '[]') === false) {
            
$attr['name'] .= '[]';
        }

        
$curVal $this->getCurValue($attr);
        
$body "";

        
// Get some options from variable?
        // All the text outside <option>...</option> container are treated as variable name.
        // E.g.: <select...> <option>...</option> ... some[global][options] ... <option>...</option> ... </select>
        
$attr['_text'] = preg_replace_callback('{
                (
                    (?:^ | </option> | </optgroup> | <optgroup[^>]*>) 
                    \s*
                )
                \$?
                ( [^<>\s]+ ) # variable name
                (?=
                    \s*
                    (?:$ | <option[\s>] | <optgroup[\s>] | </optgroup>) 
                )
            }six'

            array(&
$this'_optionsFromVar_callback'), 
            
$attr['_text']
        );
        
        
// Parse options, fetch its values and save them to array.
        // Also determine if we have at least one selected option.
        
$body $attr['_text'];
        
$parts preg_split("/<option\s*({$this->sp_reTagIn})>/si"$body, -1PREG_SPLIT_DELIM_CAPTURE); 
        
$hasSelected 0;
        for (
$i 1$n count($parts); $i $n$i += 2) {
            
$opt = array();
            
$this->parseAttrib($parts[$i], $opt);
            if (isset(
$opt['value'])) {
                
$value $opt['value'];
            } else {
                
// Option without value: spaces are shrinked (experimented on IE).
                
$text preg_replace('{</?(option|optgroup)[^>]*>.*}si'''$parts[$i 1]);
                
$value trim($text);
                
$value preg_replace('/\s\s+/'' '$value);
                if (
strpos($value'&') !== false) {
                    
$value $this->_unhtmlspecialchars($value);
                }
            }
            if (isset(
$opt['selected'])) $hasSelected++;
            
$parts[$i] = array($opt$value);
        }

        
// Modify options list - add selected attribute if needed, but ONLY
        // if we do not already have selected options!
        
if (!$hasSelected) {
            foreach (
$parts as $i=>$parsed) {
                if (!
is_array($parsed)) continue;
                list (
$opt$value) = $parsed;
                if (isset(
$attr['multiple'])) {
                    
// Inherit some <select> attributes.
                    
if ($this->getCurValue($opt $attr + array('value'=>$value), true)) { // merge
                        
$opt['selected'] = 'selected';
                    }
                } else {
                    if (
$curVal == $value) {
                        
$opt['selected'] = 'selected';
                    }
                }
                
$opt['_tagName'] = 'option';
                
$parts[$i] = $this->makeTag($opt);
            }
            
$body join(''$parts);
        }
 
        
$attr['_text'] = $body;
        unset(
$attr['default']);
        return 
$attr;
    }

    
/**
     * Other methods.
     */

    /**
     * Create set of <option> tags from array.
     */
    
function makeOptions($options$curId false)
    {
        
$body '';
        foreach (
$options as $k=>$text) {
            if (
is_array($text)) {
                
// option group
                
$options '';
                foreach (
$text as $ko=>$v) {
                    
$opt = array('_tagName'=>'option''value'=>$ko'_text'=>$this->quoteHandler(strval($v)));
                    if (
$curId !== false && strval($curId) === strval($ko)) {
                        
$opt['selected'] = "selected";
                    }
                    
$options .= HTML_SemiParser::makeTag($opt);
                }
                
$grp = array('_tagName'=>'optgroup''label'=>$k'_text'=>$options);
                
$body .= HTML_SemiParser::makeTag($grp);
            } else {
                
// single option
                
$opt = array('_tagName'=>'option''value'=>$k'_text'=>$this->quoteHandler($text));
                if (
$curId !== false && strval($curId) === strval($k)) {
                    
$opt['selected'] = "selected";
                }
                
$body .= HTML_SemiParser::makeTag($opt);
            } 
        }
        return 
$body;
    }

    
/**
     * Value extractor.
     *
     * Try to find corresponding entry in $_POST, $_GET etc. for tag 
     * with name attribute $attr['name']. Support complex form names
     * like 'fiels[one][two]', 'field[]' etc.
     *
     * If $isBoolean is set, always return true or false. Used for 
     * checkboxes and multiple selects (names usually trailed with "[]",
     * but may not be trailed too).
     *
     * @return Current "value" of specified tag.
     */
    
function getCurValue($attr$isBoolean false)
    {
        
$name = @$attr['name'];
        if (
$name === null) return null
        
$isArrayLike false// boolean AND contain [] in the name
        // Handle boolean fields.
        
if ($isBoolean && false !== ($p strpos($name'[]'))) {
            
$isArrayLike true;
            
$name substr($name0$p) . substr($name$p 2);
        } 
        
// Search for value in ALL arrays,
        // EXCEPT $_REQUEST, because it also holds Cookies!
        
$fromForm true;
        if (
false !== ($v $this->_deepFetch($_POST$name$this->fp_autoindexes[$name]))) $value $v;
        elseif (
false !== ($v $this->_deepFetch($_GET$name$this->fp_autoindexes[$name]))) $value $v;
        elseif (isset(
$attr['default'])) {
            
$value $attr['default'];
            if (
$isBoolean) return $value !== '' && $value !== "0";
            
// For array fields it is possible to enumerate all the
            // values in SCALAR using ';'.
            
if ($isArrayLike && !is_array($value)) $value explode(';'$value);
            
$fromForm false;
        } else {
           
// If no data is found in GET, POST etc., always return false in boolean mode.
           
if ($isBoolean) return false;
           
$value '';
        }
        if (
$fromForm) {
            
// Remove slashes on stupid magic_quotes_gpc mode.
            // TODO: handle nested arrays too!
            
if (is_scalar($value) && get_magic_quotes_gpc() && !@constant('MAGIC_QUOTES_GPC_DISABLED')) { 
                
$value stripslashes($value);
            }
        }
        
// Return value depending on field type.
        
$attrValue strval(isset($attr['value'])? $attr['value'] : 'on');
        if (
$isArrayLike) {
            
// Array-like field? If present, return true.
            
if (!is_array($value)) return false;
            
// Unfortunately we MUST use strict mode in in_array()
            // and cast array values to string before checking.
            // This is because in_array(0, array('one')) === true.
            
return in_array(strval($attrValue), array_map('strval'$value), true);
        } else {
            if (
$isBoolean) {
                
// Non-array boolean elements must be boolean-equal to match, OR
                // GET-POST-... values must be === true. Why? Because the followind
                // checkboxes must be turned on:
                // - $_POST['a'] = true; ... <input type="checkbox" name="a" value="x"> 
                // - $_POST['b'] = ""; ...   <input type="checkbox" name="b" value=""> 
                
return (bool)@strval($value) === (bool)$attrValue;
            } else {
                
// This is not boolean nor array field. Return it now.
                
return @strval($value);
            }
        } 
    } 

    
/**
     * Fetch an element of $arr array using "complex" key $name.
     *
     * $name can be in form of "zzz[aaa][bbb]", 
     * it means $arr[zzz][aaa][bbb].
     *
     * If $name contain auto-indexed parts (e.g. a[b][]), replace
     * it by corresponding indexes.
     * 
     * $name may be scalar name or array (already splitted name,
     * see _splitMultiArray() method).
     * 
     * @param array &$arr          Array to fetch from.
     * @param mixed &$name         Complex form-field name.
     * @param array &$autoindexes  Container to hold auto-indexes
     * @return found value, or false if $name is not found.
     */
    
function _deepFetch(&$arr, &$name, &$autoindexes// static
    
{
        if (
is_scalar($name) && strpos($name'[') === false) {
            
// Fast fetch.            
            
return isset($arr[$name])? $arr[$name] : false;
        }
        
// Else search into deep.
        
$parts HTML_FormPersister::_splitMultiArray($name);
        
$leftPrefix '';
        foreach (
$parts as $i=>$k) {
            if (!
strlen($k)) {
                
// Perform auto-indexing.
                
if (!isset($autoindexes[$leftPrefix])) $autoindexes[$leftPrefix] = 0;
                
$parts[$i] = $k $autoindexes[$leftPrefix]++;
            }
            if (!
is_array($arr)) {
                
// Current container is not array.
                
return false;
            }
            if (!
array_key_exists($k$arr)) {
                
// No such element.
                
return false;
            }
            
$arr = &$arr[$k];
            
$leftPrefix strlen($leftPrefix)? $leftPrefix "[$k]" $k;
        }
        if (!
is_scalar($name)) {
            
$name $parts;
        } else {
            
$name $leftPrefix;
        }
        return 
$arr;
    } 

    
/**
     * Highly internal function. Must be re-written if some new
     * version of would support syntax like "zzz['aaa']['b\'b']" etc.
     * For "zzz[aaa][bbb]" returns array(zzz, aaa, bbb).
     */
    
function _splitMultiArray($name// static
    
{
        if (
is_array($name)) return $name;
        if (
strpos($name'[') === false) return array($name);
        
$regs null;
        
preg_match_all('/ ( ^[^[]+ | \[ .*? \] ) (?= \[ | $) /xs'$name$regs);
        
$arr = array();
        foreach (
$regs[0] as $s) {
            if (
$s[0] == '['$arr[] = substr($s1, -1);
            else 
$arr[] = $s;
        } 
        return 
$arr;
    }

    
/**
     * Callback function to replace variables in <select> body by set of options.
     */
    
function _optionsFromVar_callback($p)
    {
        
$dummy = array();
        
$name trim($p[2]);
        
$options $this->_deepFetch($GLOBALS$name$dummy);
        if (
$options === null || $options === false) return $p[1] . "<option>???</option>";
        return 
$p[1] . $this->makeOptions($options);
    }
}