<?

/**
 * Wrapper around untrusted (tainted) values and arrays
 *
 * @package		Tainted    
 * @author		Marc Worrell <marc@mediamatic.nl>
 * @copyright	2006 Mediamatic
 * @version		$Id: Tainted.php 29301 2007-07-16 08:09:51Z rgareus $
 * @license		GPL
 * @since		File available since Release 3.1.3
 */

/*
 * Tainted.php is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 * 
 * Tainted.php is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with Tainted.php; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */


/**
 * Wrapper around a tainted (untrusted) value
 *
 * Implements several methods of accessing the data.  The basic philosophy is to make
 * it easy to get a filtered version of the data, and hard to get to the raw unfiltered data.
 *
 * Some methods for email and basic url validation are also added
 *
 * @package    Tainted
 * @author     Marc Worrell <marc@mediamatic.nl>
 * @copyright  2006 Mediamatic
 * @since      Class available since Release 3.1.3
 */
class TaintedValue implements Serializable
{
	const ALLOWQUOTES 	 = 1;
	const ALLOWHTML   	 = 2;
	const CHECKDOMAIN 	 = 4;
	const SCHEMEREQUIRED = 8;
	const HOSTREQUIRED 	 = 16;
	
	protected $safe;
	protected $raw;
	

    /**
     * Filter the incoming data - storing the raw and a safe version
     *
     * TaintedValue can wrap numerical values and strings.  A filtered
     * version is stored alongside the non filtered raw version.
     * The basic filter is a strip_tags and htmlspecialchars, combined with 
     * a filter on control characters other than linefeed. 
     *
     * @param mixed $value  string or numerical value to store
     * @access public
     */
	function __construct ( $value )
	{
		if (!is_string($value) && !is_numeric($value) && !is_bool($value) && !is_null($value))
		{
			trigger_error('TaintedValue: only use atomic values', E_USER_ERROR);
		}
		
		if (is_string($value) && function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc())
		{
			$value = stripslashes($value);
		}

		$this->raw  = $value;
		$this->safe = $this->asText();
	}


	/**
	 * Return the value as as filepath
	 *
	 * Removes all control characters
	 * Removes all html (unless ALLOWHTML is given)
	 * Escapes html special chars (unless ALLOWQUOTES is given)
	 * Translates \ into /
	 * Replaces '/+' into '/'
	 * Replaces ':;' with '_'
	 * Only accepts simple paths consisting of [a-z0-9][a-z0-9\._,\-]*
	 * Only accepts paths without '/.' and '..'
	 * Only accepts paths that do not starting or ending with '/'
	 *
	 * @access public
	 * @returns string multi-line filtered text
	 */
	public function asFilepath ()
	{
		$s = $this->asLine();
		$s = str_replace('\\', '/', $s);
		$s = preg_replace('/[:;]/', '_', $s);
		$s = preg_replace('/\/+/', '/', $s);

		if (	preg_match('/^[a-z0-9][\/a-z0-9\._,\-]*$/i', $s)
			&&	strpos($s, '..') === false
			&&	strpos($s, '/.') === false
			&&	$s{strlen($s)-1} != '/')
		{
			$ret = $s;
		}
		else
		{
			$ret = false;
		}
		return $ret;
	}


	/**
	 * Return the value as as filename, without path
	 *
	 * Removes all control characters
	 * Removes all html (unless ALLOWHTML is given)
	 * Escapes html special chars (unless ALLOWQUOTES is given)
	 * Translates \ into /
	 * Replaces ':;' with '_'
	 * Removes the path
	 * Only accepts simple filenames consisting of [a-z0-9][a-z0-9\._,\-]*
	 * Removes a path
	 *
	 * @access public
	 * @returns string multi-line filtered text
	 */
	public function asFilename ()
	{
		$s = $this->asLine();
		$s = str_replace('\\', '/', $s);
		$s = basename($s);
		$s = preg_replace('/[:;]/', '_', $s);

		if (preg_match('/[a-z0-9][a-z0-9\._,\-]*/i', $s))
		{
			$ret = $s;
		}
		else
		{
			$ret = false;
		}
		return $ret;
	}

	
	/**
	 * Return the value as text
	 *
	 * Maps \r\n to \n
	 * Maps \r to \n
	 * Removes all control characters except \n
	 * Removes all html (unless ALLOWHTML is given)
	 * Escapes html special chars (unless ALLOWQUOTES is given)
	 *
	 * @param int $flags   a combination of TaintedValue::ALLOWHTML or TaintedValue::ALLOWQUOTES
	 * @access public
	 * @returns string multi-line filtered text
	 */
	public function asText ( $flags = 0 )
	{
		$val = $this->raw;

		if (! ($flags & TaintedValue::ALLOWHTML))
		{
			$val = strip_tags($val);
		}
		if (! ($flags & TaintedValue::ALLOWQUOTES))
		{
			$val = htmlspecialchars($val);
		}

		// Normalise newlines
		$val = str_replace("\r\n", chr(0x0a), $val); 
		$val = str_replace("\r",   chr(0x0a), $val);

		// Replace all control characters with a space
		$val = preg_replace('/[\x00-\x09]/', ' ', $val);
		$val = preg_replace('/[\x0b-\x1f]/', ' ', $val);
		return $val;
	}

	
	/**
	 * Return the value as one line of text
	 * Removes all control characters
	 * Removes all html (unless ALLOWHTML is given)
	 * Escapes html special chars (unless ALLOWQUOTES is given)
	 *
	 * @param int $flags   a combination of TaintedValue::ALLOWHTML or TaintedValue::ALLOWQUOTES
	 * @access public
	 * @returns string single-line filtered text
	 */
	public function asLine ( $flags = 0 )
	{
		$val = $this->raw;
		if (! ($flags & TaintedValue::ALLOWHTML))
		{
			$val = strip_tags($val);
		}
		if (! ($flags & TaintedValue::ALLOWQUOTES))
		{
			$val = htmlspecialchars($val);
		}

		// Replace all control characters with a space
		$val = preg_replace('/[\x00-\x1f]/', ' ', $val);
		return $val;
	}

	
	/**
	 * Return a floating point value
	 * returns the value when the parameter is numeric
	 * returns false for all other values
	 *
	 * @param mixed	default
	 * @returns float 	when succesful
	 * @returns bool	false on error
	 * @access public
	 */
	public function asNumber ( $default = false )
	{
		$val = trim($this->raw);
		if (is_numeric($val))
		{
			$ret = @floatval($val);
		}
		else
		{
			$ret = $default;
		}
		return $ret;
	}


	/**
	 * Return an integer value
	 * returns the value when the parameter only consists of 
	 * returns false for all other values
	 *
	 * @param mixed	default
	 * @returns int 	when succesful
	 * @returns bool	false on error
	 * @access public
	 */
	public function asInt ( $default = false )
	{
		$val = trim($this->raw);
		if (preg_match('/^[0-9]+$/', $val))
		{
			$ret = @intval($val);
		}
		else
		{
			$ret = $default;
		}
		return $ret;
	}
	
	/**
	 * Return a boolean value
	 * returns true for <> 0, "true", "on", "yes", "ja", "y", "oui", "j"
	 * returns false for all other values
	 *
	 * @param mixed	default
	 * @returns bool	false on error
	 * @access public
	 */
	public function asBoolean ( $default = false )
	{
		if (empty($this->raw))
		{
			$ret = $default;
		}
		else if (is_numeric($this->raw))
		{
			$ret = ($this->raw != 0);
		}
		else
		{
			$val = strtolower($this->raw);
			if ($val{0} == 'n')
			{
				$ret = $default;
			}
			else
			{
				switch (strtolower($this->raw))
				{
				case 'yes':
				case 'ja':
				case 'oui':
				case 'y':
				case 'j':
				case 'true':
				case 'on':
					$ret = true;
					break;
				default:
					$ret = false;
					break;
				}
			}
		}
		return $ret;
	}


	/**
	 * Returns the value when it matches the regexp
	 *
	 * @param string $regexp	regular expression the raw value has to match
	 * @returns string	the raw value when matching the regexp
	 * @returns bool	false on error
	 * @access public
	 */
	public function asRegexp ( $regexp )
	{
		if (preg_match($regexp, $this->raw))
		{
			$ret = $this->raw;
		}
		else
		{
			$val = trim($this->raw);
			if (preg_match($regexp, $val))
			{
				$ret = $val;
			}
		}
		return $ret;
	}


	/**
	 * Returns the value with all matches removed
	 *
	 * @param string $regexp	regular expression for removals
	 * @param string $replace	optional replacement value, defaults to ''
	 * @returns string	the raw value with all matches removed
	 * @access public
	 */
	public function asRegexpReplace ( $regexp, $replace = '' )
	{
		return preg_replace($regexp, $replace, $this->raw);
	}



	/**
	 * Return an ip number.
	 * Returns the value when the parameter matches an IP number
	 *
	 * For now we only accept ip4 addresses
	 * 
	 * @returns string 	when succesful
	 * @returns bool	false on error
	 * @access public
	 */
	public function asIp ()
	{
		$val = trim($this->raw);
		if (preg_match('/^[0-9]{1,3}(\.[0-9]{1,3}){3,3}$/', $val))
		{
			$ps  = explode('.', $val);
			$ret = $val;
			foreach ($ps as $p)
			{
				if ($p >= 256)
				{
					$ret = false;
					break;
				}
			}
		}
		else if (is_numeric($val))
		{
			// Translate the numerical ip to dotted ip address
			$val = floatval($val);
			$ps  = array();
			for ($i=0;$i<4;$i++)
			{
				$ps[] = round(fmod($val, 256));
				$val  = floor($val / 256);
			}
			if ($val == 0)
			{
				$ret = implode('.', array_reverse($ps));
			}
			else
			{
				$ret = false;
			}
		}
		else
		{
			$ret = false;
		}
		return $ret;
	}
	


	/**
	 * Return the value as a valid url
	 * Returns false when the input was not a valid url
	 *
	 * @param int $flags	a combination of TaintedValue::CHECKDOMAIN, TaintedValue::SCHEMEREQUIRED, TaintedValue::HOSTREQUIRED
	 *						TaintedValue::ALLOWHTML and TaintedValue::ALLOWQUOTES
	 * @returns string	the url
	 * @returns bool	false when not an url
	 * @access public
	 */
	public function asUrl ( $flags = 0 )
	{
		$val = $this->filterUrl($flags);
		$ps	 = parse_url($val);

		if ($ps !== false)
		{
			if ($flags & TaintedValue::CHECKDOMAIN)
			{
				$flags = ($flags | TaintedValue::HOSTREQUIRED);
			}
			
			// See if we can fill in some values, by using educated guesses
			if (!empty($ps['host']))
			{
				$host = true;
			}
			else if (!empty($ps['path']))
			{
				// non country tlds, used to recognise urls
				$tld = array(	'com', 'net', 'info', 'org', 
								
								'aero', 'biz', 'cat', 'coop', 'jobs', 
								'mobi', 'museum', 'name', 'pro', 'travel', 
								
								'gov', 'edu', 'mil', 'int'
							);


				list($h) = explode('/', $ps['path']);
				if (	empty($h) 
					||	strpos($h, '.') === false
					||	preg_match('/[^a-zA-Z0-9\.\-]/', $h)
					||	preg_match('/\.(html|htm|php|asp|cgi|rb|js|css)$/', $h)		// py, pl are also country codes
					)
				{
					$is_host = false;
				}
				else if (	strncasecmp('www\.', $h, 4) != 0
						||	preg_match('/^[0-9\.]+$/', $h)
						||	preg_match('/\.('.implode('|',$tld).'|\.[a-z][a-z])$/i', $h))
				{
					$is_host = true;
				}
				else
				{
					$is_host = false;
				}

				if ($is_host)
				{
					$ps['host'] = $h;
					$ps['path'] = substr($ps['path'], strlen($h));

					if (empty($ps['path']) || $ps['path']{0} != '/')
					{
						$ps['path'] = '/' . $ps['path'];
					}
				}
			}

			if (empty($ps['scheme']) && ($flags & TaintedValue::SCHEMEREQUIRED))
			{
				$ps['scheme'] = 'http';
			}
			if (empty($ps['host']) && ($flags & TaintedValue::HOSTREQUIRED) && !empty($_SERVER['HTTP_HOST']))
			{
				$ps['scheme'] = $_SERVER['HTTP_HOST'];
			}

			// Now we are going to do the real checks
			//
			if (empty($ps['host']) && ($flags & TaintedValue::HOSTREQUIRED))
			{
				$url = false;
			}
			else if (	!empty($ps['host']) 
					&&	!preg_match('/^[0-9\.]+$/', $ps['host'])
					&&	($flags & TaintedValue::CHECKDOMAIN)
					&&	function_exists('gethostbyname')
					&&	gethostbyname($ps['host']) == $ps['host'])
			{
				// named host, and host does not exist
				$url = false;
			}
			else
			{
				// Put the url together from the parts
				if (!empty($ps['scheme']))
				{
					$url = $ps['scheme'] . '://';
				}
				else
				{
					$url = '';
				}
				
				if (!empty($ps['user']) || !empty($ps['pass']))
				{
					if (!empty($ps['user']))
					{
						$url .= $ps['user'];
					}
					if (!empty($ps['pass']))
					{
						$url .= ':' . $ps['pass'];
					}
					$url .= '@';
				}
				if (!empty($ps['host']))
				{
					$url .= $ps['host'];
				}
				if (!empty($ps['path']))
				{
					$url .= $ps['path'];
				}
				if (!empty($ps['query']))
				{
					$url .= '?' . $ps['query'];
				}
				if (!empty($ps['fragment']))
				{
					$url .= '#' . $ps['fragment'];
				}
			}
		}
		else
		{
			$url = false;
		}
		return $url;
	}
	

	/**
	 * Return the value as an url
	 * Strip all chars not part of section 5 of http://www.faqs.org/rfcs/rfc1738.html
	 * We strip " (except when ALLOWQUOTES is given)
	 * We strip < and > (except when ALLOWHTML is given)
	 *
	 * @param int $flags	a combination of TaintedValue::ALLOWHTML and TaintedValue::ALLOWQUOTES
	 * @returns string	the value, with only valid url characters
	 * @returns bool	false when the value has non-url characters
	 * @access public
	 */
	public function filterUrl ( $flags = 0 )
	{
		$val	= $this->raw;
		$val	= trim($val);
		$val	= preg_replace('/[^A-Za-z0-9\$\-_\.\+!\*\'\(\){}\|\\\^\~\[\]`#%;\/\?:@&=]/', '', $val);

		if (! ($flags & TaintedValue::ALLOWHTML))
		{
			$val = preg_replace('/[<>]/', '', $val);
		}
		if (! ($flags & TaintedValue::ALLOWQUOTES))
		{
			$val = preg_replace('/["]/', '', $val);
		}
		return $val;
	}


	/**
	 * Return the value as a valid email address
	 * Returns false when the input was not a valid email address
	 * Optionally validates the domain
	 *
	 * regexp from http://cvs.php.net/viewcvs.cgi/pear/HTML_QuickForm/QuickForm/Rule/Email.php?view=markup&rev=1.4
	 *
	 * @param int $flags	optionally TaintedValue::CHECKDOMAIN
	 * @returns string	the email address, when validated
	 * @returns bool	false when the value is not an email address
	 * @access public
	 */
	public function asEmail ( $flags = 0 )
	{
		$val	= $this->filterEmail();
		$regexp = '/^((\"[^\"\f\n\r\t\v\b]+\")|([\w\!\#\$\%\&\'\*\+\-\~\/\^\`\|\{\}]+(\.[\w\!\#\$\%\&\'\*\+\-\~\/\^\`\|\{\}]+)*))@((\[(((25[0-5])|(2[0-4][0-9])|([0-1]?[0-9]?[0-9]))\.((25[0-5])|(2[0-4][0-9])|([0-1]?[0-9]?[0-9]))\.((25[0-5])|(2[0-4][0-9])|([0-1]?[0-9]?[0-9]))\.((25[0-5])|(2[0-4][0-9])|([0-1]?[0-9]?[0-9])))\])|(((25[0-5])|(2[0-4][0-9])|([0-1]?[0-9]?[0-9]))\.((25[0-5])|(2[0-4][0-9])|([0-1]?[0-9]?[0-9]))\.((25[0-5])|(2[0-4][0-9])|([0-1]?[0-9]?[0-9]))\.((25[0-5])|(2[0-4][0-9])|([0-1]?[0-9]?[0-9])))|((([A-Za-z0-9\-])+\.)+[A-Za-z\-]+))$/';
		
		if (preg_match($regexp, $val))
		{
			if (	($flags & TaintedValue::CHECKDOMAIN)
				&&	function_exists('getmxrr')
				&&	function_exists('gethostbyname'))
			{
				$mx     = false;
				$tokens = explode('@', $val);

				if (getmxrr($tokens[1], $mx) || gethostbyname($tokens[1]) != $tokens[1])
				{
					$email = $val;
				}
				else
				{
					$email = false;
				}
			}
			else
			{
				$email = $val;
			}
		}
		else
		{
			$email = false;
		}
		return $email;
	}
	
	
	/**
	 * Return the value as an email address
	 * Strip all chars not part of section 6 of rfc 822 http://www.faqs.org/rfcs/rfc822.html
	 *
	 * @returns string	the email address, when the value consists of valid email characters
	 * @returns bool	false when the value is there are invalid characters
	 * @access public
	 */
	public function filterEmail ()
	{
		$val = $this->raw;
		$val = trim($val);
		$val = preg_replace('/[^A-Za-z0-9!#$%&\'\*+\-\/=\?\^_`{\|}~@\.\[\]]/', '', $val);
		return $val;
	}


	/**
	 * When requesting the value, we normally just return the filtered version.
	 *
	 * @returns string	the filtered version of the stored value
	 * @access public
	 */
	public function __tostring ()
	{
		return $this->safe;
	}
	
	
	/**
	 * Simple get always returns the safe value
	 *
	 * @returns string	the filtered version of the stored value
	 * @access public
	 */
	public function get ()
	{
		return $this->safe;
	}

	/**
	 * Fetch the unsafe data, do not use this, unless you know what you are doing!
	 *
	 * @returns string	the raw unsafe and very tainted value
	 * @access public
	 */
	public function getRawUnsafe ()
	{
		return $this->raw;
	}


	/**
	 * Serialize a tainted value
	 * 
	 * @return string
	 */
	public function serialize ()
	{
		return serialize($this->raw);
	}

	/**
	 * Unserialize a tainted value
	 * 
	 * @param string serialized
	 */
	public function unserialize ( $serialized )
	{
		$this->raw  = unserialize($serialized);
		$this->safe = $this->asText();
	}
}



/**
 * Wrapper around ArrayObject, keeps the tainted values and stores the safe values in the underlying ArrayObject
 *
 * Implements several methods of accessing the data.  The basic philosophy is to make
 * it easy to get a filtered version of the data, and hard to get to the raw unfiltered data.
 *
 * @package    Tainted
 * @author     Marc Worrell <marc@mediamatic.nl>
 * @copyright  2006 Mediamatic
 * @since      Class available since Release 3.1.3
 */
class TaintedArray extends ArrayObject implements Serializable
{
	protected $raw;
	protected $tainted;
	
	/**
	 * Wrap the array into a safe accessible array object
	 *
     * @param array $value  the array to wrap
     * @access public
	 */
	public function __construct ( $array = array() )
	{
		if (!is_array($array))
		{
			trigger_error('TaintedArray: only use array values', E_USER_ERROR);
		}

		parent::__construct(array());

		$this->raw     = array();
		$this->tainted = array();
		foreach ($array as $key => $value)
		{
			$this->offsetSet($key, $value);
		}
	}
	
	/**
	 * Access the wrapped value as an attribute
	 *
     * @param array $key  the array index to return
     * @returns TaintedValue	the wrapper around the value
     * @access public
     */
	public function __get ( $key )
	{
		return $this->get($key);
	}


	/**
	 * Set the array index to the given value.  Wraps the value in a TaintedValue or
	 * in a TaintedArray object. This method makes it possible to set array indices
	 * as if they are attributes.  The key will be filtered.
 	 *
     * @param array $key  	the array index to set
     * @param mixed $value  the value to wrap, either an array, a string or a numerical value
     * @access public
     */
	public function __set ( $key, $value )
	{
		$this->offsetSet($key, $value);
	}


	/**
	 * Set the array index to the given value.  Wraps the value in a TaintedValue or
	 * in a TaintedArray object.  The key will be filtered.
 	 *
     * @param array $key  	the array index to set
     * @param mixed $value  the value to wrap, either Tainted, an array, a string or a numerical value
     * @access public
     */
	public function offsetSet ( $key, $value )
	{
		$val = $this->safeValue($value);

		if (is_null($key))
		{
			$k = count($this->raw);
			while (array_key_exists($k, $this->raw))
			{
				$k++;
			}
		}
		else
		{
			$k = $this->safeKey($key);
		}

		$this->raw[$k]     = $value;
		$this->tainted[$k] = $val;

		if (get_class($val) == 'TaintedValue')
		{
			parent::offsetSet($k, $val->get());
		}
		else
		{
			parent::offsetSet($k, $val);
		}
	}


	/**
	 * Return the tainted value at the key position
	 *
     * @param array $key  the array index to return
     * @returns TaintedValue	the wrapper around the value
     * @access public
	 */
	public function get ( $key )
	{
		if (isset($this->tainted[$key]))
		{
			return $this->tainted[$key];
		}
		else
		{
			trigger_error('TaintedArray::offsetGet: unknown index ' . htmlspecialchars($key), E_USER_NOTICE);
		}
	}
	

	/**
	 * Return as safe version of this TaintedArray
	 *
     * @param string $m  name of the method for untainting the values
     * @returns array	untainted array
     * @access public
	 */
	public function asArray ( $m = 'asText' )
	{
		$a = array();
		
		foreach ($this->tainted as $key => $t)
		{
			if ($t instanceof TaintedArray)
			{
				$a[$key] = $t->asArray($m);
			}
			else
			{
				$a[$key] = $t->$m();
			}
		}
		return $a;
	}


	/**
	 * Return the keys in the array
	 *
     * @returns array	the keys in the enclosed array
     * @access public
	 */
	public function keys ()
	{
		return array_keys($this->raw);
	}


	/**
	 * Return the key of the value in the array, the value is searched in the raw values
	 *
	 * @param mixed $val	value to search in array
     * @returns 	mixed	key of value in array
     * @access public
	 */
	public function search ( $val )
	{
		return array_search($val, $this->raw);
	}


	/**
	 * Gets an array value.  Will always return the safe (filtered) version when the element is a TaintedValue
	 * This method is called when accessing the array like $myarray[$index].  Will return a TaintedArray when
	 * the entry is an array.  Use $myarray->$key to get access to the TaintedValue object.
	 *
     * @param array $key  the array index to return
     * @returns filtered value
     * @access public
	 */
	public function offsetGet ( $key )
	{
		if (isset($this->tainted[$key]))
		{
			if (is_object($this->tainted[$key]))
			{
				if (get_class($this->tainted[$key]) == 'TaintedValue')
				{
					return $this->tainted[$key]->get();
				}
			}
			return $this->tainted[$key];
		}
		else
		{
			trigger_error('TaintedArray::offsetGet: unknown index ' . htmlspecialchars($key), E_USER_NOTICE);
		}
	}


	/**
	 * We just don't allow to override the array.  To be implemented when needed.
	 *
	 * @param array $array	array to overrule the stored values
	 * @returns void
	 * @access public
	 */
	public function exchangeArray ( $array )
	{
		trigger_error('TaintedArray::exchangeArray: not allowed', E_USER_ERROR);
	}


	/**
	 * Removes an index from the wrapped array.  Unsets the safe version and the wrapped version.
	 *
	 * @param mixed $key	the key to be unset
	 * @returns void
	 * @access public
	 */
	public function offsetUnset ( $key )
	{
		$k = $this->safeKey($key);
		unset($this->tainted[$k]);
		unset($this->raw[$k]);
		parent::offsetUnset($k);
	}
	

	/**
	 * Append an unsafe value or a TaintedValue object to the wrapped array.
	 *
	 * @param mixed $value	The value to append, either Tainted, an array, a string or a numerical value
	 * @returns void
	 * @access public
	 */
	public function append ( $value )
	{
		$val = $this->safeValue($value);

		$this->tainted[] = $val;
		$this->raw[]	 = $value;

		if (get_class($val) == 'TaintedValue')
		{
			parent::append($val->get());
		}
		else
		{
			parent::append($val);
		}
	}
	
	
	/**
	 * Merge an array or a tainted array in this array
	 * 
	 * @param	array	$a	tainted array or php array
	 * @return void
	 */
	public function merge ( $a )
	{
		if (is_object($a) && $a instanceof TaintedArray)
		{
			$ks = $a->keys();
			foreach ($ks as $k)
			{
				if (is_int($k))
				{
					$this->offsetSet(null, $a->getRawUnsafe($k));
				}
				else
				{
					$this->offsetSet($k, $a->getRawUnsafe($k));
				}
			}
		}
		else if (is_array($a))
		{
			$ks = array_keys($a);
			foreach ($ks as $k)
			{
				if (is_int($k))
				{
					$this->offsetSet(null, $a[$k]);
				}
				else
				{
					$this->offsetSet($k, $a[$k]);
				}
			}
		}
		else
		{
			trigger_error('merge with a non-array', E_USER_WARNING);
		}
	}


	/**
	 * Fetch the unsafe data, do not use this, unless you know what you are doing!
	 *
	 * @returns string	the raw unsafe and very tainted value
	 * @access public
	 */
	public function getRawUnsafe ( $key )
	{
		return $this->raw[$key];
	}

	/**
	 * Convert a key to a safe format.  We don't preserve unsafe keys.
	 * Keys must be either numerical or matching '[a-zA-Z0-9_\-\.]+'
	 * 
	 * We also remove 'amp;' which is sometimes the result of double 
	 * escaping &-characters in urls.
	 * 
	 * @param mixed $key	The key, numerical or a string
	 * @returns mixed	 safe version of the key
	 * @access protected
	 */
	protected function safeKey ( $key )
	{
		if (is_numeric($key))
		{
			$k = $key;
		}
		else
		{
			$k = preg_replace('/[^a-zA-Z0-9_\-\.]/', '_', str_replace('amp;', '', $key));
		}
		return $k;
	}
	

	/**
	 * Convert a value to a safe format.  Wrapping it into either a  TaintedValue or a TaintedArray
	 *
	 * @param mixed	$value	value to wrap, TaintedValue, TaintedArray, array, numerical or a string
	 * @returns TaintedArray	when wrapping an array
	 * @returns TaintedValue	when wrapping a number or a string
	 * @access protected
	 */
	protected function safeValue ( $value )
	{
		if (is_object($value))
		{
			$class = get_class($value);
			if ($class != 'TaintedValue' && $class != 'TaintedArray')
			{
				trigger_error('TaintedArray::safeValue: you cannot assign objects other than TaintedArray of TaintedValue objects', E_USER_ERROR);
			}
			$val = $value;
		}
		else if (is_array($value))
		{
			$val = new TaintedArray($value);
		}
		else
		{
			$val = new TaintedValue($value);
		}
		return $val;
	}


	/**
	 * Serialize a tainted array
	 * 
	 * @return string
	 */
	public function serialize ()
	{
		return serialize($this->raw);
	}


	/**
	 * Unserialize a tainted array
	 * 
	 * @param string serialized
	 */
	public function unserialize ( $serialized )
	{
		$raw = unserialize($serialized);
		
		foreach ($raw as $key => $value)
		{
			$this->offsetSet($key, $value);
		}
	}
}


/**
 * Checks if $a can be handled as an array
 * 
 * @param array	a	array or tainted array
 * @return boolean	true when a is an array or array object
 */ 
function any_is_array ( $a )
{
	return is_array($a) || (is_object($a) && $a instanceof ArrayObject);
}


/**
 * Returns the keys of an array
 * 
 * @param array		a	array of tainted array
 * @return array	array with the keys of a
 */
function any_array_keys ( $a )
{
	if (@is_array($a))
	{
		$keys = array_keys($a);
	}
	else if (is_object($a) && class_exists('TaintedArray') && $a instanceof TaintedArray)
	{
		$keys = $a->keys();
	}
	else
	{
		trigger_error('any_array_keys on a non array', E_USER_NOTICE);
		
		$keys = array();
	}
	return $keys;
}


/**
 * Finds the index of val in array a
 * 
 * @param	mixed	val	value to search in array
 * @param	array	a	array or tainted array
 * @return	mixed	key of the value in the array
 * @return	false	when val is not in the array
 */
function any_array_search ( $val, $a )
{
	if (@is_array($a))
	{
		$k = array_search($val, $a);
	}
	else if (is_object($a) && class_exists('TaintedArray') && $a instanceof TaintedArray)
	{
		$k = $a->search($val);
	}
	else
	{
		trigger_error('any_array_search on a non array', E_USER_NOTICE);
		
		$k = false;
	}
	return $k;
}


/**
 * Checks if the value exists in the array
 * 
 * @param	mixed	val	value to search in array
 * @param	array	a	array or tainted array to search in
 * @return	boolean	true when the value is in the array, false when not
 */
function any_in_array ( $val, $a )
{
	return any_array_search($val, $a) !== false;
}



/**
 * array_unique, also usable for tainted arrays
 * 
 * @param	array	a	array or array object
 * @param	string	as	the method used to fetch safe values from TaintedValue
 * @return	array	array with unique values from a
 */
function any_array_unique ( $a, $as = 'asText' )
{
	if (any_is_array($a))
	{
		if (any_is_tainted($a))
		{
			$a = $a->asArray($as);
		}
		if (is_array($a))
		{
			$u = array_unique($a);
		}
		else
		{
			$u = array();
		}
	}
	else
	{
		trigger_error('any_array_unique on a non array', E_USER_NOTICE);
		
		$u = array();
	}
	return $u;
}



/**
 * Merge two arrays, works correctly for tainted arrays. If one, or both, of the
 * arrays is tainted, then the result is tainted.
 * 
 * @param	array	as	base array
 * @param	array	bs	array to merge with as
 * @return	array	new array (or tainted array) composed of as and bs
 */
function any_array_merge ( $as, $bs )
{
	if (!any_is_tainted($as) && !any_is_tainted($bs))
	{
		$ms = array_merge($as, $bs);
	}
	else
	{
		if (any_is_tainted($as))
		{
			$ms = clone($as);
		}
		else
		{
			$ms = new TaintedArray($as);
		}
		$ms->merge($bs);
	}
	return $ms;
}


/**
 * implode, also works for tainted arrays
 * 
 * @param string	sep	separator for values
 * @param array		a 	array (or tainted array) to implode
 * @param string	as	method to use for converting tainted values
 * @return string	concatenated array values
 */
function any_implode ( $sep, $a, $as = 'asText' )
{
	if (any_is_array($a))
	{
		if (any_is_tainted($a))
		{
			$a = $a->asArray($as);
		}
		if (is_array($a))
		{
			$s = implode($sep, $a);
		}
		else
		{
			$s = false;
		}
	}
	else
	{
		trigger_error('any_implode on a non array', E_USER_NOTICE);
		
		$s = false;
	}
	return $s;
}



/**
 * checks if $a is a tainted array or value
 * 
 * @param mixed	a	value to test
 * @return	boolean	true when a is tainted, false otherwise
 */
function any_is_tainted ( $a )
{
	return 		is_object($a) 
			&&	class_exists('TaintedArray') 
			&&	(	$a instanceof TaintedArray
				||	$a instanceof TaintedValue);
}



/* Just some testing code, left here till we have the unit tests in place.
echo '<h1>Wrapping _GET and _SERVER</h1>';

$_GET    = new TaintedArray($_GET);
$_SERVER = new TaintedArray($_SERVER);

echo '<h1>_GET</h1>';

/// SOME TESTING CODE

foreach ($_GET as $key=> $value)
{
	echo '[',$key, '] = "' . $value . '" ';
	echo 'raw="', nl2br(htmlspecialchars($_GET->$key->getRawUnsafe())), '"<br/>';
}

echo 'Does index "a" exists? ', array_key_exists('a',$_GET) ? 'Yes' : 'No';


echo "<br/>";
echo 'a as line: "', nl2br(htmlentities($_GET->a->asLine())), '"';
echo "<br/>";
echo 'url as url: "', nl2br(htmlentities($_GET->url->asUrl(TaintedValue::SCHEMEREQUIRED|TaintedValue::CHECKDOMAIN))), '"';
echo "<br/>";

echo ' <b>';
echo htmlentities($_GET['abcd']);
echo '</b>';

echo " xxx", htmlentities($_GET->abcd->getRawUnsafe());
*/

?><?/* vi:set ts=4 sts=4 sw=4 binary noeol: */?>