<?php
// +----------------------------------------------------------------------+
// | PHP version 4                                                        |
// +----------------------------------------------------------------------+
// | Copyright (c) 1997-2004 The PHP Group                                |
// +----------------------------------------------------------------------+
// | This source file is subject to version 3.0 of the PHP license,       |
// | that is bundled with this package in the file LICENSE, and is        |
// | available through the world-wide-web at the following url:           |
// | http://www.php.net/license/3_0.txt.                                  |
// | If you did not receive a copy of the PHP license and are unable to   |
// | obtain it through the world-wide-web, please send a note to          |
// | license@php.net so we can mail you a copy immediately.               |
// +----------------------------------------------------------------------+
// | Authors: Dmitry Koterov <pear at koterov dot ru>                     |
// +----------------------------------------------------------------------+
// $Id: FormPersister.php,v 1.3 2005/04/13 07:50:53 koterov Exp $

require_once 'HTML/SemiParser.php';

/**
 * 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 $Revision: 1.2 $
 * @package HTML 
 */
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_squareCount = 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;
        
$attr['action'] = $_SERVER['SCRIPT_NAME'];
        return 
$attr;
    }
    
    
/**
     * <INPUT> tag handler.
     * See HTML_SemiParser.
     */
    
function tag_input($attr)
    {
        static 
$uid 0;
        
$orig_attr $attr;
        switch (
$type = @strtolower($attr['type'])) {
            case 
'text': case 'password': case 'hidden': case '':
                if (!isSet(
$attr['name'])) return;
                if (!isSet(
$attr['value']))
                    
$attr['value'] = $this->getCurValue($attr);
                break;
            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 (
$this->getCurValue($attrtrue)) $attr['checked'] = 'checked';
                break;
            case 
'submit':
                if (isSet(
$attr['confirm'])) {
                    
$attr['onclick'] = 'return confirm("' $attr['confirm'] . '")';
                    unSet(
$attr['confirm']);
                } 
                break;
            default:
                return;
        }
        
// 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++);
            if (
$text[strlen($text)-1] == '^') {
                
$right 1;
                
$text substr($text0, -1);
            } 
            unSet(
$attr['label']);
            
$attr = array(
                
'_tagName' => 'label',
                
'_text'    => @$right$text $this->makeTag($attr) : $this->makeTag($attr) . $text,
                
'for'      => $attr['id'],
            );
        } 
        return 
$orig_attr===$attr$orig_attr['_orig'] : $attr;
    } 

    
/**
     * <TEXTAREA> tag handler.
     * See HTML_SemiParser.
     */
    
function container_textarea($attr)
    {
        if (
trim($attr['_text']) == '')
            
$attr['_text'] = htmlspecialchars($this->getCurValue($attr));
        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 options from variable?
        // <select...> some[global][options] </select>
        
if (preg_match('/^\s*\$?([^<]+?)\s*$/sx'$attr['_text'], $p)) {
            
$options $this->_deepFetch($GLOBALS$p[1]);
            foreach (
$options as $k=>$text) {
                if (
is_array($text)) {
                    
// option group
                    
$options '';
                    foreach (
$text as $k=>$v) {
                        
$opt = array('_tagName'=>'option''value'=>$k'_text'=>htmlspecialchars(strval($v)));
                        
$options .= $this->makeTag($opt);
                    }
                    
$grp = array('_tagName'=>'optgroup''label'=>$k'_text'=>$options);
                    
$body .= $this->makeTag($grp);
                } else {
                    
// single option
                    
$opt = array('_tagName'=>'option''value'=>$k'_text'=>$text);
                    
$body .= $this->makeTag($opt);
                } 
            }
            
$attr['_text'] = $body;
            
$body '';
        }
        
        
// Now parse options.
        
$parts preg_split("/<option\s*({$this->sp_reTagIn})>/si"$attr['_text'], -1PREG_SPLIT_DELIM_CAPTURE); 
        for (
$i 1$n count($parts); $i $n$i += 2) {
            
$opt = array();
            
$this->parseAttrib($parts[$i], $opt);
            
$text preg_replace('{</?(option|optgroup)[^>]*>.*}si'''$parts[$i 1]);
            
// Option without value: spaces are shrinked (experimented on IE).
            
if (!isset($opt['value'])) {
                
$value trim($text);
                
$value preg_replace('/\s\s+/'' '$value);
                if (
strpos($value'&') !== false)
                    
$value strtr($value$this->trans);
            } else {
                
$value $opt['value'];
            }
            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;
        return 
$attr;
    }

    
/**
     * Other methods.
     */

    /**
     * Make tag or container. Also clean some non-existed attributes 
     * (like "default").
     *
     * @param array $attr  Tag attributes (see HTML_SemiParser::makeTag)
     * @return Text representation of tag or container.
     */
    
function makeTag($attr)
    { 
        unSet(
$attr['default']);
        return 
HTML_SemiParser::makeTag($attr);
    } 

    
/**
     * 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.
     *
     * @return Current "value" of specified tag.
     */
    
function getCurValue($attr$isBoolean false)
    {
        
$name $attr['name']; 
        
// Handle many fields like <input type=TEXT name=txt[]>.
        // We need to READ it sequentially.
        
if (($p strpos($name'[]')) !== false) {
            if (!
$isBoolean) {
                if (!@
$this->fp_squareCount[$name]) $this->fp_squareCount[$name] = 0;
                
$name substr($name0$p) . '[' $this->fp_squareCount[$name] . ']' substr($name$p 2);
                
$this->fp_squareCount[$attr['name']]++;
            } else {
                
$name substr($name0$p) . substr($name$p 2);
            } 
        } 
        
// Search for value in ALL arrays,
        // EXCEPT $_REQUEST, because it also holds Cookies!
        
$fromForm true;
        if ((
$v $this->_deepFetch($_POST$name)) !== false$value $v;
        elseif ((
$v $this->_deepFetch($_GET$name)) !== false$value $v;
        elseif (isSet(
$attr['default'])) {
            
$value $attr['default'];
            if (
$isBoolean) return $value !== '';
            
$fromForm false;
        } else {
           
$value '';
        }
        if (
$fromForm) {
            
// Remove slashes on stupid magic_quotes_gpc mode.
            // FIXME: handle arrays too!
            
if (is_scalar($value) && ini_get('magic_quotes_gpc')) 
                
$value stripslashes($value);
        } 
        
// Array-like field?
        
if (strpos($attr['name'], '[]') === strlen($name)) {
            
// For array fields it is possible to enumerate all the
            // values in SCALAR using ';'.
            
if (!is_array($value)) $value explode(';'$value); 
            
// If present, returns OK.
            
return in_array($attr['value'], $value);
        } else {
            
// This is not an 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].
     *
     * @param array &$arr   Array to fetch from.
     * @param string $name  Complex form-field name.
     * @return found value, or false if $name is not found.
     */
    
function _deepFetch(&$arr$name// static
    
{
        
// Fast fetch.
        
if (strpos($name'[') === false) {
            return isSet(
$arr[$name])? $arr[$name] : false;
        } 
        
// Else search into deep.
        
$parts $this->_splitMultiArray($name);
        foreach (
$parts as $k) {
            if (!
is_array($arr)) return $arr;
            if (!isSet(
$arr[$k])) return false;
            
$arr = &$arr[$k];
        } 
        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;
        
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;
    } 

?>