Initial commit

This commit is contained in:
Benjamin Renard 2023-03-13 02:13:21 +01:00
commit d4a8821157
10 changed files with 1669 additions and 0 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
/composer.lock
/vendor
/.phpunit.cache
/coverage*

13
.pre-commit-config.yaml Normal file
View file

@ -0,0 +1,13 @@
# Pre-commit hooks to run tests and ensure code is cleaned.
# See https://pre-commit.com for more information
repos:
- repo: https://github.com/digitalpulp/pre-commit-php.git
rev: 1.4.0
hooks:
- id: php-stan
files: \.(php)$
args: ['--configuration=phpstan.neon', '--xdebug']
- repo: https://github.com/digitalpulp/pre-commit-php.git
rev: 1.4.0
hooks:
- id: php-unit

21
composer.json Normal file
View file

@ -0,0 +1,21 @@
{
"name": "brenard/eesyldap",
"type": "library",
"description": "Object oriented interface for searching and manipulating LDAP entries & filters",
"require-dev": {
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^9"
},
"authors": [
{
"name": "Benjamin Renard",
"email": "brenard@zionetrix.net"
}
],
"license": "LGPL-3.0-or-later",
"autoload": {
"psr-4": {
"EesyLDAP\\": "src/"
}
}
}

10
phpstan.neon Normal file
View file

@ -0,0 +1,10 @@
parameters:
level: 9
paths:
- src
- tests
treatPhpDocTypesAsCertain: false
ignoreErrors:
-
message: "#Method .*::test.*\\(\\) has no return type specified\\.#"
path: tests/*

27
phpunit.xml Normal file
View file

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.6/phpunit.xsd"
bootstrap="vendor/autoload.php"
cacheResultFile=".phpunit.cache/test-results"
executionOrder="depends,defects"
forceCoversAnnotation="true"
beStrictAboutCoversAnnotation="true"
beStrictAboutOutputDuringTests="true"
beStrictAboutTodoAnnotatedTests="true"
convertDeprecationsToExceptions="true"
failOnRisky="true"
failOnWarning="true"
verbose="true">
<testsuites>
<testsuite name="default">
<directory>tests</directory>
</testsuite>
</testsuites>
<coverage cacheDirectory=".phpunit.cache/code-coverage"
processUncoveredFiles="true">
<include>
<directory suffix=".php">src</directory>
</include>
</coverage>
</phpunit>

645
src/Filter.php Normal file
View file

@ -0,0 +1,645 @@
<?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
* @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 FilterException(
'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>> $args LDAP filters to combine: could be
* LDAP objects, LDAP strings or array of
* LDAP objects or LDAP strings.
* @throws CombineException
* @return Filter
* @static
*/
public static function combine($log_op, ...$args) {
// Unalias & check logical operator
$log_op = self :: unalias_log_op($log_op);
if (!$log_op) throw new CombineException('Invalid logical operator provided!');
// Convert args as filters
$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 CombineException(
'Invalid filter provided: must be a Filter object or a LDAP filter string!'
);
$filters[] = $a;
}
}
// Check number of filters against logical operator
if (self :: is_not_op($log_op)) {
if (count($filters) != 1)
throw new CombineException('NOT operator accept only one filter!');
}
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);
}
}

View file

@ -0,0 +1,5 @@
<?php
namespace EesyLDAP\Filter;
class CombineException extends FilterException {}

View file

@ -0,0 +1,5 @@
<?php
namespace EesyLDAP\Filter;
class FilterException extends \Exception {}

View file

@ -0,0 +1,5 @@
<?php
namespace EesyLDAP\Filter;
class ParserException extends FilterException {}

934
tests/FilterTest.php Normal file
View file

@ -0,0 +1,934 @@
<?php
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
use EesyLDAP\Filter;
use EesyLDAP\Filter\FilterException;
use EesyLDAP\Filter\CombineException;
use EesyLDAP\Filter\ParserException;
/**
* @covers \EesyLDAP\Filter
*/
final class FilterTest extends TestCase {
/**
* Constructor
* @covers \EesyLDAP\Filter::__construct
*/
public function testConstructWithAttrAndOpAndValue() {
$a = new Filter('attr', '=', 'value');
$this->assertEquals('attr', $a->attr_name);
$this->assertEquals('=', $a->op);
$this->assertEquals('value', $a->pattern);
}
/**
* @covers \EesyLDAP\Filter::__construct
*/
public function testConstructWithAttrAndOpAliasAndValue() {
foreach(Filter :: operator_aliases() as $alias => $op) {
$a = new Filter('attr', $alias, 'value');
$this->assertEquals($op, $a->op);
}
}
/**
* @covers \EesyLDAP\Filter::__construct
*/
public function testConstructWithAttrAndSpecialOpAndValue() {
$a = new Filter('attr', 'begins', 'value');
$this->assertEquals('attr', $a->attr_name);
$this->assertEquals('=', $a->op);
$this->assertEquals('value*', $a->pattern);
$a = new Filter('attr', 'contains', 'value');
$this->assertEquals('attr', $a->attr_name);
$this->assertEquals('=', $a->op);
$this->assertEquals('*value*', $a->pattern);
$a = new Filter('attr', 'ends', 'value');
$this->assertEquals('attr', $a->attr_name);
$this->assertEquals('=', $a->op);
$this->assertEquals('*value', $a->pattern);
}
/**
* @covers \EesyLDAP\Filter::__construct
*/
public function testConstructWithLogOpAndFilters() {
$ab = new Filter('a', '=', 'b');
$c2 = new Filter('c', '>=', '2');
$a = new Filter('&', $ab, $c2);
$this->assertEquals('&', $a->log_op);
$this->assertIsArray($a->sub_filters);
$this->assertCount(2, $a->sub_filters);
$this->assertContains($ab, $a->sub_filters);
$this->assertContains($c2, $a->sub_filters);
}
/**
* @covers \EesyLDAP\Filter::__construct
*/
public function testConstructWithLogOpAliasAndFilters() {
foreach(Filter :: logical_operator_aliases() as $alias => $op) {
$a = new Filter($alias, new Filter('a', '=', 'b'));
$this->assertEquals($op, $a->log_op);
}
}
/**
* @covers \EesyLDAP\Filter::__construct
*/
public function testConstructWithLogOpAndArrayOfFilters() {
$ab = new Filter('a', '=', 'b');
$c2 = new Filter('c', '>=', '2');
$bd = new Filter('b', '=', 'd');
$a = new Filter('&', array($ab, $c2), $bd);
$this->assertEquals('&', $a->log_op);
$this->assertIsArray($a->sub_filters);
$this->assertCount(3, $a->sub_filters);
$this->assertContains($ab, $a->sub_filters);
$this->assertContains($c2, $a->sub_filters);
$this->assertContains($bd, $a->sub_filters);
}
/**
* @covers \EesyLDAP\Filter::__construct
*/
public function testConstructWithLogOpAndArrayOfFilterStrings() {
$a = new Filter('&', array('a=b', '(c>=2)'), 'b~=test');
$this->assertEquals($a->as_string(), '(&(a=b)(c>=2)(b~=test))');
}
/**
* @covers \EesyLDAP\Filter::__construct
*/
public function testConstructWithLogOpAndMixedFilterTypes() {
$a = new Filter(
'|',
array(new Filter('a', '=', 'b'), 'c>=2'),
new Filter('b', '~=', 'test'),
'd=z'
);
$this->assertEquals($a->as_string(), '(|(a=b)(c>=2)(b~=test)(d=z))');
}
/**
* @covers \EesyLDAP\Filter::__construct
*/
public function testConstructWithNotOpAndFilter() {
$ab = new Filter('a', '=', 'b');
$a = new Filter('!', $ab);
$this->assertEquals('!', $a->log_op);
$this->assertIsArray($a->sub_filters);
$this->assertCount(1, $a->sub_filters);
$this->assertContains($ab, $a->sub_filters);
}
/**
* @covers \EesyLDAP\Filter::__construct
*/
public function testConstructWithNotOpAndMoreThanOneFilter() {
$this->expectException(FilterException::class);
$a = new Filter('!', new Filter('a', '=', 'b'), new Filter('c', '>=', '2'));
}
/**
* @covers \EesyLDAP\Filter::__construct
*/
public function testConstructWithPresentAnyOp() {
$a = new Filter('a', 'present');
$this->assertEquals('a', $a->attr_name);
$this->assertEquals('=', $a->op);
$this->assertEquals('*', $a->pattern);
$a = new Filter('a', 'any');
$this->assertEquals('a', $a->attr_name);
$this->assertEquals('=', $a->op);
$this->assertEquals('*', $a->pattern);
}
/**
* @covers \EesyLDAP\Filter::__construct
*/
public function testConstructWithEscapeArg() {
$a = new Filter('attr', '=', 'value*', true);
$this->assertEquals('attr', $a->attr_name);
$this->assertEquals('=', $a->op);
$this->assertEquals('value\2a', $a->pattern);
$a = new Filter('attr', '=', 'value*', False);
$this->assertEquals('attr', $a->attr_name);
$this->assertEquals('=', $a->op);
$this->assertEquals('value*', $a->pattern);
}
/**
* @covers \EesyLDAP\Filter::__construct
*/
public function testConstructWithoutArg() {
$this->expectException(FilterException::class);
$a = new Filter();
}
/**
* @covers \EesyLDAP\Filter::__construct
*/
public function testConstructCombineWithInvalidFilter() {
$this->expectException(FilterException::class);
// @phpstan-ignore-next-line
$a = new Filter('&', 'a=b', new FilterException('test'));
}
/**
* @covers \EesyLDAP\Filter::__construct
*/
public function testConstructWithInvalidThreeArgs() {
$this->expectException(FilterException::class);
$a = new Filter('a', 'b', 'c');
}
/**
* @covers \EesyLDAP\Filter::__construct
*/
public function testConstructWithInvalidTwoArgs() {
$this->expectException(FilterException::class);
$a = new Filter('a', '&');
}
/**
* @covers \EesyLDAP\Filter::__get()
*/
public function testGetOnLeafFilter() {
$a = new Filter('attr', '=', 'value');
$this->assertEquals('attr', $a->attr_name);
$this->assertEquals('attr', $a->attribute);
$this->assertEquals('=', $a->op);
$this->assertEquals('=', $a->operator);
$this->assertEquals('value', $a->pattern);
$this->assertNull($a->log_op);
$this->assertNull($a->logical_operator);
$this->assertIsArray($a->sub_filters);
$this->assertEmpty($a->sub_filters);
}
/**
* @covers \EesyLDAP\Filter::__get
*/
public function testGetOnCombineFilter() {
$a = new Filter('&', 'attr=value');
$this->assertNull($a->attr_name);
$this->assertNull($a->attribute);
$this->assertNull($a->op);
$this->assertNull($a->operator);
$this->assertNull($a->pattern);
$this->assertEquals('&', $a->log_op);
$this->assertEquals('&', $a->logical_operator);
$this->assertIsArray($a->sub_filters);
$this->assertCount(1, $a->sub_filters);
}
/**
* @covers \EesyLDAP\Filter::__get
*/
public function testGetOnUndefinedProperty() {
$a = new Filter('&', 'attr=value');
$this->expectException(FilterException::class);
// @phpstan-ignore-next-line
$a->undefined;
}
/**
* @covers \EesyLDAP\Filter::operators
*/
public function testOperators() {
$ops = Filter::operators();
$this->assertIsArray($ops);
$this->assertNotEmpty($ops);
foreach($ops as $op) {
$this->assertIsString($op);
}
}
/**
* @covers \EesyLDAP\Filter::operator_aliases
*/
public function testOperatorAliases() {
$op_aliases = Filter::operator_aliases();
$this->assertIsArray($op_aliases);
$this->assertNotEmpty($op_aliases);
foreach($op_aliases as $alias => $op) {
$this->assertIsString($alias);
$this->assertIsString($op);
}
}
/**
* @covers \EesyLDAP\Filter::logical_operators
*/
public function testLogicalOperators() {
$log_ops = Filter::logical_operators();
$this->assertIsArray($log_ops);
$this->assertNotEmpty($log_ops);
foreach($log_ops as $op) {
$this->assertIsString($op);
}
}
/**
* @covers \EesyLDAP\Filter::logical_operator_aliases
*/
public function testLogicalOperatorAliases() {
$log_op_aliases = Filter::logical_operator_aliases();
$this->assertIsArray($log_op_aliases);
$this->assertNotEmpty($log_op_aliases);
foreach($log_op_aliases as $alias => $op) {
$this->assertIsString($alias);
$this->assertIsString($op);
}
}
/**
* @covers \EesyLDAP\Filter::is_leaf
*/
public function testIsLeaf() {
$a = new Filter('attr', '=', 'value');
$this->assertTrue($a->is_leaf());
$a = new Filter('!', 'a=b');
$this->assertFalse($a->is_leaf());
}
/**
* @covers \EesyLDAP\Filter::is_combine
*/
public function testIsCombine() {
$a = new Filter('attr', '=', 'value');
$this->assertFalse($a->is_combine());
$a = new Filter('!', 'a=b');
$this->assertTrue($a->is_combine());
}
/**
* @covers \EesyLDAP\Filter::as_string
*/
public function testLeafFilterAsString() {
$a = new Filter('attr', '=', 'value');
$this->assertEquals('(attr=value)', $a->as_string(true));
}
/**
* @covers \EesyLDAP\Filter::as_string
*/
public function testLeafFilterAsPrettyString() {
$a = new Filter('attr', '=', 'value');
$this->assertEquals('(attr=value)', $a->as_string());
}
/**
* @covers \EesyLDAP\Filter::combine
*/
public function testCombineFilterAsString() {
$a = new Filter('&', 'a=b', 'c=d');
$this->assertEquals('(&(a=b)(c=d))', $a->as_string());
}
/**
* @covers \EesyLDAP\Filter::combine
*/
public function testCombineFilterAsPrettyString() {
$a = new Filter('&', 'a=b', 'c=d');
$this->assertEquals("(&\n\t(a=b)\n\t(c=d)\n)", $a->as_string(true));
}
/**
* @covers \EesyLDAP\Filter::combine
*/
public function testCombineStringFilters() {
$a = Filter :: combine('&', 'a=b', 'c=d');
$this->assertEquals('(&(a=b)(c=d))', $a->as_string());
}
/**
* @covers \EesyLDAP\Filter::combine
*/
public function testCombineFilters() {
$ab = new Filter('a', '=', 'b');
$bd = new Filter('b', '=', 'd');
$a = Filter :: combine('&', $ab, $bd);
$this->assertEquals('&', $a->log_op);
$this->assertIsArray($a->sub_filters);
$this->assertCount(2, $a->sub_filters);
$this->assertContains($ab, $a->sub_filters);
$this->assertContains($bd, $a->sub_filters);
}
/**
* @covers \EesyLDAP\Filter::combine
*/
public function testCombineWithLogOpAlias() {
$ab = new Filter('a', '=', 'b');
foreach(Filter :: logical_operator_aliases() as $alias => $op) {
$a = Filter::combine($alias, $ab);
$this->assertEquals($op, $a->log_op);
}
}
/**
* @covers \EesyLDAP\Filter::combine
*/
public function testCombineArrayOfFilters() {
$ab = new Filter('a', '=', 'b');
$c2 = new Filter('c', '>=', '2');
$bd = new Filter('b', '=', 'd');
$a = Filter::combine('&', array($ab, $c2), $bd);
$this->assertEquals('&', $a->log_op);
$this->assertIsArray($a->sub_filters);
$this->assertCount(3, $a->sub_filters);
$this->assertContains($ab, $a->sub_filters);
$this->assertContains($c2, $a->sub_filters);
$this->assertContains($bd, $a->sub_filters);
}
/**
* @covers \EesyLDAP\Filter::combine
*/
public function testCombineArrayOfFilterStrings() {
$a = Filter::combine('&', array('a=b', '(c>=2)'), 'b~=test');
$this->assertEquals($a->as_string(), '(&(a=b)(c>=2)(b~=test))');
}
/**
* @covers \EesyLDAP\Filter::combine
*/
public function testCombineMixedFilterTypes() {
$a = Filter::combine(
'|',
array(new Filter('a', '=', 'b'), 'c>=2'),
new Filter('b', '~=', 'test'),
'd=z'
);
$this->assertEquals($a->as_string(), '(|(a=b)(c>=2)(b~=test)(d=z))');
}
/**
* @covers \EesyLDAP\Filter::combine
*/
public function testCombineNotOpAndFilter() {
$ab = new Filter('a', '=', 'b');
$a = Filter::combine('!', $ab);
$this->assertEquals('!', $a->log_op);
$this->assertIsArray($a->sub_filters);
$this->assertCount(1, $a->sub_filters);
$this->assertContains($ab, $a->sub_filters);
}
/**
* @covers \EesyLDAP\Filter::combine
*/
public function testCombineNotOpAndMoreThanOneFilter() {
$this->expectException(CombineException::class);
$a = Filter::combine('!', new Filter('a', '=', 'b'), new Filter('c', '>=', '2'));
}
/**
* @covers \EesyLDAP\Filter::combine
*/
public function testCombineInvalidLogOp() {
$this->expectException(CombineException::class);
$a = Filter::combine('X', new Filter('a', '=', 'b'), new Filter('c', '>=', '2'));
}
/**
* @covers \EesyLDAP\Filter::combine
*/
public function testCombineInvalidFilter() {
$this->expectException(CombineException::class);
// @phpstan-ignore-next-line
$a = Filter::combine('&', new Filter('a', '=', 'b'), new FilterException('test'));
}
/**
* @covers \EesyLDAP\Filter::split_attr_string
*/
public function testSplitAttrString() {
$this-> assertEquals(array('a', '=', 'b'), Filter::split_attr_string('a=b'));
$this-> assertEquals(array('owner', '=', 'cn=admin,o=example'), Filter::split_attr_string('owner=cn=admin,o=example'));
$this-> assertFalse(Filter::split_attr_string('owner'));
}
/**
* @covers \EesyLDAP\Filter::is_op
*/
public function test_is_op() {
foreach(Filter::operators() as $op)
$this -> assertTrue(Filter::is_op($op));
foreach(Filter::operator_aliases() as $alias => $op) {
$this -> assertTrue(Filter::is_op($alias));
$this -> assertTrue(Filter::is_op($op));
}
$this -> assertFalse(Filter::is_op('fake_op', false));
}
/**
* @covers \EesyLDAP\Filter::is_op
*/
public function test_is_op_without_alias() {
foreach(Filter::operators() as $op)
$this -> assertTrue(Filter::is_op($op, false));
foreach(Filter::operator_aliases() as $alias => $op) {
$this -> assertFalse(Filter::is_op($alias, false));
$this -> assertTrue(Filter::is_op($op, false));
}
}
/**
* @covers \EesyLDAP\Filter::is_log_op
*/
public function test_is_log_op() {
foreach(Filter::logical_operators() as $op)
$this -> assertTrue(Filter::is_log_op($op));
foreach(Filter::logical_operator_aliases() as $alias => $op) {
$this -> assertTrue(Filter::is_log_op($alias));
$this -> assertTrue(Filter::is_log_op($op));
}
$this -> assertFalse(Filter::is_log_op('fake_op', false));
}
/**
* @covers \EesyLDAP\Filter::is_log_op
*/
public function test_is_log_op_without_alias() {
foreach(Filter::logical_operators() as $op)
$this -> assertTrue(Filter::is_log_op($op, false));
foreach(Filter::logical_operator_aliases() as $alias => $op) {
$this -> assertFalse(Filter::is_log_op($alias, false));
$this -> assertTrue(Filter::is_log_op($op, false));
}
}
/**
* @covers \EesyLDAP\Filter::unalias_op
*/
public function test_unalias_op() {
foreach(Filter::operators() as $op)
$this -> assertEquals($op, Filter::unalias_op($op));
foreach(Filter::operator_aliases() as $alias => $op) {
$this -> assertEquals($op, Filter::unalias_op($alias));
$this -> assertEquals($op, Filter::unalias_op($op));
}
$this -> assertFalse(Filter::unalias_op('fake_op'));
}
/**
* @covers \EesyLDAP\Filter::unalias_log_op
*/
public function test_unalias_log_op() {
foreach(Filter::logical_operators() as $op)
$this -> assertEquals($op, Filter::unalias_log_op($op));
foreach(Filter::logical_operator_aliases() as $alias => $op) {
$this -> assertEquals($op, Filter::unalias_log_op($alias));
$this -> assertEquals($op, Filter::unalias_log_op($op));
}
$this -> assertFalse(Filter::unalias_log_op('fake_op'));
}
/**
* @covers \EesyLDAP\Filter::is_not_op
*/
public function test_is_not_op() {
$this -> assertTrue(Filter :: is_not_op('!'));
$this -> assertFalse(Filter :: is_not_op('&'));
}
/**
* @covers \EesyLDAP\Filter::is_attr_name
*/
public function test_is_attr_name() {
$this -> assertTrue(Filter :: is_attr_name('o'));
$this -> assertTrue(Filter :: is_attr_name('cn'));
$this -> assertTrue(Filter :: is_attr_name('cn;x-test'));
$this -> assertTrue(Filter :: is_attr_name('cn;x-test;x-test2'));
$this -> assertTrue(Filter :: is_attr_name('cn;x-test;x-test2;x-test3'));
$this -> assertTrue(Filter :: is_attr_name('1.12.34'));
$this -> assertFalse(Filter :: is_attr_name('1.12.34.'));
$this -> assertFalse(Filter :: is_attr_name('1'));
$this -> assertFalse(Filter :: is_attr_name('1.'));
$this -> assertFalse(Filter :: is_attr_name('bad name'));
$this -> assertFalse(Filter :: is_attr_name('1word'));
$this -> assertFalse(Filter :: is_attr_name('cn:', false));
// @phpstan-ignore-next-line
$this -> assertFalse(Filter :: is_attr_name(false));
}
/**
* @covers \EesyLDAP\Filter::is_attr_name
*/
public function test_is_attr_name_allow_extended_by_default() {
$this -> assertTrue(Filter :: is_attr_name('cn:dn:'));
}
/**
* @covers \EesyLDAP\Filter::is_attr_name
*/
public function test_is_attr_name_extended() {
$this -> assertTrue(Filter :: is_attr_name('o', true));
$this -> assertTrue(Filter :: is_attr_name('cn', true));
$this -> assertTrue(Filter :: is_attr_name('cn;x-test', true));
$this -> assertTrue(Filter :: is_attr_name('cn:', true));
$this -> assertTrue(Filter :: is_attr_name('cn:dn:', true));
$this -> assertTrue(Filter :: is_attr_name('cn:DN:', true));
$this -> assertTrue(Filter :: is_attr_name('cn:dn:caseExactMatch:', true));
$this -> assertTrue(Filter :: is_attr_name('cn:DN:caseExactMatch:', true));
$this -> assertTrue(Filter :: is_attr_name('cn:dn:2.4.6.8.10:', true));
$this -> assertTrue(Filter :: is_attr_name('cn:DN:2.4.6.8.10:', true));
$this -> assertTrue(Filter :: is_attr_name(':DN:caseExactMatch:', true));
$this -> assertTrue(Filter :: is_attr_name(':DN:2.4.6.8.10:', true));
$this -> assertTrue(Filter :: is_attr_name('cn:caseExactMatch:', true));
$this -> assertTrue(Filter :: is_attr_name('cn:2.4.6.8.10:', true));
$this -> assertFalse(Filter :: is_attr_name('bad name', true));
$this -> assertFalse(Filter :: is_attr_name('cn:dn', true));
$this -> assertFalse(Filter :: is_attr_name('cn:DN', true));
$this -> assertFalse(Filter :: is_attr_name('cn:case10', true));
$this -> assertFalse(Filter :: is_attr_name('cn:case10:', true));
$this -> assertFalse(Filter :: is_attr_name('cn:test1:test2:', true));
}
/**
* @covers \EesyLDAP\Filter::escape
*/
public function testEscape(){
$this->assertEquals('test\2a', Filter::escape('test*'));
$this->assertEquals('\28test\29', Filter::escape('(test)'));
$this->assertEquals('\5ctest', Filter::escape('\\test'));
$this->assertEquals('\00', Filter::escape("\u{0000}"));
}
/**
* @covers \EesyLDAP\Filter::unescape
*/
public function testUnescape(){
$this->assertEquals('test*test', Filter::unescape('test\2atest'));
$this->assertEquals('(test)', Filter::unescape('\28test\29'));
$this->assertEquals('\test', Filter::unescape('\\5ctest'));
$this->assertEquals("\u{0000}", Filter::unescape('\00'));
}
/**
* Note: Freely inspired by test cases provided with butonic's php-ldap-filter-parser.
* @see: https://github.com/butonic/php-ldap-filter-parser
* @covers \EesyLDAP\Filter::parse
*/
public function test_parse_leaf_filter() {
$a = Filter::parse('(cn=Babs Jensen)');
$this->assertInstanceOf(Filter::class, $a);
$this->assertEquals('cn', $a->attr_name);
$this->assertEquals('=', $a->op);
$this->assertEquals('Babs Jensen', $a->pattern);
}
/**
* @covers \EesyLDAP\Filter::parse
*/
public function test_parse_leaf_filter_without_brackets() {
$a = Filter::parse('cn=Babs Jensen');
$b = Filter::parse('(cn=Babs Jensen)');
$this->assertEquals($a, $b);
}
/**
* @covers \EesyLDAP\Filter::parse
*/
public function test_parse_leaf_filter_with_one_asterisk() {
$a = Filter::parse('(cn=Babs *)');
$this->assertInstanceOf(Filter::class, $a);
$this->assertEquals('cn', $a->attr_name);
$this->assertEquals('=', $a->op);
$this->assertEquals('Babs *', $a->pattern);
}
/**
* @covers \EesyLDAP\Filter::parse
*/
public function test_parse_leaf_filter_with_multiple_asterisk() {
$a = Filter::parse('(o=univ*of*mich*)');
$this->assertInstanceOf(Filter::class, $a);
$this->assertEquals('o', $a->attr_name);
$this->assertEquals('=', $a->op);
$this->assertEquals('univ*of*mich*', $a->pattern);
}
/**
* @covers \EesyLDAP\Filter::parse
*/
public function test_parse_leaf_filter_with_empty_value() {
$a = Filter::parse('(seeAlso=)');
$this->assertInstanceOf(Filter::class, $a);
$this->assertEquals('seeAlso', $a->attr_name);
$this->assertEquals('=', $a->op);
$this->assertEquals('', $a->pattern);
}
/**
* Extended attribute name filters
* @covers \EesyLDAP\Filter::parse
*/
public function test_parse_leaf_filter_with_extended_attr_name() {
$a = Filter::parse('(cn:caseExactMatch:=Fred Flintstone)');
$this->assertInstanceOf(Filter::class, $a);
$this->assertEquals('cn:caseExactMatch:', $a->attr_name);
$this->assertEquals('=', $a->op);
$this->assertEquals('Fred Flintstone', $a->pattern);
$a = Filter::parse('(cn:=Betty Rubble)');
$this->assertInstanceOf(Filter::class, $a);
$this->assertEquals('cn:', $a->attr_name);
$this->assertEquals('=', $a->op);
$this->assertEquals('Betty Rubble', $a->pattern);
$a = Filter::parse('(sn:dn:2.4.6.8.10:=Betty Rubble)');
$this->assertInstanceOf(Filter::class, $a);
$this->assertEquals('sn:dn:2.4.6.8.10:', $a->attr_name);
$this->assertEquals('=', $a->op);
$this->assertEquals('Betty Rubble', $a->pattern);
$a = Filter::parse('(o:dn:=Ace Industry:)');
$this->assertInstanceOf(Filter::class, $a);
$this->assertEquals('o:dn:', $a->attr_name);
$this->assertEquals('=', $a->op);
$this->assertEquals('Ace Industry:', $a->pattern);
$a = Filter::parse('(:1.2.3:=Wilma Flintstone)');
$this->assertInstanceOf(Filter::class, $a);
$this->assertEquals(':1.2.3:', $a->attr_name);
$this->assertEquals('=', $a->op);
$this->assertEquals('Wilma Flintstone', $a->pattern);
$a = Filter::parse('(:DN:2.4.6.8.10:=Dino)');
$this->assertInstanceOf(Filter::class, $a);
$this->assertEquals(':DN:2.4.6.8.10:', $a->attr_name);
$this->assertEquals('=', $a->op);
$this->assertEquals('Dino', $a->pattern);
}
/**
* Escaped characters
* @covers \EesyLDAP\Filter::parse
*/
public function test_parse_leaf_filter_with_escaped_characters() {
$a = Filter::parse('(o=Parens R Us \\28for all your parenthetical needs\\29)');
$this->assertInstanceOf(Filter::class, $a);
$this->assertInstanceOf(Filter::class, $a);
$this->assertEquals('o', $a->attr_name);
$this->assertEquals('=', $a->op);
$this->assertEquals('Parens R Us \\28for all your parenthetical needs\\29', $a->pattern);
$a = Filter::parse('(cn=*\\2A*)');
$this->assertInstanceOf(Filter::class, $a);
$this->assertEquals('cn', $a->attr_name);
$this->assertEquals('=', $a->op);
$this->assertEquals('*\\2A*', $a->pattern);
$a = Filter::parse('(filename=C:\\5cMyFile)');
$this->assertInstanceOf(Filter::class, $a);
$this->assertEquals('filename', $a->attr_name);
$this->assertEquals('=', $a->op);
$this->assertEquals('C:\\5cMyFile', $a->pattern);
$a = Filter::parse('(bin=\\00\\00\\00\\04)');
$this->assertInstanceOf(Filter::class, $a);
$this->assertEquals('bin', $a->attr_name);
$this->assertEquals('=', $a->op);
$this->assertEquals('\\00\\00\\00\\04', $a->pattern);
$a = Filter::parse('(sn=Lu\\c4\\8di\\c4\\87)');
$this->assertInstanceOf(Filter::class, $a);
$this->assertEquals('sn', $a->attr_name);
$this->assertEquals('=', $a->op);
$this->assertEquals('Lu\\c4\\8di\\c4\\87', $a->pattern);
$a = Filter::parse('(1.3.6.1.4.1.1466.0=\\04\\02\\48\\69)');
$this->assertInstanceOf(Filter::class, $a);
$this->assertEquals('1.3.6.1.4.1.1466.0', $a->attr_name);
$this->assertEquals('=', $a->op);
$this->assertEquals('\\04\\02\\48\\69', $a->pattern);
}
/**
* Parse combined filters
* @covers \EesyLDAP\Filter::parse
*/
public function test_parse_combined_filter() {
$a = Filter::parse('(&(objectClass=inetOrgPerson)(memberOf=cn=owncloudusers,ou=groups,dc=example,dc=com))');
$this->assertInstanceOf(Filter::class, $a);
$this->assertTrue($a->is_combine());
$this->assertEquals('&', $a->log_op);
$this->assertIsArray($a->sub_filters);
$this->assertCount(2, $a->sub_filters);
$this->assertInstanceOf(Filter::class, $a->sub_filters[0]);
$this->assertTrue($a->sub_filters[0]->is_leaf());
$this->assertEquals('objectClass', $a->sub_filters[0]->attr_name);
$this->assertEquals('=', $a->sub_filters[0]->op);
$this->assertEquals('inetOrgPerson', $a->sub_filters[0]->pattern);
$this->assertInstanceOf(Filter::class, $a->sub_filters[1]);
$this->assertTrue($a->sub_filters[1]->is_leaf());
$this->assertEquals('memberOf', $a->sub_filters[1]->attr_name);
$this->assertEquals('=', $a->sub_filters[1]->op);
$this->assertEquals('cn=owncloudusers,ou=groups,dc=example,dc=com', $a->sub_filters[1]->pattern);
}
/**
* Parse multiple combined filters
* @covers \EesyLDAP\Filter::parse
*/
public function test_parse_multiple_combined_filters() {
$a = Filter::parse('(&(|(uid=a*)(userid=a*))(|(telephoneNumber=1234)(mobile=1234)))');
$this->assertInstanceOf(Filter::class, $a);
$this->assertTrue($a->is_combine());
$this->assertEquals('&', $a->log_op);
$this->assertIsArray($a->sub_filters);
$this->assertCount(2, $a->sub_filters);
$this->assertInstanceOf(Filter::class, $a->sub_filters[0]);
$this->assertTrue($a->sub_filters[0]->is_combine());
$this->assertEquals('|', $a->sub_filters[0]->log_op);
$this->assertIsArray($a->sub_filters[0]->sub_filters);
$this->assertCount(2, $a->sub_filters[0]->sub_filters);
$this->assertInstanceOf(Filter::class, $a->sub_filters[0]->sub_filters[0]);
$this->assertTrue($a->sub_filters[0]->sub_filters[0]->is_leaf());
$this->assertEquals('uid', $a->sub_filters[0]->sub_filters[0]->attr_name);
$this->assertEquals('=', $a->sub_filters[0]->sub_filters[0]->op);
$this->assertEquals('a*', $a->sub_filters[0]->sub_filters[0]->pattern);
$this->assertInstanceOf(Filter::class, $a->sub_filters[0]->sub_filters[1]);
$this->assertTrue($a->sub_filters[0]->sub_filters[1]->is_leaf());
$this->assertEquals('userid', $a->sub_filters[0]->sub_filters[1]->attr_name);
$this->assertEquals('=', $a->sub_filters[0]->sub_filters[1]->op);
$this->assertEquals('a*', $a->sub_filters[0]->sub_filters[1]->pattern);
$this->assertInstanceOf(Filter::class, $a->sub_filters[1]);
$this->assertTrue($a->sub_filters[1]->is_combine());
$this->assertEquals('|', $a->sub_filters[1]->log_op);
$this->assertIsArray($a->sub_filters[1]->sub_filters);
$this->assertCount(2, $a->sub_filters[1]->sub_filters);
$this->assertInstanceOf(Filter::class, $a->sub_filters[1]->sub_filters[0]);
$this->assertTrue($a->sub_filters[1]->sub_filters[0]->is_leaf());
$this->assertEquals('telephoneNumber', $a->sub_filters[1]->sub_filters[0]->attr_name);
$this->assertEquals('=', $a->sub_filters[1]->sub_filters[0]->op);
$this->assertEquals('1234', $a->sub_filters[1]->sub_filters[0]->pattern);
$this->assertInstanceOf(Filter::class, $a->sub_filters[1]->sub_filters[1]);
$this->assertTrue($a->sub_filters[1]->sub_filters[1]->is_leaf());
$this->assertEquals('mobile', $a->sub_filters[1]->sub_filters[1]->attr_name);
$this->assertEquals('=', $a->sub_filters[1]->sub_filters[1]->op);
$this->assertEquals('1234', $a->sub_filters[1]->sub_filters[1]->pattern);
}
/**
* Parse multiple combined and nested filters
* @covers \EesyLDAP\Filter::parse
*/
public function test_parse_multiple_combined_and_nested_filters() {
$a = Filter::parse('(&(objectClass=Person)(|(sn=Jensen)(cn=Babs J*)))');
$this->assertInstanceOf(Filter::class, $a);
$this->assertTrue($a->is_combine());
$this->assertEquals('&', $a->log_op);
$this->assertIsArray($a->sub_filters);
$this->assertCount(2, $a->sub_filters);
$this->assertInstanceOf(Filter::class, $a->sub_filters[0]);
$this->assertTrue($a->sub_filters[0]->is_leaf());
$this->assertEquals('objectClass', $a->sub_filters[0]->attr_name);
$this->assertEquals('=', $a->sub_filters[0]->op);
$this->assertEquals('Person', $a->sub_filters[0]->pattern);
$this->assertInstanceOf(Filter::class, $a->sub_filters[1]);
$this->assertTrue($a->sub_filters[1]->is_combine());
$this->assertEquals('|', $a->sub_filters[1]->log_op);
$this->assertIsArray($a->sub_filters[1]->sub_filters);
$this->assertCount(2, $a->sub_filters[1]->sub_filters);
$this->assertInstanceOf(Filter::class, $a->sub_filters[1]->sub_filters[0]);
$this->assertTrue($a->sub_filters[1]->sub_filters[0]->is_leaf());
$this->assertEquals('sn', $a->sub_filters[1]->sub_filters[0]->attr_name);
$this->assertEquals('=', $a->sub_filters[1]->sub_filters[0]->op);
$this->assertEquals('Jensen', $a->sub_filters[1]->sub_filters[0]->pattern);
$this->assertInstanceOf(Filter::class, $a->sub_filters[1]->sub_filters[1]);
$this->assertTrue($a->sub_filters[1]->sub_filters[1]->is_leaf());
$this->assertEquals('cn', $a->sub_filters[1]->sub_filters[1]->attr_name);
$this->assertEquals('=', $a->sub_filters[1]->sub_filters[1]->op);
$this->assertEquals('Babs J*', $a->sub_filters[1]->sub_filters[1]->pattern);
}
/**
* Parse a not filter
* @covers \EesyLDAP\Filter::parse
*/
public function test_parse_not_filter() {
$a = Filter::parse('(!(objectClass=Person))');
$this->assertInstanceOf(Filter::class, $a);
$this->assertTrue($a->is_combine());
$this->assertEquals('!', $a->log_op);
$this->assertIsArray($a->sub_filters);
$this->assertCount(1, $a->sub_filters);
$this->assertInstanceOf(Filter::class, $a->sub_filters[0]);
$this->assertTrue($a->sub_filters[0]->is_leaf());
$this->assertEquals('objectClass', $a->sub_filters[0]->attr_name);
$this->assertEquals('=', $a->sub_filters[0]->op);
$this->assertEquals('Person', $a->sub_filters[0]->pattern);
}
/**
* Parse filter with unclosed bracket
* @covers \EesyLDAP\Filter::parse
*/
public function test_parse_filter_with_unclosed_bracket() {
$this->expectException(ParserException::class);
Filter::parse('(objectClass=Person');
}
/**
* Parse NOT filter with multiple sub filters
* @covers \EesyLDAP\Filter::parse
*/
public function test_parse_not_filter_with_multiple_filter() {
$this->expectException(ParserException::class);
Filter::parse('(!(objectClass=Person)(cn=test))');
}
/**
* Parse filter with unescaped brackets
* @covers \EesyLDAP\Filter::parse
*/
public function test_parse_with_unescaped_brackets() {
$this->expectException(ParserException::class);
var_dump(Filter::parse('(cn=test(test))'));
}
/**
* Parse filter with invalid sub filter
* @covers \EesyLDAP\Filter::parse
*/
public function test_parse_with_invalid_sub_filter() {
$this->expectException(ParserException::class);
var_dump(Filter::parse('(&(cn=test)(test))'));
}
}