PHP utility collection with hybrid and fluent APIs.

βŒˆβŒ‹ βŽ‡ branch:  hybrid7 libraries


Artifact [e27a9ca464]

Artifact e27a9ca464fc6c4a66369d23c748151d17d9f490:

  • File input.php — part of check-in [037bab0b64] at 2015-02-16 20:15:17 on branch trunk — Add ArrayObject compatiblity (arrayExchange, getArrayCopy). (user: mario size: 36550)

<?php
 /**
  * api: php
  * title: Input $_REQUEST wrappers
  * type: interface
  * description: Encapsulates input variable superglobals for easy sanitization access
  * version: 2.8
  * revision: $Id$
  * license: Public Domain
  * depends: php:filter, php >5.0, html_purifier
  * config: <const name="INPUT_DIRECT" type="multi" value="disallow" multi="disallow|raw|log" description="filter method for direct $_REQUEST[var] access" />
  *         <const name="INPUT_QUIET" type="bool" value="0" multi="0=report all|1=no notices|2=no warnings" description="suppress access and behaviour notices" />
  * throws: E_USER_NOTICE, E_USER_WARNING, OutOfBoundsException
  *
  * Using these object wrappers ensures a single entry point and verification
  * spot for all user data. They wrap all superglobals and filter HTTP input
  * through sanitizing methods. For auditing casual and unverified access.
  *
  * Standardly they wrap: $_REQUEST, $_GET, $_POST, $_SERVER, $_COOKIE
  * And provide convenient access to filtered data:
  *   $_GET->int("page_num")                      // method
  *   $_POST->text["commentfield"]                // array syntax
  *   $_REQUEST->text->ascii->strtolower["xyz"]   // filter chains
  *
  * Available filter methods are:
  *   ->int
  *   ->float
  *   ->boolean
  *   ->name
  *   ->id
  *   ->words
  *   ->text
  *   ->ascii
  *   ->nocontrol
  *   ->spaces
  *   ->q
  *   ->escape
  *   ->regex
  *   ->in_array
  *   ->html
  *   ->purify
  *   ->json
  *   ->length
  *   ->range
  *   ->default
  * And most useful methods of the php filter extension.
  *   ->email
  *   ->url ->uri ->http
  *   ->ip
  *   ->ipv4->public
  * PHP string modification functions:
  *   ->urlencode
  *   ->strip_tags
  *   ->htmlentities
  *   ->strtolower
  * Automatic filter chain handler:
  *   ->array
  * Fetch multiple variables at once:
  *   ->list["var1,key,name"]
  * Process multiple entries as array:
  *   ->multi->http_build_query["id,token"]
  * Possible but should be used very consciously:
  *   ->xss
  *   ->sql
  *   ->mysql
  *   ->log
  *   ->raw
  *
  * You can also pre-define a standard filter-chain for all following calls:
  *   $_GET->nocontrol->iconv->utf7->xss->always();
  *
  * Using $__rules[] a set of filter rules can be preset per variable name.
  *
  * Parameterized filters can alternatively use the ellipsis … symbol (AltGr+:)
  * instead of the terminating method access syntax.
  *   $_GET->int->range…0…59["minutes"]
  *
  * Some filters are a mixture of sanitizing and validation. Basically
  * all can also be used independently of the superglobals with their
  * underscore name, $str = input::_text($str);
  *
  * For the superglobals it's also possible to count($_GET); or check with
  * just $_POST() if there are contents. (Use this in lieu of empty() test.)
  * There are also the three functions ->has ->no ->keys() for array lookup.
  *
  * Defining new filters can be done as methods, or since those are picked
  * up too, as plain global functions. It's also possible to assign function
  * aliases or attach closures:
  *   $_POST->_newfilter = function($s) { return modified($s); }
  * Note that the assigned filter name must have an underscore prefixed.
  *
  * --
  *
  * Input validation of course is no substitute for secure application logic,
  * parameterized sql and proper output encoding. But this methodology is a
  * good base and streamlines input data handling.
  *
  * Filter methods can be bypassed in any number of ways. There's no effort
  * made at prevention here. But it's recommended to simply use ->raw() when
  * needed - not all input can be filtered anyway. This way an audit handler
  * could always attach there - when desired.
  *
  * The goal is not to coerce, but encourage security via API *simplicity*.
  *
  */


/**
 * Project-specific identifiers.
 *
 * Since `input` is an overly generic name, one might wish to wrap it up if
 * there's an identifier conflict. (Remember; namespaces aren't taxonomies.)
 *
 */
#namespace filter;
#use \ArrayObject, \Countable, \Iterator, \OutOfBoundsException;
#use \HTMLPurifier;


/**
 * Handler name for direct $_REQUEST["array"] access.
 *
 *   "raw" = reports with warning,
 *   "disallow" = throws an exception
 */
defined("INPUT_DIRECT") or
define("INPUT_DIRECT", "raw");

/**
 * Notice suppression.
 *
 *   0 = report all,
 *   1 = no notices,
 *   2 = ignore non-existant filter
 */
defined("INPUT_QUIET") or
define("INPUT_QUIET", 0);


/**
 * @class Request variable input wrapper.
 *
 * The methods are defined with underscore prefix, but supposed to be used without
 * when invoked on the superglobal arrays:
 *
 * @method int   int[$field] converts input into integer
 * @method float float[$field]
 * @method name  name[$field] removes any non-alphanumeric characters
 * @method id    id[$field] alphanumeric string with dots
 * @method text  text[$field] textual data with interpunction
 *
 */  
class input implements ArrayAccess, Countable, Iterator {



    /**
     * Data filtering functions.
     *
     * These methods are usually not to be called directly. Instead use
     * $_GET->filtername["varname"] syntax without preceeding underscore
     * to access variable content.
     *
     * Categories: [e]=escape, [w]=whitelist, [b]=blacklist, [s]=sanitize, [v]=validate
     *
     */


    
    /**
     * @type    cast
     * @sample  22
     *
     * Integer.
     *
     */
    function _int($data) {
        return (int)$data;
    }

    /**
     * @type    cast
     * @sample  3.14159
     *
     * Float.
     *
     */
    function _float($data) {
        return (float)$data;
    }
    
    /**
     * @type    white, strip
     * @sample  "_foo123"
     *
     * Alphanumeric strings.
     * (e.g. var names, letters and numbers, may contain international letters)
     *
     */
    function _name($data) {
        return preg_replace("/\W+/u", "", $data);
    }

    /**
     * @type    white, strip
     * @sample  "xvar.1_2.x"
     *
     * Identifiers with underscores and dots,
     *
     */
    function _id($data) {
        return preg_replace("#(^[^a-z_]+)|[^\w\d_.]+|([^\w_]$)#i", "", $data);
    }

    /**
     * @type    white, strip
     * @sample  "tag-name-123"
     *
     * Alphanumeric strings with dashes. Commonly used for slugs.
     * Consecutive dashes and underscores are compacted.
     *
     */
    function _slug($data) {
        return preg_replace("/[^\w-]+|^[^a-z]+|[^\w]+$|(?<=[-_])[-_]+/", "", strtolower($s));
    }
    
    /**
     * @type    white, strip
     * @sample  "Hello - World. Foo + 3, Bar."
     * 
     * Flow text with whitespace,
     * with minimal interpunction allowed (optional parameter).
     *
     */
    function _words($data, $extra="") {
        return preg_replace("/[^\w\d\s,._\-+$extra]+/u", " ", strip_tags($data));
    }

    /**
     * [w]
     * Human-readable text with many special/interpunction characters:
     *  " and ' allowed, but no <, > or \
     *
     */
    function _text($data) {
        return preg_replace("/[^\w\d\s,._\-+?!;:\"\'\/`Β΄()*=#@]+/u", " ", strip_tags($data));
    }
    
    /**
     * Acceptable filename characters.
     *
     * Alphanumerics and dot (but not as first character).
     * You should use `->basename` as primary filter anyway.
     *
     * @t whitelist
     *
     */
    function _filename($data) {
        return preg_replace("/^[.\s]|[^\w._+-]/", "_", $data);
    }
    
    
    /**
     * [b]
     * Filter non-ascii text out.
     * Does not remove control characters. (Similar to FILTER_FLAG_STRIP_HIGH.)
     * 
     */
    function _ascii($data) {
        return preg_replace("/[\\200-\\377]+/", "", $data);
    }

    /**
     * [b]
     * Remove control characters. (Similar to FILTER_FLAG_STRIP_LOW.)
     * 
     */
    function _nocontrol($data) {
        return preg_replace("/[\\000-\\010\\013\\014\\016-\\037\\177\\377]+/", "", $data); // all except \r \n \t
    }
    
    /**
     * [e] 
     * Exchange \r \n \t and \f \v \0 for normal spaces.
     * 
     */
    function _spaces($data) {
        return strtr($data, "\r\n\t\f\v\0", "      ");
    }

    /**
     * [e] 
     * Normalize all linebreaks to just \n
     * 
     */
    function _nl($data) {
        return preg_replace("/\R/", "\n", $data);
    }

    /**
     * [x]
     * Regular expression filter.
     *
     * This either extracts (preg_match) data if you have a () capture group,
     * or functions as filter (pref_replace) if there's a [^ character class.
     * 
     */
    function _regex($data, $rx="", $match=1) {
        # validating
        if (strpos($rx, "(")) {
            if (preg_match($rx, $data, $result)) {
                return($result[$match]);
            }
        }
        # cropping
        elseif (strpos($rx, "[^")) {
            return preg_replace($rx, "", $data);
        }
        # replacing
        else {
            return preg_replace($rx, $match, $data);
        }
    }
    
    /**
     * [w]
     * Miximum and minimum string length.
     *
     */
    function _length($data, $max=65535, $cut=NULL) {
        // just the length limit
        if (is_null($cut)) {
            return substr($data, 0, $max);
        }
        // require minimum string length, else drop value completely
        else {
            return strlen($data) >= $max ? substr($data, 0, $cut)  : NULL;
        }
    }
    
    /**
     * [w]
     * Range ensures value is between given minimum and maximum.
     * (Does not convert to integer itself.)
     *
     */
    function _range($data, $min, $max) {
        return ($data > $max) ? $max : (($data < $min) ? $min : $data);
    }

    /**
     * [b]
     * Fallback value for absent/falsy values.
     *
     */
    function _default($data, $default) {
        return empty($data) ? $default : $data;
    }

    /**
     * [w] 
     * Boolean recognizes 1 or 0 and textual values like "false" and "off" or "no".
     *
     */
    function _boolean($data) {
        if (empty($data) || $data==="0" || in_array(strtolower($data), array("false", "off", "no", "n", "wrong", "not", "-"))) {
            return false;
        }
        elseif ($data==="1" || in_array(strtolower($data), array("true", "on", "yes", "right", "y", "ok"))) {
            return true;
        }
        else return NULL;
    }

    /**
     * [w]
     * Ensures field is in array of allowed values.
     *
     * Works with arrays, but also string list. If you supply a "list,list,list" then
     * the comparison is case-insensitive.
     *
     */
    function _in_array($data, $array) {
        if (is_array($array) ? in_array($data, $array) : in_array(strtolower($data), explode(",", strtolower($array)))) {
            return $data;
        }
    }
    
    
    ###### filter_var() wrappers #########

    /**
     * [w]
     * Common case email syntax.
     *
     * (Close to RFC2822 but disallows square brackets or double quotes, no verification of TLDs,
     * doesn't restrict underscores in domain names, ignores i@an and anyone@localhost.)
     *
     */
    function _email($data, $validate=1) {
        $data = preg_replace("/[^\w!#$%&'*+/=?_`{|}~@.\[\]\-]/", "", $data);  // like filter_var
        if (!$validate || preg_match("/^(?!\.)[.\w!#$%&'*+/=?^_`{|}~-]+@(?:(?!-)[\w-]{2,}\.)+[\w]{2,6}$/i", trim($data))) {
            return $data;
        }
    }

    /**
     * [s] 
     * URI characters. (Actually IRI)
     *
     */
    function _uri($data) {
    # we should encode all-non chars
        return preg_replace("/[^-\w\d\$.+!*'(),{}\|\\~\^\[\]\`<>#%\";\/?:@&=]+/u", "", $data);  // same as SANITIZE_URL
    }
    
    /**
     * [w]
     * URL syntax
     *
     * This is an alias for FILTER_VALIDATE_URL. Beware that it lets a few unwanted schemes
     * through (file:// and mailto:) and what you'd consider misformed URLs (http://http://whatever).
     *
     */
    function _url($data) {
        return filter_var($data, FILTER_VALIDATE_URL);
    }

    /**
     * [v]
     * More restrictive HTTP/FTP url syntax.
     * No usernames allowed, no empty port, pathnames/qs/anchor are not checked.
     *
     # see also http://internet.ls-la.net/folklore/url-regexpr.html
     */
    function _http($data) {
        return preg_match("~
            (?(DEFINE)  (?<byte> 2[0-4]\d |25[0-5] |1\d\d |[1-9]?\d)  (?<ip>(?&byte)(\.(?&byte)){3})  (?<hex>[0-9a-fA-F]{1,4})  )
        ^   (?<proto>https?|ftps?)  ://
            # (?<user> \w+(:\w+)?@ )?
            ( (?<host>  (?:[a-z\d][a-z\d_\-\$]*\.?)+)
             |(?<ipv6>  \[     (?! [:\w]*:::          # ASSERT: no triple ::: colons
                                 |(:\w+){8}|(\w+:){8} # not more than 7 : colons
                                 |(\w*:){7}\w+\.      # not more than 6 : if there's a .
                                 | [:\w]*::[:\w]+:: ) # double :: colon must be unique
                               (?= [:\w]*::           # don't count if there is one ::
                                 |(\w+:){6}\w+[:.])   # else require six : and one . or :
              (?: :|(?&hex):)+((?&hex)|(?&ip)|:) \])  # MATCH: combinations of HEX : IP
             |(?<ipv4>  (?&ip) )     )
            (?<port> :\d{1,5} )?   # the integer isn't optional if port : colon present (unlike FILTER_VALIDATE_URL)
            (?<path> [/][^?#\s]* )?
            (?<qury> [?][^#\s]* )?
            (?<frgm> [#]\S* )?
        \z~ix", $data, $uu) ? $data : NULL;#(print_r($uu) ? $data : $data)
    }
    
    /**
     * [w] 
     * IP address
     *
     */
    function _ip($data) {
        return filter_var($data, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4|FILTER_FLAG_IPV6) ? $data : NULL;
    }

    /**
     * [w]
     * IPv4 address
     *
     */
    function _ipv4($data) {
        return filter_var($data, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) ? $data : NULL;
    }
    
    /**
     * [w]
     * must be public IP address
     *
     */
    function _public($data) {
        return filter_var($data, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE|FILTER_FLAG_NO_RES_RANGE) ? $data : NULL;
    }


    ###### format / representation #########
    
    
    /**
     * [v]
     * HTML5 datetime / datetime_local, date, time
     *
     */
    function _datetime($data) {
        return preg_match("/^\d{0}\d\d\d\d -(0\d|1[012]) -([012]\d|3[01]) T ([01]\d|2[0-3]) :[0-5]\d :[0-5]\d   (Z|[+-]\d\d:\d\d|\.\d\d)$/x", $data) ? $data : NULL;
    }
    function _date($data) {
        return preg_match("/^\d\d\d\d -(0\d|1[012]) -([012]\d|3[01])$/x", $data) ? $data : NULL;
    }
    function _time($data) {
        return preg_match("/^([01]\d|2[0-3]) :[0-5]\d :[0-5]\d  (\.\d\d)?$/x", $data) ? $data : NULL;
    }

    /**
     * [v]
     * HTML5 color
     *
     */
    function _color($data) {
        return preg_match("/^#[0-9A-F]{6}$/i", $data) ? strtoupper($data) : NULL;
    }


    /**
     * [w]
     * Telephone numbers (HTML5 <input type=tel> makes no constraints.)
     *
     */
    function _tel($data) {
        $data = preg_replace("#[/.\s]+#", "-", $data);
        if (preg_match("/^(\+|00)?(-?\d{2,}|\(\d+\)){2,}(-\d{2,}){,3}(\#\d+)?$/", $data)) {
            return trim($data, "-");
        }
    }


    /**
     * [v]
     * Verify minimum consistency (RFC4627 regex) and decode json data.
     *
     */
    function _json($data) { 
        if (!preg_match('/[^,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]/', preg_replace('/"(\\.|[^"\\])*"/g', '', $data))) {
            return json_decode($data);
        }
    }
    
    /**
     * [v]
     * XML tree.
     *
     */
    function _xml($data) {
        return simplexml_load_string($data);
    }

    /**
     * [w]
     * Clean html string via HTMLPurifier.
     *
     */
    function _purify($data) {
        $h = new HTMLPurifier;
        return $h->purify( $data );
    }

    /**
     * [e]
     * HTML escapes.
     *
     * This is actually an output filter. But might be useful to mirror input back into
     * form fields instantly `<input name=field value="<?= $_GET->html["field"] ?>">`
     *
     * @param $data string
     * @return string
     */
    function _html($data) {

        return htmlspecialchars($data, ENT_QUOTES, "UTF-8", false);
    }

    /**
     * [b]
     * Removes residual and certain HTML tags.
     * WARNING: This is no `strip_tags()` substitute, but purposefully leaves
     * lonesome < angle brackets > alone. Only strips coherent and well-known
     * presentational markup.
     *
     */
    function _strip_markup($data, $with="") {
        return preg_replace("~(<\s*/?\s*(a|b|i|em|strong|sup|sub|ins|del|br|hr|big|small|font|span|p|div|table|tr|td|ol|ul|li|dl|dd|dt|abbr|tt|code|pre|h[1-6])\b".
               "(?>[^>\"\']+|\"[^\"]*\"|'[^']*')*>)+~", $with, $data);
    }


    /**
     * [e]
     * Escape all significant special chars.
     *
     */
    function _escape($data) {
        return preg_replace("/[\\\\\[\]\{\}\[\]\'\"\`\Β΄\$\!\&\?\/\>\<\|\*\~\;\^]/", "\\$1", $data);
    }

    /**
     * [e]
     * Addslashes
     *
     */
    function _q($data) {
        return addslashes($data);
    }

    /**
     * [b]
     * Minimal XSS detection.
     * Attempts no cleaning, just bails if anything looks suspicious.
     *
     * If something is XSS contaminated, it's spam and not worth to process further.
     * WEAK filters are better than no filters, but you should ultimatively use ->html purifier instead.
     *
     */
    function _xss($data) {
        if (preg_match("/[<&>]/", $data)) {   // looks remotely like html
            $html = $data;
            
            // remove filler
            $html = preg_replace("/&#(\d);*/e", "ord('$1')", $html);   // escapes
            $html = preg_replace("/&#x(\w);*/e", "ord(dechex('$1'))", $html);   // escapes
            $html = preg_replace("/[\x00-\x20\"\'\`\Β΄]/", "", $html);   //  whitespace + control characters, also any quotes
            $html .= preg_replace("#/\*[^<>]*\*/#", "", $html);   // in-JS obfuscation comments

            // alert patterns
            if (preg_match("#[<<]/*(\?import|applet|embed|object|script|style|!\[CDATA\[|title|body|link|meta|base|i?frame|frameset|i?layer)#iUu", $html, $uu)
             or preg_match("#[<>]\w[^>]*(\w{3,}[=:]+(javascript|ecmascript|vbscript|jscript|python|actionscript|livescript):)#iUu", $html, $uu)
             or preg_match("#[<>]\w[^>]*(on(mouse\w+|key\w+|focus\w*|blur|click|dblclick|reset|select|change|submit|load|unload|error|abort|drag|Abort|Activate|AfterPrint|AfterUpdate|BeforeActivate|BeforeCopy|BeforeCut|BeforeDeactivate|BeforeEditFocus|BeforePaste|BeforePrint|BeforeUnload|Begin|Blur|Bounce|CellChange|Change|Click|ContextMenu|ControlSelect|Copy|Cut|DataAvailable|DataSetChanged|DataSetComplete|DblClick|Deactivate|Drag|DragEnd|DragLeave|DragEnter|DragOver|DragDrop|Drop|End|Error|ErrorUpdate|FilterChange|Finish|Focus|FocusIn|FocusOut|Help|KeyDown|KeyPress|KeyUp|LayoutComplete|Load|LoseCapture|MediaComplete|MediaError|MouseDown|MouseEnter|MouseLeave|MouseMove|MouseOut|MouseOver|MouseUp|MouseWheel|Move|MoveEnd|MoveStart|OutOfSync|Paste|Pause|Progress|PropertyChange|ReadyStateChange|Repeat|Reset|Resize|ResizeEnd|ResizeStart|Resume|Reverse|RowsEnter|RowExit|RowDelete|RowInserted|Scroll|Seek|Select|SelectionChange|SelectStart|Start|Stop|SyncRestored|Submit|TimeError|TrackChange|Unload|URLFlip)[^<\w=>]*[=:]+)[^<>]{3,}#iUu", $html, $uu)
             or preg_match("#[<>]\w[^>]*(\w{3,}[=:]+(-moz-binding:))#iUu", $html, $uu)
             or preg_match("#[<>]\w[^>]*(style[=:]+[^<>]*(expression\(|behaviour:|script:))#iUu", $html, $uu))
            {
                $this->_log($data, "DETECTED XSS PATTERN ({$uu['1']}),");
                $data = "";
                die("input::_xss");
            }
        }
        return $data;
    }

    /**
     * [w]
     * Cleans utf-8 from invalid sequences and alternative representations.
     * (BEWARE: performance drain)
     *
     */
    function _iconv($data) {
        return iconv("UTF-8", "UTF-8//IGNORE", $data);
    }

    /**
     * [b]
     * Few dangerous UTF-7 sequences
     * (only necessary if output pages don't have a charset specified)
     *
     */
    function _utf7($data) {
        return preg_replace("/[+]A(D[w40]|C[IYQU])(-|$)?/", "", $data);;
    }

    /**
     * [e] 
     * Escape for concatenating data into sql query.
     * (suboptimal, use parameterized queries instead)
     *
     */
    function _sql($data) {
        INPUT_QUIET or trigger_error("SQL escaping of input variable '$this->__varname'.", E_USER_NOTICE);
        return db()->quote($data);  // global object
    }
    function _mysql($data) {
        INPUT_QUIET or trigger_error("SQL escaping of input variable '$this->__varname'.", E_USER_NOTICE);
        return mysql_real_escape_string($data);
    }
    



    ###### pseudo filters ##############


    /**
     * [x]
     * Unfiltered access should obviously be avoided. But it's not always possible,
     * so this method exists and will just trigger a notice in debug mode.
     *
     */
    function _raw($data) {
        INPUT_QUIET or trigger_error("Unfiltered input variable \${$this->__title}['{$this->__varname}'] accessed.", E_USER_NOTICE);
        return $data;
    }
    /**
     * [x]
     * Unfiltered access, but logs variable name and value.
     *
     */
    function _log($data, $reason="manual log") {
        syslog(LOG_NOTICE, "php7://input:_log@{$_SERVER['SERVER_NAME']} accessing \${$this->__title}['{$this->__varname}'] variable, $reason, content=" . substr($this->_id(json_encode($data)), 0, 48));
        return $data;
    }

    /**
     * [b]
     * Abort with fatal error. (Used as fallback for INPUT_DIRECT access.)
     *
     */
    function _disallow($data) {
        throw new OutOfBoundsException("Direct \$_REQUEST[\"$this->__varname\"] is not allowed, add ->filter method, or change INPUT_DIRECT if needed.");
    }
    
    
    
    /**
     * Validate instead of sanitize.
     *
     */
    function _is($data) {
        $this->__filter[] = array("is_still", array());
        return $data;
    }
    function _is_still($data) {
        return $data === $this->__vars[$this->__varname];
    }



   

    ######  implementation  ################################################




    /**
     * Array data from previous superglobal.
     *
     * (It's pointless to make this a priv/proteced attribute, as raw data could be accessed
     * in any number of ways. Not the least would be to just not use the input filters.)
     *
     */
    var $__vars = array();

    
    /**
     * Name of superarray this filter object wraps.
     * (e.g. "_GET" or "_SERVER")
     *
     */
    var $__title = "";


    /**
     * Currently accessed array key.
     *
     */
    var $__varname = "";


    /**
     * Amassed filter list.
     * Each ->method->chain name will be appended here. Gets automatically reset after
     * a succesful variable access.
     *
     * Each entry is in `array("methodname", array("param1", 2, 3))` format.
     *
     */
    var $__filter = array();  // filterchain method stack


    /**
     * Automatically appended filter list. (Simply combined with current `$__filter`s).
     *
     */
    var $__always = array();


    /**
     * Currently accessed array keys.
     *
     */
    var $__rules = array(     // pre-defined varname filters
        // "varname.." => array(  array("length",array(256), array("nocontrol",array())  ),
    );


    /**
     * Initialize object from
     *
     * @param array  one of $_REQUEST, $_GET or $_POST etc.
     */
    function __construct($_INPUT, $title="") {
        $this->__vars = $_INPUT;   // stripslashes on magic_quotes_gpc might go here, but we have no word if we actually receive a superglobal or if it wasn't already corrected
        $this->__title = $title;
    }

    
    /**
     * Sets default filter chain.
     * These are ALWAYS applied in CONJUNCTION to ->manually->specified->filters.
     *
     */
    function always() {
        $this->__always = $this->__filter;
        $this->__filter = array();
    }
    
    
    /**
     * Normalize array keys (to UPPER case), for e.g. $_SERVER vars.
     *
     */
    function key_case($case=CASE_UPPER) {
        $this->__vars = array_change_key_case($this->__vars, $case);
    }
    
    
    /**
     * Executes current filter or filter chain on given $varname.
     *
     */
    function filter($varname, $reset_filter_afterwards=NULL) {

        // single array [["name"]] becomes varname
        if (is_array($varname) and count($varname)===1) {
            $varname = current($varname);
        }
        $this->__varname = $varname;

        // direct/raw access without any ->filtername prior offsetGet[] or plain filter() call
        if (empty($this->__filter)) {
            if (isset($this->__rules[$varname])) {
                $this->__filter = $this->rules[$varname];  // use a predefined filterset
            } else {
                $this->__filter = array( INPUT_DIRECT );  // direct access handler
            }
        }
        $first_handler = reset($this->__filter);

        // retrieve value for selected input variable
        $data = NULL;
        if (!is_scalar($varname) or $first_handler == "list" or $first_handler == "multi") {
            // one of the multiplex handlers supposedly picks it up
        }
        elseif (isset($this->__vars[$varname])) {
            // entry exists
            $data = $this->__vars[$varname];
        }
        elseif (!INPUT_QUIET) {
            trigger_error("Undefined input variable \${$this->__title}['{$this->__varname}']", E_USER_NOTICE);
            // run through filters anyway (for ->log)
        }
        
        // implicit ->array filter handling
        if (is_array($data) and $first_handler != "array") {
            array_unshift($this->__filter, "array");
        }
        
        // apply filters (we're building an ad-hoc merged array here, because ->apply works on the reference, and some filters expect ->__filter to contain the complete current list)
        $this->__filter = array_merge($this->__filter, $this->__always);
        $data = $this->apply($data, $this->__filter);
        
        // the Traversable array interface resets the filter list after each request, see ->current()
        if ($reset_filter_afterwards) {
            $this->__filter = $reset_filter_afterwards;
        }

        // done
        return $data;
    }


    /**
     * Runs list of filters on data. Uses either methods, bound closures, or global functions
     * if the filter name matches.
     *
     * It works on an array reference and with array_shift() because filters are allowed to
     * modify the list at runtime (->array requires to).
     *
     */
    function apply($data, &$filterchain) {
        while ($f = array_shift($filterchain)) {
            list($filtername, $args) = is_array($f) ? $f : array($f, array());

            // an override function name or closure exists
            if (isset($this->{"_$filtername"})) {
                $filtername = $this->{"_$filtername"};
            }
            // call _filter method
            if (is_string($filtername) && method_exists($this, "_$filtername")) {
                $data = call_user_func_array(array($this, "_$filtername"), array_merge(array($data), (array)$args));
            }
            // ordinary php function, or closure, or rebound method
            elseif (is_callable($filtername)) {
                $data = call_user_func_array($filtername, array_merge(array($data), $args));
            }
            else {
                INPUT_QUIET>=2 or trigger_error("unknown filter '" . (is_scalar($filtername) ? $filtername : "closure") . "', falling back on wiping non-alpha characters from '{$this->__varname}'", E_USER_WARNING);
                $data = $this->_name($data);
            }
        }
        return $data;
    }


    /**
     * @multiplex
     *
     * List data / array value handling.
     *
     * This virtual filter hijacks the original filter chain, and applies it
     * to sub values.
     *
     */
    function _array($data) {

        // save + swap out the current filter chain
        list($multiplex, $this->__filter) = array($this->__filter, array());

        // iteratively apply original filter chain on each array entry
        $data = (array) $data;
        foreach (array_keys($data) as $i) {
            $chain = $multiplex;
            $data[$i] = $this->apply($data[$i], $chain);
        }

        return $data;
    }


    /**
     * @multiplex
     *
     * Grab a collection of input variables, names delimited by comma.
     * Implicitly makes it an ->_array() list.
     * The _array handler is implicitly used for indexed values. _list
     * and _multi can be used for associative arrays, given a key list.
     * 
     * @example  extract($_GET->list->text["name,title,date,email,comment"]);
     * @php5.4+  $_GET->list[['key1','key2','key3']];
     *
     * @bugs  hacky, improper way to intercept, fetches from $__vars[] directly,
     *        uses $__varname instead of $data, may prevent replays,
     *        main will trigger a notice anyway as VAR1,VAR2,.. is undefined
     *
     */
    function _list($keys, $pass_array=FALSE) {

        // get key list
        if (is_array($this->__varname)) {
            $keys = $this->__varname;
        }
        else {
            $keys = explode(",", $this->__varname);
        }

        // slice out named values from ->__vars
        $data = array_merge(
            array_fill_keys($keys, NULL),
            array_intersect_key($this->__vars, array_flip($keys))
        );

        // chain to _array multiplex handler
        if (!$pass_array) {
            return $this->_array($data);
        }
        // process fetched list as array (for user-land functions like http_build_query)
        else {
            return $data;
        }
    }
    
    /**
     * Processes collection of input variables.
     * Passes list on as array to subsequent filters.
     *
     */
    function _multi($keys) {
        return $this->_list($keys, "process_as_array");
    }


    
    /**
     * @hide
     *
     * Ordinary method calls are captured here. Any ->method("name") will trigger
     * the filter and return variable data, just like ->method["name"] would. It
     * just allows to supply additional method parameters.
     *
     */
    function __call($filtername, $args) {  // can have arguments
        $this->__filter[] = array($filtername, array_slice($args, 1));
        return $this->filter($args[0]);
    }

    
    /**
     * @hide
     *
     * Wrapper to capture ->name->chain accesses.
     *
     */
    function __get($filtername) {
        //
        // we could do some heuristic chaining here,
        // if the last entry in the ->attrib->attrib list is not a valid method name,
        // but a valid varname, we should execute the filter chain rather than add.
        //
        
        // Unpack parameterized filter attributes, use U+2022 ellipsis … as delimiter
        if (strpos($filtername, "…")) {
            $args = explode("…", $filtername);
            $filtername = array_shift($args);
        }
        else {
            $args=array();
        }

        // Add filter to list
        $this->__filter[] = count($args) ? array($filtername, $args) : $filtername;

        // fluent interface
        return $this;
    }
    


    /**
     * @hide ArrayAccess
     *
     * Normal $_ARRAY["name"] syntax access is redirected through the filter here.
     *
     */
    function offsetGet($varname) {
        // never chains
        return $this->filter($varname);
    }

    /**
     * @hide ArrayAccess
     *
     * Needed for commonplace isset($_POST["var"]) checks.
     *
     */
    function offsetExists($name) {
        return isset($this->__vars[$name]);
    }

    /**
     * @hide
     * Sets value in array. Note that it only works for array["xy"]= top-level
     * assignments. Subarrays are always retrieved by value (due to filtering)
     * and cannot be set item-wise.
     *
     * @discouraged
     * Manipulating the input array is indicative for state tampering and thus
     * throws a notice per default.
     *
     */
    function offsetSet($name, $value) {
        INPUT_QUIET or trigger_error("Manipulation of input variable \${$this->__title}['{$this->__varname}']", E_USER_NOTICE);
        $this->__vars[$name] = $value;
    }
    
    /**
     * @hide
     * Removes entry.
     *
     * @discouraged
     * Triggers a notice per default.
     *
     */
    function offsetUnset($name) {
        INPUT_QUIET or trigger_error("Removed input variable \${$this->__title}['{$this->__varname}']", E_USER_NOTICE);
        unset($this->__vars[$name]);
    }



    /**
     * Generic array features.
     * Due to being reserved words `isset` and `empty` need custom method names here:
     *
     *  ->has(isset)
     *  ->no(empty)
     *  ->keys()
     * 
     * Has No Keys seems easy to remember.
     *
     */

    /**
     * isset/array_key_exists check;
     * may list multiple keys to check.
     *
     */
    function has($args) {
        $args = (func_num_args() > 1) ? func_get_args() : explode(",", $args);
        return count(array_intersect_key($this->__vars, array_flip($args))) == count($args);
    }
    
    /**
     * empty() probing,
     * Tests if named keys are absent or falsy.
     *
     */
    function no($args) {
        $args = (func_num_args() > 1) ? func_get_args() : explode(",", $args);
        return count(array_filter(array_intersect_key($this->__vars, array_flip($args)))) == 0;
    }
    
    /**
     * Returns list of all contained keys.
     *
     */
    function keys() {
        return array_keys($this->__vars);
    }


    /**
     * @hide Traversable
     *
     * Allows to loop over all array entries in a foreach loop. A supplied filterlist
     * is reapplied for each iteration.
     *
     * - Basically all calls are mirrored onto ->__vars` internal array pointer.
     * 
     */
    function current() {
        return $this->filter(key($this->__vars), /*reset_filter_afterwards=to what it was before*/$this->__filter);
    }
    function key() {
        return key($this->__vars);
    }
    function next() {
        return next($this->__vars);
    }
    function rewind() {
        return reset($this->__vars);
    }
    function valid() {
        if (key($this->__vars) !== NULL) {
            return TRUE;
        }
        else {
            // also reset the filter list after we're done traversing all entries
            $this->__filters=array();
            return FALSE;
        }
    }


    /**
     * @hide Countable
     * 
     */
    function count() {
        return count($this->__vars);
    }
    
    
    /**
     * @contract ArrayObject::getArrayCopy()
     *
     */
    function getArrayCopy() {
        return $this->__vars;
    }


    /**
     * @contract ArrayObject::exchangeArray()
     *
     */
    function exchangeArray($new) {
        list($old, $this->__vars) = array($this->__vars, $new);
        return $old;
    }


    /**
     * @hide Make filtering functions available as static methods
     *       without underscore prefix for external invocation.
     */ 
    static function __callStatic($func, $args) {
        static $filter = "input";

        // Also eschew E_STRICT notices
        if (!is_object($filter)) {
            $filter = new input(array(), "\$_INLINE_INPUT");
        }

        return isset($filter->{"_$func"})
             ? call_user_func_array($filter->{"_$func"}, $args)
             : call_user_func_array(array($filter, "_$func"), $args);
    }

    /**
     * @hide Allows testing variable presence with e.g. if ( $_POST() )
     *       Alternatively $_POST("var") is an alias to $_POST["var"].
     *
     */
    function __invoke($varname=NULL) {
    
        // treat it as variable access
        if (!is_null($varname)) {
            return $this->offsetGet($varname);
        }
        
        // do the count() call
        else {
            return $this->count();
        }
    }

}



/**
 * @autorun
 *
 */
$_SERVER = new input($_SERVER, "_SERVER");
$_REQUEST = new input($_REQUEST, "_REQUEST");
$_GET = new input($_GET, "_GET");
$_POST = new input($_POST, "_POST");
$_COOKIE = new input($_COOKIE, "_COOKIE");
#$_SESSION
#$_ENV
#$_FILES required a special handler


?>