src/AppserverIo/WebServer/Modules/Rewrite/Entities/Condition.php
<?php
/**
* \AppserverIo\WebServer\Modules\Rewrite\Entities\Condition
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is available through the world-wide-web at this URL:
* http://opensource.org/licenses/osl-3.0.php
*
* PHP version 5
*
* @author Bernhard Wick <bw@appserver.io>
* @copyright 2017 TechDivision GmbH <info@appserver.io>
* @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
* @link https://github.com/appserver-io/webserver
* @link http://www.appserver.io/
*/
namespace AppserverIo\WebServer\Modules\Rewrite\Entities;
use AppserverIo\WebServer\Modules\Rewrite\Dictionaries\ConditionActions;
use AppserverIo\Server\Dictionaries\ServerVars;
use AppserverIo\WebServer\Modules\Rewrite\Dictionaries\RuleFlags;
/**
* Class Condition
*
* This class provides an object based representation of a rewrite rules condition including logic for checking itself.
*
* @author Bernhard Wick <bw@appserver.io>
* @copyright 2017 TechDivision GmbH <info@appserver.io>
* @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
* @link https://github.com/appserver-io/webserver
* @link http://www.appserver.io/
*/
class Condition
{
/**
* The allowed values for the $types member
*
* @var string[] $allowedTypes
*/
protected $allowedTypes = array();
/**
* All possible modifiers aka flags
*
* @var string[] $allowedModifiers
*/
protected $allowedModifiers = array();
/**
* Possible additions to the known PCRE regex.
* These additions get used by htaccess notation only.
*
* @var string[] $htaccessAdditions
*/
protected $htaccessAdditions = array();
/**
* The type of this condition
*
* @var string $type
*/
protected $type;
/**
* The value to check with the given action
*
* @var string $operand
*/
protected $operand;
/**
* The original operand as given on instantiation, kept for better re-iteration
*
* @var string $originalOperand
*/
protected $originalOperand;
/**
* In some cases, e.g.
* string comparison or regex, we need another operand to work with
*
* @var string $additionalOperand
*/
protected $additionalOperand;
/**
* How the operand has to be checked, this will hold the needed action as a string and cannot be
* processed automatically.
*
* @var string $action
*/
protected $action;
/**
* Modifier which should be used to integrate things like apache flags and others
*
* @var string[] $modifiers
*/
protected $modifiers = array();
/**
* At least in the apache universe we can negate the logical meaning with a "!"
*
* @var boolean $isNegated
*/
protected $isNegated;
/**
* Default constructor
*
* @param string $operand The value to check with the given action
* @param string $action How the operand has to be checked, this will hold the needed action
* @param string $flags Flags which will be filtered into valid modifiers for our conditions
*
* @throws \InvalidArgumentException
*/
public function __construct($operand, $action, $flags = array())
{
// Fill the default values for our members here
$this->allowedTypes = array(
'regex',
'check'
);
$this->htaccessAdditions = array(
ConditionActions::STR_LESS,
ConditionActions::STR_GREATER,
ConditionActions::STR_EQUAL,
ConditionActions::IS_DIR,
ConditionActions::IS_FILE,
ConditionActions::IS_USED_FILE,
ConditionActions::IS_LINK,
ConditionActions::IS_EXECUTABLE
);
$this->allowedModifiers = array_flip(
array(
RuleFlags::NOCASE
)
);
$this->modifiers = array();
// We do not negate by default, nor do we combine with the following condition via "or"
$this->isNegated = false;
// Check if the passed modifier is valid (or empty)
foreach ($flags as $flag) {
if (isset($this->allowedModifiers[$flag])) {
$this->modifiers[] = $flag;
}
}
// Fill the more important properties
$this->originalOperand = $operand;
$this->operand = $this->originalOperand;
$this->action = $action;
$this->additionalOperand = '';
// Check if we have a negation
$this->preparePossibleNegation();
// check what type we have. Per default it's regex
$this->prepareOperandAdditions();
// If we got a regex we have to re-organize a few things
if ($this->type !== 'check') {
// we have to set the type correctly, collect the regex as additional operand and set the regex flag as
// action to allow proper switching of functions later
$this->type = 'regex';
$this->additionalOperand = $this->action;
$this->action = ConditionActions::REGEX;
}
}
/**
* Checks if we are we able to find any of the additions htaccess syntax offers.
*
* @return null
*/
protected function prepareOperandAdditions()
{
foreach ($this->htaccessAdditions as $addition) {
// The string has to start with an addition (any negating ! was cut of before)
if (strpos($this->action, $addition) === 0) {
// If we have a string comparing action we have to cut it to know what to compare to, otherwise we
// need the document root as an additional operand
$tmp = substr($this->action, 0, 1);
if ($tmp === '<' || $tmp === '>' || $tmp === '=') {
// We have to extract the needed parts of our operation and refill it into
// our additional operand string
$this->additionalOperand = substr($this->action, 1);
$this->action = substr($this->action, 0, 1);
}
// If we reach this point we are of the check type
$this->type = 'check';
break;
}
}
}
/**
* Will check if we have a possible negation and act accordingly
*
* @return null
*/
protected function preparePossibleNegation()
{
// Check if we have a negation
if (strpos($this->action, '!') === 0) {
// Tell them we have to negate the check
$this->isNegated = true;
// Remove the "!" as it might kill the regex otherwise
$this->action = ltrim($this->action, '!');
}
}
/**
* Getter for the $type member
*
* @return string
*/
public function getType()
{
return $this->type;
}
/**
* Getter for the $operand member
*
* @return string
*/
public function getOperand()
{
return $this->operand;
}
/**
* Getter for the $modifiers member
*
* @return string
*/
public function getModifiers()
{
return $this->modifiers;
}
/**
* Will resolve the directive's parts by substituting placeholders with the corresponding backreferences
*
* @param array $backreferences The backreferences used for resolving placeholders
*
* @return void
*/
public function resolve(array $backreferences)
{
// Separate the keys from the values so we can use them in str_replace
$backreferenceHolders = array_keys($backreferences);
$backreferenceValues = array_values($backreferences);
// Substitute the backreferences in our operand and additionalOperand
$this->operand = str_replace($backreferenceHolders, $backreferenceValues, $this->originalOperand);
// prepare our operand to be usable for filesystem checks
$this->prepareFilesystemOperand();
$this->additionalOperand = str_replace($backreferenceHolders, $backreferenceValues, $this->additionalOperand);
}
/**
* Will return true if the condition is true, false if not
*
* @return boolean
* @throws \InvalidArgumentException
*/
public function matches()
{
// Switching between different actions we have to take.
// Using an if cascade as it seems to be faster than switch...case
$result = false;
if ($this->action === ConditionActions::REGEX) {
// Get the result for a regex
$modifiers = array_flip($this->modifiers);
$modifier = isset($modifiers[RuleFlags::NOCASE])?'i':'';
$result = preg_match('`' . $this->additionalOperand . '`' . $modifier, $this->operand) === 1;
} elseif ($this->action === ConditionActions::IS_DIR) {
// Is it an existing directory?
$result = is_dir($this->additionalOperand . $this->operand);
} elseif ($this->action === ConditionActions::IS_EXECUTABLE) {
// Is the file an executable?
$result = is_executable($this->additionalOperand . $this->operand);
} elseif ($this->action === ConditionActions::IS_FILE) {
// Is it a regular file?
$result = is_file($this->additionalOperand . $this->operand);
} elseif ($this->action === ConditionActions::IS_LINK) {
// Is it a symlink?
$result = is_link($this->additionalOperand . $this->operand);
} elseif ($this->action === ConditionActions::IS_USED_FILE) {
// Is it a real file which has a size greater 0?
$result = (is_file($this->additionalOperand . $this->operand) && (int) filesize($this->additionalOperand . $this->operand) > 0);
} elseif ($this->action === ConditionActions::STR_EQUAL) {
// Or the compared strings equal
$result = strcmp($this->operand, $this->additionalOperand) == 0;
} elseif ($this->action === ConditionActions::STR_GREATER) {
// Is the operand bigger?
$result = strcmp($this->operand, $this->additionalOperand) > 0;
} elseif ($this->action === ConditionActions::STR_LESS) {
// Is the operand smaller?
$result = strcmp($this->operand, $this->additionalOperand) < 0;
}
// If the check got negated we will just negate what we got from our preceding checks
if ($this->isNegated) {
$result = ! $result;
}
return $result;
}
/**
* Will prepare a filesystem enabled operand by cutting of any traces of a query string
*
* @return null
*/
protected function prepareFilesystemOperand()
{
if ($this->action === ConditionActions::IS_DIR || $this->action === ConditionActions::IS_EXECUTABLE || $this->action === ConditionActions::IS_FILE || $this->action === ConditionActions::IS_LINK || $this->action === ConditionActions::IS_USED_FILE) {
if (strpos($this->operand, '?') !== false) {
$this->operand = strstr($this->operand, '?', true);
}
if (! is_readable($this->additionalOperand . $this->operand)) {
// Set the placeholder for the document root, it will be resolved anyway
// If we got ourselves a complete path, we do not need the document root
$this->additionalOperand = '$' . ServerVars::DOCUMENT_ROOT;
}
}
}
/**
* Will collect all backreferences based on regex typed conditions
*
* @return array
*/
public function getBackreferences()
{
$backreferences = array();
$matches = array();
if ($this->type === 'regex') {
preg_match('`' . $this->additionalOperand . '`', $this->operand, $matches);
// Unset the first find of our backreferences, so we can use it automatically
unset($matches[0]);
}
// Iterate over all our found matches and give them a fine name
foreach ($matches as $key => $match) {
$backreferences['$' . (string) $key] = $match;
}
return $backreferences;
}
}