Tuesday, February 17, 2009

PHP Input Cleaning

I've been buried in my be-all, end-all, does-everything CMS / e-commerce site.
I thought I'd come up for err (or air) and here's what happened.

Below is an object which does lazy input cleaning of GET and POST data.

You use it by creating a new object [surprise].

You access cleaned data as attributes of an object instance.

All the cleaned data is cached in a class variable, so you can either pass around
a global or create a bunch of instances and everything will work - within the same
instance of the PHP program, of course. Once the Object
is required, it acts kind of like a Singleton Pattern.

You can restrict the query sources to GET, POST or REQUEST and control how it
reacts to undefined query parameters.

The documentation is in a comment at the top in Textile.

In some ways, I think this is kind of neat, but on the other hand it's
really depressing how much effort goes into something with such a simple
objective. Seems like there should be an easier way.

Cheers,


<?php
/*
#doc-start
h1. RequestCleaner

Created by on 2009-02-14.
Copyright (c) 2009 Clove Technologies, Inc. All rights reserved.

h3. Usage

Create a new request cleaner:

$rc = new RequestCleaner(sources, use_modes, error_mode) - where:

* sources is a comma separated string OR an array with one or more of: GET, POST, REQUEST
* use_modes is the bitwise OR of
** RequestCleaner::USE_HTMLENTITIES - process each parameter with _htmlentities()_
** RequestCleaner::USE_HTMLSPECIALCHARS - process each parameter with _htmlspecialchars()_
** RequestCleaner::USE_NL2BR - process each parameter with _nl2br()_
* error_mode - determines how the beast responds to errors - such as non-existing query parameters.
Use one of:
** RequestCleaner::RETURN_NULL - returns NULL
** RequestCleaner::RETURN_FALSE - returns FALSE - can be distinguished from NULL by '===' and '!=='
** RequestCleaner::THROW_EXCEPTION - throws an exception

For example:

$rc = new RequestCleaner('POST', RequestCleaner::USE_HTMLENTITIES | RequestCleaner::USE_NL2BR,
RequestCleaner::THROW_EXCEPTION);


To get a 'cleaned' query parameter, use '$rc->parm', where _parm_ is the parameter name.
For example, if 'foo' is a POST parameter, then '$rc->foo' will return the value of 'foo'
from after running the 'cleaning' routines on it.

h3. Fine points

All Cleaned data is cached in a RequestCleaner Class Variable, as are the sources, use and error
modes. This means that a top level PHP program can set up the method of cleaning and
allowed sources and all included code which uses _any_ instance of a RequestCleaner
will use the same methods and cache.

Consequently, you can either create a RequestCleaner instance at top level OR in any included or
required file which needs to access query parameters. It doesn't matter.

Attempts to access undefined attributes generate an error - as specified by _error_mode_.

Query parameters which return arrays - as in <input type=... name="foo[]" ...> -
are turned into arrays of cleaned strings which can be processed using normal loops
and array_...() functions

#end-doc
*/

/**
* RequestCleaner(sources = NULL) - where sources defines a comma separated list of Super Globals
* for attribute values. Legal names are: POST, GET, and REQUEST
*/
class RequestCleaner
{
const ERROR_MODE_MASK = 3;
const RETURN_NULL = 1;
const RETURN_FALSE = 2;
const THROW_EXCEPTION = 3;
const NORMAL = RequestCleaner::RETURN_NULL;

const CLEANER_MODE_MASK = 0x07; // IMPORTANT: Change This as you ADD USE_modes
const USE_HTMLENTITIES = 1;
const USE_HTMLSPECIALCHARS = 2;
const USE_NL2BR = 4;
private static $use_modes = array(
RequestCleaner::USE_HTMLENTITIES => 'htmlentities()',
RequestCleaner::USE_HTMLSPECIALCHARS => 'htmlspecialchars()',
RequestCleaner::USE_NL2BR => 'nl2br()',
);
private static $error_modes = array(
RequestCleaner::RETURN_NULL => 'return NULL',
RequestCleaner::RETURN_FALSE => 'return FALSE',
RequestCleaner::THROW_EXCEPTION => 'throw Exception'
);
private static $sources = NULL;
private static $source_names = array();
private static $error_mode = NULL;
private static $use_mode = NULL;
private static $cache = array();
function __construct($sources = array('GET', 'POST'), $use_mode = RequestCleaner::USE_HTMLENTITIES,
$error_mode = RequestCleaner::RETURN_NULL)
{
if (!RequestCleaner::$sources) {
if ($sources) {
RequestCleaner::$sources = array();
if (is_string($sources)) {
$sources = preg_split("/,\s*/", trim($sources));
}
foreach ($sources as $src) {
RequestCleaner::$source_names[] = $src;
switch ($src) {
case 'POST':
RequestCleaner::$sources[] = $_POST;
break;
case 'GET':
RequestCleaner::$sources[] = $_GET;
break;
case 'REQUEST':
RequestCleaner::$sources[] = $_REQUEST;
break;
default:
throw new Exception("RequestCleaner::__construct($sources): Illegal Source: $tmp");
}
}
} else {
RequestCleaner::$sources = array($_POST, $_GET);
}
if (RequestCleaner::CLEANER_MODE_MASK & $use_mode) {
RequestCleaner::$use_mode = RequestCleaner::CLEANER_MODE_MASK & $use_mode;
}
if (RequestCleaner::ERROR_MODE_MASK & $error_mode) {
RequestCleaner::$error_mode = RequestCleaner::ERROR_MODE_MASK & $error_mode;
}
}
}

private function stringCleaner($x)
{
if (RequestCleaner::$use_mode & RequestCleaner::USE_HTMLSPECIALCHARS) {
$x = htmlspecialchars($x);
}
if (RequestCleaner::$use_mode & RequestCleaner::USE_HTMLENTITIES) {
$x = htmlentities($x);
}
if (RequestCleaner::$use_mode & RequestCleaner::USE_NL2BR) {
$x = nl2br($x);
}
return $x;
} // end of arrayHelper()

private function useModesToString()
{
$ar = array();
foreach (array(RequestCleaner::USE_HTMLENTITIES, RequestCleaner::USE_NL2BR,
RequestCleaner::USE_HTMLSPECIALCHARS) as $mode) {
if (RequestCleaner::$use_mode & $mode) {
$ar[] = RequestCleaner::$use_modes[$mode];
}
}
return implode(',', $ar);
} // end of useModeToString()

public function __toString()
{
return "RequestCleaner: examining " . implode(', ', RequestCleaner::$source_names)
. " Using " . $this->useModesToString()
. " / Exit Mode: " . RequestCleaner::$error_modes[RequestCleaner::$error_mode];
} // end of __toString()

public function __get($name)
{
if (array_key_exists($name, RequestCleaner::$cache)) {
return RequestCleaner::$cache[$name];
}
foreach (RequestCleaner::$sources as $source) {
if (array_key_exists($name, $source)) {
$val = $source[$name];
if (is_string($val)) {
return (RequestCleaner::$cache[$name] = RequestCleaner::stringCleaner($val));
} elseif (is_array($val)) {
return (RequestCleaner::$cache[$name] = array_map(array('RequestCleaner', 'stringCleaner'), $val));
}
}
}
switch (RequestCleaner::$error_mode) {
case RequestCleaner::NORMAL: return NULL;
case RequestCleaner::RETURN_FALSE: return FALSE;
case RequestCleaner::THROW_EXCEPTION:
throw new Exception("RequestCleaner::__get($name): Value Not Defined");
default:
throw new Exception("RequestCleaner::__get($name): ERROR: Value Not Defined / Illegal Error Mode");
}
return NULL;
} // end of __get()

public function __set($name, $value)
{
switch (RequestCleaner::$error_mode) {
case RequestCleaner::NORMAL: return NULL;
case RequestCleaner::RETURN_FALSE: return FALSE;
case RequestCleaner::THROW_EXCEPTION:
throw new Exception("RequestCleaner::__set($name, $value): Setting Attributes Not Allowed");
default:
throw new Exception("RequestCleaner::__set($name, $value): Setting Attributes Not Allowed / Illegal Error Mode");
}
} // end of __set()

public function __unset($name)
{
switch (RequestCleaner::$error_mode) {
case RequestCleaner::NORMAL: return NULL;
case RequestCleaner::RETURN_FALSE: return FALSE;
case RequestCleaner::THROW_EXCEPTION:
throw new Exception("RequestCleaner::__unset($name): Unsetting Attributes Not Allowed");
default:
throw new Exception("RequestCleaner::__unset($name): Unsetting Attributes Not Allowed / Illegal Error Mode");
}
} // end of __set()

public function __isset($name)
{
if (array_key_exists($name, RequestCleaner::$cache)) {
return TRUE;
}
foreach (RequestCleaner::$sources as $source) {
if (array_key_exists($name, $source)) {
return TRUE;
}
}
return FALSE;
} // end of __isset()
}

// end class definitions

?>


Why did I post this dramatic exposition of Programming Prowess?

No particular reason, just felt like it.