$hosts * @property-read array $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 * @property-read bool $use_schema * @property-read array $config_aliases * @property-read array $default_config * @property-read Schema|false $schema */ class Ldap { /** * Configuration * @var array */ protected $config = array(); /** * Default configuration * @var array */ 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, // Use schema 'use_schema' => false, ); /** * Configuration parameter aliases * @var array */ 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 Link|false */ protected $_link = false; /** * The Schema object * @var Schema|false */ protected $schema = false; /** * Scopes associated with their search function * @var array */ protected static $search_function_by_scope = array( 'one' => 'list', 'base' => 'read', 'sub' => 'search', ); /** * Constructor * @param array|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 $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 = Filter::parse($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 (!$this -> check_ldap_extension($raise)) return false; foreach($this -> hosts as $host) { $this->_link = new Link(); if ($this->_link->connect($host, $this->port) === false) { $this->_link = false; $this -> error("Fail to connect on $host", false); continue; } if ($this->starttls) { if ($this->_link->start_tls() === 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, false)) { $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 2; } } } 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 ($this->_link->bind($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 ($this->_link->set_option(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) $this->_link->close(); } /** * Log the last error occured on LDAP link * @param string $prefix * @param bool|null $raise See error() (optional, default: null) * @param array $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?$this->_link->errno():false; if (!$errno) return $this -> error("%s: unkown error", $raise, $prefix); $err = $this->_link?$this->_link->err2str($errno):false; if (!$err) return $this -> error("%s: error #%s", $raise, $prefix, $errno); return $this -> error("%s: %s (#%s)", $raise, $prefix, $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 $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 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; case 'schema': if (!$this->use_schema) return false; if (!$this->schema) $this->schema = Schema :: load($this); return $this->schema && $this->schema->loaded?$this->schema:false; } 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 $filter Filter of the search (optional, default: null == use config) * @param string|Entry|null $base Base DN of the search (optional, default: null == use config) * @param string|null $scope Scope of the search (optional, default: null == use params or config) * @param array|null $attributes Expected attributes returned by the search (optional, * default: null == use params or config) * @param array|null $params Other optional parameters of the search (optional, default: null == use config) * @param bool|null $raise See error() (optional, default: null) * @return array|false Array of Entry object with DN as key, or False in case of error * @throws LdapException */ public function search($filter=null, $base=null, $scope=null, $attributes=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 = $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); // Compute expected attributes $attributes = $attributes?$attributes:self :: get_param($params, 'attributes', array(), 'array'); if (!is_array($attributes)) return $this -> error("Invalid expected attributes specified: must be an array", $raise); // Run the search $search = @call_user_func( // @phpstan-ignore-next-line array($this->_link, self :: $search_function_by_scope[$scope]), $base, $filter, $attributes, 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 = $this->_link->get_entries($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, $this); } // @phpstan-ignore-next-line return $entries; } /** * Get one entry from LDAP directory by DN * * This method make easier to retreive a specific entry by its DN. A search will be runned on the * specified DN with a base scope and if one and only one entry is found, it will be returned. * * @param string $dn DN of the expected entry * @param string|null $filter Filter of the search (optional, default: null == use config) * @param array|null $attributes Expected attributes returned by the search (optional, * default: null == use params or config) * @param array|null $params Other optional parameters of the search (optional, * default: null == use config, see search()) * @param bool|null $raise See error() (optional, default: null) * @return Entry|false The requested Entry object, or False in case of error * @throws LdapException */ public function get_entry($dn, $filter=null, $attributes=null, $params=null, $raise=null) { $params = is_array($params)?$params:array(); $entries = $this -> search($filter, $dn, 'base', $attributes, $params, $raise); if (is_array($entries) && count($entries) == 1) return array_pop($entries); return false; } /** * Get parameter value * @param array|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; } }