Compare commits
2 commits
6b61ecd6f2
...
9520502ece
Author | SHA1 | Date | |
---|---|---|---|
9520502ece | |||
cd303a3a12 |
7 changed files with 698 additions and 34 deletions
|
@ -4,6 +4,8 @@ parameters:
|
||||||
- src
|
- src
|
||||||
- tests
|
- tests
|
||||||
treatPhpDocTypesAsCertain: false
|
treatPhpDocTypesAsCertain: false
|
||||||
|
universalObjectCratesClasses:
|
||||||
|
- EesyLDAP\Entry
|
||||||
ignoreErrors:
|
ignoreErrors:
|
||||||
-
|
-
|
||||||
message: "#Method .*::test.*\\(\\) has no return type specified\\.#"
|
message: "#Method .*::test.*\\(\\) has no return type specified\\.#"
|
||||||
|
|
144
src/Entry.php
Normal file
144
src/Entry.php
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
<?php
|
||||||
|
namespace EesyLDAP;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property string|false $dn
|
||||||
|
* @property-read array<string,array<string>> $changes
|
||||||
|
*/
|
||||||
|
class Entry {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entry DN
|
||||||
|
* @var string|false
|
||||||
|
*/
|
||||||
|
protected $dn = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attributes data
|
||||||
|
* @var array<string,array<string>>
|
||||||
|
*/
|
||||||
|
protected $data = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changed data
|
||||||
|
* @var array<string,array<string>>
|
||||||
|
*/
|
||||||
|
protected $changes = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LDAP connection
|
||||||
|
* @var Ldap|null
|
||||||
|
*/
|
||||||
|
protected $ldap = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
* @param string|null $dn Object DN (optional, default=null)
|
||||||
|
* @param array<string,array<string>> $data Object attributes data (optional, default=null)
|
||||||
|
* @param Ldap|null $ldap The LDAP connection (optional, default=null)
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct($dn=null, $data=null, $ldap=null) {
|
||||||
|
if (is_string($dn))
|
||||||
|
$this -> dn = $dn;
|
||||||
|
if (is_array($data))
|
||||||
|
foreach($data as $attr => $value)
|
||||||
|
$this -> data[mb_strtolower($attr)] = $data[$attr];
|
||||||
|
$this -> ldap = $ldap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get entry key
|
||||||
|
* @param string $key
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function __get($key) {
|
||||||
|
switch ($key) {
|
||||||
|
case 'dn':
|
||||||
|
return $this -> dn;
|
||||||
|
case 'changes':
|
||||||
|
return $this -> changes;
|
||||||
|
default:
|
||||||
|
return $this -> get_values($key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set entry key value
|
||||||
|
* @param string $key
|
||||||
|
* @param mixed $value
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __set($key, $value) {
|
||||||
|
switch ($key) {
|
||||||
|
case 'dn':
|
||||||
|
if (is_string($value) || $value === false)
|
||||||
|
$this -> dn = $value;
|
||||||
|
else
|
||||||
|
$this -> error('Unexcepted DN value provided: must be a string or false');
|
||||||
|
default:
|
||||||
|
$key = mb_strtolower($key);
|
||||||
|
if (is_null($value))
|
||||||
|
$value = array();
|
||||||
|
if (!is_array($value))
|
||||||
|
$value = array($value);
|
||||||
|
$this -> changes[$key] = array();
|
||||||
|
foreach($value as $v)
|
||||||
|
$this -> changes[$key][] = strval($v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if entry key is set
|
||||||
|
* @param string $key
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function __isset($key) {
|
||||||
|
return (
|
||||||
|
in_array($key, array('dn', 'changes'))
|
||||||
|
|| array_key_exists($key, $this -> data)
|
||||||
|
|| array_key_exists($key, $this -> changes)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an attribute values
|
||||||
|
* @param string $attr The expected attribute name
|
||||||
|
* @param mixed $default The default value if attribute is not set
|
||||||
|
* (optional, default: null or array() if $all_values is True)
|
||||||
|
* @param bool $all_values If True, return all the attribute values, otherwise return only the
|
||||||
|
* first one (optional, default: true)
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function get_values($attr, $default=null, $all_values=true) {
|
||||||
|
$attr = mb_strtolower($attr);
|
||||||
|
$values = false;
|
||||||
|
if (array_key_exists($attr, $this -> changes))
|
||||||
|
$values = &$this -> changes[$attr];
|
||||||
|
elseif (array_key_exists($attr, $this -> data))
|
||||||
|
$values = &$this -> data[$attr];
|
||||||
|
if (!$values)
|
||||||
|
return is_null($default) && $all_values?array():$default;
|
||||||
|
return $all_values?$values:$values[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log and eventually raise an error
|
||||||
|
* @param string $error The error message
|
||||||
|
* @param array<mixed> $extra_args If passed, will be used to compute the error message using sprintf
|
||||||
|
* @return false
|
||||||
|
* @throws LdapException
|
||||||
|
*/
|
||||||
|
protected function error($error, ...$extra_args) {
|
||||||
|
if ($extra_args)
|
||||||
|
$error = call_user_func_array('sprintf', array_merge(array($error), $extra_args));
|
||||||
|
// Note: sprintf always return string
|
||||||
|
if ($this->ldap)
|
||||||
|
// @phpstan-ignore-next-line
|
||||||
|
return $this->ldap->error($error);
|
||||||
|
// @phpstan-ignore-next-line
|
||||||
|
error_log($error);
|
||||||
|
throw new LdapException($error); // @phpstan-ignore-line
|
||||||
|
}
|
||||||
|
}
|
|
@ -146,6 +146,9 @@ class Filter {
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
* @param array<string|Filter|array<string|Filter>|bool> $args
|
* @param array<string|Filter|array<string|Filter>|bool> $args
|
||||||
|
* @throws CombineException
|
||||||
|
* @throws FilterException
|
||||||
|
* @throws ParserException
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function __construct(...$args) {
|
public function __construct(...$args) {
|
||||||
|
@ -181,7 +184,7 @@ class Filter {
|
||||||
|
|
||||||
// Check number of filters against logical operator
|
// Check number of filters against logical operator
|
||||||
if (self :: is_not_op($this -> log_op) && count($this -> sub_filters) != 1)
|
if (self :: is_not_op($this -> log_op) && count($this -> sub_filters) != 1)
|
||||||
throw new FilterException(
|
throw new CombineException(
|
||||||
'Invalid constructor arguments: NOT operator must be followed by exactly one filter');
|
'Invalid constructor arguments: NOT operator must be followed by exactly one filter');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -361,40 +364,17 @@ class Filter {
|
||||||
*
|
*
|
||||||
* @param string $log_op The locical operator. May be "and", "or", "not" or the subsequent logical
|
* @param string $log_op The locical operator. May be "and", "or", "not" or the subsequent logical
|
||||||
* equivalents "&", "|", "!"
|
* equivalents "&", "|", "!"
|
||||||
* @param array<string|Filter|array<string|Filter>> $args LDAP filters to combine: could be
|
* @param array<string|Filter|array<string|Filter>> $filters LDAP filters to combine: could be
|
||||||
* LDAP objects, LDAP strings or array of
|
* LDAP objects, LDAP strings or array
|
||||||
* LDAP objects or LDAP strings.
|
* of LDAP objects or LDAP strings.
|
||||||
* @throws CombineException
|
* @throws CombineException
|
||||||
|
* @throws FilterException
|
||||||
|
* @throws ParserException
|
||||||
* @return Filter
|
* @return Filter
|
||||||
* @static
|
* @static
|
||||||
*/
|
*/
|
||||||
public static function combine($log_op, ...$args) {
|
public static function combine($log_op, ...$filters) {
|
||||||
// Unalias & check logical operator
|
// @phpstan-ignore-next-line
|
||||||
$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);
|
return new Filter($log_op, ...$filters);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,4 +2,4 @@
|
||||||
|
|
||||||
namespace EesyLDAP\Filter;
|
namespace EesyLDAP\Filter;
|
||||||
|
|
||||||
class FilterException extends \Exception {}
|
class FilterException extends \EesyLDAP\LdapException {}
|
||||||
|
|
533
src/Ldap.php
Normal file
533
src/Ldap.php
Normal file
|
@ -0,0 +1,533 @@
|
||||||
|
<?php
|
||||||
|
namespace EesyLDAP;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property-read array<string> $hosts
|
||||||
|
* @property-read array<string> $host
|
||||||
|
* @property-read int $port
|
||||||
|
* @property-read int $version
|
||||||
|
* @property-read bool $starttls
|
||||||
|
* @property-read string|null $bind_dn
|
||||||
|
* @property-read string|null $binddn
|
||||||
|
* @property-read string|null $dn
|
||||||
|
* @property-read string|null $bind_password
|
||||||
|
* @property-read string|null $bindpw
|
||||||
|
* @property-read string|null $password
|
||||||
|
* @property-read string|null $pwd
|
||||||
|
* @property-read string|null $basedn
|
||||||
|
* @property-read array $options
|
||||||
|
* @property-read array $required_options
|
||||||
|
* @property-read string $filter
|
||||||
|
* @property-read string $scope
|
||||||
|
* @property-read bool $raise_on_error
|
||||||
|
*/
|
||||||
|
class Ldap {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration
|
||||||
|
* @var array<string,mixed>
|
||||||
|
*/
|
||||||
|
protected $config = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default configuration
|
||||||
|
* @var array<string,mixed>
|
||||||
|
*/
|
||||||
|
protected static $default_config = array(
|
||||||
|
// LDAP server name to connect to. You can provide several hosts in an array in which case the
|
||||||
|
// hosts are tried from left to right.
|
||||||
|
'hosts' => array('localhost'),
|
||||||
|
// Port on the server
|
||||||
|
'port' => 389,
|
||||||
|
// LDAP protocol version
|
||||||
|
'version' => 3,
|
||||||
|
// TLS is started after connecting
|
||||||
|
'starttls' => false,
|
||||||
|
// The distinguished name to bind as (username). If you don't supply this, an anonymous bind
|
||||||
|
// will be established.
|
||||||
|
'bind_dn' => null,
|
||||||
|
// Password for the binddn. If the credentials are wrong, the bind will fail server-side and an
|
||||||
|
// anonymous bind will be established instead. An empty bindpw string requests an
|
||||||
|
// unauthenticated bind. This can cause security problems in your application, if you rely on a
|
||||||
|
// bind to make security decisions (see RFC-4513, section 6.3.1).
|
||||||
|
'bind_password' => null,
|
||||||
|
// LDAP base name (root directory)
|
||||||
|
'basedn' => null,
|
||||||
|
// Array of additional ldap options as key-value pairs
|
||||||
|
'options' => array(),
|
||||||
|
// Required options: consider LDAP link as bad if the required option could not be set.
|
||||||
|
// Possibles values:
|
||||||
|
// - array of expected options
|
||||||
|
// - True to consider all options as required (default)
|
||||||
|
// - False to consider all options as optional
|
||||||
|
'required_options' => true,
|
||||||
|
// Default search filter (string or preferably \EesyLDAP\Filter object).
|
||||||
|
'filter' => '(objectClass=*)',
|
||||||
|
// Default search scope
|
||||||
|
'scope' => 'sub',
|
||||||
|
// Raise LdapException on error
|
||||||
|
'raise_on_error' => true,
|
||||||
|
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration parameter aliases
|
||||||
|
* @var array<string,string>
|
||||||
|
*/
|
||||||
|
protected static $config_aliases = array(
|
||||||
|
'host' => 'hosts',
|
||||||
|
'binddn' => 'bind_dn',
|
||||||
|
'dn' => 'bind_dn',
|
||||||
|
'bindpw' => 'bind_password',
|
||||||
|
'password' => 'bind_password',
|
||||||
|
'pwd' => 'bind_password',
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The LDAP connection
|
||||||
|
* @var resource|false
|
||||||
|
*/
|
||||||
|
protected $_link = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scopes associated with their search function
|
||||||
|
* @var array<string,callable>
|
||||||
|
*/
|
||||||
|
protected static $search_function_by_scope = array(
|
||||||
|
'one' => 'ldap_list',
|
||||||
|
'base' => 'ldap_read',
|
||||||
|
'sub' => 'ldap_search',
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
* @param array<string,mixed>|null $config Configuration (see $default_config for expected content)
|
||||||
|
* @param bool $connect If true, start LDAP connection (optional, default=true)
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct($config=null, $connect=true) {
|
||||||
|
if (is_array($config))
|
||||||
|
$this -> set_config($config);
|
||||||
|
if ($connect)
|
||||||
|
$this -> connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set configuration
|
||||||
|
* @param array<string,mixed> $config
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function set_config($config) {
|
||||||
|
if (!is_array($config))
|
||||||
|
return $this->error('Expect $config to be an array');
|
||||||
|
|
||||||
|
foreach($config as $key => $value) {
|
||||||
|
if (array_key_exists($key, self :: $config_aliases))
|
||||||
|
$key = self :: $config_aliases[$key];
|
||||||
|
if (!array_key_exists($key, self :: $default_config))
|
||||||
|
return $this -> error("Invalid configuration parameter '$key'");
|
||||||
|
switch ($key) {
|
||||||
|
case 'hosts':
|
||||||
|
if (is_string($value)) {
|
||||||
|
$hosts = array();
|
||||||
|
foreach(explode(',', $value) as $host) {
|
||||||
|
$host = trim($host);
|
||||||
|
if (!$host)
|
||||||
|
return $this -> error("Empty host found in configuration!");
|
||||||
|
$hosts[] = $host;
|
||||||
|
}
|
||||||
|
$value = $hosts;
|
||||||
|
}
|
||||||
|
else if (!is_array($value)) {
|
||||||
|
$this -> error(
|
||||||
|
"Invalid hosts found in configuration: expect to be an array of LDAP host URI (or ".
|
||||||
|
"hostname) or a comma separated list of LDAP host URI (or hostname)."
|
||||||
|
);
|
||||||
|
continue 2;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'port':
|
||||||
|
case 'version':
|
||||||
|
$value = intval($value);
|
||||||
|
break;
|
||||||
|
case 'starttls':
|
||||||
|
$value = boolval($value);
|
||||||
|
break;
|
||||||
|
case 'options':
|
||||||
|
if (!is_array($value)) {
|
||||||
|
$this -> error(
|
||||||
|
"Invalid options found in configuration: expect to be an array."
|
||||||
|
);
|
||||||
|
continue 2;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'required_options':
|
||||||
|
if (!is_array($value) && !is_bool($value)) {
|
||||||
|
$this -> error(
|
||||||
|
"Invalid required_options found in configuration: expect to be an array or a boolean."
|
||||||
|
);
|
||||||
|
continue 2;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'filter':
|
||||||
|
if (is_string($value))
|
||||||
|
$value = new Filter($value);
|
||||||
|
elseif (!$value instanceof Filter) {
|
||||||
|
$this -> error(
|
||||||
|
"Invalid filter found in configuration: expect to be a string or a \EesyLDAP\Filter ".
|
||||||
|
"object."
|
||||||
|
);
|
||||||
|
continue 2;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'scope':
|
||||||
|
if (!is_string($value) || !array_key_exists($value, self :: $search_function_by_scope)){
|
||||||
|
$this -> error(
|
||||||
|
"Invalid scope '%s' found in configuration: expect to be one of the following: %s",
|
||||||
|
null,
|
||||||
|
is_string($value)?$value:gettype($value),
|
||||||
|
implode(', ', array_keys(self :: $search_function_by_scope))
|
||||||
|
);
|
||||||
|
continue 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
$this -> config[$key] = $value;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if phps ldap-extension is loaded
|
||||||
|
*
|
||||||
|
* Note: If it is not loaded, it tries to load it manually using PHPs dl().
|
||||||
|
* It knows both windows-dll and *nix-so.
|
||||||
|
*
|
||||||
|
* @param bool|null $raise See error() (optional, default: null)
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function check_ldap_extension($raise=null) {
|
||||||
|
if (!extension_loaded('ldap') && !@dl('ldap.' . PHP_SHLIB_SUFFIX)) {
|
||||||
|
return $this -> error(
|
||||||
|
"It seems that you do not have the ldap-extension installed. Please install it before ".
|
||||||
|
"using the EesyLDAP.",
|
||||||
|
$raise
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start connection on configured LDAP hosts
|
||||||
|
* @param bool|null $raise See error() (optional, default: null)
|
||||||
|
* @param string|null $bind_dn Bind DN (optional, default: null = use configuration)
|
||||||
|
* @param string|null $bind_password Bind password (optional, default: null = use configuration)
|
||||||
|
* @return bool
|
||||||
|
* @throws LdapException
|
||||||
|
*/
|
||||||
|
public function connect($raise=null, $bind_dn=null, $bind_password=null) {
|
||||||
|
if ($this -> _link)
|
||||||
|
return true;
|
||||||
|
if (!self :: check_ldap_extension($raise))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
foreach($this -> hosts as $host) {
|
||||||
|
$this->_link = @ldap_connect($host, $this->port);
|
||||||
|
if ($this->_link === false) {
|
||||||
|
$this -> error("Fail to connect on $host", false);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->starttls) {
|
||||||
|
if (@ldap_start_tls($this->_link) === false) {
|
||||||
|
$this -> close();
|
||||||
|
$this -> error("Fail to start TLS on $host", false);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->version && !$this->set_option("LDAP_OPT_PROTOCOL_VERSION", $this->version, false)) {
|
||||||
|
$this -> close();
|
||||||
|
$this -> error(
|
||||||
|
"Fail to switch to LDAP protocol version %d on %s", false, $this->version, $host);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this -> bind($bind_dn, $bind_password)) {
|
||||||
|
$this -> close();
|
||||||
|
$this -> error(
|
||||||
|
"Fail to bind on %s", false);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set LDAP parameters, now we know we have a valid connection.
|
||||||
|
foreach ($this->options as $option => $value) {
|
||||||
|
if (!$this -> set_option($option, $value, false)) {
|
||||||
|
if (in_array($option, $this -> required_options)) {
|
||||||
|
$this -> close();
|
||||||
|
$this -> error('Fail to set option %s on %s', false, $option, $host);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this -> error('Fail to connect on configured host(s)', $raise);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bind on LDAP link
|
||||||
|
* Note: if not already connected, start connection with the specified credentials.
|
||||||
|
* @param string|null $dn Bind DN (optional, default: null = use configuration)
|
||||||
|
* @param string|null $password Bind password (optional, default: null = use configuration)
|
||||||
|
* @param bool|null $raise See error() (optional, default: null)
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function bind($dn=null, $password=null, $raise=null) {
|
||||||
|
if (is_null($dn)) $dn = $this -> bind_dn;
|
||||||
|
if (is_null($password)) $password = $this -> bind_password;
|
||||||
|
if (!$this -> _link) return $this -> connect($raise, $dn, $password);
|
||||||
|
if (@ldap_bind($this->_link, $dn, $password) === false)
|
||||||
|
return $this -> log_error($dn?"Fail to bind as $dn":"Fail to anonymously bind", $raise);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set an LDAP option
|
||||||
|
*
|
||||||
|
* @param string $option Option to set
|
||||||
|
* @param mixed $value Value to set Option to
|
||||||
|
* @param bool|null $raise See error() (optional, default: null)
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @return bool
|
||||||
|
* @throws LdapException
|
||||||
|
*/
|
||||||
|
public function set_option($option, $value, $raise=null) {
|
||||||
|
if (!$this->_link)
|
||||||
|
return $this->error("Could not set LDAP option: No LDAP connection", $raise);
|
||||||
|
if (!is_string($option) || !defined($option) || strpos($option, 'LDAP_') !== 0)
|
||||||
|
return $this->error("Unkown Option requested", $raise);
|
||||||
|
// @phpstan-ignore-next-line
|
||||||
|
if (@ldap_set_option($this->_link, constant($option), $value))
|
||||||
|
return true;
|
||||||
|
return $this -> log_error("Fail to set option $option", $raise);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close LDAP link (if established)
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function close() {
|
||||||
|
if ($this->_link)
|
||||||
|
@ldap_close($this->_link);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log the last error occured on LDAP link
|
||||||
|
* @param string $prefix
|
||||||
|
* @param bool|null $raise See error() (optional, default: null)
|
||||||
|
* @param array<mixed> $extra_args If passed, will be used to compute the prefix message using sprintf
|
||||||
|
* @return false
|
||||||
|
*/
|
||||||
|
protected function log_error($prefix, $raise=null, ...$extra_args) {
|
||||||
|
if ($extra_args)
|
||||||
|
$prefix = call_user_func_array('sprintf', array_merge(array($prefix), $extra_args));
|
||||||
|
$errno = $this->_link?@ldap_errno($this->_link):false;
|
||||||
|
if (!$errno)
|
||||||
|
return $this -> error("%s: unkown error", $raise, $prefix);
|
||||||
|
$err = @ldap_err2str($errno);
|
||||||
|
if (!$err)
|
||||||
|
return $this -> error("%s: error #%s", $raise, $errno);
|
||||||
|
return $this -> error("%s: %s (#%s)", $raise, $err, $errno);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log and eventually raise an error
|
||||||
|
* @param string $error The error message
|
||||||
|
* @param bool|null $raise If true, raise an LdapException, otherwise only log it and return false
|
||||||
|
* @param array<mixed> $extra_args If passed, will be used to compute the error message using sprintf
|
||||||
|
* @return false
|
||||||
|
* @throws LdapException
|
||||||
|
*/
|
||||||
|
public function error($error, $raise=null, ...$extra_args) {
|
||||||
|
if ($extra_args)
|
||||||
|
$error = call_user_func_array('sprintf', array_merge(array($error), $extra_args));
|
||||||
|
// Note: sprintf always return string
|
||||||
|
// @phpstan-ignore-next-line
|
||||||
|
error_log($error);
|
||||||
|
if (is_null($raise))
|
||||||
|
$raise = $this -> raise_on_error;
|
||||||
|
if ($raise)
|
||||||
|
throw new LdapException($error); // @phpstan-ignore-line
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get property
|
||||||
|
* @param string $key
|
||||||
|
* @return mixed
|
||||||
|
* @throws LdapException
|
||||||
|
*/
|
||||||
|
public function __get($key) {
|
||||||
|
if (
|
||||||
|
array_key_exists($key, self :: $default_config)
|
||||||
|
|| array_key_exists($key, self :: $config_aliases)
|
||||||
|
)
|
||||||
|
return $this -> get_config($key);
|
||||||
|
switch ($key) {
|
||||||
|
case 'config_aliases':
|
||||||
|
return self :: $config_aliases;
|
||||||
|
case 'default_config':
|
||||||
|
return self :: $default_config;
|
||||||
|
}
|
||||||
|
return $this -> error("Invalid property '$key' requested");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get configuration parameter
|
||||||
|
* @param string $key
|
||||||
|
* @return mixed
|
||||||
|
* @throws LdapException
|
||||||
|
*/
|
||||||
|
public function get_config($key) {
|
||||||
|
if (array_key_exists($key, self :: $config_aliases))
|
||||||
|
$key = self :: $config_aliases[$key];
|
||||||
|
|
||||||
|
$value = null;
|
||||||
|
if (array_key_exists($key, $this -> config))
|
||||||
|
$value = $this -> config[$key];
|
||||||
|
elseif (array_key_exists($key, self :: $default_config))
|
||||||
|
$value = self :: $default_config[$key];
|
||||||
|
else
|
||||||
|
return $this -> error("Invalid configuration parameter '$key'");
|
||||||
|
switch ($key) {
|
||||||
|
case 'required_options':
|
||||||
|
if (is_array($value))
|
||||||
|
return $value;
|
||||||
|
if ($value === false)
|
||||||
|
return array();
|
||||||
|
return array_keys($this->options);
|
||||||
|
case 'filter':
|
||||||
|
if (is_string($value))
|
||||||
|
return Filter :: parse($value);
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search on LDAP directory
|
||||||
|
*
|
||||||
|
* Optional expected parameters are:
|
||||||
|
* - scope: The scope which will be used for searching.
|
||||||
|
* Possible values:
|
||||||
|
* - base: Just one entry (the specified base DN, see $base)
|
||||||
|
* - sub: The whole subtree
|
||||||
|
* - one: Immediately below the specified base DN (see $base)
|
||||||
|
* - sizelimit: Limit the number of entries returned (default: 0 = unlimited),
|
||||||
|
* - timelimit: Limit the time spent for searching in seconds (default: 0 = unlimited),
|
||||||
|
* - attrsonly: If true, the search will only return the attribute names,
|
||||||
|
* - attributes: Array of attribute names, which the entries in the expected result should contain.
|
||||||
|
* It is good practice to limit this to just the ones you need.
|
||||||
|
*
|
||||||
|
* @param string|null $base Base DN of the search (optional, default: null == use config)
|
||||||
|
* @param string|null $filter Filter of the search (optional, default: null == use config)
|
||||||
|
* @param array<string,mixed>|null $params Other optional parameters of the search (optional, default: null == use config)
|
||||||
|
* @param bool|null $raise See error() (optional, default: null)
|
||||||
|
* @return array<string|Entry>|false Array of Entry object with DN as key, or False in case of error
|
||||||
|
* @throws LdapException
|
||||||
|
*/
|
||||||
|
public function search($base=null, $filter=null, $params=null, $raise=null) {
|
||||||
|
if (!$this -> _link)
|
||||||
|
return $this -> error("Can't search: no LDAP link");
|
||||||
|
if (is_null($base))
|
||||||
|
$base = $this->basedn;
|
||||||
|
elseif ($base instanceof Entry)
|
||||||
|
$base = $base->dn?$base->dn:null;
|
||||||
|
|
||||||
|
if (is_null($filter)) $filter = $this->filter;
|
||||||
|
if ($filter instanceof Filter)
|
||||||
|
$filter = $filter->as_string(); // convert Filter object as string
|
||||||
|
|
||||||
|
// Adjust search function to expected scope
|
||||||
|
$scope = self :: get_param($params, 'scope', $this->scope, 'string');
|
||||||
|
if (!is_string($scope) || !array_key_exists($scope, self :: $search_function_by_scope))
|
||||||
|
return $this -> error("Invalid scope '%s' specified", $raise, $scope);
|
||||||
|
|
||||||
|
// Run the search
|
||||||
|
$search = @call_user_func(
|
||||||
|
self :: $search_function_by_scope[$scope],
|
||||||
|
$this->_link,
|
||||||
|
$base,
|
||||||
|
$filter,
|
||||||
|
self :: get_param($params, 'attributes', array(), 'array'),
|
||||||
|
self :: get_param($params, 'attrsonly', false, 'bool')?1:0,
|
||||||
|
self :: get_param($params, 'sizelimit', 0, 'int'),
|
||||||
|
self :: get_param($params, 'timelimit', 0, 'int')
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!$search)
|
||||||
|
return $this -> log_error(
|
||||||
|
"Error occured searching on base '%s' with filter '%s'", $raise, $base, $filter);
|
||||||
|
|
||||||
|
// @phpstan-ignore-next-line
|
||||||
|
$result = ldap_get_entries($this->_link, $search);
|
||||||
|
if (!is_array($result))
|
||||||
|
return $this -> log_error(
|
||||||
|
"Error occured retreiving the result of the search on base '%s' with filter '%s'",
|
||||||
|
$raise, $base, $filter
|
||||||
|
);
|
||||||
|
$entries = array();
|
||||||
|
for ($i=0; $i<$result['count']; $i++) {
|
||||||
|
$dn = $result[$i]['dn'];
|
||||||
|
$attrs = array();
|
||||||
|
for($j=0; $j<$result[$i]['count']; $j++) {
|
||||||
|
$attr = $result[$i][$j];
|
||||||
|
$attrs[$attr] = array();
|
||||||
|
for($k=0; $k<$result[$i][$attr]['count']; $k++)
|
||||||
|
$attrs[$attr][$k] = $result[$i][$attr][$k];
|
||||||
|
}
|
||||||
|
// @phpstan-ignore-next-line
|
||||||
|
$entries[$dn] = new Entry($dn, $attrs);
|
||||||
|
}
|
||||||
|
return $entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get parameter value
|
||||||
|
* @param array<string,mixed>|null $params Array of provided parameter values
|
||||||
|
* @param string $key Expected parameter key
|
||||||
|
* @param mixed $default Default value if parameter if not set (optional, default: null)
|
||||||
|
* @param string|null $cast Expected type of value: if provided, the parameter value will be casted
|
||||||
|
* as the expected type (optional, default: null == no cast)
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
protected static function get_param($params, $key, $default=null, $cast=null) {
|
||||||
|
if (!is_array($params) || !array_key_exists($key, $params))
|
||||||
|
return $default;
|
||||||
|
$value = $params[$key];
|
||||||
|
switch($cast) {
|
||||||
|
case 'bool':
|
||||||
|
case 'boolean':
|
||||||
|
return boolval($value);
|
||||||
|
case 'int':
|
||||||
|
case 'integer':
|
||||||
|
return intval($value);
|
||||||
|
case 'float':
|
||||||
|
return floatval($value);
|
||||||
|
case 'str':
|
||||||
|
case 'string':
|
||||||
|
return strval($value);
|
||||||
|
case 'array':
|
||||||
|
if (is_array($value))
|
||||||
|
return $value;
|
||||||
|
if (is_null($value))
|
||||||
|
return array();
|
||||||
|
return array($value);
|
||||||
|
}
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
}
|
5
src/LdapException.php
Normal file
5
src/LdapException.php
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace EesyLDAP;
|
||||||
|
|
||||||
|
class LdapException extends \Exception {}
|
|
@ -434,7 +434,7 @@ final class FilterTest extends TestCase {
|
||||||
* @covers \EesyLDAP\Filter::combine
|
* @covers \EesyLDAP\Filter::combine
|
||||||
*/
|
*/
|
||||||
public function testCombineInvalidLogOp() {
|
public function testCombineInvalidLogOp() {
|
||||||
$this->expectException(CombineException::class);
|
$this->expectException(FilterException::class);
|
||||||
$a = Filter::combine('X', new Filter('a', '=', 'b'), new Filter('c', '>=', '2'));
|
$a = Filter::combine('X', new Filter('a', '=', 'b'), new Filter('c', '>=', '2'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -442,7 +442,7 @@ final class FilterTest extends TestCase {
|
||||||
* @covers \EesyLDAP\Filter::combine
|
* @covers \EesyLDAP\Filter::combine
|
||||||
*/
|
*/
|
||||||
public function testCombineInvalidFilter() {
|
public function testCombineInvalidFilter() {
|
||||||
$this->expectException(CombineException::class);
|
$this->expectException(FilterException::class);
|
||||||
// @phpstan-ignore-next-line
|
// @phpstan-ignore-next-line
|
||||||
$a = Filter::combine('&', new Filter('a', '=', 'b'), new FilterException('test'));
|
$a = Filter::combine('&', new Filter('a', '=', 'b'), new FilterException('test'));
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue