<?php

interface wfWAFRuleInterface {

	/**
	 * @return string
	 */
	public function render();

	public function renderRule();

	public function evaluate();
}

class wfWAFRuleException extends wfWAFException {
}

class wfWAFRuleLogicalOperatorException extends wfWAFException {
}

class wfWAFRule implements wfWAFRuleInterface {

	private $ruleID;
	private $type;
	private $category;
	private $score;
	private $description;
	private $whitelist;
	private $action;
	private $comparisonGroup;
	/**
	 * @var wfWAF
	 */
	private $waf;

	/**
	 * @param wfWAF $waf
	 * @param int $ruleID
	 * @param string $type
	 * @param string $category
	 * @param int $score
	 * @param string $description
	 * @param int $whitelist
	 * @param string $action
	 * @param wfWAFRuleComparisonGroup $comparisonGroup
	 * @return wfWAFRule
	 */
	public static function create() {
		$waf = func_get_arg(0);
		$ruleID = func_get_arg(1);
		$type = func_get_arg(2);
		$category = func_get_arg(3);
		$score = func_get_arg(4);
		$description = func_get_arg(5);
		$whitelist = 1;
		$action = '';
		$comparisonGroup = null;
		//Compatibility with old compiled rules
		if (func_num_args() == 8) { //Pre-whitelist flag
			$action = func_get_arg(6);
			$comparisonGroup = func_get_arg(7);
		}
		else if (func_num_args() == 9) { //Whitelist flag
			$whitelist = func_get_arg(6);
			$action = func_get_arg(7);
			$comparisonGroup = func_get_arg(8);
		}
		return new self($waf, $ruleID, $type, $category, $score, $description, $whitelist, $action, $comparisonGroup);
	}

	/**
	 * @param string $value
	 * @return string
	 */
	public static function exportString($value) {
		return sprintf("'%s'", str_replace("'", "\\'", $value));
	}

	/**
	 * @param wfWAF $waf
	 * @param int $ruleID
	 * @param string $type
	 * @param string $category
	 * @param int $score
	 * @param string $description
	 * @param int $whitelist
	 * @param string $action
	 * @param wfWAFRuleComparisonGroup $comparisonGroup
	 */
	public function __construct($waf, $ruleID, $type, $category, $score, $description, $whitelist, $action, $comparisonGroup) {
		$this->setWAF($waf);
		$this->setRuleID($ruleID);
		$this->setType($type);
		$this->setCategory($category);
		$this->setScore($score);
		$this->setDescription($description);
		$this->setWhitelist($whitelist);
		$this->setAction($action);
		$this->setComparisonGroup($comparisonGroup);
	}

	/**
	 * @return string
	 */
	public function render() {
		return sprintf('%s::create($this, %d, %s, %s, %s, %s, %d, %s, %s)', get_class($this),
			$this->getRuleID(),
			var_export($this->getType(), true),
			var_export($this->getCategory(), true),
			var_export($this->getScore(), true),
			var_export($this->getDescription(), true),
			var_export($this->getWhitelist(), true),
			var_export($this->getAction(), true),
			$this->getComparisonGroup()->render()
		);
	}

	/**
	 * @return string
	 */
	public function renderRule() {
		return sprintf(<<<RULE
if %s:
	%s(%s)
RULE
			,
			$this->getComparisonGroup()->renderRule(),
			$this->getAction(),
			join(', ', array_filter(array(
				$this->getRuleID() ? 'id=' . (int) $this->getRuleID() : '',
				$this->getCategory() ? 'category=' . self::exportString($this->getCategory()) : '',
				$this->getScore() > 0 ? 'score=' . (int) $this->getScore() : '',
				$this->getDescription() ? 'description=' . self::exportString($this->getDescription()) : '',
				$this->getWhitelist() == 0 ? 'whitelist=0' : '',
			)))
		);
	}

	public function evaluate() {
		$comparisons = $this->getComparisonGroup();
		$waf = $this->getWAF();
		if ($comparisons instanceof wfWAFRuleComparisonGroup && $waf instanceof wfWAF) {
			$comparisons->setRule($this);
			if ($comparisons->evaluate()) {
				$waf->tripRule($this);
				return true;
			}
		}
		return false;
	}

	public function debug() {
		return $this->getComparisonGroup()->debug();
	}

	/**
	 * For JSON.
	 *
	 * @return array
	 */
	public function toArray() {
		return array(
			'ruleID'      => $this->getRuleID(),
			'type'        => $this->getType(),
			'category'    => $this->getCategory(),
			'score'       => $this->getScore(),
			'description' => $this->getDescription(),
			'whitelist'   => $this->getWhitelist(),
			'action'      => $this->getAction(),
		);
	}

	/**
	 * @return int
	 */
	public function getRuleID() {
		return $this->ruleID;
	}

	/**
	 * @param int $ruleID
	 */
	public function setRuleID($ruleID) {
		$this->ruleID = $ruleID;
	}

	/**
	 * @return string
	 */
	public function getType() {
		return $this->type;
	}

	/**
	 * @param string $type
	 */
	public function setType($type) {
		$this->type = $type;
	}

	/**
	 * @return string
	 */
	public function getCategory() {
		return $this->category;
	}

	/**
	 * @param string $category
	 */
	public function setCategory($category) {
		$this->category = $category;
	}

	/**
	 * @return int
	 */
	public function getScore() {
		return $this->score;
	}

	/**
	 * @param int $score
	 */
	public function setScore($score) {
		$this->score = $score;
	}

	/**
	 * @return string
	 */
	public function getDescription() {
		return $this->description;
	}

	/**
	 * @param string $description
	 */
	public function setDescription($description) {
		$this->description = $description;
	}

	/**
	 * @return int
	 */
	public function getWhitelist() {
		return $this->whitelist;
	}

	/**
	 * @param string $whitelist
	 */
	public function setWhitelist($whitelist) {
		$this->whitelist = $whitelist;
	}

	/**
	 * @return string
	 */
	public function getAction() {
		return $this->action;
	}

	/**
	 * @param string $action
	 */
	public function setAction($action) {
		$this->action = $action;
	}

	/**
	 * @return wfWAFRuleComparisonGroup
	 */
	public function getComparisonGroup() {
		return $this->comparisonGroup;
	}

	/**
	 * @param wfWAFRuleComparisonGroup $comparisonGroup
	 */
	public function setComparisonGroup($comparisonGroup) {
		$this->comparisonGroup = $comparisonGroup;
	}

	/**
	 * @return wfWAF
	 */
	public function getWAF() {
		return $this->waf;
	}

	/**
	 * @param wfWAF $waf
	 */
	public function setWAF($waf) {
		$this->waf = $waf;
	}
}

class wfWAFRuleLogicalOperator implements wfWAFRuleInterface {

	/**
	 * @var string
	 */
	private $operator;

	/**
	 * @var array
	 */
	protected $validOperators = array(
		'||',
		'&&',
		'and',
		'or',
		'xor',
	);
	/**
	 * @var bool
	 */
	private $currentValue = false;
	/**
	 * @var wfWAFRuleInterface
	 */
	private $comparison;

	/**
	 * @param string $operator
	 * @param bool $currentValue
	 * @param wfWAFRuleInterface $comparison
	 */
	public function __construct($operator, $currentValue = false, $comparison = null) {
		$this->setOperator($operator);
		$this->setCurrentValue($currentValue);
		$this->setComparison($comparison);
	}

	/**
	 * @return string
	 * @throws wfWAFRuleLogicalOperatorException
	 */
	public function render() {
		if (!$this->isValid()) {
			throw new wfWAFRuleLogicalOperatorException(sprintf('Invalid logical operator "%s", must be one of %s', $this->getOperator(), join(", ", $this->validOperators)));
		}
		return sprintf("new %s(%s)", get_class($this), var_export(trim(wfWAFUtils::strtoupper($this->getOperator())), true));
	}

	/**
	 * @return string
	 * @throws wfWAFRuleLogicalOperatorException
	 */
	public function renderRule() {
		if (!$this->isValid()) {
			throw new wfWAFRuleLogicalOperatorException(sprintf('Invalid logical operator "%s", must be one of %s', $this->getOperator(), join(", ", $this->validOperators)));
		}
		return trim(wfWAFUtils::strtolower($this->getOperator()));
	}

	public function evaluate() {
		$currentValue = $this->getCurrentValue();
		$comparison = $this->getComparison();
		if (is_bool($currentValue) && $comparison instanceof wfWAFRuleInterface) {
			switch (wfWAFUtils::strtolower($this->getOperator())) {
				case '&&':
				case 'and':
					return $currentValue && $comparison->evaluate();

				case '||':
				case 'or':
					return $currentValue || $comparison->evaluate();

				case 'xor':
					return $currentValue xor $comparison->evaluate();
			}
		}
		return false;
	}

	/**
	 * @return bool
	 */
	public function isValid() {
		return in_array(wfWAFUtils::strtolower($this->getOperator()), $this->validOperators);
	}

	/**
	 * @return string
	 */
	public function getOperator() {
		return $this->operator;
	}

	/**
	 * @param string $operator
	 */
	public function setOperator($operator) {
		$this->operator = $operator;
	}

	/**
	 * @return boolean
	 */
	public function getCurrentValue() {
		return $this->currentValue;
	}

	/**
	 * @param boolean $currentValue
	 */
	public function setCurrentValue($currentValue) {
		$this->currentValue = $currentValue;
	}

	/**
	 * @return wfWAFRuleInterface
	 */
	public function getComparison() {
		return $this->comparison;
	}

	/**
	 * @param wfWAFRuleInterface $comparison
	 */
	public function setComparison($comparison) {
		$this->comparison = $comparison;
	}
}

class wfWAFRuleComparison implements wfWAFRuleInterface {

	private $matches;
	private $failedSubjects;
	private $result;
	/**
	 * @var wfWAFRule
	 */
	private $rule;

	protected static $allowedActions = array(
		'contains',
		'notcontains',
		'match',
		'notmatch',
		'matchcount',
		'containscount',
		'equals',
		'notequals',
		'identical',
		'notidentical',
		'greaterthan',
		'greaterthanequalto',
		'lessthan',
		'lessthanequalto',
		'lengthgreaterthan',
		'lengthlessthan',
		'currentuseris',
		'currentuserisnot',
		'md5equals',
		'filepatternsmatch',
		'filehasphp',
	);

	/**
	 * @var mixed
	 */
	private $expected;
	/**
	 * @var mixed
	 */
	private $subjects;
	/**
	 * @var string
	 */
	private $action;
	private $multiplier;
	/**
	 * @var wfWAF
	 */
	private $waf;

	/**
	 * @param wfWAF $waf
	 * @param string $action
	 * @param mixed $expected
	 * @param mixed $subjects
	 */
	public function __construct($waf, $action, $expected, $subjects = null) {
		$this->setWAF($waf);
		$this->setAction($action);
		$this->setExpected($expected);
		$this->setSubjects($subjects);
	}

	/**
	 * @param string|array $subject
	 * @return string
	 */
	public static function getSubjectKey($subject) {
		if (!is_array($subject)) {
			return (string) $subject;
		}
		$return = '';
		$global = array_shift($subject);
		foreach ($subject as $key) {
			$return .= '[' . $key . ']';
		}
		return $global . $return;
	}

	/**
	 * @return string
	 * @throws wfWAFRuleException
	 */
	public function render() {
		if (!$this->isActionValid()) {
			throw new wfWAFRuleException('Invalid action passed to ' . get_class($this) . ', action: ' . var_export($this->getAction(), true));
		}
		$subjectExport = '';
		/** @var wfWAFRuleComparisonSubject $subject */
		foreach ($this->getSubjects() as $subject) {
			$subjectExport .= $subject->render() . ",\n";
		}
		$subjectExport = 'array(' . wfWAFUtils::substr($subjectExport, 0, -2) . ')';

		$expected = $this->getExpected();
		return sprintf('new %s($this, %s, %s, %s)', get_class($this), var_export((string) $this->getAction(), true),
			($expected instanceof wfWAFRuleVariable ? $expected->render() : var_export($expected, true)), $subjectExport);
	}

	/**
	 * @return string
	 * @throws wfWAFRuleException
	 */
	public function renderRule() {
		if (!$this->isActionValid()) {
			throw new wfWAFRuleException('Invalid action passed to ' . get_class($this) . ', action: ' . var_export($this->getAction(), true));
		}
		$subjectExport = '';
		/** @var wfWAFRuleComparisonSubject $subject */
		foreach ($this->getSubjects() as $subject) {
			$subjectExport .= $subject->renderRule() . ", ";
		}
		$subjectExport = wfWAFUtils::substr($subjectExport, 0, -2);

		$expected = $this->getExpected();
		return sprintf('%s(%s, %s)', $this->getAction(),
			($expected instanceof wfWAFRuleVariable ? $expected->renderRule() : wfWAFRule::exportString($expected)),
			$subjectExport);
	}

	public function isActionValid() {
		return in_array(wfWAFUtils::strtolower($this->getAction()), self::$allowedActions);
	}

	public function evaluate() {
		if (!$this->isActionValid()) {
			return false;
		}
		$subjects = $this->getSubjects();
		if (!is_array($subjects)) {
			return false;
		}

		$this->result = false;
		/** @var wfWAFRuleComparisonSubject $subject */
		foreach ($subjects as $subject) {
			$global = $subject->getValue();
			$subjectKey = $subject->getKey();

			if ($this->_evaluate(array($this, $this->getAction()), $global, $subjectKey)) {
				$this->result = true;
			}
		}
		return $this->result;
	}

	/**
	 * @param callback $callback
	 * @param mixed $global
	 * @param string $subjectKey
	 * @return bool
	 */
	private function _evaluate($callback, $global, $subjectKey) {
		$result = false;

		if ($this->getWAF() && $this->getRule() &&
			$this->getWAF()->isRuleParamWhitelisted($this->getRule()->getRuleID(), $this->getWAF()->getRequest()->getPath(), $subjectKey)
		) {
			return $result;
		}

		if (is_array($global)) {
			foreach ($global as $key => $value) {
				if ($this->_evaluate($callback, $value, $subjectKey . '[' . $key . ']')) {
					$result = true;
				}
			}
		} else if (call_user_func($callback, $global)) {
			$result = true;
			$this->failedSubjects[] = array(
				'subject'    => $subjectKey,
				'value'      => $global,
				'multiplier' => $this->getMultiplier(),
				'matches'    => $this->getMatches(),
			);
		}
		return $result;
	}

	public function contains($subject) {
		if (is_array($this->getExpected())) {
			return in_array($this->getExpected(), $subject);
		}
		return wfWAFUtils::strpos((string) $subject, (string) $this->getExpected()) !== false;
	}

	public function notContains($subject) {
		return !$this->contains($subject);
	}

	public function match($subject) {
		return preg_match((string) $this->getExpected(), (string) $subject, $this->matches) > 0;
	}

	public function notMatch($subject) {
		return !$this->match($subject);
	}

	public function matchCount($subject) {
		$this->multiplier = preg_match_all((string) $this->getExpected(), (string) $subject, $this->matches);
		return $this->multiplier > 0;
	}

	public function containsCount($subject) {
		if (is_array($this->getExpected())) {
			$this->multiplier = 0;
			foreach ($this->getExpected() as $val) {
				if ($val == $subject) {
					$this->multiplier++;
				}
			}
			return $this->multiplier > 0;
		}
		$this->multiplier = wfWAFUtils::substr_count($subject, $this->getExpected());
		return $this->multiplier > 0;
	}

	public function equals($subject) {
		return $this->getExpected() == $subject;
	}

	public function notEquals($subject) {
		return $this->getExpected() != $subject;
	}

	public function identical($subject) {
		return $this->getExpected() === $subject;
	}

	public function notIdentical($subject) {
		return $this->getExpected() !== $subject;
	}

	public function greaterThan($subject) {
		return $subject > $this->getExpected();
	}

	public function greaterThanEqualTo($subject) {
		return $subject >= $this->getExpected();
	}

	public function lessThan($subject) {
		return $subject < $this->getExpected();
	}

	public function lessThanEqualTo($subject) {
		return $subject <= $this->getExpected();
	}

	public function lengthGreaterThan($subject) {
		return wfWAFUtils::strlen(is_array($subject) ? join('', $subject) : (string) $subject) > $this->getExpected();
	}

	public function lengthLessThan($subject) {
		return wfWAFUtils::strlen(is_array($subject) ? join('', $subject) : (string) $subject) < $this->getExpected();
	}

	public function currentUserIs($subject) {
		if ($authCookie = $this->getWAF()->parseAuthCookie()) {
			return $authCookie['role'] === $this->getExpected();
		}
		return false;
	}

	public function currentUserIsNot($subject) {
		return !$this->currentUserIs($subject);
	}

	public function md5Equals($subject) {
		return md5((string) $subject) === $this->getExpected();
	}
	
	public function filePatternsMatch($subject) {
		$request = $this->getWAF()->getRequest();
		$files = $request->getFiles();
		$patterns = $this->getWAF()->getMalwareSignatures();
		if (!is_array($patterns) || !is_array($files)) {
			return false;
		}
		
		foreach ($files as $file) {
			if ($file['name'] == (string) $subject) {
				$fh = @fopen($file['tmp_name'], 'r');
				if (!$fh) {
					continue;
				}
				$totalRead = 0;
				
				$readsize = max(min(10 * 1024 * 1024, wfWAFUtils::iniSizeToBytes(ini_get('upload_max_filesize'))), 1 * 1024 * 1024);
				while (!feof($fh)) {
					$data = fread($fh, $readsize);
					$totalRead += strlen($data);
					if ($totalRead < 1) {
						return false;
					}
				
					foreach ($patterns as $rule) {
						if (preg_match('/(' . $rule . ')/i', $data, $matches)) {
							return true;
						}
					}
				}	
			}
		}
		
		return false;
	}
	
	public function fileHasPHP($subject) {
		$request = $this->getWAF()->getRequest();
		$files = $request->getFiles();
		if (!is_array($files)) {
			return false;
		}
		
		foreach ($files as $file) {
			if ($file['name'] == (string) $subject) {
				$fh = @fopen($file['tmp_name'], 'r');
				if (!$fh) {
					continue;
				}
				
				$totalRead = 0;
				$insideOpenTag = false;
				$hasExecutablePHP = false;
				$possiblyHasExecutablePHP = false;
				$hasOpenParen = false;
				$hasCloseParen = false;
				$backtickCount = 0;
				$wrappedTokenCheckBytes = '';
				$maxTokenSize = 15; //__halt_compiler
				$possibleWrappedTokens = array('<?php', '<?=', '<?', '?>', 'exit', 'new', 'clone', 'echo', 'print', 'require', 'include', 'require_once', 'include_once', '__halt_compiler');
				
				$readsize = 100 * 1024; //100k at a time
				while (!feof($fh)) {
					$data = fread($fh, $readsize);
					$actualReadsize = strlen($data);
					$totalRead += $actualReadsize;
					if ($totalRead < 1) {
						break;
					}
					
					//Make sure we didn't miss PHP split over a chunking boundary
					$wrappedCheckLength = strlen($wrappedTokenCheckBytes);
					if ($wrappedCheckLength > 0) {
						$testBytes = $wrappedTokenCheckBytes . substr($data, 0, min($maxTokenSize, $actualReadsize));
						foreach ($possibleWrappedTokens as $t) {
							$position = strpos($testBytes, $t);
							if ($position !== false && $position < $wrappedCheckLength && $position + strlen($t) >= $wrappedCheckLength) { //Found a token that starts before this segment of data and ends within it
								$data = substr($wrappedTokenCheckBytes, $position) . $data;
								break;
							}
						}
					}
					
					//Make sure it tokenizes correctly if chunked
					if ($insideOpenTag) {
						if ($possiblyHasExecutablePHP) {
							$data = '<?= ' . $data; 
						}
						else {
							$data = '<?php ' . $data;
						}
					}
					
					//Tokenize the data and check for PHP
					$this->_resetErrors();
					$tokens = @token_get_all($data);
					$error = error_get_last();
					if ($error !== null && stripos($error['message'], 'Unexpected character in input') !== false) {
						break;
					}
					
					if ($error !== null && feof($fh) && stripos($error['message'], 'Unterminated comment') !== false) {
						break;
					}
					
					$offset = 0;
					foreach ($tokens as $token) {
						if (is_array($token)) {
							$offset += strlen($token[1]);
							switch ($token[0]) {
								case T_OPEN_TAG:
									$insideOpenTag = true;
									$hasOpenParen = false;
									$hasCloseParen = false;
									$backtickCount = 0;
									$possiblyHasExecutablePHP = false;
									
									if ($error !== null && stripos($error['message'], 'Unterminated comment') !== false) {
										$testOffset = $offset - strlen($token[1]);
										$commentStart = strpos($data, '/*', $testOffset);
										if ($commentStart !== false) {
											$testBytes = substr($data, $testOffset, $commentStart - $testOffset);
											$this->_resetErrors();
											@token_get_all($testBytes);
											$error = error_get_last();
											if ($error !== null && stripos($error['message'], 'Unexpected character in input') !== false) {
												break 3;
											}
										}
									}
									
									break;
								
								case T_OPEN_TAG_WITH_ECHO:
									$insideOpenTag = true;
									$hasOpenParen = false;
									$hasCloseParen = false;
									$backtickCount = 0;
									$possiblyHasExecutablePHP = true;
									
									if ($error !== null && stripos($error['message'], 'Unterminated comment') !== false) {
										$testOffset = $offset - strlen($token[1]);
										$commentStart = strpos($data, '/*', $testOffset);
										if ($commentStart !== false) {
											$testBytes = substr($data, $testOffset, $commentStart - $testOffset);
											$this->_resetErrors();
											@token_get_all($testBytes);
											$error = error_get_last();
											if ($error !== null && stripos($error['message'], 'Unexpected character in input') !== false) {
												break 3;
											}
										}
									}
									
									break;
								
								case T_CLOSE_TAG:
									$insideOpenTag = false;
									if ($possiblyHasExecutablePHP) {
										$hasExecutablePHP = true; //Assume the echo short tag outputted something useful
									}
									break 2;
									
								case T_NEW:
								case T_CLONE:
								case T_ECHO:
								case T_PRINT:
								case T_REQUIRE:
								case T_INCLUDE:
								case T_REQUIRE_ONCE:
								case T_INCLUDE_ONCE:
								case T_HALT_COMPILER:
								case T_EXIT:
									$hasExecutablePHP = true;
									break 2;
							}
						}
						else {
							$offset += strlen($token);
							switch ($token) {
								case '(':
									$hasOpenParen = true;
									break;
								case ')':
									$hasCloseParen = true;
									break;
								case '`':
									$backtickCount++;
									break;
							}
						}
						if (!$hasExecutablePHP && (($hasOpenParen && $hasCloseParen) || ($backtickCount > 1 && $backtickCount % 2 === 0))) {
							$hasExecutablePHP = true;
							break;
						}
					}
					
					if ($hasExecutablePHP) {
						fclose($fh);
						return true;
					}
					
					$wrappedTokenCheckBytes = substr($data, - min($maxTokenSize, $actualReadsize)); 
				}
				
				fclose($fh);
			}
		}
		
		return false;
	}
	
	private function _resetErrors() {
		if (function_exists('error_clear_last')) {
			error_clear_last();
		}
		else {
			// set error_get_last() to defined state by forcing an undefined variable error
			set_error_handler(array($this, '_resetErrorsHandler'), 0);
			@$undefinedVariable;
			restore_error_handler();
		}
	}
	
	public function _resetErrorsHandler($errno, $errstr, $errfile, $errline) {
		//Do nothing
	}

	/**
	 * @return mixed
	 */
	public function getAction() {
		return $this->action;
	}

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

	/**
	 * @return mixed
	 */
	public function getExpected() {
		return $this->expected;
	}

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

	/**
	 * @return mixed
	 */
	public function getSubjects() {
		return $this->subjects;
	}

	/**
	 * @param mixed $subjects
	 * @return $this
	 */
	public function setSubjects($subjects) {
		$this->subjects = $subjects;
		return $this;
	}

	/**
	 * @return mixed
	 */
	public function getMatches() {
		return $this->matches;
	}

	/**
	 * @return mixed
	 */
	public function getFailedSubjects() {
		return $this->failedSubjects;
	}

	/**
	 * @return mixed
	 */
	public function getResult() {
		return $this->result;
	}

	/**
	 * @return mixed
	 */
	public function getMultiplier() {
		return $this->multiplier;
	}

	/**
	 * @return wfWAF
	 */
	public function getWAF() {
		return $this->waf;
	}

	/**
	 * @param wfWAF $waf
	 */
	public function setWAF($waf) {
		$this->waf = $waf;
	}

	/**
	 * @return wfWAFRule
	 */
	public function getRule() {
		return $this->rule;
	}

	/**
	 * @param wfWAFRule $rule
	 */
	public function setRule($rule) {
		$this->rule = $rule;
	}
}

class wfWAFRuleComparisonGroup implements wfWAFRuleInterface {

	private $items = array();
	private $failedComparisons = array();
	private $result = false;
	/**
	 * @var wfWAFRule
	 */
	private $rule;

	public function __construct() {
		$args = func_get_args();
		foreach ($args as $arg) {
			$this->add($arg);
		}
	}

	public function add($item) {
		$this->items[] = $item;
	}

	public function remove($item) {
		$key = array_search($item, $this->items);
		if ($key !== false) {
			unset($this->items[$key]);
		}
	}

	/**
	 *
	 * @throws wfWAFRuleException
	 */
	public function evaluate() {
		if (count($this->items) % 2 != 1) {
			throw new wfWAFRuleException('Invalid number of rules and logical operators.  Should be odd number of rules and logical operators.');
		}

		$this->result = false;
		$operator = null;
		/** @var wfWAFRuleComparison|wfWAFRuleLogicalOperator|wfWAFRuleComparisonGroup $comparison */
		for ($i = 0; $i < count($this->items); $i++) {
			$comparison = $this->items[$i];
			if ($i % 2 == 1 && !($comparison instanceof wfWAFRuleLogicalOperator)) {
				throw new wfWAFRuleException('Invalid WAF rule format, expected wfWAFRuleLogicalOperator, got ' . get_class($comparison));
			}
			if ($i % 2 == 0 && !($comparison instanceof wfWAFRuleComparison || $comparison instanceof wfWAFRuleComparisonGroup)) {
				throw new wfWAFRuleException('Invalid WAF rule format, expected wfWAFRuleComparison or wfWAFRuleComparisonGroup, got ' . get_class($comparison));
			}

			if ($comparison instanceof wfWAFRuleLogicalOperator) {
				$operator = $comparison;
				continue;
			}
			if ($comparison instanceof wfWAFRuleComparison || $comparison instanceof wfWAFRuleComparisonGroup) {
				$comparison->setRule($this->getRule());
				if ($operator instanceof wfWAFRuleLogicalOperator) {
					$operator->setCurrentValue($this->result);
					$operator->setComparison($comparison);
					$this->result = $operator->evaluate();
				} else {
					$this->result = $comparison->evaluate();
				}
			}
			if ($comparison instanceof wfWAFRuleComparison && $comparison->getResult()) {
				foreach ($comparison->getFailedSubjects() as $failedSubject) {
					$this->failedComparisons[] = new wfWAFRuleComparisonFailure(
						$failedSubject['subject'], $failedSubject['value'], $comparison->getExpected(),
						$comparison->getAction(), $failedSubject['multiplier'], $failedSubject['matches']
					);
				}
			}
			if ($comparison instanceof wfWAFRuleComparisonGroup && $comparison->getResult()) {
				foreach ($comparison->getFailedComparisons() as $comparisonFail) {
					$this->failedComparisons[] = $comparisonFail;
				}
			}
		}
		return $this->result;
	}

	/**
	 * @return string
	 * @throws wfWAFRuleException
	 */
	public function render() {
		if (count($this->items) % 2 != 1) {
			throw new wfWAFRuleException('Invalid number of rules and logical operators.  Should be odd number of rules and logical operators.');
		}

		$return = array();
		/**
		 * @var wfWAFRuleInterface $item
		 */
		for ($i = 0; $i < count($this->items); $i++) {
			$item = $this->items[$i];
			if ($i % 2 == 1 && !($item instanceof wfWAFRuleLogicalOperator)) {
				throw new wfWAFRuleException('Invalid WAF rule format, expected wfWAFRuleLogicalOperator, got ' . get_class($item));
			}
			if ($i % 2 == 0 && !($item instanceof wfWAFRuleComparison || $item instanceof wfWAFRuleComparisonGroup)) {
				throw new wfWAFRuleException('Invalid WAF rule format, expected wfWAFRule or wfWAFRuleComparisonGroup, got ' . get_class($item));
			}
			$return[] = $item->render();
		}
		return sprintf('new %s(%s)', get_class($this), join(', ', $return));
	}

	/**
	 * @return string
	 * @throws wfWAFRuleException
	 */
	public function renderRule() {
		if (count($this->items) % 2 != 1) {
			throw new wfWAFRuleException('Invalid number of rules and logical operators.  Should be odd number of rules and logical operators.');
		}

		$return = array();
		/**
		 * @var wfWAFRuleInterface $item
		 */
		for ($i = 0; $i < count($this->items); $i++) {
			$item = $this->items[$i];
			if ($i % 2 == 1 && !($item instanceof wfWAFRuleLogicalOperator)) {
				throw new wfWAFRuleException('Invalid WAF rule format, expected wfWAFRuleLogicalOperator, got ' . get_class($item));
			}
			if ($i % 2 == 0 && !($item instanceof wfWAFRuleComparison || $item instanceof wfWAFRuleComparisonGroup)) {
				throw new wfWAFRuleException('Invalid WAF rule format, expected wfWAFRule or wfWAFRuleComparisonGroup, got ' . get_class($item));
			}
			$return[] = $item->renderRule();
		}
		return sprintf('(%s)', join(' ', $return));
	}

	public function debug() {
		$debug = '';
		/** @var wfWAFRuleComparisonFailure $failedComparison */
		foreach ($this->getFailedComparisons() as $failedComparison) {
			$debug .= $failedComparison->getParamKey() . ' ' . $failedComparison->getAction() . ' ' . $failedComparison->getExpected() . "\n";
		}
		return $debug;
	}

	/**
	 * @return array
	 */
	public function getItems() {
		return $this->items;
	}

	/**
	 * @param array $items
	 */
	public function setItems($items) {
		$this->items = $items;
	}

	/**
	 * @return mixed
	 */
	public function getFailedComparisons() {
		return $this->failedComparisons;
	}

	/**
	 * @return boolean
	 */
	public function getResult() {
		return $this->result;
	}

	/**
	 * @return wfWAFRule
	 */
	public function getRule() {
		return $this->rule;
	}

	/**
	 * @param wfWAFRule $rule
	 */
	public function setRule($rule) {
		$this->rule = $rule;
	}
}

class wfWAFRuleComparisonFailure {

	private $paramKey;
	private $expected;
	private $action;
	/**
	 * @var null|int
	 */
	private $multiplier;
	/**
	 * @var string
	 */
	private $paramValue;
	/**
	 * @var mixed
	 */
	private $matches;

	/**
	 * @param string $paramKey
	 * @param string $paramValue
	 * @param string $expected
	 * @param string $action
	 * @param mixed $multiplier
	 * @param mixed $matches
	 */
	public function __construct($paramKey, $paramValue, $expected, $action, $multiplier = null, $matches = null) {
		$this->setParamKey($paramKey);
		$this->setExpected($expected);
		$this->setAction($action);
		$this->setMultiplier($multiplier);
		$this->setParamValue($paramValue);
		$this->setMatches($matches);
	}

	/**
	 * @return mixed
	 */
	public function getParamKey() {
		return $this->paramKey;
	}

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

	/**
	 * @return mixed
	 */
	public function getExpected() {
		return $this->expected;
	}

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

	/**
	 * @return mixed
	 */
	public function getAction() {
		return $this->action;
	}

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

	/**
	 * @return int|null
	 */
	public function getMultiplier() {
		return $this->multiplier;
	}

	/**
	 * @param int|null $multiplier
	 */
	public function setMultiplier($multiplier) {
		$this->multiplier = $multiplier;
	}

	/**
	 * @return bool
	 */
	public function hasMultiplier() {
		return $this->getMultiplier() > 1;
	}

	/**
	 * @return string
	 */
	public function getParamValue() {
		return $this->paramValue;
	}

	/**
	 * @param string $paramValue
	 */
	public function setParamValue($paramValue) {
		$this->paramValue = $paramValue;
	}

	/**
	 * @return mixed
	 */
	public function getMatches() {
		return $this->matches;
	}

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

class wfWAFRuleComparisonSubject {

	/**
	 * @var array
	 */
	private $subject;
	/**
	 * @var array
	 */
	private $filters;

	/** @var wfWAF */
	private $waf;

	public static function create($waf, $subject, $filters) {
		return new self($waf, $subject, $filters);
	}

	/**
	 * wfWAFRuleComparisonSubject constructor.
	 * @param wfWAF $waf
	 * @param array $subject
	 * @param array $filters
	 */
	public function __construct($waf, $subject, $filters) {
		$this->waf = $waf;
		$this->subject = $subject;
		$this->filters = $filters;
	}

	/**
	 * @return mixed|null
	 */
	public function getValue() {
		$subject = $this->getSubject();
		if (!is_array($subject)) {
			return $this->runFilters($this->getWAF()->getGlobal($subject));
		}
		if (is_array($subject) && count($subject) > 0) {
			$globalKey = array_shift($subject);
			return $this->runFilters($this->_getValue($subject, $this->getWAF()->getGlobal($globalKey)));
		}
		return null;
	}

	/**
	 * @param array $subjectKey
	 * @param array $global
	 * @return null
	 */
	private function _getValue($subjectKey, $global) {
		if (!is_array($global) || !is_array($subjectKey)) {
			return null;
		}

		$key = array_shift($subjectKey);
		if (array_key_exists($key, $global)) {
			if (count($subjectKey) > 0) {
				return $this->_getValue($subjectKey, $global[$key]);
			} else {
				return $global[$key];
			}
		}
		return null;
	}


	/**
	 * @return string
	 */
	public function getKey() {
		return wfWAFRuleComparison::getSubjectKey($this->getSubject());
	}

	/**
	 * @param mixed $value
	 * @return mixed
	 */
	private function runFilters($value) {
		$filters = $this->getFilters();
		if (is_array($filters)) {
			foreach ($filters as $filter) {
				if (method_exists($this, 'filter' . $filter)) {
					$value = call_user_func(array($this, 'filter' . $filter), $value);
				}
			}
		}
		return $value;
	}

	/**
	 * @param mixed $value
	 * @return string
	 */
	public function filterBase64decode($value) {
		if (is_string($value)) {
			return base64_decode($value);
		}
		return $value;
	}

	/**
	 * @return string
	 */
	public function render() {
		return sprintf('%s::create($this, %s, %s)', get_class($this), var_export($this->getSubject(), true),
			var_export($this->getFilters(), true));
	}

	/**
	 * @return string
	 */
	public function renderRule() {
		$subjects = $this->getSubject();
		if (is_array($subjects)) {
			if (strpos($subjects[0], '.') !== false) {
				list($superGlobal, $global) = explode('.', $subjects[0], 2);
				unset($subjects[0]);
				$subjects = array_merge(array($superGlobal, $global), $subjects);
			}
			$rule = '';
			foreach ($subjects as $subject) {
				if (preg_match("/^[a-zA-Z_][a-zA-Z0-9_]*$/", $subject)) {
					$rule .= "$subject.";
				} else {
					$rule = rtrim($rule, '.');
					$rule .= sprintf("['%s']", str_replace("'", "\\'", $subject));
				}
			}
			$rule = rtrim($rule, '.');
		} else {
			$rule = $this->getSubject();
		}

		foreach ($this->getFilters() as $filter) {
			$rule = $filter . '(' . $rule . ')';
		}
		return $rule;
	}

	/**
	 * @return array
	 */
	public function getSubject() {
		return $this->subject;
	}

	/**
	 * @param array $subject
	 */
	public function setSubject($subject) {
		$this->subject = $subject;
	}

	/**
	 * @return array
	 */
	public function getFilters() {
		return $this->filters;
	}

	/**
	 * @param array $filters
	 */
	public function setFilters($filters) {
		$this->filters = $filters;
	}

	/**
	 * @return wfWAF
	 */
	public function getWAF() {
		return $this->waf;
	}

	/**
	 * @param wfWAF $waf
	 */
	public function setWAF($waf) {
		$this->waf = $waf;
	}
}
