File: /var/www/Gosurya/WP2/wp-content/plugins/akeebabackupwp/app/Awf/Router/Rule.php
<?php
/**
* @package awf
* @copyright Copyright (c)2014-2021 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 3 or later
*/
namespace Awf\Router;
use Awf\Uri\Uri;
class Rule
{
/**
* The routing path to use
*
* @var string
*/
protected $path = '';
/**
* The named variable types
*
* @var array
*/
protected $types = array();
/**
* The match vars that have to be satisfied for this rule
*
* @var array
*/
protected $matchVars = array();
/**
* The push vars to use when parsing this rule
*
* @var array
*/
protected $pushVars = array();
/**
* The routing callback to use instead of the built-in router
*
* @var callable|null
*/
protected $routeCallable = null;
/**
* The route parsing callback to use instead of the built-in parser
*
* @var callable|null
*/
protected $parseCallable = null;
/**
* Should I use a callback for routing URLs?
*
* @var boolean
*/
protected $useCallableForRoute = false;
/**
* Should I use a callback for parsing URLs?
*
* @var boolean
*/
protected $useCallableForParse = false;
/**
* Create a routing rule, optionally initialising it from the $definition array. The known keys are:
* - path @see \Awf\Route\Rule::setPath
* - types @see \Awf\Route\Rule::setTypes
* - matchVars @see \Awf\Route\Rule::setMatchVars
* - pushVars @see \Awf\Route\Rule::setPushVars
* - routeCallable @see \Awf\Route\Rule::setRouteCallable
* - parseCallable @see \Awf\Route\Rule::setParseCallable
*
* @param array $definition See above
*
* @return Rule
*
* @codeCoverageIgnore
*/
public function __construct($definition = array())
{
$knownKeys = array('path', 'types', 'matchVars', 'pushVars', 'routeCallable', 'parseCallable');
foreach ($knownKeys as $key)
{
if (isset($definition[$key]))
{
$method = 'set' . ucfirst($key);
$this->$method($definition[$key]);
}
}
}
/**
* Routes a non-SEF URL to its SEF counterpart.
*
* If this rule is not applicable to this URL we return null
*
* If this rule applies to this URL we return a hash array with the keys 'segments' and 'vars' containing the SEF
* URL's paths and any remaining query string parameters respectively.
*
* @param string $url The non-SEF URL
*
* @return null|array
*/
public function route($url)
{
if ($this->useCallableForRoute)
{
return call_user_func($this->routeCallable, $url);
}
else
{
// Extract the query parameters
$uri = new Uri($url);
$params = $uri->getQuery(true);
// Make sure the "match variables" do match
if (!$this->matchesVars($params))
{
return null;
}
// Get the route segments
$segments = $this->buildRoute($params);
// If we got null instead of segments we are not an applicable router to this URL
if (is_null($segments))
{
return null;
}
// Otherwise return the path segments and the remaining vars
return array(
'segments' => $segments,
'vars' => $params,
);
}
}
/**
* Parse a SEF URL path into URL parameters
*
* @param string $path The path to parse, e.g. /foo/bar/1/2/3
*
* @return array|null The URL parameters or null if we can't parse the route with this rule
*/
public function parse($path)
{
$extraParams = array();
if (strpos($path, '?') !== false)
{
$uri = new Uri($path);
$path = $uri->getPath();
$extraParams = $uri->getQuery(true);
}
if ($this->useCallableForParse)
{
$ret = call_user_func($this->parseCallable, $path);
if (is_null($ret))
{
return null;
}
}
else
{
// Explode the path parts
$segments = explode('/', $path);
// Try to extract the URL parameters by parsing the segments
$params = $this->parseRoute($segments);
// If we got null back we can't parse this route
if (is_null($params))
{
return null;
}
// Mix in the push variables
$params = array_merge($this->pushVars, $params);
// Return the URL parameters
$ret = $params;
}
return array_merge($ret, $extraParams);
}
/**
* Set the "match variables" for this routing rule. These variables must be present in the URL being routed for this
* routing rule to be used. You have to provide a hashed array, e.g.
* array( 'foo' => 1, 'bar' => null )
*
* When a match variable has a non-null value, this exact value MUST be present in the URL to trigger this rule.
* Moreover this variable will be removed from the query parameters used for building the route.
*
* When a match variable has a null value, this rule will be triggered if the variable is present in the URL, no
* matter what its value is. The variable will be available to the query parameters used for building the route.
*
* @param array $matchVars
*
* @return void
*
* @codeCoverageIgnore
*/
public function setMatchVars($matchVars)
{
$this->matchVars = $matchVars;
}
/**
* Get the "match variables" for this routing rule
*
* @return array
*
* @codeCoverageIgnore
*/
public function getMatchVars()
{
return $this->matchVars;
}
/**
* Set the callable to use for parsing routes. The callable must return null if it can't parse the URL (it's not
* applicable) or an array of the same format as the Rule::parse() method.
*
* @param callable|null $parseCallable
*
* @return void
*/
public function setParseCallable($parseCallable)
{
$this->useCallableForParse = !(is_null($parseCallable) || empty($parseCallable));
$this->parseCallable = $parseCallable;
}
/**
* Get the callable to use for parsing routes
*
* @return null|callable
*
* @codeCoverageIgnore
*/
public function getParseCallable()
{
return $this->parseCallable;
}
/**
* Set the "push variables" for routing rule. These variables are pushed to the input when a URL is successfully
* parsed by this rule. You have to provide a hash array, e.g.
* array( 'foo' => 1, 'bar' => 2 )
*
* @param array $pushVars The push variables to set
*
* @return void
*
* @codeCoverageIgnore
*/
public function setPushVars($pushVars)
{
$this->pushVars = $pushVars;
}
/**
* Get the "push variables" for this routing rule
*
* @return array
*
* @codeCoverageIgnore
*/
public function getPushVars()
{
return $this->pushVars;
}
/**
* Set the callable to use for routing a URL using this rule. The callable must return null if it can't route the
* URL (it's not applicable) or an array of the same format as the Rule::route() method.
*
* @param callable|null $routeCallable The callable to use for routing URLs
*
* @return void
*/
public function setRouteCallable($routeCallable)
{
$this->useCallableForRoute = !(is_null($routeCallable) || empty($routeCallable));
$this->routeCallable = $routeCallable;
}
/**
* Get the callable to use for routing a URL using this rule
*
* @return null|callable
*
* @codeCoverageIgnore
*/
public function getRouteCallable()
{
return $this->routeCallable;
}
/**
* Set the routing path. A routing path looks like this: foo/bar/ * /:id/:cat?/:tags* (the spaces are not part of
* the example, they are added to prevent PHP from breaking the comment block). It is a series of path segments:
*
* - any static string not starting with colon means that we are looking for an exact match
* - :something means that the value in this position will be assigned to query parameter "something"
* - :something? means that if a value exists in this position it will be assigned to query parameter "something"
* - :something* means that zero or more values in this position will be assigned to query array parameter "something"
* - * will match any value, but it will be ignored (not assigned to a query parameter)
*
* By default any kind of value is matched, unless there is a type regex for the named variable. The matching is
* greedy, therefore having :foo* / :bar? where both foo and bar are of the same type will always result in bar not
* being matched. In case bar is not followed by a question mark this means that the rule itself won't be matched!
*
* The same word of caution applies to the lone star operator. It will match anything that doesn't match the next
* segment. If the next segment doesn't have a type specified and it's not a static string, the lone start operator
* will never match anything at all.
*
* Moreover, you should NEVER use two or more lone stars in succession. This will completely screw up the route
* parsing.
*
* A lone start followed by an optional parameter will always try to match the optional parameter's type,
* essentially ignoring the fact that it's optional. This is because the forward type lookup is limited to a single
* position to improve performance. Therefore NEVER put an optional parameter after a lone star.
*
* @param string $routePath The routing path to set
*
* @return void
*
* @codeCoverageIgnore
*/
public function setPath($routePath)
{
$this->path = $routePath;
}
/**
* Get the routing path.
*
* @return string
*
* @codeCoverageIgnore
*/
public function getPath()
{
return $this->path;
}
/**
* Set the types (matching RegEx) for named parameters of the routing path (the :something strings)
*
* @param array $types The types to set
*
* @return void
*
* @codeCoverageIgnore
*/
public function setTypes($types)
{
$this->types = $types;
}
/**
* Get the types for named parameters of the routing path
*
* @return array
*
* @codeCoverageIgnore
*/
public function getTypes()
{
return $this->types;
}
/**
* Check whether the query string parameters $params match the "match variables" of this rule
*
* @param array $params
*
* @return boolean True on success
*/
protected function matchesVars(array &$params)
{
// If we don't have match vars we assume this URL can be routed by our rule
if (empty($this->matchVars))
{
return true;
}
foreach ($this->matchVars as $k => $v)
{
// If the variable doesn't exist we don't have a match; break and return false
if (!isset($params[$k]))
{
return false;
}
// We have a match for a variable with a null value. Nothing else to do for it, continue to the next one
if (is_null($v))
{
continue;
}
if ($params[$k] == $v)
{
// We have an exact variable match. Remove it from the $params array.
unset($params[$k]);
}
else
{
// Exact match failed. We don't have a match.
return false;
}
}
// All checks passed, we were successful!
return true;
}
/**
* Build a route segment based on the provided URL parameters and the routing path of this rule
*
* @param array $params
*
* @return array|null An array of path segments, or null if this rule is not applicable
*/
protected function buildRoute(array &$params)
{
$pathRules = explode('/', $this->path);
$segments = array();
foreach ($pathRules as $rule)
{
$rule = trim($rule);
if (substr($rule, 0, 1) == ':')
{
// Init
$rule = substr($rule, 1);
$isArray = false;
$isOptional = false;
// Is this an array or optional variable?
$lastChar = substr($rule, -1);
if ($lastChar == '*')
{
$isArray = true;
}
elseif ($lastChar == '?')
{
$isOptional = true;
}
if ($isArray || $isOptional)
{
$rule = substr($rule, 0, -1);
}
// What happens if this variable doesn't exist in my variables list?
if (!isset($params[$rule]))
{
// If it's optional, skip it
if ($isOptional)
{
continue;
}
// If it's not optional return null, meaning that we can't parse this routing rule
else
{
return null;
}
}
// Get the value
$value = $params[$rule];
// Make sure the type is right
if ($isArray && !is_array($value))
{
if (is_object($value))
{
$value = (array)$value;
}
else
{
$value = array($value);
}
}
elseif (!$isArray && (is_array($value)))
{
$value = array_shift($value);
}
elseif (!$isArray && (is_object($value)))
{
$value = (array)$value;
$value = array_shift($value);
}
// Push the value of the variable to the segments
if (!$isArray)
{
$segments[] = (string)$value;
}
else
{
foreach ($value as $v)
{
$segments[] = (string)$v;
}
}
// Finally, unset the variable
unset ($params[$rule]);
}
elseif ($rule == '*')
{
// Lone star rules are ignored during route building
}
else
{
// Static string, append verbose
$segments[] = $rule;
}
}
return $segments;
}
/**
* Parse the segments of the SEF URL and convert them to query parameters
*
* @param array $segments
*
* @return array|null The query parameters, or null if we can't parse this route
*/
protected function parseRoute(array $segments)
{
$pathRules = explode('/', $this->path);
$vars = array();
$isGreedy = false;
$rule = null;
$varName = null;
$varType = null;
$isArray = false;
$isOptional = false;
$segment = null;
while (!empty($segments))
{
if (is_null($segment))
{
$segment = array_shift($segments);
}
// No current rule. Let's fetch one.
if (is_null($rule))
{
// Do we have more path parts but no more rules?
if (empty($pathRules))
{
if (!$isGreedy)
{
// If we are not in a star rule, we shouldn't be parsing this route.
return null;
}
else
{
// It's a star rule. The rest of the query is ignored. We can just return now.
return $vars;
}
}
// Re-initialise
$isGreedy = false;
$rule = null;
$varName = null;
$varType = null;
$isArray = false;
$isOptional = false;
// Get the next rule
$rule = array_shift($pathRules);
$rule = trim($rule);
if ($rule == '*')
{
// We have a greedy lonely star. Set the greedy flag and fetch the next rule
$isGreedy = true;
$rule = array_shift($pathRules);
$rule = trim($rule);
}
}
// Do we have a rule with a variable name in it?
if (substr($rule, 0, 1) == ':')
{
// Do I have to parse the rule first?
if (empty($varName))
{
$varName = substr($rule, 1);
$varType = null;
$isArray = false;
$isOptional = false;
// Is this an array or optional variable?
$lastChar = substr($varName, -1);
if ($lastChar == '*')
{
$isArray = true;
}
elseif ($lastChar == '?')
{
$isOptional = true;
}
if ($isArray || $isOptional)
{
$varName = substr($varName, 0, -1);
}
// Get the variable type
if (isset($this->types[$varName]))
{
$varType = $this->types[$varName];
}
}
// Try to match the variable type
if (!empty($varType))
{
// What to do if we don't have a match?
$matched = preg_match($varType, $segment);
if (!$matched)
{
if ($isGreedy)
{
// If we are in a greedy match ignore this segment
$segment = null;
continue;
}
elseif ($isOptional)
{
// If it's an optional variable, ignore this rule
$rule = null;
}
elseif ($isArray && count($vars[$varName]))
{
// It's an array variable and we're done parsing it
$rule = null;
$segments[] = $segment;
$segment = null;
continue;
}
else
{
// We can't parse this rule
return null;
}
}
}
// The type matched. First, unset the greedy flag.
$isGreedy = false;
// Extract the variable.
if ($isArray)
{
// Make sure the array variable is an array
if (!isset($vars[$varName]))
{
$vars[$varName] = array();
}
// Push the segment to the array variable
$vars[$varName][] = $segment;
}
else
{
// Push the segment to the variable
$vars[$varName] = $segment;
// Since this is a single match rule, go to the next rule
$rule = null;
}
// Unset the segment so that we can proceed
$segment = null;
}
// Do we have an exact match rule (warning: case sensitive!!)?
else
{
if ($segment !== $rule)
{
// No match and we already have a greedy lone star? Ignore the segment!
if ($isGreedy)
{
$segment = null;
continue;
}
else
{
// Not greedy and no match? We can't parse this rule.
return null;
}
}
else
{
// We have a match. Kill the greedy flag and mark this segment as complete
$isGreedy = false;
$rule = null;
$segment = null;
}
}
}
// We ran out of segments. Have we also run out of rules?
if (empty($pathRules) || ((count($pathRules) == 1) && ($pathRules[0] == '*')))
{
// No rules left, or just a lone star. All good!
return $vars;
}
else
{
// Unmatched rules left. Are all the rules left optional or greedy?
$canSkip = true;
foreach($pathRules as $rule)
{
$firstChar = substr($rule, 0, 1);
$lastChar = substr($rule, -1);
if ($firstChar == '*')
{
continue;
}
if ($firstChar != ':')
{
$canSkip = false;
break;
}
if (($lastChar != '*') && ($lastChar != '?'))
{
$canSkip = false;
break;
}
}
return $canSkip ? $vars : null;
}
}
}