<?php

interface wfWAFRequestInterface {

	public function getBody();

	public function getQueryString();

	public function getHeaders();

	public function getCookies();

	public function getFiles();

	public function getFileNames();

	public function getHost();

	public function getURI();

	public function getPath();

	public function getIP();

	public function getMethod();

	public function getProtocol();

	public function getAuth();

	public function getTimestamp();

	public function __toString();

}


class wfWAFRequest implements wfWAFRequestInterface {

	/**
	 * @param string $requestString
	 * @return wfWAFRequest
	 */
	public static function parseString($requestString) {
		if (!is_string($requestString)) {
			throw new InvalidArgumentException(__METHOD__ . ' expects a string for first parameter, recieved ' . gettype($requestString));
		}

		if (version_compare(phpversion(), '5.3.0') > 0) {
			$class = get_called_class();
			$request = new $class();
		} else {
			$request = new self();
		}

		$request->setAuth(array());
		$request->setBody(array());
		$request->setCookies(array());
		$request->setFileNames(array());
		$request->setFiles(array());
		$request->setHeaders(array());
		$request->setHost('');
		$request->setIP('');
		$request->setMethod('');
		$request->setPath('');
		$request->setProtocol('');
		$request->setQueryString(array());
		$request->setTimestamp('');
		$request->setURI('');

		list($headersString, $bodyString) = explode("\n\n", $requestString, 2);
		$headersString = trim($headersString);
		$bodyString = trim($bodyString);
		$headers = explode("\n", $headersString);
		// Assume first is method
		if (preg_match('/^([a-z]+) (.*?) HTTP\/1.[0-9]/i', $headers[0], $matches)) {
			$request->setMethod($matches[1]);
			$uri = $matches[2];
			$request->setUri($uri);
			if (($pos = wfWAFUtils::strpos($uri, '?')) !== false) {
				$queryString = wfWAFUtils::substr($uri, $pos + 1);
				parse_str($queryString, $queryStringArray);
				$request->setQueryString($queryStringArray);

				$path = wfWAFUtils::substr($uri, 0, $pos);
				$request->setPath($path);
			} else {
				$request->setPath($uri);
			}
		}
		$kvHeaders = array();
		for ($i = 1; $i < count($headers); $i++) {
			$headerString = $headers[$i];
			list($header, $headerValue) = explode(':', $headerString, 2);
			$header = trim($header);
			$headerValue = trim($headerValue);
			$kvHeaders[$header] = $headerValue;

			switch (wfWAFUtils::strtolower($header)) {
				case 'authorization':
					if (preg_match('/basic ([A-Za-z0-9\+\/=]+)/i', $headerValue, $matches)) {
						list($authUser, $authPass) = explode(':', base64_decode($matches[1]), 2);
						$auth['user'] = $authUser;
						$auth['password'] = $authPass;
						$request->setAuth($auth);
					}
					break;

				case 'host':
					$request->setHost($headerValue);
					break;

				case 'cookie':
					$cookieArray = array();
					$cookies = explode(';', $headerValue);
					foreach ($cookies as $cookie) {
						if (wfWAFUtils::strpos($cookie, '=') !== false) {
							list($cookieName, $cookieValue) = explode('=', $cookie, 2);
							$cookieArray[trim($cookieName)] = urldecode(trim($cookieValue));
						}
					}
					$request->setCookies($cookieArray);
					break;
			}

		}
		$request->setHeaders($kvHeaders);

		if (wfWAFUtils::strlen($bodyString) > 0) {
			if (preg_match('/^multipart\/form\-data; boundary=(.*?)$/i', $request->getHeaders('Content-Type'), $boundaryMatches)) {
				$body = '';
				$files = array();
				$fileNames = array();

				$boundary = $boundaryMatches[1];
				$bodyChunks = explode("--$boundary", $bodyString);
				foreach ($bodyChunks as $chunk) {
					if (!$chunk || $chunk == '--') {
						continue;
					}

					list($chunkHeaders, $chunkData) = explode("\n\n", $chunk, 2);
					$chunkHeaders = explode("\n", $chunkHeaders);
					$param = array(
						'value' => wfWAFUtils::substr($chunkData, 0, -1),
					);
					foreach ($chunkHeaders as $chunkHeader) {
						if (wfWAFUtils::strpos($chunkHeader, ':') !== false) {
							list($chunkHeaderKey, $chunkHeaderValue) = explode(':', $chunkHeader, 2);
							$chunkHeaderKey = trim($chunkHeaderKey);
							$chunkHeaderValue = trim($chunkHeaderValue);
							switch ($chunkHeaderKey) {
								case 'Content-Disposition':
									$dataAttributes = explode(';', $chunkHeaderValue);
									foreach ($dataAttributes as $attr) {
										$attr = trim($attr);
										if (preg_match('/^name="(.*?)"$/i', $attr, $attrMatch)) {
											$param['name'] = $attrMatch[1];
											continue;
										}
										if (preg_match('/^filename="(.*?)"$/i', $attr, $attrMatch)) {
											$param['filename'] = $attrMatch[1];
											continue;
										}
									}
									break;
								case 'Content-Type':
									$param['type'] = $chunkHeaderValue;
									break;
							}
						}
					}
					if (array_key_exists('name', $param)) {
						if (array_key_exists('filename', $param)) {
							$files[$param['name']] = array(
								'name'    => $param['filename'],
								'type'    => $param['type'],
								'size'    => wfWAFUtils::strlen($param['value']),
								'content' => $param['value'],
							);
							$fileNames[$param['name']] = $param['filename'];
						} else {
							$body .= urlencode($param['name']) . '=' . urlencode($param['value']) . '&';
						}
					}
				}

				if ($body) {
					parse_str($body, $postBody);
					if (is_array($postBody)) {
						$request->setBody($postBody);
					} else {
						$request->setBody($body);
					}
				}
				if ($files) {
					$request->setFiles($files);
				}
				if ($fileNames) {
					$request->setFileNames($fileNames);
				}

			} else {
				parse_str($bodyString, $postBody);
				if (is_array($postBody)) {
					$request->setBody($postBody);
				} else {
					$request->setBody($bodyString);
				}
			}
		}

		return $request;
	}

	/**
	 * @param wfWAFRequest|null $request
	 * @return wfWAFRequest
	 */
	public static function createFromGlobals($request = null) {
		if ($request === null) {
			if (version_compare(phpversion(), '5.3.0') > 0) {
				$class = get_called_class();
				$request = new $class();
			} else {
				$request = new self();
			}
		}

		$request->setAuth(array());
		$request->setCookies(array());
		$request->setFileNames(array());
		$request->setFiles(array());
		$request->setHeaders(array());
		$request->setHost('');
		$request->setIP('');
		$request->setMethod('');
		$request->setPath('');
		$request->setProtocol('');
		$request->setTimestamp('');
		$request->setURI('');

		$request->setBody(wfWAFUtils::stripMagicQuotes($_POST));
		$request->setQueryString(wfWAFUtils::stripMagicQuotes($_GET));
		$request->setCookies(wfWAFUtils::stripMagicQuotes($_COOKIE));
		$request->setFiles(wfWAFUtils::stripMagicQuotes($_FILES));

		if (!empty($_FILES)) {
			$fileNames = array();
			foreach ($_FILES as $input => $file) {
				$fileNames[$input] = wfWAFUtils::stripMagicQuotes($file['name']);
			}
			$request->setFileNames($fileNames);
		}

		if (is_array($_SERVER)) { //All of these depend on $_SERVER being non-null and an array
			$auth = array();
			if (array_key_exists('PHP_AUTH_USER', $_SERVER)) {
				$auth['user'] = wfWAFUtils::stripMagicQuotes($_SERVER['PHP_AUTH_USER']);
			}
			if (array_key_exists('PHP_AUTH_PW', $_SERVER)) {
				$auth['password'] = wfWAFUtils::stripMagicQuotes($_SERVER['PHP_AUTH_PW']);
			}
			$request->setAuth($auth);

			if (array_key_exists('REQUEST_TIME_FLOAT', $_SERVER)) {
				$timestamp = $_SERVER['REQUEST_TIME_FLOAT'];
			} else if (array_key_exists('REQUEST_TIME', $_SERVER)) {
				$timestamp = $_SERVER['REQUEST_TIME'];
			} else {
				$timestamp = time();
			}
			$request->setTimestamp($timestamp);

			$headers = array();
			foreach ($_SERVER as $key => $value) {
				if (wfWAFUtils::strpos($key, 'HTTP_') === 0) {
					$header = wfWAFUtils::substr($key, 5);
					$header = str_replace(array(' ', '_'), array('', ' '), $header);
					$header = ucwords(wfWAFUtils::strtolower($header));
					$header = str_replace(' ', '-', $header);
					$headers[$header] = wfWAFUtils::stripMagicQuotes($value);
				}
			}
			if (array_key_exists('CONTENT_TYPE', $_SERVER)) {
				$headers['Content-Type'] = wfWAFUtils::stripMagicQuotes($_SERVER['CONTENT_TYPE']);
			}
			if (array_key_exists('CONTENT_LENGTH', $_SERVER)) {
				$headers['Content-Length'] = wfWAFUtils::stripMagicQuotes($_SERVER['CONTENT_LENGTH']);
			}
			$request->setHeaders($headers);

			$host = '';
			if (array_key_exists('Host', $headers)) {
				$host = $headers['Host'];
			} else if (array_key_exists('SERVER_NAME', $_SERVER)) {
				$host = wfWAFUtils::stripMagicQuotes($_SERVER['SERVER_NAME']);
			}
			$request->setHost($host);

			$request->setMethod(array_key_exists('REQUEST_METHOD', $_SERVER) ? wfWAFUtils::stripMagicQuotes($_SERVER['REQUEST_METHOD']) : 'GET');
			$request->setProtocol((array_key_exists('HTTPS', $_SERVER) && $_SERVER['HTTPS'] && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http');
			$request->setUri(array_key_exists('REQUEST_URI', $_SERVER) ? wfWAFUtils::stripMagicQuotes($_SERVER['REQUEST_URI']) : '');

			$uri = parse_url($request->getURI());
			if (is_array($uri) && array_key_exists('path', $uri)) {
				$path = $uri['path'];
			} else {
				$path = $request->getURI();
			}
			$request->setPath($path);
		}

		return $request;
	}

	private $auth;
	private $body;
	private $cookies;
	private $fileNames;
	private $files;
	private $headers;
	private $host;
	private $ip;
	private $method;
	private $path;
	private $protocol;
	private $queryString;
	private $timestamp;
	private $uri;

	private $highlightParamFormat;
	private $highlightMatchFormat;
	private $highlightMatches;
	private $highlightMatchFilter = 'urlencode';


	protected function _arrayValueByKeys($global, $key) {
		if (is_array($global)) {
			if (is_array($key)) {
				$_key = array_shift($key);
				if (array_key_exists($_key, $global)) {
					if (count($key) > 0) {
						return $this->_arrayValueByKeys($global[$_key], $key);
					} else {
						return $global[$_key];
					}
				}
			} else {
				return array_key_exists($key, $global) ? $global[$key] : null;
			}
		}
		return null;
	}

	public function getBody() {
		if (func_num_args() > 0) {
			$args = func_get_args();
			return $this->_arrayValueByKeys($this->body, $args);
		}
		return $this->body;
	}

	public function getQueryString() {
		if (func_num_args() > 0) {
			$args = func_get_args();
			return $this->_arrayValueByKeys($this->queryString, $args);
		}
		return $this->queryString;
	}

	public function getHeaders() {
		if (func_num_args() > 0) {
			$args = func_get_args();
			return $this->_arrayValueByKeys($this->headers, $args);
		}
		return $this->headers;
	}

	public function getCookies() {
		if (func_num_args() > 0) {
			$args = func_get_args();
			return $this->_arrayValueByKeys($this->cookies, $args);
		}
		return $this->cookies;
	}

	public function getFiles() {
		if (func_num_args() > 0) {
			$args = func_get_args();
			return $this->_arrayValueByKeys($this->files, $args);
		}
		return $this->files;
	}

	public function getFileNames() {
		if (func_num_args() > 0) {
			$args = func_get_args();
			return $this->_arrayValueByKeys($this->fileNames, $args);
		}
		return $this->fileNames;
	}

	public function getHost() {
		return $this->host;
	}

	public function getURI() {
		return $this->uri;
	}

	public function getPath() {
		return $this->path;
	}

	public function getIP() {
		return $this->ip;
	}

	public function getMethod() {
		return $this->method;
	}

	public function getProtocol() {
		return $this->protocol;
	}

	public function getAuth($arg1 = null) {
		if ($arg1) {
			if (is_array($this->auth) && array_key_exists($arg1, $this->auth)) {
				return $this->auth[$arg1];
			}
			return null;
		}
		return $this->auth;
	}

	public function getTimestamp() {
		return $this->timestamp;
	}

	public function __toString() {
		return $this->highlightFailedParams();
	}

	/**
	 * @param array $failedParams
	 * @param string $highlightParamFormat
	 * @param string $highlightMatchFormat
	 * @return string
	 */
	public function highlightFailedParams($failedParams = array(), $highlightParamFormat = '[param]%s[/param]',
	                                      $highlightMatchFormat = '[match]%s[/match]') {
		$highlights = array();

		// Cap at 50kb
		$maxRequestLen = 1024 * 50;

		$this->highlightParamFormat = $highlightParamFormat;
		$this->highlightMatchFormat = $highlightMatchFormat;

		if (is_array($failedParams)) {
			foreach ($failedParams as $paramKey => $categories) {
				foreach ($categories as $categoryKey => $failedRules) {
					foreach ($failedRules as $failedRule) {
						$rule = $failedRule['rule'];
						/** @var wfWAFRuleComparisonFailure $failedComparison */
						$failedComparison = $failedRule['failedComparison'];
						$action = $failedRule['action'];

						$paramKey = $failedComparison->getParamKey();
						if (preg_match('/request\.([a-z0-9]+)(?:\[(.*?)\](.*?))?$/i', $paramKey, $matches)) {
							$global = $matches[1];
							if (method_exists('wfWAFRequestInterface', "get" . ucfirst($global))) {
								$highlight = array(
									'match' => $failedComparison->getMatches(),
								);
								if (isset($matches[2])) {
									$highlight['param'] = "$matches[2]$matches[3]";
								}
								$highlights[$global][] = $highlight;
							}
						}
					}
				}
			}
		}

		$uri = $this->getURI();
		$queryStringPos = wfWAFUtils::strpos($uri, '?');
		if ($queryStringPos !== false) {
			$uri = wfWAFUtils::substr($uri, 0, $queryStringPos);
		}
		$queryString = $this->getQueryString();
		if ($queryString) {
			$uri .= '?' . http_build_query($queryString);
		}
		if (!empty($highlights['queryString'])) {
			foreach ($highlights['queryString'] as $matches) {
				if (!empty($matches['param'])) {
					$this->highlightMatches = $matches['match'];
					$uri = preg_replace_callback('/(&|\?|^)(' . preg_quote(urlencode($matches['param']), '/') . ')=(.*?)(&|$)/', array(
						$this, 'highlightParam',
					), $uri);
				}
			}
		}

		if (!empty($highlights['uri'])) {
			foreach ($highlights['uri'] as $matches) {
				if ($matches) {

				}
			}
			$uri = sprintf($highlightParamFormat, $uri);
		}

		$request = "{$this->getMethod()} $uri HTTP/1.1\n";
		$hasAuth = false;
		$auth = $this->getAuth();

		if (is_array($this->getHeaders())) {
			foreach ($this->getHeaders() as $header => $value) {
				switch (wfWAFUtils::strtolower($header)) {
					case 'cookie':
						// TODO: Hook up highlights to cookies
						$cookies = '';
						foreach ($this->getCookies() as $cookieName => $cookieValue) {
							$cookies .= $cookieName . '=' . urlencode($cookieValue) . '; ';
						}
						$request .= 'Cookie: ' . trim($cookies) . "\n";
						break;

					case 'host':
						$request .= 'Host: ' . $this->getHost() . "\n";
						break;

					case 'authorization':
						$hasAuth = true;
						if ($auth) {
							$request .= 'Authorization: Basic ' . base64_encode($auth['user'] . ':' . $auth['password']) . "\n";
						}
						break;

					default:
						$request .= $header . ': ' . $value . "\n";
						break;
				}
			}
		}

		if (!$hasAuth && $auth) {
			$request .= 'Authorization: Basic ' . base64_encode($auth['user'] . ':' . $auth['password']) . "\n";
		}

		$body = $this->getBody();
		$contentType = $this->getHeaders('Content-Type');
		if (is_array($body)) {
			if (wfWAFUtils::stripos($contentType, 'application/x-www-form-urlencoded') === 0) {
				$body = http_build_query($body);
				if (!empty($highlights['body'])) {
					foreach ($highlights['body'] as $matches) {
						if (!empty($matches['param'])) {
							$this->highlightMatches = $matches['match'];
							$body = preg_replace_callback('/(&|^)(' . preg_quote(urlencode($matches['param']), '/') . ')=(.*?)(&|$)/', array(
								$this, 'highlightParam',
							), $body);
						}
					}
				}
			} else if (preg_match('/^multipart\/form\-data; boundary=(.*?)$/i', $contentType, $boundaryMatches)) {
				$boundary = $boundaryMatches[1];
				$bodyArray = array();
				foreach ($body as $key => $value) {
					$bodyArray = array_merge($bodyArray, $this->reduceBodyParameter($key, $value));
				}
				$body = '';
				foreach ($bodyArray as $param => $value) {
					if (!empty($highlights['body'])) {
						foreach ($highlights['body'] as $matches) {
							if (!empty($matches['param']) && $matches['param'] === $param) {
								$value = sprintf($this->highlightParamFormat, $value);
								if (is_array($matches['match'][0])) {
									$replace = array();
									foreach ($matches['match'][0] as $key => $match) {
										$replace[$match] = sprintf($this->highlightMatchFormat, $match);
									}
									if ($replace) {
										$value = str_replace(array_keys($replace), $replace, $value);
									}
								} else { // preg_match
									$value = str_replace($matches['match'][0], sprintf($this->highlightMatchFormat, $matches['match'][0]), $value);
								}
								break;
							}
						}
					}

					$body .= <<<FORM
--$boundary
Content-Disposition: form-data; name="$param"

$value

FORM;
				}

				foreach ($this->getFiles() as $param => $file) {
					$name = array_key_exists('name', $file) ? $file['name'] : '';
					if (is_array($name)) {
						continue; // TODO: implement files as arrays
					}
					$mime = array_key_exists('type', $file) ? $file['type'] : '';
					$value = '';
					$lenToRead = $maxRequestLen - (wfWAFUtils::strlen($request) + wfWAFUtils::strlen($body) + 1);
					if (array_key_exists('content', $file)) {
						$value = $file['content'];
					} else if ($lenToRead > 0 && file_exists($file['tmp_name'])) {
						$handle = fopen($file['tmp_name'], 'r');
						$value = fread($handle, $lenToRead);
						fclose($handle);
					}

					if (!empty($highlights['fileNames'])) {
						foreach ($highlights['fileNames'] as $matches) {
							if (!empty($matches['param']) && $matches['param'] === $param) {
								$name = sprintf($this->highlightParamFormat, $name);
								$name = str_replace($matches['match'][0], sprintf($this->highlightMatchFormat, $matches['match'][0]), $name);
								break;
							}
						}
					}

					$body .= <<<FORM
--$boundary
Content-Disposition: form-data; name="$param"; filename="$name"
Content-Type: $mime
Expires: 0

$value

FORM;
				}

				if ($body) {
					$body .= "--$boundary--\n";
				}
			}
		}
		if (!is_string($body)) {
			$body = '';
		}

		$request .= "\n" . $body;

		if (wfWAFUtils::strlen($request) > $maxRequestLen) {
			$request = wfWAFUtils::substr($request, 0, $maxRequestLen);
		}
		return $request;
	}

	/**
	 * @param array $matches
	 * @return string
	 */
	private function highlightParam($matches) {
		$value = '';
		if (is_array($this->highlightMatches)) {
			// preg_match_all
			if (is_array($this->highlightMatches[0])) {
				$value = $matches[3];
				$replace = array();
				foreach ($this->highlightMatches[0] as $key => $match) {
					$this->highlightMatches[0][$key] = $this->callHighlightMatchFilter($match);
					$replace[] = sprintf($this->highlightMatchFormat, $this->callHighlightMatchFilter($match));
				}
				if ($replace) {
					$value = str_replace($this->highlightMatches[0], $replace, $value);
				}

			} else { // preg_match
				$param = $this->callHighlightMatchFilter($this->highlightMatches[0]);
				$value = str_replace($param, sprintf($this->highlightMatchFormat, $param), $matches[3]);
			}
		}
		if (wfWAFUtils::strlen($value) === 0) {
			$value = sprintf($this->highlightMatchFormat, $value);
		}

		return $matches[1] . sprintf($this->highlightParamFormat, $matches[2] . '=' . $value) . $matches[4];
	}

	/**
	 * @param $match
	 * @return mixed
	 */
	private function callHighlightMatchFilter($match) {
		return is_callable($this->highlightMatchFilter) ? call_user_func($this->highlightMatchFilter, $match) : $match;
	}

	/**
	 * @param string $key
	 * @param string|array $value
	 * @return array
	 */
	private function reduceBodyParameter($key, $value) {
		if (is_array($value)) {
			$param = array();
			foreach ($value as $index => $val) {
				$param = array_merge($param, $this->reduceBodyParameter("{$key}[$index]", $val));
			}
			return $param;
		}
		return array(
			$key => $value,
		);
	}

	/**
	 * @param mixed $auth
	 */
	public function setAuth($auth) {
		$this->auth = $auth;
	}

	/**
	 * @param mixed $body
	 */
	public function setBody($body) {
		$this->body = $body;
	}

	/**
	 * @param mixed $cookies
	 */
	public function setCookies($cookies) {
		$this->cookies = $cookies;
	}

	/**
	 * @param mixed $fileNames
	 */
	public function setFileNames($fileNames) {
		$this->fileNames = $fileNames;
	}

	/**
	 * @param mixed $files
	 */
	public function setFiles($files) {
		$this->files = $files;
	}

	/**
	 * @param mixed $headers
	 */
	public function setHeaders($headers) {
		$this->headers = $headers;
	}

	/**
	 * @param mixed $host
	 */
	public function setHost($host) {
		$this->host = $host;
	}

	/**
	 * @param mixed $ip
	 */
	public function setIP($ip) {
		$this->ip = $ip;
	}

	/**
	 * @param mixed $method
	 */
	public function setMethod($method) {
		$this->method = $method;
	}

	/**
	 * @param mixed $path
	 */
	public function setPath($path) {
		$this->path = $path;
	}

	/**
	 * @param mixed $protocol
	 */
	public function setProtocol($protocol) {
		$this->protocol = $protocol;
	}

	/**
	 * @param mixed $queryString
	 */
	public function setQueryString($queryString) {
		$this->queryString = $queryString;
	}

	/**
	 * @param mixed $timestamp
	 */
	public function setTimestamp($timestamp) {
		$this->timestamp = $timestamp;
	}

	/**
	 * @param mixed $uri
	 */
	public function setUri($uri) {
		$this->uri = $uri;
	}
}

