626 lines
18 KiB
PHP
626 lines
18 KiB
PHP
<?php
|
|
|
|
namespace EesyLDAP;
|
|
|
|
use EesyLDAP\Filter\CombineException;
|
|
use EesyLDAP\Filter\FilterException;
|
|
use EesyLDAP\Filter\ParserException;
|
|
|
|
|
|
/**
|
|
* LDAP Filter String abstraction class
|
|
*
|
|
* Note: originally based on Net_LDAP2_Filter implementation.
|
|
* @see https://github.com/pear/Net_LDAP2
|
|
*
|
|
* @property-read string|null $attr_name
|
|
* @property-read string|null $attribute
|
|
* @property-read string|null $op
|
|
* @property-read string|null $operator
|
|
* @property-read string|null $pattern
|
|
* @property-read string|null $log_op
|
|
* @property-read string|null $logical_operator
|
|
* @property-read array<Filter> $sub_filters
|
|
*/
|
|
class Filter {
|
|
|
|
/*
|
|
*************************************************************************************************
|
|
* Leaf mode
|
|
*************************************************************************************************
|
|
*/
|
|
|
|
/**
|
|
* The attribute name
|
|
* @var string|null
|
|
*/
|
|
private $attr_name;
|
|
|
|
/**
|
|
* The operator
|
|
* @var string|null
|
|
*/
|
|
private $op;
|
|
|
|
/**
|
|
* The attribute value pattern
|
|
* @var string|null
|
|
*/
|
|
private $pattern;
|
|
|
|
|
|
/*
|
|
*************************************************************************************************
|
|
* Combine mode
|
|
*************************************************************************************************
|
|
*/
|
|
|
|
/**
|
|
* The logical operator
|
|
* @var string|null
|
|
*/
|
|
private $log_op;
|
|
|
|
/**
|
|
* The LDAP sub-filters objects
|
|
* @var array<Filter>|null
|
|
*/
|
|
private $sub_filters;
|
|
|
|
|
|
/*
|
|
*************************************************************************************************
|
|
* Some constants
|
|
*************************************************************************************************
|
|
*/
|
|
|
|
/**
|
|
* List of possible logical operators
|
|
* @var array<string>
|
|
*/
|
|
private static $log_ops = array('&', '|', '!');
|
|
|
|
/**
|
|
* List of logical operators aliases
|
|
* @var array<string,string>
|
|
*/
|
|
private static $log_op_aliases = array('and' => '&', 'or' => '|', 'not' => '!');
|
|
|
|
/**
|
|
* The NOT logical operator
|
|
* @var string
|
|
*/
|
|
private static $not_op = '!';
|
|
|
|
/**
|
|
* List of possible operators
|
|
* @var array<string>
|
|
*/
|
|
private static $ops = array('=', '<', '>', '<=', '>=', '~=');
|
|
|
|
/**
|
|
* List of operators aliases
|
|
* @var array<string,string>
|
|
*/
|
|
private static $op_aliases = array(
|
|
'equals' => '=',
|
|
'==' => '=',
|
|
'lower' => '<',
|
|
'greater' => '>',
|
|
'lower_or_equals' => '<=',
|
|
'greater_or_equals' => '>=',
|
|
'approx' => '~=',
|
|
);
|
|
|
|
/**
|
|
* Characters to escape in a LDAP filter string
|
|
* @var array<string,string>
|
|
*/
|
|
private static $espace_chars = array (
|
|
'\\' => '\5c', // \
|
|
'(' => '\28', // (
|
|
')' => '\29', // )
|
|
'*' => '\2a', // *
|
|
"\u{0000}" => '\00', // NUL
|
|
);
|
|
|
|
/**
|
|
* Regex to match on OID
|
|
* @var string
|
|
*/
|
|
private static $oid_regex = '/^[0-9]+(\.[0-9]+)+$/';
|
|
|
|
/**
|
|
* Regex to match attribute name
|
|
* @var string
|
|
*/
|
|
private static $attr_name_regex = '/^(?P<name>[a-zA-Z][a-zA-Z0-9\-]*)(;(?P<option>[a-zA-Z0-9\-]+))*$/';
|
|
|
|
/**
|
|
* Regex to match extensible attribute name
|
|
* @throws FilterException
|
|
* @var string
|
|
*/
|
|
private static $ext_attr_name_regex = '/^(?P<name>[a-zA-Z0-9\-]+)?\:((?P<dn>dn|DN)\:)?((?P<matching_rule>[a-zA-Z]+|[0-9]+(\.[0-9]+)*)\:)?$/';
|
|
|
|
/**
|
|
* Constructor
|
|
* @param array<string|Filter|array<string|Filter>|bool> $args
|
|
* @throws CombineException
|
|
* @throws FilterException
|
|
* @throws ParserException
|
|
* @return void
|
|
*/
|
|
public function __construct(...$args) {
|
|
// Handle espace optional argument
|
|
$escape = true;
|
|
if ($args && is_bool(end($args)))
|
|
$escape = array_pop($args);
|
|
|
|
if (!$args)
|
|
throw new FilterException('Invalid constructor arguments: no argument provided!');
|
|
|
|
// One logical operator followed by some filters
|
|
if (self :: is_log_op($args[0])) {
|
|
// @phpstan-ignore-next-line
|
|
$this -> log_op = self :: unalias_log_op(array_shift($args));
|
|
|
|
// Convert args as filters
|
|
$this -> sub_filters = array();
|
|
foreach ($args as $arg) {
|
|
if (!is_array($arg))
|
|
$arg = array($arg);
|
|
foreach ($arg as $a) {
|
|
if (is_string($a))
|
|
$a = self :: parse($a);
|
|
if (!$a instanceof Filter)
|
|
throw new FilterException(
|
|
'Invalid constructor arguments: logical operator must be followed by Filter object, '.
|
|
'filter string or array of Filter or filter string.'
|
|
);
|
|
$this -> sub_filters[] = $a;
|
|
}
|
|
}
|
|
|
|
// Check number of filters against logical operator
|
|
if (self :: is_not_op($this -> log_op) && count($this -> sub_filters) != 1)
|
|
throw new CombineException(
|
|
'Invalid constructor arguments: NOT operator must be followed by exactly one filter');
|
|
return;
|
|
}
|
|
|
|
// array('attribute', 'operator', 'pattern')
|
|
if (count($args) == 3) {
|
|
if (
|
|
self :: is_attr_name($args[0])
|
|
&& (self :: is_op($args[1]) || in_array($args[1], array('begins', 'contains', 'ends')))
|
|
&& is_string($args[2])
|
|
) {
|
|
$this -> attr_name = $args[0];
|
|
$this -> pattern = $escape?self :: escape($args[2]):$args[2];
|
|
switch ($args[1]) {
|
|
case 'begins':
|
|
$this -> op = '=';
|
|
$this -> pattern = sprintf('%s*', $this -> pattern);
|
|
break;
|
|
case 'contains':
|
|
$this -> op = '=';
|
|
$this -> pattern = sprintf('*%s*', $this -> pattern);
|
|
break;
|
|
case 'ends':
|
|
$this -> op = '=';
|
|
$this -> pattern = sprintf('*%s', $this -> pattern);
|
|
break;
|
|
default:
|
|
// @phpstan-ignore-next-line
|
|
$this -> op = self :: unalias_op($args[1]);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
// array('attribute', 'present|any')
|
|
if (count($args) == 2) {
|
|
if (
|
|
self :: is_attr_name($args[0])
|
|
&& in_array($args[1], array('present', 'any'))
|
|
) {
|
|
$this -> attr_name = $args[0];
|
|
$this -> op = '=';
|
|
$this -> pattern = '*';
|
|
return;
|
|
}
|
|
}
|
|
|
|
throw new FilterException(
|
|
'Invalid constructor arguments provided to construct a Filter object');
|
|
}
|
|
|
|
/**
|
|
* Get filter property
|
|
* @param string $key
|
|
* @return mixed
|
|
*/
|
|
public function __get($key) {
|
|
switch ($key) {
|
|
case 'attr_name':
|
|
case 'attribute':
|
|
return isset($this -> attr_name)?$this -> attr_name:null;
|
|
|
|
case 'op':
|
|
case 'operator':
|
|
return isset($this -> op)?$this -> op:null;
|
|
|
|
case 'pattern':
|
|
return isset($this -> pattern)?$this -> pattern:null;
|
|
|
|
case 'log_op':
|
|
case 'logical_operator':
|
|
return isset($this -> log_op)?$this -> log_op:null;
|
|
|
|
case 'sub_filters':
|
|
return isset($this -> sub_filters)?$this -> sub_filters:array();
|
|
}
|
|
throw new FilterException("Invalid property '$key' requested");
|
|
}
|
|
|
|
/**
|
|
* Get operators
|
|
* @return array<string>
|
|
*/
|
|
public static function operators() {
|
|
return self :: $ops;
|
|
}
|
|
|
|
/**
|
|
* Get operator aliases
|
|
* @return array<string,string>
|
|
*/
|
|
public static function operator_aliases() {
|
|
return self :: $op_aliases;
|
|
}
|
|
|
|
/**
|
|
* Get logical operators
|
|
* @return array<string>
|
|
*/
|
|
public static function logical_operators() {
|
|
return self :: $log_ops;
|
|
}
|
|
|
|
/**
|
|
* Get logical operator aliases
|
|
* @return array<string,string>
|
|
*/
|
|
public static function logical_operator_aliases() {
|
|
return self :: $log_op_aliases;
|
|
}
|
|
|
|
/**
|
|
* Check filter is a leaf one
|
|
* @phpstan-assert-if-true string $this->attr_name
|
|
* @phpstan-assert-if-true string $this->op
|
|
* @phpstan-assert-if-true string $this->pattern
|
|
* @return bool
|
|
*/
|
|
public function is_leaf() {
|
|
return isset($this -> attr_name) && isset($this -> op) && isset($this -> pattern);
|
|
}
|
|
|
|
/**
|
|
* Check filter is a combine one
|
|
* @phpstan-assert-if-true string $this->log_op
|
|
* @phpstan-assert-if-true non-empty-array<Filter> $this->sub_filters
|
|
* @return bool
|
|
*/
|
|
public function is_combine() {
|
|
return (
|
|
isset($this -> log_op) && isset($this -> sub_filters) && is_array($this -> sub_filters)
|
|
&& $this -> sub_filters
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Return LDAP filter as a string
|
|
* @param bool $pretty Enable pretty format: as line return and tabulations to easily distinguate
|
|
* logical groups.
|
|
* @param int $level Level of iteration (internally use in pretty format mode)
|
|
* @return string
|
|
*/
|
|
public function as_string($pretty=false, $level=0) {
|
|
// Leaf filter
|
|
if ($this -> is_leaf()) {
|
|
if ($pretty)
|
|
return (
|
|
str_repeat("\t", $level).
|
|
sprintf("(%s%s%s)", $this -> attr_name, $this -> op, $this -> pattern).
|
|
($level?"\n":"")
|
|
);
|
|
return sprintf("(%s%s%s)", $this -> attr_name, $this -> op, $this -> pattern);
|
|
}
|
|
|
|
// Combine filter
|
|
if ($this -> is_combine()) {
|
|
$return = '';
|
|
foreach ($this->sub_filters as $filter)
|
|
$return .= $filter -> as_string($pretty, $level+1);
|
|
if ($pretty)
|
|
return (
|
|
str_repeat("\t", $level).
|
|
"(".$this -> log_op."\n".
|
|
$return.
|
|
str_repeat("\t", $level).")".
|
|
($level?"\n":"")
|
|
);
|
|
return sprintf("(%s%s)", $this -> log_op, $return);
|
|
}
|
|
|
|
// May never occured
|
|
throw new FilterException('Filter is not leaf nor combine one?!');
|
|
}
|
|
|
|
/**
|
|
* Combine LDAP filter objects or strings using a logical operator
|
|
*
|
|
* @param string $log_op The locical operator. May be "and", "or", "not" or the subsequent logical
|
|
* equivalents "&", "|", "!"
|
|
* @param array<string|Filter|array<string|Filter>> $filters LDAP filters to combine: could be
|
|
* LDAP objects, LDAP strings or array
|
|
* of LDAP objects or LDAP strings.
|
|
* @throws CombineException
|
|
* @throws FilterException
|
|
* @throws ParserException
|
|
* @return Filter
|
|
* @static
|
|
*/
|
|
public static function combine($log_op, ...$filters) {
|
|
// @phpstan-ignore-next-line
|
|
return new Filter($log_op, ...$filters);
|
|
}
|
|
|
|
/**
|
|
* Parse an LDAP filter string
|
|
* @param string $value The LDAP filter string to parse
|
|
* @throws ParserException
|
|
* @static
|
|
* @return Filter
|
|
*/
|
|
public static function parse($value) {
|
|
// Only handle filter enclosed with brackets
|
|
if (!preg_match('/^\((.+?)\)$/', $value, $matches))
|
|
return self :: parse("($value)");
|
|
|
|
// Check for right bracket syntax: count of unescaped opening
|
|
// brackets must match count of unescaped closing brackets.
|
|
// At this stage we may have:
|
|
// 1. one filter component with already removed outer brackets
|
|
// 2. one or more subfilter components
|
|
$c_openbracks = preg_match_all('/(?<!\\\\)\(/' , $matches[1], $notrelevant);
|
|
$c_closebracks = preg_match_all('/(?<!\\\\)\)/' , $matches[1], $notrelevant);
|
|
if ($c_openbracks != $c_closebracks) {
|
|
throw new ParserException(
|
|
"Filter parsing error: invalid filter syntax - opening brackets do not match close ".
|
|
"brackets!"
|
|
);
|
|
}
|
|
|
|
if (in_array(substr($matches[1], 0, 1), array('!', '|', '&'))) {
|
|
// Subfilter processing: pass subfilters to parse() and combine
|
|
// the objects using the logical operator detected
|
|
// we have now something like "&(...)(...)(...)" but at least one part ("!(...)").
|
|
// Each subfilter could be an arbitary complex subfilter.
|
|
|
|
// extract logical operator and filter arguments
|
|
$log_op = substr($matches[1], 0, 1);
|
|
$remaining_component = substr($matches[1], 1);
|
|
|
|
// split $remaining_component into individual subfilters
|
|
// we cannot use split() for this, because we do not know the
|
|
// complexiness of the subfilter. Thus, we look trough the filter
|
|
// string and just recognize ending filters at the first level.
|
|
// We record the index number of the char and use that information
|
|
// later to split the string.
|
|
$sub_index_pos = array();
|
|
$prev_char = ''; // previous character looked at
|
|
$level = 0; // denotes the current bracket level we are,
|
|
// >1 is too deep, 1 is ok, 0 is outside any
|
|
// subcomponent
|
|
for ($curpos = 0; $curpos < strlen($remaining_component); $curpos++) {
|
|
$cur_char = substr($remaining_component, $curpos, 1);
|
|
|
|
// rise/lower bracket level
|
|
if ($cur_char == '(' && $prev_char != '\\') {
|
|
$level++;
|
|
} elseif ($cur_char == ')' && $prev_char != '\\') {
|
|
$level--;
|
|
}
|
|
|
|
if ($cur_char == '(' && $prev_char == ')' && $level == 1) {
|
|
array_push($sub_index_pos, $curpos); // mark the position for splitting
|
|
}
|
|
$prev_char = $cur_char;
|
|
}
|
|
|
|
// now perform the splits. To get also the last part, we
|
|
// need to add the "END" index to the split array
|
|
array_push($sub_index_pos, strlen($remaining_component));
|
|
$subfilters = array();
|
|
$oldpos = 0;
|
|
foreach ($sub_index_pos as $s_pos) {
|
|
$str_part = substr($remaining_component, $oldpos, $s_pos - $oldpos);
|
|
array_push($subfilters, $str_part);
|
|
$oldpos = $s_pos;
|
|
}
|
|
|
|
// some error checking...
|
|
if (count($subfilters) > 1) {
|
|
// several subfilters found
|
|
if ($log_op == "!") {
|
|
throw new ParserException(
|
|
"Filter parsing error: invalid filter syntax - NOT operator detected but ".
|
|
"several arguments given!"
|
|
);
|
|
}
|
|
}
|
|
|
|
// Now parse the subfilters into objects and combine them using the operator
|
|
$subfilters_o = array();
|
|
foreach ($subfilters as $subfilter) {
|
|
array_push($subfilters_o, self :: parse($subfilter));
|
|
}
|
|
|
|
return self :: combine($log_op, $subfilters_o);
|
|
}
|
|
|
|
// This is one leaf filter component, do some syntax checks, then escape and build filter_o
|
|
// $matches[1] should be now something like "foo=bar"
|
|
|
|
// detect multiple leaf components
|
|
// [TODO] Maybe this will make problems with filters containing brackets inside the value
|
|
if (stristr($matches[1], ')') || stristr($matches[1], '(')) {
|
|
throw new ParserException(
|
|
"Filter parsing error: invalid filter syntax - multiple leaf components ".
|
|
"detected!"
|
|
);
|
|
}
|
|
$filter_parts = self :: split_attr_string($matches[1]);
|
|
if (!is_array($filter_parts) || count($filter_parts) != 3) {
|
|
throw new ParserException(
|
|
"Filter parsing error: invalid filter syntax - unknown matching rule used");
|
|
}
|
|
$filter_parts[] = false; // Disable escaping
|
|
return new Filter(...$filter_parts);
|
|
}
|
|
|
|
/**
|
|
* Splits an attribute=value syntax into an array
|
|
* @param string $value
|
|
* @static
|
|
* @return array<int,string>|false
|
|
*/
|
|
public static function split_attr_string($value) {
|
|
$splited_value = preg_split(
|
|
'/(?<!\\\\)('.implode('|', self :: $ops).')/',
|
|
$value,
|
|
2,
|
|
PREG_SPLIT_DELIM_CAPTURE
|
|
);
|
|
if (!is_array($splited_value) || count($splited_value) != 3)
|
|
return false;
|
|
return $splited_value;
|
|
}
|
|
|
|
/**
|
|
* Check if it's an operator
|
|
* @param mixed $value The value to check
|
|
* @param bool $allow_alias Set if operator alias is allowed (optional, default: true)
|
|
* @static
|
|
* @return bool
|
|
* @phpstan-assert-if-true string $value
|
|
*/
|
|
public static function is_op($value, $allow_alias=true) {
|
|
if (is_string($value) && in_array($value, self :: $ops))
|
|
return true;
|
|
if ($allow_alias && is_string($value) && array_key_exists($value, self :: $op_aliases))
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Check if it's an logical operator
|
|
* @param mixed $value The value to check
|
|
* @param bool $allow_alias Set if logical operator alias is allowed (optional, default: true)
|
|
* @static
|
|
* @return bool
|
|
* @phpstan-assert-if-true string $value
|
|
*/
|
|
public static function is_log_op($value, $allow_alias=true) {
|
|
if (is_string($value) && in_array($value, self :: $log_ops))
|
|
return true;
|
|
if ($allow_alias && is_string($value) && array_key_exists($value, self :: $log_op_aliases))
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Resolve operator alias
|
|
* @param mixed $value
|
|
* @static
|
|
* @return string|false The real operator or false if invalid value specified
|
|
*/
|
|
public static function unalias_op($value) {
|
|
if (is_string($value) && array_key_exists($value, self :: $op_aliases))
|
|
return self :: $op_aliases[$value];
|
|
if (self :: is_op($value, false))
|
|
return $value;
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Resolve logical operator alias
|
|
* @param mixed $value
|
|
* @static
|
|
* @return string|false The real operator or false if invalid value specified
|
|
*/
|
|
public static function unalias_log_op($value) {
|
|
if (is_string($value) && array_key_exists($value, self :: $log_op_aliases))
|
|
return self :: $log_op_aliases[$value];
|
|
if (self :: is_log_op($value, false))
|
|
return $value;
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Check if it's the NOT logical operator
|
|
* @param mixed $value The value to check
|
|
* @static
|
|
* @return bool
|
|
* @phpstan-assert-if-true string $value
|
|
*/
|
|
public static function is_not_op($value) {
|
|
return $value == self :: $not_op;
|
|
}
|
|
|
|
/**
|
|
* Check if it's a valid attribute name filter part
|
|
* @param mixed $value The value to check
|
|
* @phpstan-assert-if-true string $value
|
|
* @param bool $allow_extended Allow extended attribute name filter part (optional, default: true)
|
|
* @static
|
|
* @return bool
|
|
*/
|
|
public static function is_attr_name($value, $allow_extended=true) {
|
|
if (!is_string($value))
|
|
return false;
|
|
if (preg_match(self :: $attr_name_regex, $value))
|
|
return true;
|
|
if (preg_match(self :: $oid_regex, $value))
|
|
return true;
|
|
if ($allow_extended && preg_match(self :: $ext_attr_name_regex, $value))
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Escape a string for LDAP filter pattern
|
|
* @param string $value
|
|
* @static
|
|
* @return string
|
|
*/
|
|
public static function escape($value) {
|
|
return str_replace(array_keys(self :: $espace_chars), array_values(self :: $espace_chars), $value);
|
|
}
|
|
|
|
|
|
/**
|
|
* Unescape a string from LDAP filter pattern
|
|
* @param string $value
|
|
* @static
|
|
* @return string
|
|
*/
|
|
public static function unescape($value) {
|
|
return str_replace(array_values(self :: $espace_chars), array_keys(self :: $espace_chars), $value);
|
|
}
|
|
|
|
}
|