authentication.phpB^By#I plugin.php8^8i"'h config.php$^$Xinclude/Net/LDAP2.php~^~\ë include/Net/LDAP2/RootDSE.phpN^NWinclude/Net/LDAP2/LDIF.php^rB+include/Net/LDAP2/SchemaCache.interface.php^L+include/Net/LDAP2/Search.phpJ^JVinclude/Net/LDAP2/Entry.php6^6(ܤinclude/Net/LDAP2/Filter.phpk^ks|դinclude/Net/LDAP2/Util.phpa^a include/Net/LDAP2/Schema.phpR^R ~+include/Net/LDAP2/SimpleFileSchemaCache.php ^ U/ array( 'user' => array( 'filter' => '(objectClass=user)', 'base' => 'CN=Users', 'first' => 'givenName', 'last' => 'sn', 'full' => 'displayName', 'email' => 'mail', 'phone' => 'telephoneNumber', 'mobile' => false, 'username' => 'sAMAccountName', 'dn' => '{username}@{domain}', 'search' => '(&(objectCategory=person)(objectClass=user)(|(sAMAccountName={q}*)(firstName={q}*)(lastName={q}*)(displayName={q}*)))', 'lookup' => '(&(objectCategory=person)(objectClass=user)({attr}={q}))', ), 'group' => array( 'ismember' => '(&(objectClass=user)(sAMAccountName={username}) (|(memberOf={distinguishedName})(primaryGroupId={primaryGroupToken})))', 'lookup' => '(&(objectClass=group)(sAMAccountName={groupname}))', ), ), // A general approach for RFC-2307 '2307' => array( 'user' => array( 'filter' => '(objectClass=inetOrgPerson)', 'first' => 'gn', 'last' => 'sn', 'full' => array('displayName', 'gecos', 'cn'), 'email' => 'mail', 'phone' => 'telephoneNumber', 'mobile' => 'mobileTelephoneNumber', 'username' => 'uid', 'dn' => 'uid={username},{search_base}', 'search' => '(&(objectClass=inetOrgPerson)(|(uid={q}*)(displayName={q}*)(cn={q}*)))', 'lookup' => '(&(objectClass=inetOrgPerson)({attr}={q}))', ), ), ); var $config; var $type = 'staff'; function __construct($config, $type='staff') { $this->config = $config; $this->type = $type; } function getConfig() { return $this->config; } function autodiscover($domain, $dns=array()) { require_once(PEAR_DIR.'Net/DNS2.php'); // TODO: Lookup DNS server from hosts file if not set $q = new Net_DNS2_Resolver(); if ($dns) $q->setServers($dns); $servers = array(); try { $r = $q->query('_ldap._tcp.'.$domain, 'SRV'); } catch (Net_DNS2_Exception $e) { // TODO: Log warning or something return $servers; } foreach ($r->answer as $srv) { // TODO: Get the actual IP of the server (?) $servers[] = array( 'host' => "{$srv->target}:{$srv->port}", 'priority' => $srv->priority, 'weight' => $srv->weight, ); } // Sort servers by priority ASC, then weight DESC usort($servers, function($a, $b) { return ($a['priority'] << 15) - $a['weight'] - ($b['priority'] << 15) + $b['weight']; }); return $servers; } function getServers() { if (!($servers = $this->getConfig()->get('servers')) || !($servers = preg_split('/\s+/', $servers))) { if ($domain = $this->getConfig()->get('domain')) { $dns = preg_split('/,?\s+/', $this->getConfig()->get('dns')); return $this->autodiscover($domain, array_filter($dns)); } } if ($servers) { $hosts = array(); foreach ($servers as $h) $hosts[] = array('host'=>$h); return $hosts; } } function getConnection($force_reconnect=false) { static $connection = null; if ($connection && !$force_reconnect) return $connection; require_once('include/Net/LDAP2.php'); // Set reasonable timeout limits $defaults = array( 'options' => array( 'LDAP_OPT_TIMELIMIT' => 5, 'LDAP_OPT_NETWORK_TIMEOUT' => 5, ) ); if ($this->getConfig()->get('tls')) $defaults['starttls'] = true; if ($this->getConfig()->get('schema') == 'msad') { // Special options for Active Directory (2000+) servers //$defaults['starttls'] = true; $defaults['options'] += array( 'LDAP_OPT_PROTOCOL_VERSION' => 3, 'LDAP_OPT_REFERRALS' => 0, ); // Active Directory servers almost always use self-signed certs putenv('LDAPTLS_REQCERT=never'); } foreach ($this->getServers() as $s) { $params = $defaults + $s; $c = new Net_LDAP2($params); $r = $c->bind(); if (!PEAR::isError($r)) { $connection = $c; return $c; } } } /** * Binds to the directory under the search-user credentials configured */ function _bind($connection) { if ($dn = $this->getConfig()->get('bind_dn')) { $pw = Crypto::decrypt($this->getConfig()->get('bind_pw'), SECRET_SALT, $this->getConfig()->getNamespace()); $r = $connection->bind($dn, $pw); unset($pw); return !PEAR::isError($r); } else { // try anonymous bind $r = $connection->bind(); return !PEAR::isError($r); } } function authenticate($username, $password=null) { // Thanks, http://stackoverflow.com/a/764651 // Binding with an empty password implies an anonymous bind which // will likely be successful and incorrect if (!$password) return null; $c = $this->getConnection(); $config = $this->getConfig(); $schema_type = $this->getSchema($c); $schema = static::$schemas[$schema_type]['user']; $domain = false; if ($schema_type == 'msad') { // Allow username specification of DOMAIN\user, LDAP already // allows user@domain if (strpos($username, '\\') !== false) list($domain, $username) = explode('\\', $username); else $domain = $config->get('domain'); } // Create the DN string for the bind based on the directory schema $dn = preg_replace_callback(':\{([^}]+)\}:', function($match) use ($username, $domain, $config) { switch ($match[1]) { case 'username': return $username; case 'domain': return $domain; case 'search_base': if (!$config->get('search_base')) return 'dc=' . implode(',dc=', explode('.', $config->get('domain'))); // Fall through to default default: return $config->get($match[1]); } }, $schema['dn'] ); $r = $c->bind($dn, $password); if (!PEAR::isError($r)) return $this->lookupAndSync($username, $dn); // Another effort is to search for the user if (!$this->_bind($c)) return null; $r = $c->search( $this->getSearchBase(), str_replace( array('{attr}','{q}'), // Assume email address if the $username contains an @ sign array(strpos($username, '@') ? $schema['email'] : $schema['username'], $username), $schema['lookup']), array('sizelimit' => 1) ); if (PEAR::isError($r) || !$r->count()) return null; // Attempt to bind as the DN of the user looked up with the password // specified $bound = $c->bind($r->current()->dn(), $password); if (PEAR::isError($bound)) return null; // TODO: Save the DN in the config table so a lookup isn't necessary // in the future return $this->lookupAndSync($username, $r->current()->dn()); } /** * Retrieve currently configured LDAP schema, perhaps by inspecting the * server's advertised DSE information */ function getSchema($connection) { $schema = $this->getConfig()->get('schema'); if (!$schema || $schema == 'auto') { $dse = $connection->rootDse(array('supportedCapabilities')); // Microsoft Active Directory // http://www.alvestrand.no/objectid/1.2.840.113556.1.4.800.html if (($caps = $dse->getValue('supportedCapabilities')) && in_array('1.2.840.113556.1.4.800', $caps)) { $this->getConfig()->set('schema', 'msad'); return 'msad'; } } elseif ($schema) return $schema; // Fallback return '2307'; } function lookup($lookup_dn, $bind=true) { $c = $this->getConnection(); if ($bind && !$this->_bind($c)) return null; $schema = static::$schemas[$this->getSchema($c)]; $schema = $schema['user']; $opts = array( 'scope' => 'base', 'sizelimit' => 1, 'attributes' => array_filter(flatten(array( $schema['first'], $schema['last'], $schema['full'], $schema['phone'], $schema['mobile'], $schema['email'], $schema['username'], ))) ); $r = $c->search($lookup_dn, '(objectClass=*)', $opts); if (PEAR::isError($r) || !$r->count()) return null; return $this->_getUserInfoArray($r->current(), $schema); } function search($query) { $c = $this->getConnection(); // TODO: Include bind information $users = array(); if (!$this->_bind($c)) return $users; $schema = static::$schemas[$this->getSchema($c)]; $schema = $schema['user']; $r = $c->search( $this->getSearchBase(), str_replace('{q}', $query, $schema['search']), array('attributes' => array_filter(flatten(array( $schema['first'], $schema['last'], $schema['full'], $schema['phone'], $schema['mobile'], $schema['email'], $schema['username'], 'dn', )))) ); // XXX: Log or return some kind of error? if (PEAR::isError($r)) return $users; foreach ($r as $e) $users[] = $this->_getUserInfoArray($e, $schema); return $users; } function getSearchBase() { $base = $this->getConfig()->get('search_base'); if (!$base && ($domain=$this->getConfig()->get('domain'))) $base = 'dc='.str_replace('.', ',dc=', $domain); return $base; } function _getValue($entry, $names) { foreach (array_filter(splat($names)) as $n) // Support multi-value attributes foreach (splat($entry->getValue($n, 'all')) as $val) // Return the first non-bool-false value of the entries if ($val) return $val; } function _getUserInfoArray($e, $schema) { // Detect first and last name if only full name is given if (!($first = $this->_getValue($e, $schema['first'])) || !($last = $this->_getValue($e, $schema['last']))) { $name = new PersonsName($this->_getValue($e, $schema['full'])); $first = $name->getFirst(); $last = $name->getLast(); } else $name = "$first $last"; return array( 'username' => $this->_getValue($e, $schema['username']), 'first' => $first, 'last' => $last, 'name' => $name, 'email' => $this->_getValue($e, $schema['email']), 'phone' => $this->_getValue($e, $schema['phone']), 'mobile' => $this->_getValue($e, $schema['mobile']), 'dn' => $e->dn(), ); } function lookupAndSync($username, $dn) { switch ($this->type) { case 'staff': if (($user = StaffSession::lookup($username)) && $user->getId()) { if (!$user instanceof StaffSession) { // osTicket <= v1.9.7 or so $user = new StaffSession($user->getId()); } return $user; } break; case 'client': $c = $this->getConnection(); if ('msad' == $this->getSchema($c) && stripos($dn, ',dc=') === false) { // The user login DN will be user@domain. We need an LDAP DN // -- fetch the real DN which looks like `CN=blah,DC=` // NOTE: Already bound, so no need to bind again list($samid) = explode('@', $dn); $r = $c->search( $this->getSearchBase(), sprintf('(|(userPrincipalName=%s)(samAccountName=%s))', $dn, $samid), $opts); if (!PEAR::isError($r) && $r->count()) $dn = $r->current()->dn(); } // Lookup all the information on the user. Try to get the email // addresss as well as the username when looking up the user // locally. if (!($info = $this->lookup($dn, false))) return; $acct = false; foreach (array($username, $info['username'], $info['email']) as $name) { if ($name && ($acct = ClientAccount::lookupByUsername($name))) break; } if (!$acct) return new ClientCreateRequest($this, $username, $info); if (($client = new ClientSession(new EndUser($acct->getUser()))) && !$client->getId()) return; return $client; } // TODO: Auto-create users, etc. } } class StaffLDAPAuthentication extends StaffAuthenticationBackend implements AuthDirectorySearch { static $name = /* trans */ "Active Directory or LDAP"; static $id = "ldap"; function __construct($config) { $this->_ldap = new LDAPAuthentication($config); $this->config = $config; } function authenticate($username, $password=false, $errors=array()) { return $this->_ldap->authenticate($username, $password); } function getName() { $config = $this->config; list($__, $_N) = $config::translate(); return $__(static::$name); } function lookup($dn) { $hit = $this->_ldap->lookup($dn); if ($hit) { $hit['backend'] = static::$id; $hit['id'] = static::$id . ':' . $hit['dn']; } return $hit; } function search($query) { if (strlen($query) < 3) return array(); $hits = $this->_ldap->search($query); foreach ($hits as &$h) { $h['backend'] = static::$id; $h['id'] = static::$id . ':' . $h['dn']; } return $hits; } } class ClientLDAPAuthentication extends UserAuthenticationBackend { static $name = /* trans */ "Active Directory or LDAP"; static $id = "ldap.client"; function __construct($config) { $this->_ldap = new LDAPAuthentication($config, 'client'); $this->config = $config; if ($domain = $config->get('domain')) self::$name .= sprintf(' (%s)', $domain); } function getName() { $config = $this->config; list($__, $_N) = $config::translate(); return $__(static::$name); } function authenticate($username, $password=false, $errors=array()) { $object = $this->_ldap->authenticate($username, $password); if ($object instanceof ClientCreateRequest) $object->setBackend($this); return $object; } } require_once(INCLUDE_DIR.'class.plugin.php'); require_once('config.php'); class LdapAuthPlugin extends Plugin { var $config_class = 'LdapConfig'; function bootstrap() { $config = $this->getConfig(); if ($config->get('auth-staff')) StaffAuthenticationBackend::register(new StaffLDAPAuthentication($config)); if ($config->get('auth-client')) UserAuthenticationBackend::register(new ClientLDAPAuthentication($config)); } } 'auth:ldap', # notrans 'version' => '0.6.2', 'name' => /* trans */ 'LDAP Authentication and Lookup', 'author' => 'Jared Hancock', 'description' => /* trans */ 'Provides a configurable authentication backend which works against Microsoft Active Directory and OpenLdap servers', 'url' => 'http://www.osticket.com/plugins/auth/ldap', 'plugin' => 'authentication.php:LdapAuthPlugin', 'requires' => array( "pear-pear.php.net/Net_LDAP2" => array( "version" => "*", "map" => array( 'pear-pear.php.net/Net_LDAP2' => 'include', ), ), ), ); ?> new SectionBreakField(array( 'label' => 'Microsoft® Active Directory', 'hint' => $__('This section should be all that is required for Active Directory domains'), )), 'domain' => new TextboxField(array( 'label' => $__('Default Domain'), 'hint' => $__('Default domain used in authentication and searches'), 'configuration' => array('size'=>40, 'length'=>60), 'validators' => array( function($self, $val) use ($__) { if (strpos($val, '.') === false) $self->addError( $__('Fully-qualified domain name is expected')); }), )), 'dns' => new TextboxField(array( 'label' => $__('DNS Servers'), 'hint' => $__('(optional) DNS servers to query about AD servers. Useful if the AD server is not on the same network as this web server or does not have its DNS configured to point to the AD servers'), 'configuration' => array('size'=>40), 'validators' => array( function($self, $val) use ($__) { if (!$val) return; $servers = explode(',', $val); foreach ($servers as $s) { if (!Validator::is_ip(trim($s))) $self->addError(sprintf( $__('%s: Expected an IP address'), $s)); } }), )), 'ldap' => new SectionBreakField(array( 'label' => $__('Generic configuration for LDAP'), 'hint' => $__('Not necessary if Active Directory is configured above'), )), 'servers' => new TextareaField(array( 'id' => 'servers', 'label' => $__('LDAP servers'), 'configuration' => array('html'=>false, 'rows'=>2, 'cols'=>40), 'hint' => $__('Use "server" or "server:port". Place one server entry per line'), )), 'tls' => new BooleanField(array( 'id' => 'tls', 'label' => $__('Use TLS'), 'configuration' => array( 'desc' => $__('Use TLS to communicate with the LDAP server')) )), 'conn_info' => new SectionBreakField(array( 'label' => $__('Connection Information'), 'hint' => $__('Useful only for information lookups. Not necessary for authentication. NOTE that this data is not necessary if your server allows anonymous searches') )), 'bind_dn' => new TextboxField(array( 'label' => $__('Search User'), 'hint' => $__('Bind DN (distinguished name) to bind to the LDAP server as in order to perform searches'), 'configuration' => array('size'=>40, 'length'=>120), )), 'bind_pw' => new TextboxField(array( 'widget' => 'PasswordWidget', 'label' => $__('Password'), 'hint' => $__("Password associated with the DN's account"), 'configuration' => array('size'=>40), )), 'search_base' => new TextboxField(array( 'label' => $__('Search Base'), 'hint' => $__('Used when searching for users'), 'configuration' => array('size'=>70, 'length'=>120), )), 'schema' => new ChoiceField(array( 'label' => $__('LDAP Schema'), 'hint' => $__('Layout of the user data in the LDAP server'), 'default' => 'auto', 'choices' => array( 'auto' => '— '.$__('Automatically Detect').' —', 'msad' => 'Microsoft® Active Directory', '2307' => 'Posix Account (rfc 2307)', ), )), 'auth' => new SectionBreakField(array( 'label' => $__('Authentication Modes'), 'hint' => $__('Authentication modes for clients and staff members can be enabled independently'), )), 'auth-staff' => new BooleanField(array( 'label' => $__('Staff Authentication'), 'default' => true, 'configuration' => array( 'desc' => $__('Enable authentication of staff members') ) )), 'auth-client' => new BooleanField(array( 'label' => $__('Client Authentication'), 'default' => false, 'configuration' => array( 'desc' => $__('Enable authentication of clients') ) )), ); } function pre_save(&$config, &$errors) { require_once('include/Net/LDAP2.php'); list($__, $_N) = self::translate(); global $ost; if ($ost && !extension_loaded('ldap')) { $ost->setWarning($__('LDAP extension is not available')); $errors['err'] = $__('LDAP extension is not available. Please install or enable the `php-ldap` extension on your web server'); return; } if ($config['domain'] && !$config['servers']) { if (!($servers = LDAPAuthentication::autodiscover($config['domain'], preg_split('/,?\s+/', $config['dns'])))) $this->getForm()->getField('servers')->addError( $__("Unable to find LDAP servers for this domain. Try giving an address of one of the DNS servers or manually specify the LDAP servers for this domain below.")); } else { if (!$config['servers']) $this->getForm()->getField('servers')->addError( $__("No servers specified. Either specify a Active Directory domain or a list of servers")); else { $servers = array(); foreach (preg_split('/\s+/', $config['servers']) as $host) $servers[] = array('host' => $host); } } $connection_error = false; foreach ($servers as $info) { // Assume MSAD $info['options']['LDAP_OPT_REFERRALS'] = 0; if ($config['tls']) { $info['starttls'] = true; // Don't require a certificate here putenv('LDAPTLS_REQCERT=never'); } if ($config['bind_dn']) { $info['binddn'] = $config['bind_dn']; $info['bindpw'] = $config['bind_pw'] ? $config['bind_pw'] : Crypto::decrypt($this->get('bind_pw'), SECRET_SALT, $this->getNamespace()); } // Set reasonable timeouts so we dont exceed max_execution_time $info['options'] = array( 'LDAP_OPT_TIMELIMIT' => 5, 'LDAP_OPT_NETWORK_TIMEOUT' => 5, ); $c = new Net_LDAP2($info); $r = $c->bind(); if (PEAR::isError($r)) { if (false === strpos($config['bind_dn'], '@') && false === strpos($config['bind_dn'], ',dc=')) { // Assume Active Directory, add the default domain in $config['bind_dn'] .= '@' . $config['domain']; $info['bind_dn'] = $config['bind_dn']; $c = new Net_LDAP2($info); $r = $c->bind(); } } if (PEAR::isError($r)) { $connection_error = sprintf($__( '%s: Unable to bind to server %s'), $r->getMessage(), $info['host']); } else { $connection_error = false; break; } } if ($connection_error) { $this->getForm()->getField('servers')->addError($connection_error); $errors['err'] = $__('Unable to connect any listed LDAP servers'); } if (!$errors && $config['bind_pw']) $config['bind_pw'] = Crypto::encrypt($config['bind_pw'], SECRET_SALT, $this->getNamespace()); else $config['bind_pw'] = $this->get('bind_pw'); global $msg; if (!$errors) $msg = $__('LDAP configuration updated successfully'); return !$errors; } } ?> * @author Jan Wagner * @author Del * @author Benedikt Hallinger * @copyright 2003-2007 Tarjej Huse, Jan Wagner, Del Elson, Benedikt Hallinger * @license http://www.gnu.org/licenses/lgpl-3.0.txt LGPLv3 * @version SVN: $Id$ * @link http://pear.php.net/package/Net_LDAP2/ */ /** * Package includes. */ require_once 'PEAR.php'; require_once 'Net/LDAP2/RootDSE.php'; require_once 'Net/LDAP2/Schema.php'; require_once 'Net/LDAP2/Entry.php'; require_once 'Net/LDAP2/Search.php'; require_once 'Net/LDAP2/Util.php'; require_once 'Net/LDAP2/Filter.php'; require_once 'Net/LDAP2/LDIF.php'; require_once 'Net/LDAP2/SchemaCache.interface.php'; require_once 'Net/LDAP2/SimpleFileSchemaCache.php'; /** * Error constants for errors that are not LDAP errors. */ define('NET_LDAP2_ERROR', 1000); /** * Net_LDAP2 Version */ define('NET_LDAP2_VERSION', '2.1.0'); /** * Net_LDAP2 - manipulate LDAP servers the right way! * * @category Net * @package Net_LDAP2 * @author Tarjej Huse * @author Jan Wagner * @author Del * @author Benedikt Hallinger * @copyright 2003-2007 Tarjej Huse, Jan Wagner, Del Elson, Benedikt Hallinger * @license http://www.gnu.org/copyleft/lesser.html LGPL * @link http://pear.php.net/package/Net_LDAP2/ */ class Net_LDAP2 extends PEAR { /** * Class configuration array * * host = the ldap host to connect to * (may be an array of several hosts to try) * port = the server port * version = ldap version (defaults to v 3) * starttls = when set, ldap_start_tls() is run after connecting. * bindpw = no explanation needed * binddn = the DN to bind as. * basedn = ldap base * options = hash of ldap options to set (opt => val) * filter = default search filter * scope = default search scope * * Newly added in 2.0.0RC4, for auto-reconnect: * auto_reconnect = if set to true then the class will automatically * attempt to reconnect to the LDAP server in certain * failure conditionswhen attempting a search, or other * LDAP operation. Defaults to false. Note that if you * set this to true, calls to search() may block * indefinitely if there is a catastrophic server failure. * min_backoff = minimum reconnection delay period (in seconds). * current_backoff = initial reconnection delay period (in seconds). * max_backoff = maximum reconnection delay period (in seconds). * * @access protected * @var array */ protected $_config = array('host' => 'localhost', 'port' => 389, 'version' => 3, 'starttls' => false, 'binddn' => '', 'bindpw' => '', 'basedn' => '', 'options' => array(), 'filter' => '(objectClass=*)', 'scope' => 'sub', 'auto_reconnect' => false, 'min_backoff' => 1, 'current_backoff' => 1, 'max_backoff' => 32); /** * List of hosts we try to establish a connection to * * @access protected * @var array */ protected $_host_list = array(); /** * List of hosts that are known to be down. * * @access protected * @var array */ protected $_down_host_list = array(); /** * LDAP resource link. * * @access protected * @var resource */ protected $_link = false; /** * Net_LDAP2_Schema object * * This gets set and returned by {@link schema()} * * @access protected * @var object Net_LDAP2_Schema */ protected $_schema = null; /** * Schema cacher function callback * * @see registerSchemaCache() * @var string */ protected $_schema_cache = null; /** * Cache for attribute encoding checks * * @access protected * @var array Hash with attribute names as key and boolean value * to determine whether they should be utf8 encoded or not. */ protected $_schemaAttrs = array(); /** * Cache for rootDSE objects * * Hash with requested rootDSE attr names as key and rootDSE object as value * * Since the RootDSE object itself may request a rootDSE object, * {@link rootDse()} caches successful requests. * Internally, Net_LDAP2 needs several lookups to this object, so * caching increases performance significally. * * @access protected * @var array */ protected $_rootDSE_cache = array(); /** * Returns the Net_LDAP2 Release version, may be called statically * * @static * @return string Net_LDAP2 version */ public static function getVersion() { return NET_LDAP2_VERSION; } /** * Configure Net_LDAP2, connect and bind * * Use this method as starting point of using Net_LDAP2 * to establish a connection to your LDAP server. * * Static function that returns either an error object or the new Net_LDAP2 * object. Something like a factory. Takes a config array with the needed * parameters. * * @param array $config Configuration array * * @access public * @return Net_LDAP2_Error|Net_LDAP2 Net_LDAP2_Error or Net_LDAP2 object */ public static function connect($config = array()) { $ldap_check = self::checkLDAPExtension(); if (self::iserror($ldap_check)) { return $ldap_check; } @$obj = new Net_LDAP2($config); // todo? better errorhandling for setConfig()? // connect and bind with credentials in config $err = $obj->bind(); if (self::isError($err)) { return $err; } return $obj; } /** * Net_LDAP2 constructor * * Sets the config array * * Please note that the usual way of getting Net_LDAP2 to work is * to call something like: * $ldap = Net_LDAP2::connect($ldap_config); * * @param array $config Configuration array * * @access protected * @return void * @see $_config */ public function __construct($config = array()) { parent::__construct('Net_LDAP2_Error'); $this->setConfig($config); } /** * Sets the internal configuration array * * @param array $config Configuration array * * @access protected * @return void */ protected function setConfig($config) { // // Parameter check -- probably should raise an error here if config // is not an array. // if (! is_array($config)) { return; } foreach ($config as $k => $v) { if (isset($this->_config[$k])) { $this->_config[$k] = $v; } else { // map old (Net_LDAP2) parms to new ones switch($k) { case "dn": $this->_config["binddn"] = $v; break; case "password": $this->_config["bindpw"] = $v; break; case "tls": $this->_config["starttls"] = $v; break; case "base": $this->_config["basedn"] = $v; break; } } } // // Ensure the host list is an array. // if (is_array($this->_config['host'])) { $this->_host_list = $this->_config['host']; } else { if (strlen($this->_config['host']) > 0) { $this->_host_list = array($this->_config['host']); } else { $this->_host_list = array(); // ^ this will cause an error in performConnect(), // so the user is notified about the failure } } // // Reset the down host list, which seems like a sensible thing to do // if the config is being reset for some reason. // $this->_down_host_list = array(); } /** * Bind or rebind to the ldap-server * * This function binds with the given dn and password to the server. In case * no connection has been made yet, it will be started and startTLS issued * if appropiate. * * The internal bind configuration is not being updated, so if you call * bind() without parameters, you can rebind with the credentials * provided at first connecting to the server. * * @param string $dn Distinguished name for binding * @param string $password Password for binding * * @access public * @return Net_LDAP2_Error|true Net_LDAP2_Error object or true */ public function bind($dn = null, $password = null) { // fetch current bind credentials if (is_null($dn)) { $dn = $this->_config["binddn"]; } if (is_null($password)) { $password = $this->_config["bindpw"]; } // Connect first, if we haven't so far. // This will also bind us to the server. if ($this->_link === false) { // store old credentials so we can revert them later // then overwrite config with new bind credentials $olddn = $this->_config["binddn"]; $oldpw = $this->_config["bindpw"]; // overwrite bind credentials in config // so performConnect() knows about them $this->_config["binddn"] = $dn; $this->_config["bindpw"] = $password; // try to connect with provided credentials $msg = $this->performConnect(); // reset to previous config $this->_config["binddn"] = $olddn; $this->_config["bindpw"] = $oldpw; // see if bind worked if (self::isError($msg)) { return $msg; } } else { // do the requested bind as we are // asked to bind manually if (is_null($dn)) { // anonymous bind $msg = @ldap_bind($this->_link); } else { // privileged bind $msg = @ldap_bind($this->_link, $dn, $password); } if (false === $msg) { return PEAR::raiseError("Bind failed: " . @ldap_error($this->_link), @ldap_errno($this->_link)); } } return true; } /** * Connect to the ldap-server * * This function connects to the LDAP server specified in * the configuration, binds and set up the LDAP protocol as needed. * * @access protected * @return Net_LDAP2_Error|true Net_LDAP2_Error object or true */ protected function performConnect() { // Note: Connecting is briefly described in RFC1777. // Basicly it works like this: // 1. set up TCP connection // 2. secure that connection if neccessary // 3a. setLDAPVersion to tell server which version we want to speak // 3b. perform bind // 3c. setLDAPVersion to tell server which version we want to speak // together with a test for supported versions // 4. set additional protocol options // Return true if we are already connected. if ($this->_link !== false) { return true; } // Connnect to the LDAP server if we are not connected. Note that // with some LDAP clients, ldapperformConnect returns a link value even // if no connection is made. We need to do at least one anonymous // bind to ensure that a connection is actually valid. // // Ref: http://www.php.net/manual/en/function.ldap-connect.php // Default error message in case all connection attempts // fail but no message is set $current_error = new PEAR_Error('Unknown connection error'); // Catch empty $_host_list arrays. if (!is_array($this->_host_list) || count($this->_host_list) == 0) { $current_error = PEAR::raiseError('No Servers configured! Please '. 'pass in an array of servers to Net_LDAP2'); return $current_error; } // Cycle through the host list. foreach ($this->_host_list as $host) { // Ensure we have a valid string for host name if (is_array($host)) { $current_error = PEAR::raiseError('No Servers configured! '. 'Please pass in an one dimensional array of servers to '. 'Net_LDAP2! (multidimensional array detected!)'); continue; } // Skip this host if it is known to be down. if (in_array($host, $this->_down_host_list)) { continue; } // Record the host that we are actually connecting to in case // we need it later. $this->_config['host'] = $host; // Attempt a connection. $this->_link = @ldap_connect($host, $this->_config['port']); if (false === $this->_link) { $current_error = PEAR::raiseError('Could not connect to ' . $host . ':' . $this->_config['port']); $this->_down_host_list[] = $host; continue; } // If we're supposed to use TLS, do so before we try to bind, // as some strict servers only allow binding via secure connections if ($this->_config["starttls"] === true) { if (self::isError($msg = $this->startTLS())) { $current_error = $msg; $this->_link = false; $this->_down_host_list[] = $host; continue; } } // Try to set the configured LDAP version on the connection if LDAP // server needs that before binding (eg OpenLDAP). // This could be necessary since rfc-1777 states that the protocol version // has to be set at the bind request. // We use force here which means that the test in the rootDSE is skipped; // this is neccessary, because some strict LDAP servers only allow to // read the LDAP rootDSE (which tells us the supported protocol versions) // with authenticated clients. // This may fail in which case we try again after binding. // In this case, most probably the bind() or setLDAPVersion()-call // below will also fail, providing error messages. $version_set = false; $ignored_err = $this->setLDAPVersion(0, true); if (!self::isError($ignored_err)) { $version_set = true; } // Attempt to bind to the server. If we have credentials configured, // we try to use them, otherwise its an anonymous bind. // As stated by RFC-1777, the bind request should be the first // operation to be performed after the connection is established. // This may give an protocol error if the server does not support // V2 binds and the above call to setLDAPVersion() failed. // In case the above call failed, we try an V2 bind here and set the // version afterwards (with checking to the rootDSE). $msg = $this->bind(); if (self::isError($msg)) { // The bind failed, discard link and save error msg. // Then record the host as down and try next one if ($msg->getCode() == 0x02 && !$version_set) { // provide a finer grained error message // if protocol error arieses because of invalid version $msg = new Net_LDAP2_Error($msg->getMessage(). " (could not set LDAP protocol version to ". $this->_config['version'].")", $msg->getCode()); } $this->_link = false; $current_error = $msg; $this->_down_host_list[] = $host; continue; } // Set desired LDAP version if not successfully set before. // Here, a check against the rootDSE is performed, so we get a // error message if the server does not support the version. // The rootDSE entry should tell us which LDAP versions are // supported. However, some strict LDAP servers only allow // bound suers to read the rootDSE. if (!$version_set) { if (self::isError($msg = $this->setLDAPVersion())) { $current_error = $msg; $this->_link = false; $this->_down_host_list[] = $host; continue; } } // Set LDAP parameters, now we know we have a valid connection. if (isset($this->_config['options']) && is_array($this->_config['options']) && count($this->_config['options'])) { foreach ($this->_config['options'] as $opt => $val) { $err = $this->setOption($opt, $val); if (self::isError($err)) { $current_error = $err; $this->_link = false; $this->_down_host_list[] = $host; continue 2; } } } // At this stage we have connected, bound, and set up options, // so we have a known good LDAP server. Time to go home. return true; } // All connection attempts have failed, return the last error. return $current_error; } /** * Reconnect to the ldap-server. * * In case the connection to the LDAP * service has dropped out for some reason, this function will reconnect, * and re-bind if a bind has been attempted in the past. It is probably * most useful when the server list provided to the new() or connect() * function is an array rather than a single host name, because in that * case it will be able to connect to a failover or secondary server in * case the primary server goes down. * * This doesn't return anything, it just tries to re-establish * the current connection. It will sleep for the current backoff * period (seconds) before attempting the connect, and if the * connection fails it will double the backoff period, but not * try again. If you want to ensure a reconnection during a * transient period of server downtime then you need to call this * function in a loop. * * @access protected * @return Net_LDAP2_Error|true Net_LDAP2_Error object or true */ protected function performReconnect() { // Return true if we are already connected. if ($this->_link !== false) { return true; } // Default error message in case all connection attempts // fail but no message is set $current_error = new PEAR_Error('Unknown connection error'); // Sleep for a backoff period in seconds. sleep($this->_config['current_backoff']); // Retry all available connections. $this->_down_host_list = array(); $msg = $this->performConnect(); // Bail out if that fails. if (self::isError($msg)) { $this->_config['current_backoff'] = $this->_config['current_backoff'] * 2; if ($this->_config['current_backoff'] > $this->_config['max_backoff']) { $this->_config['current_backoff'] = $this->_config['max_backoff']; } return $msg; } // Now we should be able to safely (re-)bind. $msg = $this->bind(); if (self::isError($msg)) { $this->_config['current_backoff'] = $this->_config['current_backoff'] * 2; if ($this->_config['current_backoff'] > $this->_config['max_backoff']) { $this->_config['current_backoff'] = $this->_config['max_backoff']; } // _config['host'] should have had the last connected host stored in it // by performConnect(). Since we are unable to bind to that host we can safely // assume that it is down or has some other problem. $this->_down_host_list[] = $this->_config['host']; return $msg; } // At this stage we have connected, bound, and set up options, // so we have a known good LDAP server. Time to go home. $this->_config['current_backoff'] = $this->_config['min_backoff']; return true; } /** * Starts an encrypted session * * @access public * @return Net_LDAP2_Error|true Net_LDAP2_Error object or true */ public function startTLS() { /* Test to see if the server supports TLS first. This is done via testing the extensions offered by the server. The OID 1.3.6.1.4.1.1466.20037 tells us, if TLS is supported. Note, that not all servers allow to feth either the rootDSE or attributes over an unencrypted channel, so we must ignore errors. */ $rootDSE = $this->rootDse(); if (self::isError($rootDSE)) { /* IGNORE this error, because server may refuse fetching the RootDSE over an unencrypted connection. */ //return $this->raiseError("Unable to fetch rootDSE entry ". //"to see if TLS is supoported: ".$rootDSE->getMessage(), $rootDSE->getCode()); } else { /* Fetch suceeded, see, if the server supports TLS. Again, we ignore errors, because the server may refuse to return attributes over unencryted connections. */ $supported_extensions = $rootDSE->getValue('supportedExtension'); if (self::isError($supported_extensions)) { /* IGNORE error, because server may refuse attribute returning over an unencrypted connection. */ //return $this->raiseError("Unable to fetch rootDSE attribute 'supportedExtension' ". //"to see if TLS is supoported: ".$supported_extensions->getMessage(), $supported_extensions->getCode()); } else { // fetch succeedet, lets see if the server supports it. // if not, then drop an error. If supported, then do nothing, // because then we try to issue TLS afterwards. if (!in_array('1.3.6.1.4.1.1466.20037', $supported_extensions)) { return $this->raiseError("Server reports that it does not support TLS."); } } } // Try to establish TLS. if (false === @ldap_start_tls($this->_link)) { // Starting TLS failed. This may be an error, or because // the server does not support it but did not enable us to // detect that above. return $this->raiseError("TLS could not be started: " . @ldap_error($this->_link), @ldap_errno($this->_link)); } else { return true; // TLS is started now. } } /** * alias function of startTLS() for perl-ldap interface * * @return void * @see startTLS() */ public function start_tls() { $args = func_get_args(); return call_user_func_array(array( $this, 'startTLS' ), $args); } /** * Close LDAP connection. * * Closes the connection. Use this when the session is over. * * @return void */ public function done() { $this->_Net_LDAP2(); } /** * Alias for {@link done()} * * @return void * @see done() */ public function disconnect() { $this->done(); } /** * Destructor * * @access protected */ public function _Net_LDAP2() { @ldap_close($this->_link); } /** * Add a new entryobject to a directory. * * Use add to add a new Net_LDAP2_Entry object to the directory. * This also links the entry to the connection used for the add, * if it was a fresh entry ({@link Net_LDAP2_Entry::createFresh()}) * * @param Net_LDAP2_Entry $entry Net_LDAP2_Entry * * @return Net_LDAP2_Error|true Net_LDAP2_Error object or true */ public function add($entry) { if (!$entry instanceof Net_LDAP2_Entry) { return PEAR::raiseError('Parameter to Net_LDAP2::add() must be a Net_LDAP2_Entry object.'); } // Continue attempting the add operation in a loop until we // get a success, a definitive failure, or the world ends. $foo = 0; while (true) { $link = $this->getLink(); if ($link === false) { // We do not have a successful connection yet. The call to // getLink() would have kept trying if we wanted one. Go // home now. return PEAR::raiseError("Could not add entry " . $entry->dn() . " no valid LDAP connection could be found."); } if (@ldap_add($link, $entry->dn(), $entry->getValues())) { // entry successfully added, we should update its $ldap reference // in case it is not set so far (fresh entry) if (!$entry->getLDAP() instanceof Net_LDAP2) { $entry->setLDAP($this); } // store, that the entry is present inside the directory $entry->markAsNew(false); return true; } else { // We have a failure. What type? We may be able to reconnect // and try again. $error_code = @ldap_errno($link); $error_name = Net_LDAP2::errorMessage($error_code); if (($error_name === 'LDAP_OPERATIONS_ERROR') && ($this->_config['auto_reconnect'])) { // The server has become disconnected before trying the // operation. We should try again, possibly with a different // server. $this->_link = false; $this->performReconnect(); } else { // Errors other than the above catched are just passed // back to the user so he may react upon them. return PEAR::raiseError("Could not add entry " . $entry->dn() . " " . $error_name, $error_code); } } } } /** * Delete an entry from the directory * * The object may either be a string representing the dn or a Net_LDAP2_Entry * object. When the boolean paramter recursive is set, all subentries of the * entry will be deleted as well. * * @param string|Net_LDAP2_Entry $dn DN-string or Net_LDAP2_Entry * @param boolean $recursive Should we delete all children recursive as well? * * @access public * @return Net_LDAP2_Error|true Net_LDAP2_Error object or true */ public function delete($dn, $recursive = false) { if ($dn instanceof Net_LDAP2_Entry) { $dn = $dn->dn(); } if (false === is_string($dn)) { return PEAR::raiseError("Parameter is not a string nor an entry object!"); } // Recursive delete searches for children and calls delete for them if ($recursive) { $result = @ldap_list($this->_link, $dn, '(objectClass=*)', array(null), 0, 0); if (@ldap_count_entries($this->_link, $result)) { $subentry = @ldap_first_entry($this->_link, $result); $this->delete(@ldap_get_dn($this->_link, $subentry), true); while ($subentry = @ldap_next_entry($this->_link, $subentry)) { $this->delete(@ldap_get_dn($this->_link, $subentry), true); } } } // Continue attempting the delete operation in a loop until we // get a success, a definitive failure, or the world ends. while (true) { $link = $this->getLink(); if ($link === false) { // We do not have a successful connection yet. The call to // getLink() would have kept trying if we wanted one. Go // home now. return PEAR::raiseError("Could not add entry " . $dn . " no valid LDAP connection could be found."); } if (@ldap_delete($link, $dn)) { // entry successfully deleted. return true; } else { // We have a failure. What type? // We may be able to reconnect and try again. $error_code = @ldap_errno($link); $error_name = Net_LDAP2::errorMessage($error_code); if ((Net_LDAP2::errorMessage($error_code) === 'LDAP_OPERATIONS_ERROR') && ($this->_config['auto_reconnect'])) { // The server has become disconnected before trying the // operation. We should try again, possibly with a // different server. $this->_link = false; $this->performReconnect(); } elseif ($error_code == 66) { // Subentries present, server refused to delete. // Deleting subentries is the clients responsibility, but // since the user may not know of the subentries, we do not // force that here but instead notify the developer so he // may take actions himself. return PEAR::raiseError("Could not delete entry $dn because of subentries. Use the recursive parameter to delete them."); } else { // Errors other than the above catched are just passed // back to the user so he may react upon them. return PEAR::raiseError("Could not delete entry " . $dn . " " . $error_name, $error_code); } } } } /** * Modify an ldapentry directly on the server * * This one takes the DN or a Net_LDAP2_Entry object and an array of actions. * This array should be something like this: * * array('add' => array('attribute1' => array('val1', 'val2'), * 'attribute2' => array('val1')), * 'delete' => array('attribute1'), * 'replace' => array('attribute1' => array('val1')), * 'changes' => array('add' => ..., * 'replace' => ..., * 'delete' => array('attribute1', 'attribute2' => array('val1'))) * * The changes array is there so the order of operations can be influenced * (the operations are done in order of appearance). * The order of execution is as following: * 1. adds from 'add' array * 2. deletes from 'delete' array * 3. replaces from 'replace' array * 4. changes (add, replace, delete) in order of appearance * All subarrays (add, replace, delete, changes) may be given at the same time. * * The function calls the corresponding functions of an Net_LDAP2_Entry * object. A detailed description of array structures can be found there. * * Unlike the modification methods provided by the Net_LDAP2_Entry object, * this method will instantly carry out an update() after each operation, * thus modifying "directly" on the server. * * @param string|Net_LDAP2_Entry $entry DN-string or Net_LDAP2_Entry * @param array $parms Array of changes * * @access public * @return Net_LDAP2_Error|true Net_LDAP2_Error object or true */ public function modify($entry, $parms = array()) { if (is_string($entry)) { $entry = $this->getEntry($entry); if (self::isError($entry)) { return $entry; } } if (!$entry instanceof Net_LDAP2_Entry) { return PEAR::raiseError("Parameter is not a string nor an entry object!"); } // Perform changes mentioned separately foreach (array('add', 'delete', 'replace') as $action) { if (isset($parms[$action])) { $msg = $entry->$action($parms[$action]); if (self::isError($msg)) { return $msg; } $entry->setLDAP($this); // Because the @ldap functions are called inside Net_LDAP2_Entry::update(), // we have to trap the error codes issued from that if we want to support // reconnection. while (true) { $msg = $entry->update(); if (self::isError($msg)) { // We have a failure. What type? We may be able to reconnect // and try again. $error_code = $msg->getCode(); $error_name = Net_LDAP2::errorMessage($error_code); if ((Net_LDAP2::errorMessage($error_code) === 'LDAP_OPERATIONS_ERROR') && ($this->_config['auto_reconnect'])) { // The server has become disconnected before trying the // operation. We should try again, possibly with a different // server. $this->_link = false; $this->performReconnect(); } else { // Errors other than the above catched are just passed // back to the user so he may react upon them. return PEAR::raiseError("Could not modify entry: ".$msg->getMessage()); } } else { // modification succeedet, evaluate next change break; } } } } // perform combined changes in 'changes' array if (isset($parms['changes']) && is_array($parms['changes'])) { foreach ($parms['changes'] as $action => $value) { // Because the @ldap functions are called inside Net_LDAP2_Entry::update, // we have to trap the error codes issued from that if we want to support // reconnection. while (true) { $msg = $this->modify($entry, array($action => $value)); if (self::isError($msg)) { // We have a failure. What type? We may be able to reconnect // and try again. $error_code = $msg->getCode(); $error_name = Net_LDAP2::errorMessage($error_code); if ((Net_LDAP2::errorMessage($error_code) === 'LDAP_OPERATIONS_ERROR') && ($this->_config['auto_reconnect'])) { // The server has become disconnected before trying the // operation. We should try again, possibly with a different // server. $this->_link = false; $this->performReconnect(); } else { // Errors other than the above catched are just passed // back to the user so he may react upon them. return $msg; } } else { // modification succeedet, evaluate next change break; } } } } return true; } /** * Run a ldap search query * * Search is used to query the ldap-database. * $base and $filter may be ommitted. The one from config will * then be used. $base is either a DN-string or an Net_LDAP2_Entry * object in which case its DN willb e used. * * Params may contain: * * scope: The scope which will be used for searching * base - Just one entry * sub - The whole tree * one - Immediately below $base * sizelimit: Limit the number of entries returned (default: 0 = unlimited), * timelimit: Limit the time spent for searching (default: 0 = unlimited), * attrsonly: If true, the search will only return the attribute names, * attributes: Array of attribute names, which the entry should contain. * It is good practice to limit this to just the ones you need. * [NOT IMPLEMENTED] * deref: By default aliases are dereferenced to locate the base object for the search, but not when * searching subordinates of the base object. This may be changed by specifying one of the * following values: * * never - Do not dereference aliases in searching or in locating the base object of the search. * search - Dereference aliases in subordinates of the base object in searching, but not in * locating the base object of the search. * find * always * * Please note, that you cannot override server side limitations to sizelimit * and timelimit: You can always only lower a given limit. * * @param string|Net_LDAP2_Entry $base LDAP searchbase * @param string|Net_LDAP2_Filter $filter LDAP search filter or a Net_LDAP2_Filter object * @param array $params Array of options * * @access public * @return Net_LDAP2_Search|Net_LDAP2_Error Net_LDAP2_Search object or Net_LDAP2_Error object * @todo implement search controls (sorting etc) */ public function search($base = null, $filter = null, $params = array()) { if (is_null($base)) { $base = $this->_config['basedn']; } if ($base instanceof Net_LDAP2_Entry) { $base = $base->dn(); // fetch DN of entry, making searchbase relative to the entry } if (is_null($filter)) { $filter = $this->_config['filter']; } if ($filter instanceof Net_LDAP2_Filter) { $filter = $filter->asString(); // convert Net_LDAP2_Filter to string representation } if (PEAR::isError($filter)) { return $filter; } if (PEAR::isError($base)) { return $base; } /* setting searchparameters */ (isset($params['sizelimit'])) ? $sizelimit = $params['sizelimit'] : $sizelimit = 0; (isset($params['timelimit'])) ? $timelimit = $params['timelimit'] : $timelimit = 0; (isset($params['attrsonly'])) ? $attrsonly = $params['attrsonly'] : $attrsonly = 0; (isset($params['attributes'])) ? $attributes = $params['attributes'] : $attributes = array(); // Ensure $attributes to be an array in case only one // attribute name was given as string if (!is_array($attributes)) { $attributes = array($attributes); } // reorganize the $attributes array index keys // sometimes there are problems with not consecutive indexes $attributes = array_values($attributes); // scoping makes searches faster! $scope = (isset($params['scope']) ? $params['scope'] : $this->_config['scope']); switch ($scope) { case 'one': $search_function = 'ldap_list'; break; case 'base': $search_function = 'ldap_read'; break; default: $search_function = 'ldap_search'; } // Continue attempting the search operation until we get a success // or a definitive failure. while (true) { $link = $this->getLink(); $search = @call_user_func($search_function, $link, $base, $filter, $attributes, $attrsonly, $sizelimit, $timelimit); if ($err = @ldap_errno($link)) { if ($err == 32) { // Errorcode 32 = no such object, i.e. a nullresult. return $obj = new Net_LDAP2_Search ($search, $this, $attributes); } elseif ($err == 4) { // Errorcode 4 = sizelimit exeeded. return $obj = new Net_LDAP2_Search ($search, $this, $attributes); } elseif ($err == 87) { // bad search filter return $this->raiseError(Net_LDAP2::errorMessage($err) . "($filter)", $err); } elseif (($err == 1) && ($this->_config['auto_reconnect'])) { // Errorcode 1 = LDAP_OPERATIONS_ERROR but we can try a reconnect. $this->_link = false; $this->performReconnect(); } else { $msg = "\nParameters:\nBase: $base\nFilter: $filter\nScope: $scope"; return $this->raiseError(Net_LDAP2::errorMessage($err) . $msg, $err); } } else { return $obj = new Net_LDAP2_Search($search, $this, $attributes); } } } /** * Set an LDAP option * * @param string $option Option to set * @param mixed $value Value to set Option to * * @access public * @return Net_LDAP2_Error|true Net_LDAP2_Error object or true */ public function setOption($option, $value) { if ($this->_link) { if (defined($option)) { if (@ldap_set_option($this->_link, constant($option), $value)) { return true; } else { $err = @ldap_errno($this->_link); if ($err) { $msg = @ldap_err2str($err); } else { $err = NET_LDAP2_ERROR; $msg = Net_LDAP2::errorMessage($err); } return $this->raiseError($msg, $err); } } else { return $this->raiseError("Unkown Option requested"); } } else { return $this->raiseError("Could not set LDAP option: No LDAP connection"); } } /** * Get an LDAP option value * * @param string $option Option to get * * @access public * @return Net_LDAP2_Error|string Net_LDAP2_Error or option value */ public function getOption($option) { if ($this->_link) { if (defined($option)) { if (@ldap_get_option($this->_link, constant($option), $value)) { return $value; } else { $err = @ldap_errno($this->_link); if ($err) { $msg = @ldap_err2str($err); } else { $err = NET_LDAP2_ERROR; $msg = Net_LDAP2::errorMessage($err); } return $this->raiseError($msg, $err); } } else { $this->raiseError("Unkown Option requested"); } } else { $this->raiseError("No LDAP connection"); } } /** * Get the LDAP_PROTOCOL_VERSION that is used on the connection. * * A lot of ldap functionality is defined by what protocol version the ldap server speaks. * This might be 2 or 3. * * @return int */ public function getLDAPVersion() { if ($this->_link) { $version = $this->getOption("LDAP_OPT_PROTOCOL_VERSION"); } else { $version = $this->_config['version']; } return $version; } /** * Set the LDAP_PROTOCOL_VERSION that is used on the connection. * * @param int $version LDAP-version that should be used * @param boolean $force If set to true, the check against the rootDSE will be skipped * * @return Net_LDAP2_Error|true Net_LDAP2_Error object or true * @todo Checking via the rootDSE takes much time - why? fetching and instanciation is quick! */ public function setLDAPVersion($version = 0, $force = false) { if (!$version) { $version = $this->_config['version']; } // // Check to see if the server supports this version first. // // Todo: Why is this so horribly slow? // $this->rootDse() is very fast, as well as Net_LDAP2_RootDSE::fetch() // seems like a problem at copiyng the object inside PHP?? // Additionally, this is not always reproducable... // if (!$force) { $rootDSE = $this->rootDse(); if ($rootDSE instanceof Net_LDAP2_Error) { return $rootDSE; } else { $supported_versions = $rootDSE->getValue('supportedLDAPVersion'); if (is_string($supported_versions)) { $supported_versions = array($supported_versions); } $check_ok = in_array($version, $supported_versions); } } if ($force || $check_ok) { return $this->setOption("LDAP_OPT_PROTOCOL_VERSION", $version); } else { return $this->raiseError("LDAP Server does not support protocol version " . $version); } } /** * Tells if a DN does exist in the directory * * @param string|Net_LDAP2_Entry $dn The DN of the object to test * * @return boolean|Net_LDAP2_Error */ public function dnExists($dn) { if (PEAR::isError($dn)) { return $dn; } if ($dn instanceof Net_LDAP2_Entry) { $dn = $dn->dn(); } if (false === is_string($dn)) { return PEAR::raiseError('Parameter $dn is not a string nor an entry object!'); } // search LDAP for that DN by performing a baselevel search for any // object. We can only find the DN in question this way, or nothing. $s_opts = array( 'scope' => 'base', 'sizelimit' => 1, 'attributes' => '1.1' // select no attrs ); $search = $this->search($dn, '(objectClass=*)', $s_opts); if (self::isError($search)) { return $search; } // retun wehter the DN exists; that is, we found an entry return ($search->count() == 0)? false : true; } /** * Get a specific entry based on the DN * * @param string $dn DN of the entry that should be fetched * @param array $attr Array of Attributes to select. If ommitted, all attributes are fetched. * * @return Net_LDAP2_Entry|Net_LDAP2_Error Reference to a Net_LDAP2_Entry object or Net_LDAP2_Error object * @todo Maybe check against the shema should be done to be sure the attribute type exists */ public function getEntry($dn, $attr = array()) { if (!is_array($attr)) { $attr = array($attr); } $result = $this->search($dn, '(objectClass=*)', array('scope' => 'base', 'attributes' => $attr)); if (self::isError($result)) { return $result; } elseif ($result->count() == 0) { return PEAR::raiseError('Could not fetch entry '.$dn.': no entry found'); } $entry = $result->shiftEntry(); if (false == $entry) { return PEAR::raiseError('Could not fetch entry (error retrieving entry from search result)'); } return $entry; } /** * Rename or move an entry * * This method will instantly carry out an update() after the move, * so the entry is moved instantly. * You can pass an optional Net_LDAP2 object. In this case, a cross directory * move will be performed which deletes the entry in the source (THIS) directory * and adds it in the directory $target_ldap. * A cross directory move will switch the Entrys internal LDAP reference so * updates to the entry will go to the new directory. * * Note that if you want to do a cross directory move, you need to * pass an Net_LDAP2_Entry object, otherwise the attributes will be empty. * * @param string|Net_LDAP2_Entry $entry Entry DN or Entry object * @param string $newdn New location * @param Net_LDAP2 $target_ldap (optional) Target directory for cross server move; should be passed via reference * * @return Net_LDAP2_Error|true */ public function move($entry, $newdn, $target_ldap = null) { if (is_string($entry)) { $entry_o = $this->getEntry($entry); } else { $entry_o = $entry; } if (!$entry_o instanceof Net_LDAP2_Entry) { return PEAR::raiseError('Parameter $entry is expected to be a Net_LDAP2_Entry object! (If DN was passed, conversion failed)'); } if (null !== $target_ldap && !$target_ldap instanceof Net_LDAP2) { return PEAR::raiseError('Parameter $target_ldap is expected to be a Net_LDAP2 object!'); } if ($target_ldap && $target_ldap !== $this) { // cross directory move if (is_string($entry)) { return PEAR::raiseError('Unable to perform cross directory move: operation requires a Net_LDAP2_Entry object'); } if ($target_ldap->dnExists($newdn)) { return PEAR::raiseError('Unable to perform cross directory move: entry does exist in target directory'); } $entry_o->dn($newdn); $res = $target_ldap->add($entry_o); if (self::isError($res)) { return PEAR::raiseError('Unable to perform cross directory move: '.$res->getMessage().' in target directory'); } $res = $this->delete($entry_o->currentDN()); if (self::isError($res)) { $res2 = $target_ldap->delete($entry_o); // undo add if (self::isError($res2)) { $add_error_string = 'Additionally, the deletion (undo add) of $entry in target directory failed.'; } return PEAR::raiseError('Unable to perform cross directory move: '.$res->getMessage().' in source directory. '.$add_error_string); } $entry_o->setLDAP($target_ldap); return true; } else { // local move $entry_o->dn($newdn); $entry_o->setLDAP($this); return $entry_o->update(); } } /** * Copy an entry to a new location * * The entry will be immediately copied. * Please note that only attributes you have * selected will be copied. * * @param Net_LDAP2_Entry $entry Entry object * @param string $newdn New FQF-DN of the entry * * @return Net_LDAP2_Error|Net_LDAP2_Entry Error Message or reference to the copied entry */ public function copy($entry, $newdn) { if (!$entry instanceof Net_LDAP2_Entry) { return PEAR::raiseError('Parameter $entry is expected to be a Net_LDAP2_Entry object!'); } $newentry = Net_LDAP2_Entry::createFresh($newdn, $entry->getValues()); $result = $this->add($newentry); if ($result instanceof Net_LDAP2_Error) { return $result; } else { return $newentry; } } /** * Returns the string for an ldap errorcode. * * Made to be able to make better errorhandling * Function based on DB::errorMessage() * Tip: The best description of the errorcodes is found here: * http://www.directory-info.com/LDAP2/LDAPErrorCodes.html * * @param int $errorcode Error code * * @return string The errorstring for the error. */ public static function errorMessage($errorcode) { $errorMessages = array( 0x00 => "LDAP_SUCCESS", 0x01 => "LDAP_OPERATIONS_ERROR", 0x02 => "LDAP_PROTOCOL_ERROR", 0x03 => "LDAP_TIMELIMIT_EXCEEDED", 0x04 => "LDAP_SIZELIMIT_EXCEEDED", 0x05 => "LDAP_COMPARE_FALSE", 0x06 => "LDAP_COMPARE_TRUE", 0x07 => "LDAP_AUTH_METHOD_NOT_SUPPORTED", 0x08 => "LDAP_STRONG_AUTH_REQUIRED", 0x09 => "LDAP_PARTIAL_RESULTS", 0x0a => "LDAP_REFERRAL", 0x0b => "LDAP_ADMINLIMIT_EXCEEDED", 0x0c => "LDAP_UNAVAILABLE_CRITICAL_EXTENSION", 0x0d => "LDAP_CONFIDENTIALITY_REQUIRED", 0x0e => "LDAP_SASL_BIND_INPROGRESS", 0x10 => "LDAP_NO_SUCH_ATTRIBUTE", 0x11 => "LDAP_UNDEFINED_TYPE", 0x12 => "LDAP_INAPPROPRIATE_MATCHING", 0x13 => "LDAP_CONSTRAINT_VIOLATION", 0x14 => "LDAP_TYPE_OR_VALUE_EXISTS", 0x15 => "LDAP_INVALID_SYNTAX", 0x20 => "LDAP_NO_SUCH_OBJECT", 0x21 => "LDAP_ALIAS_PROBLEM", 0x22 => "LDAP_INVALID_DN_SYNTAX", 0x23 => "LDAP_IS_LEAF", 0x24 => "LDAP_ALIAS_DEREF_PROBLEM", 0x30 => "LDAP_INAPPROPRIATE_AUTH", 0x31 => "LDAP_INVALID_CREDENTIALS", 0x32 => "LDAP_INSUFFICIENT_ACCESS", 0x33 => "LDAP_BUSY", 0x34 => "LDAP_UNAVAILABLE", 0x35 => "LDAP_UNWILLING_TO_PERFORM", 0x36 => "LDAP_LOOP_DETECT", 0x3C => "LDAP_SORT_CONTROL_MISSING", 0x3D => "LDAP_INDEX_RANGE_ERROR", 0x40 => "LDAP_NAMING_VIOLATION", 0x41 => "LDAP_OBJECT_CLASS_VIOLATION", 0x42 => "LDAP_NOT_ALLOWED_ON_NONLEAF", 0x43 => "LDAP_NOT_ALLOWED_ON_RDN", 0x44 => "LDAP_ALREADY_EXISTS", 0x45 => "LDAP_NO_OBJECT_CLASS_MODS", 0x46 => "LDAP_RESULTS_TOO_LARGE", 0x47 => "LDAP_AFFECTS_MULTIPLE_DSAS", 0x50 => "LDAP_OTHER", 0x51 => "LDAP_SERVER_DOWN", 0x52 => "LDAP_LOCAL_ERROR", 0x53 => "LDAP_ENCODING_ERROR", 0x54 => "LDAP_DECODING_ERROR", 0x55 => "LDAP_TIMEOUT", 0x56 => "LDAP_AUTH_UNKNOWN", 0x57 => "LDAP_FILTER_ERROR", 0x58 => "LDAP_USER_CANCELLED", 0x59 => "LDAP_PARAM_ERROR", 0x5a => "LDAP_NO_MEMORY", 0x5b => "LDAP_CONNECT_ERROR", 0x5c => "LDAP_NOT_SUPPORTED", 0x5d => "LDAP_CONTROL_NOT_FOUND", 0x5e => "LDAP_NO_RESULTS_RETURNED", 0x5f => "LDAP_MORE_RESULTS_TO_RETURN", 0x60 => "LDAP_CLIENT_LOOP", 0x61 => "LDAP_REFERRAL_LIMIT_EXCEEDED", 1000 => "Unknown Net_LDAP2 Error" ); return isset($errorMessages[$errorcode]) ? $errorMessages[$errorcode] : $errorMessages[NET_LDAP2_ERROR] . ' (' . $errorcode . ')'; } /** * Gets a rootDSE object * * This either fetches a fresh rootDSE object or returns it from * the internal cache for performance reasons, if possible. * * @param array $attrs Array of attributes to search for * * @access public * @return Net_LDAP2_Error|Net_LDAP2_RootDSE Net_LDAP2_Error or Net_LDAP2_RootDSE object */ public function rootDse($attrs = null) { if ($attrs !== null && !is_array($attrs)) { return PEAR::raiseError('Parameter $attr is expected to be an array!'); } $attrs_signature = serialize($attrs); // see if we need to fetch a fresh object, or if we already // requested this object with the same attributes if (true || !array_key_exists($attrs_signature, $this->_rootDSE_cache)) { $rootdse = Net_LDAP2_RootDSE::fetch($this, $attrs); if ($rootdse instanceof Net_LDAP2_Error) { return $rootdse; } // search was ok, store rootDSE in cache $this->_rootDSE_cache[$attrs_signature] = $rootdse; } return $this->_rootDSE_cache[$attrs_signature]; } /** * Alias function of rootDse() for perl-ldap interface * * @access public * @see rootDse() * @return Net_LDAP2_Error|Net_LDAP2_RootDSE */ public function root_dse() { $args = func_get_args(); return call_user_func_array(array($this, 'rootDse'), $args); } /** * Get a schema object * * @param string $dn (optional) Subschema entry dn * * @access public * @return Net_LDAP2_Schema|Net_LDAP2_Error Net_LDAP2_Schema or Net_LDAP2_Error object */ public function schema($dn = null) { // Schema caching by Knut-Olav Hoven // If a schema caching object is registered, we use that to fetch // a schema object. // See registerSchemaCache() for more info on this. if ($this->_schema === null) { if ($this->_schema_cache) { $cached_schema = $this->_schema_cache->loadSchema(); if ($cached_schema instanceof Net_LDAP2_Error) { return $cached_schema; // route error to client } else { if ($cached_schema instanceof Net_LDAP2_Schema) { $this->_schema = $cached_schema; } } } } // Fetch schema, if not tried before and no cached version available. // If we are already fetching the schema, we will skip fetching. if ($this->_schema === null) { // store a temporary error message so subsequent calls to schema() can // detect, that we are fetching the schema already. // Otherwise we will get an infinite loop at Net_LDAP2_Schema::fetch() $this->_schema = new Net_LDAP2_Error('Schema not initialized'); $this->_schema = Net_LDAP2_Schema::fetch($this, $dn); // If schema caching is active, advise the cache to store the schema if ($this->_schema_cache) { $caching_result = $this->_schema_cache->storeSchema($this->_schema); if ($caching_result instanceof Net_LDAP2_Error) { return $caching_result; // route error to client } } } return $this->_schema; } /** * Enable/disable persistent schema caching * * Sometimes it might be useful to allow your scripts to cache * the schema information on disk, so the schema is not fetched * every time the script runs which could make your scripts run * faster. * * This method allows you to register a custom object that * implements your schema cache. Please see the SchemaCache interface * (SchemaCache.interface.php) for informations on how to implement this. * To unregister the cache, pass null as $cache parameter. * * For ease of use, Net_LDAP2 provides a simple file based cache * which is used in the example below. You may use this, for example, * to store the schema in a linux tmpfs which results in the schema * beeing cached inside the RAM which allows nearly instant access. * * // Create the simple file cache object that comes along with Net_LDAP2 * $mySchemaCache_cfg = array( * 'path' => '/tmp/Net_LDAP2_Schema.cache', * 'max_age' => 86400 // max age is 24 hours (in seconds) * ); * $mySchemaCache = new Net_LDAP2_SimpleFileSchemaCache($mySchemaCache_cfg); * $ldap = new Net_LDAP2::connect(...); * $ldap->registerSchemaCache($mySchemaCache); // enable caching * // now each call to $ldap->schema() will get the schema from disk! * * * @param Net_LDAP2_SchemaCache|null $cache Object implementing the Net_LDAP2_SchemaCache interface * * @return true|Net_LDAP2_Error */ public function registerSchemaCache($cache) { if (is_null($cache) || (is_object($cache) && in_array('Net_LDAP2_SchemaCache', class_implements($cache))) ) { $this->_schema_cache = $cache; return true; } else { return new Net_LDAP2_Error('Custom schema caching object is either no '. 'valid object or does not implement the Net_LDAP2_SchemaCache interface!'); } } /** * Checks if phps ldap-extension is loaded * * If it is not loaded, it tries to load it manually using PHPs dl(). * It knows both windows-dll and *nix-so. * * @static * @return Net_LDAP2_Error|true */ public static function checkLDAPExtension() { if (!extension_loaded('ldap') && !@dl('ldap.' . PHP_SHLIB_SUFFIX)) { return new Net_LDAP2_Error("It seems that you do not have the ldap-extension installed. Please install it before using the Net_LDAP2 package."); } else { return true; } } /** * Encodes given attributes from ISO-8859-1 to UTF-8 if needed by schema * * This function takes attributes in an array and then checks against the schema if they need * UTF8 encoding. If that is so, they will be encoded. An encoded array will be returned and * can be used for adding or modifying. * * $attributes is expected to be an array with keys describing * the attribute names and the values as the value of this attribute: * $attributes = array('cn' => 'foo', 'attr2' => array('mv1', 'mv2')); * * @param array $attributes Array of attributes * * @access public * @return array|Net_LDAP2_Error Array of UTF8 encoded attributes or Error */ public function utf8Encode($attributes) { return $this->utf8($attributes, 'utf8_encode'); } /** * Decodes the given attribute values from UTF-8 to ISO-8859-1 if needed by schema * * $attributes is expected to be an array with keys describing * the attribute names and the values as the value of this attribute: * $attributes = array('cn' => 'foo', 'attr2' => array('mv1', 'mv2')); * * @param array $attributes Array of attributes * * @access public * @see utf8Encode() * @return array|Net_LDAP2_Error Array with decoded attribute values or Error */ public function utf8Decode($attributes) { return $this->utf8($attributes, 'utf8_decode'); } /** * Encodes or decodes UTF-8/ISO-8859-1 attribute values if needed by schema * * @param array $attributes Array of attributes * @param array $function Function to apply to attribute values * * @access protected * @return array|Net_LDAP2_Error Array of attributes with function applied to values or Error */ protected function utf8($attributes, $function) { if (!is_array($attributes) || array_key_exists(0, $attributes)) { return PEAR::raiseError('Parameter $attributes is expected to be an associative array'); } if (!$this->_schema) { $this->_schema = $this->schema(); } if (!$this->_link || self::isError($this->_schema) || !function_exists($function)) { return $attributes; } if (is_array($attributes) && count($attributes) > 0) { foreach ($attributes as $k => $v) { if (!isset($this->_schemaAttrs[$k])) { $attr = $this->_schema->get('attribute', $k); if (self::isError($attr)) { continue; } // Encoding is needed if this is a DIR_STR. We assume also // needed encoding in case the schema contains no syntax // information (he does not need to, see rfc2252, 4.2) if (!array_key_exists('syntax', $attr) || false !== strpos($attr['syntax'], '1.3.6.1.4.1.1466.115.121.1.15')) { $encode = true; } else { $encode = false; } $this->_schemaAttrs[$k] = $encode; } else { $encode = $this->_schemaAttrs[$k]; } if ($encode) { if (is_array($v)) { foreach ($v as $ak => $av) { $v[$ak] = call_user_func($function, $av); } } else { $v = call_user_func($function, $v); } } $attributes[$k] = $v; } } return $attributes; } /** * Get the LDAP link resource. It will loop attempting to * re-establish the connection if the connection attempt fails and * auto_reconnect has been turned on (see the _config array documentation). * * @access public * @return resource LDAP link */ public function getLink() { if ($this->_config['auto_reconnect']) { while (true) { // // Return the link handle if we are already connected. Otherwise // try to reconnect. // if ($this->_link !== false) { return $this->_link; } else { $this->performReconnect(); } } } return $this->_link; } } /** * Net_LDAP2_Error implements a class for reporting portable LDAP error messages. * * @category Net * @package Net_LDAP2 * @author Tarjej Huse * @license http://www.gnu.org/copyleft/lesser.html LGPL * @link http://pear.php.net/package/Net_LDAP22/ */ class Net_LDAP2_Error extends PEAR_Error { /** * Net_LDAP2_Error constructor. * * @param string $message String with error message. * @param integer $code Net_LDAP2 error code * @param integer $mode what "error mode" to operate in * @param mixed $level what error level to use for $mode & PEAR_ERROR_TRIGGER * @param mixed $debuginfo additional debug info, such as the last query * * @access public * @see PEAR_Error */ public function __construct($message = 'Net_LDAP2_Error', $code = NET_LDAP2_ERROR, $mode = PEAR_ERROR_RETURN, $level = E_USER_NOTICE, $debuginfo = null) { if (is_int($code)) { parent::__construct($message . ': ' . Net_LDAP2::errorMessage($code), $code, $mode, $level, $debuginfo); } else { parent::__construct("$message: $code", NET_LDAP2_ERROR, $mode, $level, $debuginfo); } } } ?> * @copyright 2009 Jan Wagner * @license http://www.gnu.org/licenses/lgpl-3.0.txt LGPLv3 * @version SVN: $Id$ * @link http://pear.php.net/package/Net_LDAP2/ */ /** * Includes */ require_once 'PEAR.php'; /** * Getting the rootDSE entry of a LDAP server * * @category Net * @package Net_LDAP2 * @author Jan Wagner * @license http://www.gnu.org/copyleft/lesser.html LGPL * @link http://pear.php.net/package/Net_LDAP22/ */ class Net_LDAP2_RootDSE extends PEAR { /** * @access protected * @var object Net_LDAP2_Entry **/ protected $_entry; /** * Class constructor * * @param Net_LDAP2_Entry &$entry Net_LDAP2_Entry object of the RootDSE */ public function __construct(&$entry) { $this->_entry = $entry; } /** * Fetches a RootDSE object from an LDAP connection * * @param Net_LDAP2 $ldap Directory from which the RootDSE should be fetched * @param array $attrs Array of attributes to search for * * @access static * @return Net_LDAP2_RootDSE|Net_LDAP2_Error */ public static function fetch($ldap, $attrs = null) { if (!$ldap instanceof Net_LDAP2) { return PEAR::raiseError("Unable to fetch Schema: Parameter \$ldap must be a Net_LDAP2 object!"); } if (is_array($attrs) && count($attrs) > 0 ) { $attributes = $attrs; } else { $attributes = array('vendorName', 'vendorVersion', 'namingContexts', 'altServer', 'supportedExtension', 'supportedControl', 'supportedSASLMechanisms', 'supportedLDAPVersion', 'subschemaSubentry' ); } $result = $ldap->search('', '(objectClass=*)', array('attributes' => $attributes, 'scope' => 'base')); if (self::isError($result)) { return $result; } $entry = $result->shiftEntry(); if (false === $entry) { return PEAR::raiseError('Could not fetch RootDSE entry'); } $ret = new Net_LDAP2_RootDSE($entry); return $ret; } /** * Gets the requested attribute value * * Same usuage as {@link Net_LDAP2_Entry::getValue()} * * @param string $attr Attribute name * @param array $options Array of options * * @access public * @return mixed Net_LDAP2_Error object or attribute values * @see Net_LDAP2_Entry::get_value() */ public function getValue($attr = '', $options = '') { return $this->_entry->get_value($attr, $options); } /** * Alias function of getValue() for perl-ldap interface * * @see getValue() * @return mixed */ public function get_value() { $args = func_get_args(); return call_user_func_array(array( &$this, 'getValue' ), $args); } /** * Determines if the extension is supported * * @param array $oids Array of oids to check * * @access public * @return boolean */ public function supportedExtension($oids) { return $this->checkAttr($oids, 'supportedExtension'); } /** * Alias function of supportedExtension() for perl-ldap interface * * @see supportedExtension() * @return boolean */ public function supported_extension() { $args = func_get_args(); return call_user_func_array(array( &$this, 'supportedExtension'), $args); } /** * Determines if the version is supported * * @param array $versions Versions to check * * @access public * @return boolean */ public function supportedVersion($versions) { return $this->checkAttr($versions, 'supportedLDAPVersion'); } /** * Alias function of supportedVersion() for perl-ldap interface * * @see supportedVersion() * @return boolean */ public function supported_version() { $args = func_get_args(); return call_user_func_array(array(&$this, 'supportedVersion'), $args); } /** * Determines if the control is supported * * @param array $oids Control oids to check * * @access public * @return boolean */ public function supportedControl($oids) { return $this->checkAttr($oids, 'supportedControl'); } /** * Alias function of supportedControl() for perl-ldap interface * * @see supportedControl() * @return boolean */ public function supported_control() { $args = func_get_args(); return call_user_func_array(array(&$this, 'supportedControl' ), $args); } /** * Determines if the sasl mechanism is supported * * @param array $mechlist SASL mechanisms to check * * @access public * @return boolean */ public function supportedSASLMechanism($mechlist) { return $this->checkAttr($mechlist, 'supportedSASLMechanisms'); } /** * Alias function of supportedSASLMechanism() for perl-ldap interface * * @see supportedSASLMechanism() * @return boolean */ public function supported_sasl_mechanism() { $args = func_get_args(); return call_user_func_array(array(&$this, 'supportedSASLMechanism'), $args); } /** * Checks for existance of value in attribute * * @param array $values values to check * @param string $attr attribute name * * @access protected * @return boolean */ protected function checkAttr($values, $attr) { if (!is_array($values)) $values = array($values); foreach ($values as $value) { if (!@in_array($value, $this->get_value($attr, 'all'))) { return false; } } return true; } } ?> * @copyright 2009 Benedikt Hallinger * @license http://www.gnu.org/licenses/lgpl-3.0.txt LGPLv3 * @version SVN: $Id$ * @link http://pear.php.net/package/Net_LDAP2/ */ /** * Includes */ require_once 'PEAR.php'; require_once 'Net/LDAP2.php'; require_once 'Net/LDAP2/Entry.php'; require_once 'Net/LDAP2/Util.php'; /** * LDIF capabilitys for Net_LDAP2, closely taken from PERLs Net::LDAP * * It provides a means to convert between Net_LDAP2_Entry objects and LDAP entries * represented in LDIF format files. Reading and writing are supported and may * manipulate single entries or lists of entries. * * Usage example: * * // Read and parse an ldif-file into Net_LDAP2_Entry objects * // and print out the DNs. Store the entries for later use. * require 'Net/LDAP2/LDIF.php'; * $options = array( * 'onerror' => 'die' * ); * $entries = array(); * $ldif = new Net_LDAP2_LDIF('test.ldif', 'r', $options); * do { * $entry = $ldif->read_entry(); * $dn = $entry->dn(); * echo " done building entry: $dn\n"; * array_push($entries, $entry); * } while (!$ldif->eof()); * $ldif->done(); * * * // write those entries to another file * $ldif = new Net_LDAP2_LDIF('test.out.ldif', 'w', $options); * $ldif->write_entry($entries); * $ldif->done(); * * * @category Net * @package Net_LDAP2 * @author Benedikt Hallinger * @license http://www.gnu.org/copyleft/lesser.html LGPL * @link http://pear.php.net/package/Net_LDAP22/ * @see http://www.ietf.org/rfc/rfc2849.txt * @todo Error handling should be PEARified * @todo LDAPv3 controls are not implemented yet */ class Net_LDAP2_LDIF extends PEAR { /** * Options * * @access protected * @var array */ protected $_options = array('encode' => 'base64', 'onerror' => null, 'change' => 0, 'lowercase' => 0, 'sort' => 0, 'version' => null, 'wrap' => 78, 'raw' => '' ); /** * Errorcache * * @access protected * @var array */ protected $_error = array('error' => null, 'line' => 0 ); /** * Filehandle for read/write * * @access protected * @var array */ protected $_FH = null; /** * Says, if we opened the filehandle ourselves * * @access protected * @var array */ protected $_FH_opened = false; /** * Linecounter for input file handle * * @access protected * @var array */ protected $_input_line = 0; /** * counter for processed entries * * @access protected * @var int */ protected $_entrynum = 0; /** * Mode we are working in * * Either 'r', 'a' or 'w' * * @access protected * @var string */ protected $_mode = false; /** * Tells, if the LDIF version string was already written * * @access protected * @var boolean */ protected $_version_written = false; /** * Cache for lines that have build the current entry * * @access protected * @var boolean */ protected $_lines_cur = array(); /** * Cache for lines that will build the next entry * * @access protected * @var boolean */ protected $_lines_next = array(); /** * Open LDIF file for reading or for writing * * new (FILE): * Open the file read-only. FILE may be the name of a file * or an already open filehandle. * If the file doesn't exist, it will be created if in write mode. * * new (FILE, MODE, OPTIONS): * Open the file with the given MODE (see PHPs fopen()), eg "w" or "a". * FILE may be the name of a file or an already open filehandle. * PERLs Net_LDAP2 "FILE|" mode does not work curently. * * OPTIONS is an associative array and may contain: * encode => 'none' | 'canonical' | 'base64' * Some DN values in LDIF cannot be written verbatim and have to be encoded in some way: * 'none' No encoding. * 'canonical' See "canonical_dn()" in Net::LDAP::Util. * 'base64' Use base64. (default, this differs from the Perl interface. * The perl default is "none"!) * * onerror => 'die' | 'warn' | NULL * Specify what happens when an error is detected. * 'die' Net_LDAP2_LDIF will croak with an appropriate message. * 'warn' Net_LDAP2_LDIF will warn (echo) with an appropriate message. * NULL Net_LDAP2_LDIF will not warn (default), use error(). * * change => 1 * Write entry changes to the LDIF file instead of the entries itself. I.e. write LDAP * operations acting on the entries to the file instead of the entries contents. * This writes the changes usually carried out by an update() to the LDIF file. * * lowercase => 1 * Convert attribute names to lowercase when writing. * * sort => 1 * Sort attribute names when writing entries according to the rule: * objectclass first then all other attributes alphabetically sorted by attribute name * * version => '1' * Set the LDIF version to write to the resulting LDIF file. * According to RFC 2849 currently the only legal value for this option is 1. * When this option is set Net_LDAP2_LDIF tries to adhere more strictly to * the LDIF specification in RFC2489 in a few places. * The default is NULL meaning no version information is written to the LDIF file. * * wrap => 78 * Number of columns where output line wrapping shall occur. * Default is 78. Setting it to 40 or lower inhibits wrapping. * * raw => REGEX * Use REGEX to denote the names of attributes that are to be * considered binary in search results if writing entries. * Example: raw => "/(?i:^jpegPhoto|;binary)/i" * * @param string|ressource $file Filename or filehandle * @param string $mode Mode to open filename * @param array $options Options like described above */ public function __construct($file, $mode = 'r', $options = array()) { parent::__construct('Net_LDAP2_Error'); // default error class // First, parse options // todo: maybe implement further checks on possible values foreach ($options as $option => $value) { if (!array_key_exists($option, $this->_options)) { $this->dropError('Net_LDAP2_LDIF error: option '.$option.' not known!'); return; } else { $this->_options[$option] = strtolower($value); } } // setup LDIF class $this->version($this->_options['version']); // setup file mode if (!preg_match('/^[rwa]\+?$/', $mode)) { $this->dropError('Net_LDAP2_LDIF error: file mode '.$mode.' not supported!'); } else { $this->_mode = $mode; // setup filehandle if (is_resource($file)) { // TODO: checks on mode possible? $this->_FH =& $file; } else { $imode = substr($this->_mode, 0, 1); if ($imode == 'r') { if (!file_exists($file)) { $this->dropError('Unable to open '.$file.' for read: file not found'); $this->_mode = false; } if (!is_readable($file)) { $this->dropError('Unable to open '.$file.' for read: permission denied'); $this->_mode = false; } } if (($imode == 'w' || $imode == 'a')) { if (file_exists($file)) { if (!is_writable($file)) { $this->dropError('Unable to open '.$file.' for write: permission denied'); $this->_mode = false; } } else { if (!@touch($file)) { $this->dropError('Unable to create '.$file.' for write: permission denied'); $this->_mode = false; } } } if ($this->_mode) { $this->_FH = @fopen($file, $this->_mode); if (false === $this->_FH) { // Fallback; should never be reached if tests above are good enough! $this->dropError('Net_LDAP2_LDIF error: Could not open file '.$file); } else { $this->_FH_opened = true; } } } } } /** * Read one entry from the file and return it as a Net::LDAP::Entry object. * * @return Net_LDAP2_Entry */ public function read_entry() { // read fresh lines, set them as current lines and create the entry $attrs = $this->next_lines(true); if (count($attrs) > 0) { $this->_lines_cur = $attrs; } return $this->current_entry(); } /** * Returns true when the end of the file is reached. * * @return boolean */ public function eof() { return feof($this->_FH); } /** * Write the entry or entries to the LDIF file. * * If you want to build an LDIF file containing several entries AND * you want to call write_entry() several times, you must open the filehandle * in append mode ("a"), otherwise you will always get the last entry only. * * @param Net_LDAP2_Entry|array $entries Entry or array of entries * * @return void * @todo implement operations on whole entries (adding a whole entry) */ public function write_entry($entries) { if (!is_array($entries)) { $entries = array($entries); } foreach ($entries as $entry) { $this->_entrynum++; if (!$entry instanceof Net_LDAP2_Entry) { $this->dropError('Net_LDAP2_LDIF error: entry '.$this->_entrynum.' is not an Net_LDAP2_Entry object'); } else { if ($this->_options['change']) { // LDIF change mode // fetch change information from entry $entry_attrs_changes = $entry->getChanges(); $num_of_changes = count($entry_attrs_changes['add']) + count($entry_attrs_changes['replace']) + count($entry_attrs_changes['delete']); $is_changed = ($num_of_changes > 0 || $entry->willBeDeleted() || $entry->willBeMoved()); // write version if not done yet // also write DN of entry if ($is_changed) { if (!$this->_version_written) { $this->write_version(); } $this->writeDN($entry->currentDN()); } // process changes // TODO: consider DN add! if ($entry->willBeDeleted()) { $this->writeLine("changetype: delete".PHP_EOL); } elseif ($entry->willBeMoved()) { $this->writeLine("changetype: modrdn".PHP_EOL); $olddn = Net_LDAP2_Util::ldap_explode_dn($entry->currentDN(), array('casefold' => 'none')); // maybe gives a bug if using multivalued RDNs $oldrdn = array_shift($olddn); $oldparent = implode(',', $olddn); $newdn = Net_LDAP2_Util::ldap_explode_dn($entry->dn(), array('casefold' => 'none')); // maybe gives a bug if using multivalued RDNs $rdn = array_shift($newdn); $parent = implode(',', $newdn); $this->writeLine("newrdn: ".$rdn.PHP_EOL); $this->writeLine("deleteoldrdn: 1".PHP_EOL); if ($parent !== $oldparent) { $this->writeLine("newsuperior: ".$parent.PHP_EOL); } // TODO: What if the entry has attribute changes as well? // I think we should check for that and make a dummy // entry with the changes that is written to the LDIF file } elseif ($num_of_changes > 0) { // write attribute change data $this->writeLine("changetype: modify".PHP_EOL); foreach ($entry_attrs_changes as $changetype => $entry_attrs) { foreach ($entry_attrs as $attr_name => $attr_values) { $this->writeLine("$changetype: $attr_name".PHP_EOL); if ($attr_values !== null) $this->writeAttribute($attr_name, $attr_values, $changetype); $this->writeLine("-".PHP_EOL); } } } // finish this entrys data if we had changes if ($is_changed) { $this->finishEntry(); } } else { // LDIF-content mode // fetch attributes for further processing $entry_attrs = $entry->getValues(); // sort and put objectclass-attrs to first position if ($this->_options['sort']) { ksort($entry_attrs); if (array_key_exists('objectclass', $entry_attrs)) { $oc = $entry_attrs['objectclass']; unset($entry_attrs['objectclass']); $entry_attrs = array_merge(array('objectclass' => $oc), $entry_attrs); } } // write data if (!$this->_version_written) { $this->write_version(); } $this->writeDN($entry->dn()); foreach ($entry_attrs as $attr_name => $attr_values) { $this->writeAttribute($attr_name, $attr_values); } $this->finishEntry(); } } } } /** * Write version to LDIF * * If the object's version is defined, this method allows to explicitely write the version before an entry is written. * If not called explicitely, it gets called automatically when writing the first entry. * * @return void */ public function write_version() { $this->_version_written = true; if (!is_null($this->version())) { return $this->writeLine('version: '.$this->version().PHP_EOL, 'Net_LDAP2_LDIF error: unable to write version'); } } /** * Get or set LDIF version * * If called without arguments it returns the version of the LDIF file or NULL if no version has been set. * If called with an argument it sets the LDIF version to VERSION. * According to RFC 2849 currently the only legal value for VERSION is 1. * * @param int $version (optional) LDIF version to set * * @return int */ public function version($version = null) { if ($version !== null) { if ($version != 1) { $this->dropError('Net_LDAP2_LDIF error: illegal LDIF version set'); } else { $this->_options['version'] = $version; } } return $this->_options['version']; } /** * Returns the file handle the Net_LDAP2_LDIF object reads from or writes to. * * You can, for example, use this to fetch the content of the LDIF file yourself * * @return null|resource */ public function &handle() { if (!is_resource($this->_FH)) { $this->dropError('Net_LDAP2_LDIF error: invalid file resource'); $null = null; return $null; } else { return $this->_FH; } } /** * Clean up * * This method signals that the LDIF object is no longer needed. * You can use this to free up some memory and close the file handle. * The file handle is only closed, if it was opened from Net_LDAP2_LDIF. * * @return void */ public function done() { // close FH if we opened it if ($this->_FH_opened) { fclose($this->handle()); } // free variables foreach (get_object_vars($this) as $name => $value) { unset($this->$name); } } /** * Returns last error message if error was found. * * Example: * * $ldif->someAction(); * if ($ldif->error()) { * echo "Error: ".$ldif->error()." at input line: ".$ldif->error_lines(); * } * * * @param boolean $as_string If set to true, only the message is returned * * @return false|Net_LDAP2_Error */ public function error($as_string = false) { if (Net_LDAP2::isError($this->_error['error'])) { return ($as_string)? $this->_error['error']->getMessage() : $this->_error['error']; } else { return false; } } /** * Returns lines that resulted in error. * * Perl returns an array of faulty lines in list context, * but we always just return an int because of PHPs language. * * @return int */ public function error_lines() { return $this->_error['line']; } /** * Returns the current Net::LDAP::Entry object. * * @return Net_LDAP2_Entry|false */ public function current_entry() { return $this->parseLines($this->current_lines()); } /** * Parse LDIF lines of one entry into an Net_LDAP2_Entry object * * @param array $lines LDIF lines for one entry * * @return Net_LDAP2_Entry|false Net_LDAP2_Entry object for those lines * @todo what about file inclusions and urls? "jpegphoto:< file:///usr/local/directory/photos/fiona.jpg" */ public function parseLines($lines) { // parse lines into an array of attributes and build the entry $attributes = array(); $dn = false; foreach ($lines as $line) { if (preg_match('/^(\w+(;binary)?)(:|::|:<)\s(.+)$/', $line, $matches)) { $attr =& $matches[1] . $matches[2]; $delim =& $matches[3]; $data =& $matches[4]; if ($delim == ':') { // normal data $attributes[$attr][] = $data; } elseif ($delim == '::') { // base64 data $attributes[$attr][] = base64_decode($data); } elseif ($delim == ':<') { // file inclusion // TODO: Is this the job of the LDAP-client or the server? $this->dropError('File inclusions are currently not supported'); //$attributes[$attr][] = ...; } else { // since the pattern above, the delimeter cannot be something else. $this->dropError('Net_LDAP2_LDIF parsing error: invalid syntax at parsing entry line: '.$line); continue; } if (strtolower($attr) == 'dn') { // DN line detected $dn = $attributes[$attr][0]; // save possibly decoded DN unset($attributes[$attr]); // remove wrongly added "dn: " attribute } } else { // line not in "attr: value" format -> ignore // maybe we should rise an error here, but this should be covered by // next_lines() already. A problem arises, if users try to feed data of // several entries to this method - the resulting entry will // get wrong attributes. However, this is already mentioned in the // methods documentation above. } } if (false === $dn) { $this->dropError('Net_LDAP2_LDIF parsing error: unable to detect DN for entry'); return false; } else { $newentry = Net_LDAP2_Entry::createFresh($dn, $attributes); return $newentry; } } /** * Returns the lines that generated the current Net::LDAP::Entry object. * * Note that this returns an empty array if no lines have been read so far. * * @return array Array of lines */ public function current_lines() { return $this->_lines_cur; } /** * Returns the lines that will generate the next Net::LDAP::Entry object. * * If you set $force to TRUE then you can iterate over the lines that build * up entries manually. Otherwise, iterating is done using {@link read_entry()}. * Force will move the file pointer forward, thus returning the next entries lines. * * Wrapped lines will be unwrapped. Comments are stripped. * * @param boolean $force Set this to true if you want to iterate over the lines manually * * @return array */ public function next_lines($force = false) { // if we already have those lines, just return them, otherwise read if (count($this->_lines_next) == 0 || $force) { $this->_lines_next = array(); // empty in case something was left (if used $force) $entry_done = false; $fh = &$this->handle(); $commentmode = false; // if we are in an comment, for wrapping purposes $datalines_read = 0; // how many lines with data we have read while (!$entry_done && !$this->eof()) { $this->_input_line++; // Read line. Remove line endings, we want only data; // this is okay since ending spaces should be encoded $data = rtrim(fgets($fh)); if ($data === false) { // error only, if EOF not reached after fgets() call if (!$this->eof()) { $this->dropError('Net_LDAP2_LDIF error: error reading from file at input line '.$this->_input_line, $this->_input_line); } break; } else { if (count($this->_lines_next) > 0 && preg_match('/^$/', $data)) { // Entry is finished if we have an empty line after we had data $entry_done = true; // Look ahead if the next EOF is nearby. Comments and empty // lines at the file end may cause problems otherwise $current_pos = ftell($fh); $data = fgets($fh); while (!feof($fh)) { if (preg_match('/^\s*$/', $data) || preg_match('/^#/', $data)) { // only empty lines or comments, continue to seek // TODO: Known bug: Wrappings for comments are okay but are treaten as // error, since we do not honor comment mode here. // This should be a very theoretically case, however // i am willing to fix this if really necessary. $this->_input_line++; $current_pos = ftell($fh); $data = fgets($fh); } else { // Data found if non emtpy line and not a comment!! // Rewind to position prior last read and stop lookahead fseek($fh, $current_pos); break; } } // now we have either the file pointer at the beginning of // a new data position or at the end of file causing feof() to return true } else { // build lines if (preg_match('/^version:\s(.+)$/', $data, $match)) { // version statement, set version $this->version($match[1]); } elseif (preg_match('/^\w+(;binary)?::?\s.+$/', $data)) { // normal attribute: add line $commentmode = false; $this->_lines_next[] = trim($data); $datalines_read++; } elseif (preg_match('/^\s(.+)$/', $data, $matches)) { // wrapped data: unwrap if not in comment mode // note that the \s above is some more liberal than // the RFC requests as it also matches tabs etc. if (!$commentmode) { if ($datalines_read == 0) { // first line of entry: wrapped data is illegal $this->dropError('Net_LDAP2_LDIF error: illegal wrapping at input line '.$this->_input_line, $this->_input_line); } else { $last = array_pop($this->_lines_next); $last = $last.$matches[1]; $this->_lines_next[] = $last; $datalines_read++; } } } elseif (preg_match('/^#/', $data)) { // LDIF comments $commentmode = true; } elseif (preg_match('/^\s*$/', $data)) { // empty line but we had no data for this // entry, so just ignore this line $commentmode = false; } else { $this->dropError('Net_LDAP2_LDIF error: invalid syntax at input line '.$this->_input_line, $this->_input_line); continue; } } } } } return $this->_lines_next; } /** * Convert an attribute and value to LDIF string representation * * It honors correct encoding of values according to RFC 2849. * Line wrapping will occur at the configured maximum but only if * the value is greater than 40 chars. * * @param string $attr_name Name of the attribute * @param string $attr_value Value of the attribute * * @access protected * @return string LDIF string for that attribute and value */ protected function convertAttribute($attr_name, $attr_value) { // Handle empty attribute or process if (strlen($attr_value) == 0) { $attr_value = " "; } else { $base64 = false; // ASCII-chars that are NOT safe for the // start and for being inside the value. // These are the int values of those chars. $unsafe_init = array(0, 10, 13, 32, 58, 60); $unsafe = array(0, 10, 13); // Test for illegal init char $init_ord = ord(substr($attr_value, 0, 1)); if ($init_ord > 127 || in_array($init_ord, $unsafe_init)) { $base64 = true; } // Test for illegal content char for ($i = 0; $i < strlen($attr_value); $i++) { $char_ord = ord(substr($attr_value, $i, 1)); if ($char_ord > 127 || in_array($char_ord, $unsafe)) { $base64 = true; } } // Test for ending space if (substr($attr_value, -1) == ' ') { $base64 = true; } // If converting is needed, do it // Either we have some special chars or a matching "raw" regex if ($base64 || ($this->_options['raw'] && preg_match($this->_options['raw'], $attr_name))) { $attr_name .= ':'; $attr_value = base64_encode($attr_value); } // Lowercase attr names if requested if ($this->_options['lowercase']) $attr_name = strtolower($attr_name); // Handle line wrapping if ($this->_options['wrap'] > 40 && strlen($attr_value) > $this->_options['wrap']) { $attr_value = wordwrap($attr_value, $this->_options['wrap'], PHP_EOL." ", true); } } return $attr_name.': '.$attr_value; } /** * Convert an entries DN to LDIF string representation * * It honors correct encoding of values according to RFC 2849. * * @param string $dn UTF8-Encoded DN * * @access protected * @return string LDIF string for that DN * @todo I am not sure, if the UTF8 stuff is correctly handled right now */ protected function convertDN($dn) { $base64 = false; // ASCII-chars that are NOT safe for the // start and for being inside the dn. // These are the int values of those chars. $unsafe_init = array(0, 10, 13, 32, 58, 60); $unsafe = array(0, 10, 13); // Test for illegal init char $init_ord = ord(substr($dn, 0, 1)); if ($init_ord >= 127 || in_array($init_ord, $unsafe_init)) { $base64 = true; } // Test for illegal content char for ($i = 0; $i < strlen($dn); $i++) { $char = substr($dn, $i, 1); if (ord($char) >= 127 || in_array($init_ord, $unsafe)) { $base64 = true; } } // Test for ending space if (substr($dn, -1) == ' ') { $base64 = true; } // if converting is needed, do it return ($base64)? 'dn:: '.base64_encode($dn) : 'dn: '.$dn; } /** * Writes an attribute to the filehandle * * @param string $attr_name Name of the attribute * @param string|array $attr_values Single attribute value or array with attribute values * * @access protected * @return void */ protected function writeAttribute($attr_name, $attr_values) { // write out attribute content if (!is_array($attr_values)) { $attr_values = array($attr_values); } foreach ($attr_values as $attr_val) { $line = $this->convertAttribute($attr_name, $attr_val).PHP_EOL; $this->writeLine($line, 'Net_LDAP2_LDIF error: unable to write attribute '.$attr_name.' of entry '.$this->_entrynum); } } /** * Writes a DN to the filehandle * * @param string $dn DN to write * * @access protected * @return void */ protected function writeDN($dn) { // prepare DN if ($this->_options['encode'] == 'base64') { $dn = $this->convertDN($dn).PHP_EOL; } elseif ($this->_options['encode'] == 'canonical') { $dn = Net_LDAP2_Util::canonical_dn($dn, array('casefold' => 'none')).PHP_EOL; } else { $dn = $dn.PHP_EOL; } $this->writeLine($dn, 'Net_LDAP2_LDIF error: unable to write DN of entry '.$this->_entrynum); } /** * Finishes an LDIF entry * * @access protected * @return void */ protected function finishEntry() { $this->writeLine(PHP_EOL, 'Net_LDAP2_LDIF error: unable to close entry '.$this->_entrynum); } /** * Just write an arbitary line to the filehandle * * @param string $line Content to write * @param string $error If error occurs, drop this message * * @access protected * @return true|false */ protected function writeLine($line, $error = 'Net_LDAP2_LDIF error: unable to write to filehandle') { if (is_resource($this->handle()) && fwrite($this->handle(), $line, strlen($line)) === false) { $this->dropError($error); return false; } else { return true; } } /** * Optionally raises an error and pushes the error on the error cache * * @param string $msg Errortext * @param int $line Line in the LDIF that caused the error * * @access protected * @return void */ protected function dropError($msg, $line = null) { $this->_error['error'] = new Net_LDAP2_Error($msg); if ($line !== null) $this->_error['line'] = $line; if ($this->_options['onerror'] == 'die') { die($msg.PHP_EOL); } elseif ($this->_options['onerror'] == 'warn') { echo $msg.PHP_EOL; } } } ?> * @copyright 2009 Benedikt Hallinger * @license http://www.gnu.org/licenses/lgpl-3.0.txt LGPLv3 * @version SVN: $Id$ * @link http://pear.php.net/package/Net_LDAP2/ */ /** * Interface describing a custom schema cache object * * To implement a custom schema cache, one must implement this interface and * pass the instanciated object to Net_LDAP2s registerSchemaCache() method. */ interface Net_LDAP2_SchemaCache { /** * Return the schema object from the cache * * Net_LDAP2 will consider anything returned invalid, except * a valid Net_LDAP2_Schema object. * In case you return a Net_LDAP2_Error, this error will be routed * to the return of the $ldap->schema() call. * If you return something else, Net_LDAP2 will * fetch a fresh Schema object from the LDAP server. * * You may want to implement a cache aging mechanism here too. * * @return Net_LDAP2_Schema|Net_LDAP2_Error|false */ public function loadSchema(); /** * Store a schema object in the cache * * This method will be called, if Net_LDAP2 has fetched a fresh * schema object from LDAP and wants to init or refresh the cache. * * In case of errors you may return a Net_LDAP2_Error which will * be routet to the client. * Note that doing this prevents, that the schema object fetched from LDAP * will be given back to the client, so only return errors if storing * of the cache is something crucial (e.g. for doing something else with it). * Normaly you dont want to give back errors in which case Net_LDAP2 needs to * fetch the schema once per script run and instead use the error * returned from loadSchema(). * * @return true|Net_LDAP2_Error */ public function storeSchema($schema); } * @author Benedikt Hallinger * @copyright 2009 Tarjej Huse, Benedikt Hallinger * @license http://www.gnu.org/licenses/lgpl-3.0.txt LGPLv3 * @version SVN: $Id$ * @link http://pear.php.net/package/Net_LDAP2/ */ /** * Includes */ require_once 'PEAR.php'; /** * Result set of an LDAP search * * @category Net * @package Net_LDAP2 * @author Tarjej Huse * @author Benedikt Hallinger * @license http://www.gnu.org/copyleft/lesser.html LGPL * @link http://pear.php.net/package/Net_LDAP22/ */ class Net_LDAP2_Search extends PEAR implements Iterator { /** * Search result identifier * * @access protected * @var resource */ protected $_search; /** * LDAP resource link * * @access protected * @var resource */ protected $_link; /** * Net_LDAP2 object * * A reference of the Net_LDAP2 object for passing to Net_LDAP2_Entry * * @access protected * @var object Net_LDAP2 */ protected $_ldap; /** * Result entry identifier * * @access protected * @var resource */ protected $_entry = null; /** * The errorcode the search got * * Some errorcodes might be of interest, but might not be best handled as errors. * examples: 4 - LDAP_SIZELIMIT_EXCEEDED - indicates a huge search. * Incomplete results are returned. If you just want to check if there's anything in the search. * than this is a point to handle. * 32 - no such object - search here returns a count of 0. * * @access protected * @var int */ protected $_errorCode = 0; // if not set - sucess! /** * Cache for all entries already fetched from iterator interface * * @access protected * @var array */ protected $_iteratorCache = array(); /** * What attributes we searched for * * The $attributes array contains the names of the searched attributes and gets * passed from $Net_LDAP2->search() so the Net_LDAP2_Search object can tell * what attributes was searched for ({@link searchedAttrs()) * * This variable gets set from the constructor and returned * from {@link searchedAttrs()} * * @access protected * @var array */ protected $_searchedAttrs = array(); /** * Cache variable for storing entries fetched internally * * This currently is not used by all functions and need consolidation. * * @access protected * @var array */ protected $_entry_cache = false; /** * Cache variable for count() * * @see count() * @access protected * @var int */ protected $_count_cache = null; /** * Constructor * * @param resource $search Search result identifier * @param Net_LDAP2|resource $ldap Net_LDAP2 object or just a LDAP-Link resource * @param array $attributes (optional) Array with searched attribute names. (see {@link $_searchedAttrs}) * * @access public */ public function __construct($search, $ldap, $attributes = array()) { parent::__construct('Net_LDAP2_Error'); $this->setSearch($search); if ($ldap instanceof Net_LDAP2) { $this->_ldap = $ldap; $this->setLink($this->_ldap->getLink()); } else { $this->setLink($ldap); } $this->_errorCode = @ldap_errno($this->_link); if (is_array($attributes) && !empty($attributes)) { $this->_searchedAttrs = $attributes; } } /** * Returns an array of entry objects. * * @return array Array of entry objects. */ public function entries() { $entries = array(); if (false === $this->_entry_cache) { // cache is empty: fetch from LDAP while ($entry = $this->shiftEntry()) { $entries[] = $entry; } $this->_entry_cache = $entries; // store result in cache } return $this->_entry_cache; } /** * Get the next entry in the searchresult from LDAP server. * * This will return a valid Net_LDAP2_Entry object or false, so * you can use this method to easily iterate over the entries inside * a while loop. * * @return Net_LDAP2_Entry|false Reference to Net_LDAP2_Entry object or false */ public function shiftEntry() { if (is_null($this->_entry)) { if(!$this->_entry = @ldap_first_entry($this->_link, $this->_search)) { $false = false; return $false; } $entry = Net_LDAP2_Entry::createConnected($this->_ldap, $this->_entry); if ($entry instanceof PEAR_Error) $entry = false; } else { if (!$this->_entry = @ldap_next_entry($this->_link, $this->_entry)) { $false = false; return $false; } $entry = Net_LDAP2_Entry::createConnected($this->_ldap, $this->_entry); if ($entry instanceof PEAR_Error) $entry = false; } return $entry; } /** * Alias function of shiftEntry() for perl-ldap interface * * @see shiftEntry() * @return Net_LDAP2_Entry|false */ public function shift_entry() { $args = func_get_args(); return call_user_func_array(array( $this, 'shiftEntry' ), $args); } /** * Retrieve the next entry in the searchresult, but starting from last entry * * This is the opposite to {@link shiftEntry()} and is also very useful * to be used inside a while loop. * * @return Net_LDAP2_Entry|false */ public function popEntry() { if (false === $this->_entry_cache) { // fetch entries into cache if not done so far $this->_entry_cache = $this->entries(); } $return = array_pop($this->_entry_cache); return (null === $return)? false : $return; } /** * Alias function of popEntry() for perl-ldap interface * * @see popEntry() * @return Net_LDAP2_Entry|false */ public function pop_entry() { $args = func_get_args(); return call_user_func_array(array( $this, 'popEntry' ), $args); } /** * Return entries sorted as array * * This returns a array with sorted entries and the values. * Sorting is done with PHPs {@link array_multisort()}. * This method relies on {@link as_struct()} to fetch the raw data of the entries. * * Please note that attribute names are case sensitive! * * Usage example: * * // to sort entries first by location, then by surename, but descending: * $entries = $search->sorted_as_struct(array('locality','sn'), SORT_DESC); * * * @param array $attrs Array of attribute names to sort; order from left to right. * @param int $order Ordering direction, either constant SORT_ASC or SORT_DESC * * @return array|Net_LDAP2_Error Array with sorted entries or error * @todo what about server side sorting as specified in http://www.ietf.org/rfc/rfc2891.txt? */ public function sorted_as_struct($attrs = array('cn'), $order = SORT_ASC) { /* * Old Code, suitable and fast for single valued sorting * This code should be used if we know that single valued sorting is desired, * but we need some method to get that knowledge... */ /* $attrs = array_reverse($attrs); foreach ($attrs as $attribute) { if (!ldap_sort($this->_link, $this->_search, $attribute)){ $this->raiseError("Sorting failed for Attribute " . $attribute); } } $results = ldap_get_entries($this->_link, $this->_search); unset($results['count']); //for tidier output if ($order) { return array_reverse($results); } else { return $results; }*/ /* * New code: complete "client side" sorting */ // first some parameterchecks if (!is_array($attrs)) { return PEAR::raiseError("Sorting failed: Parameterlist must be an array!"); } if ($order != SORT_ASC && $order != SORT_DESC) { return PEAR::raiseError("Sorting failed: sorting direction not understood! (neither constant SORT_ASC nor SORT_DESC)"); } // fetch the entries data $entries = $this->as_struct(); // now sort each entries attribute values // this is neccessary because later we can only sort by one value, // so we need the highest or lowest attribute now, depending on the // selected ordering for that specific attribute foreach ($entries as $dn => $entry) { foreach ($entry as $attr_name => $attr_values) { sort($entries[$dn][$attr_name]); if ($order == SORT_DESC) { array_reverse($entries[$dn][$attr_name]); } } } // reformat entrys array for later use with array_multisort() $to_sort = array(); // <- will be a numeric array similar to ldap_get_entries foreach ($entries as $dn => $entry_attr) { $row = array(); $row['dn'] = $dn; foreach ($entry_attr as $attr_name => $attr_values) { $row[$attr_name] = $attr_values; } $to_sort[] = $row; } // Build columns for array_multisort() // each requested attribute is one row $columns = array(); foreach ($attrs as $attr_name) { foreach ($to_sort as $key => $row) { $columns[$attr_name][$key] =& $to_sort[$key][$attr_name][0]; } } // sort the colums with array_multisort, if there is something // to sort and if we have requested sort columns if (!empty($to_sort) && !empty($columns)) { $sort_params = ''; foreach ($attrs as $attr_name) { $sort_params .= '$columns[\''.$attr_name.'\'], '.$order.', '; } eval("array_multisort($sort_params \$to_sort);"); // perform sorting } return $to_sort; } /** * Return entries sorted as objects * * This returns a array with sorted Net_LDAP2_Entry objects. * The sorting is actually done with {@link sorted_as_struct()}. * * Please note that attribute names are case sensitive! * Also note, that it is (depending on server capabilitys) possible to let * the server sort your results. This happens through search controls * and is described in detail at {@link http://www.ietf.org/rfc/rfc2891.txt} * * Usage example: * * // to sort entries first by location, then by surename, but descending: * $entries = $search->sorted(array('locality','sn'), SORT_DESC); * * * @param array $attrs Array of sort attributes to sort; order from left to right. * @param int $order Ordering direction, either constant SORT_ASC or SORT_DESC * * @return array|Net_LDAP2_Error Array with sorted Net_LDAP2_Entries or error * @todo Entry object construction could be faster. Maybe we could use one of the factorys instead of fetching the entry again */ public function sorted($attrs = array('cn'), $order = SORT_ASC) { $return = array(); $sorted = $this->sorted_as_struct($attrs, $order); if (PEAR::isError($sorted)) { return $sorted; } foreach ($sorted as $key => $row) { $entry = $this->_ldap->getEntry($row['dn'], $this->searchedAttrs()); if (!PEAR::isError($entry)) { array_push($return, $entry); } else { return $entry; } } return $return; } /** * Return entries as array * * This method returns the entries and the selected attributes values as * array. * The first array level contains all found entries where the keys are the * DNs of the entries. The second level arrays contian the entries attributes * such that the keys is the lowercased name of the attribute and the values * are stored in another indexed array. Note that the attribute values are stored * in an array even if there is no or just one value. * * The array has the following structure: * * $return = array( * 'cn=foo,dc=example,dc=com' => array( * 'sn' => array('foo'), * 'multival' => array('val1', 'val2', 'valN') * ) * 'cn=bar,dc=example,dc=com' => array( * 'sn' => array('bar'), * 'multival' => array('val1', 'valN') * ) * ) * * * @return array associative result array as described above */ public function as_struct() { $return = array(); $entries = $this->entries(); foreach ($entries as $entry) { $attrs = array(); $entry_attributes = $entry->attributes(); foreach ($entry_attributes as $attr_name) { $attr_values = $entry->getValue($attr_name, 'all'); if (!is_array($attr_values)) { $attr_values = array($attr_values); } $attrs[$attr_name] = $attr_values; } $return[$entry->dn()] = $attrs; } return $return; } /** * Set the search objects resource link * * @param resource $search Search result identifier * * @access public * @return void */ public function setSearch($search) { $this->_search = $search; } /** * Set the ldap ressource link * * @param resource $link Link identifier * * @access public * @return void */ public function setLink($link) { $this->_link = $link; } /** * Returns the number of entries in the searchresult * * @return int Number of entries in search. */ public function count() { // this catches the situation where OL returned errno 32 = no such object! if (!$this->_search) { return 0; } // ldap_count_entries is slow (see pear bug #18752) with large results, // so we cache the result internally. if ($this->_count_cache === null) { $this->_count_cache = @ldap_count_entries($this->_link, $this->_search); } return $this->_count_cache; } /** * Get the errorcode the object got in its search. * * @return int The ldap error number. */ public function getErrorCode() { return $this->_errorCode; } /** * Destructor * * @access protected */ public function _Net_LDAP2_Search() { @ldap_free_result($this->_search); } /** * Closes search result * * @return void */ public function done() { $this->_Net_LDAP2_Search(); } /** * Return the attribute names this search selected * * @return array * @see $_searchedAttrs * @access protected */ protected function searchedAttrs() { return $this->_searchedAttrs; } /** * Tells if this search exceeds a sizelimit * * @return boolean */ public function sizeLimitExceeded() { return ($this->getErrorCode() == 4); } /* * SPL Iterator interface methods. * This interface allows to use Net_LDAP2_Search * objects directly inside a foreach loop! */ /** * SPL Iterator interface: Return the current element. * * The SPL Iterator interface allows you to fetch entries inside * a foreach() loop: foreach ($search as $dn => $entry) { ... * * Of course, you may call {@link current()}, {@link key()}, {@link next()}, * {@link rewind()} and {@link valid()} yourself. * * If the search throwed an error, it returns false. * False is also returned, if the end is reached * In case no call to next() was made, we will issue one, * thus returning the first entry. * * @return Net_LDAP2_Entry|false */ public function current() { if (count($this->_iteratorCache) == 0) { $this->next(); reset($this->_iteratorCache); } $entry = current($this->_iteratorCache); return ($entry instanceof Net_LDAP2_Entry)? $entry : false; } /** * SPL Iterator interface: Return the identifying key (DN) of the current entry. * * @see current() * @return string|false DN of the current entry; false in case no entry is returned by current() */ public function key() { $entry = $this->current(); return ($entry instanceof Net_LDAP2_Entry)? $entry->dn() :false; } /** * SPL Iterator interface: Move forward to next entry. * * After a call to {@link next()}, {@link current()} will return * the next entry in the result set. * * @see current() * @return void */ public function next() { // fetch next entry. // if we have no entrys anymore, we add false (which is // returned by shiftEntry()) so current() will complain. if (count($this->_iteratorCache) - 1 <= $this->count()) { $this->_iteratorCache[] = $this->shiftEntry(); } // move on array pointer to current element. // even if we have added all entries, this will // ensure proper operation in case we rewind() next($this->_iteratorCache); } /** * SPL Iterator interface: Check if there is a current element after calls to {@link rewind()} or {@link next()}. * * Used to check if we've iterated to the end of the collection. * * @see current() * @return boolean FALSE if there's nothing more to iterate over */ public function valid() { return ($this->current() instanceof Net_LDAP2_Entry); } /** * SPL Iterator interface: Rewind the Iterator to the first element. * * After rewinding, {@link current()} will return the first entry in the result set. * * @see current() * @return void */ public function rewind() { reset($this->_iteratorCache); } } ?> * @author Tarjej Huse * @author Benedikt Hallinger * @copyright 2009 Tarjej Huse, Jan Wagner, Benedikt Hallinger * @license http://www.gnu.org/licenses/lgpl-3.0.txt LGPLv3 * @version SVN: $Id$ * @link http://pear.php.net/package/Net_LDAP2/ */ /** * Includes */ require_once 'PEAR.php'; require_once 'Net/LDAP2/Util.php'; /** * Object representation of a directory entry * * This class represents a directory entry. You can add, delete, replace * attributes and their values, rename the entry, delete the entry. * * @category Net * @package Net_LDAP2 * @author Jan Wagner * @author Tarjej Huse * @author Benedikt Hallinger * @license http://www.gnu.org/copyleft/lesser.html LGPL * @link http://pear.php.net/package/Net_LDAP2/ */ class Net_LDAP2_Entry extends PEAR { /** * Entry ressource identifier * * @access protected * @var ressource */ protected $_entry = null; /** * LDAP ressource identifier * * @access protected * @var ressource */ protected $_link = null; /** * Net_LDAP2 object * * This object will be used for updating and schema checking * * @access protected * @var object Net_LDAP2 */ protected $_ldap = null; /** * Distinguished name of the entry * * @access protected * @var string */ protected $_dn = null; /** * Attributes * * @access protected * @var array */ protected $_attributes = array(); /** * Original attributes before any modification * * @access protected * @var array */ protected $_original = array(); /** * Map of attribute names * * @access protected * @var array */ protected $_map = array(); /** * Is this a new entry? * * @access protected * @var boolean */ protected $_new = true; /** * New distinguished name * * @access protected * @var string */ protected $_newdn = null; /** * Shall the entry be deleted? * * @access protected * @var boolean */ protected $_delete = false; /** * Map with changes to the entry * * @access protected * @var array */ protected $_changes = array("add" => array(), "delete" => array(), "replace" => array() ); /** * Internal Constructor * * Constructor of the entry. Sets up the distinguished name and the entries * attributes. * You should not call this method manually! Use {@link Net_LDAP2_Entry::createFresh()} * or {@link Net_LDAP2_Entry::createConnected()} instead! * * @param Net_LDAP2|ressource|array $ldap Net_LDAP2 object, ldap-link ressource or array of attributes * @param string|ressource $entry Either a DN or a LDAP-Entry ressource * * @access protected * @return none */ public function __construct($ldap, $entry = null) { parent::__construct('Net_LDAP2_Error'); // set up entry resource or DN if (is_resource($entry)) { $this->_entry = $entry; } else { $this->_dn = $entry; } // set up LDAP link if ($ldap instanceof Net_LDAP2) { $this->_ldap = $ldap; $this->_link = $ldap->getLink(); } elseif (is_resource($ldap)) { $this->_link = $ldap; } elseif (is_array($ldap)) { // Special case: here $ldap is an array of attributes, // this means, we have no link. This is a "virtual" entry. // We just set up the attributes so one can work with the object // as expected, but an update() fails unless setLDAP() is called. $this->setAttributes($ldap); } // if this is an entry existing in the directory, // then set up as old and fetch attrs if (is_resource($this->_entry) && is_resource($this->_link)) { $this->_new = false; $this->_dn = @ldap_get_dn($this->_link, $this->_entry); $this->setAttributes(); // fetch attributes from server } } /** * Creates a fresh entry that may be added to the directory later on * * Use this method, if you want to initialize a fresh entry. * * The method should be called statically: $entry = Net_LDAP2_Entry::createFresh(); * You should put a 'objectClass' attribute into the $attrs so the directory server * knows which object you want to create. However, you may omit this in case you * don't want to add this entry to a directory server. * * The attributes parameter is as following: * * $attrs = array( 'attribute1' => array('value1', 'value2'), * 'attribute2' => 'single value' * ); * * * @param string $dn DN of the Entry * @param array $attrs Attributes of the entry * * @static * @return Net_LDAP2_Entry|Net_LDAP2_Error */ public static function createFresh($dn, $attrs = array()) { if (!is_array($attrs)) { return PEAR::raiseError("Unable to create fresh entry: Parameter \$attrs needs to be an array!"); } $entry = new Net_LDAP2_Entry($attrs, $dn); return $entry; } /** * Creates a Net_LDAP2_Entry object out of an ldap entry resource * * Use this method, if you want to initialize an entry object that is * already present in some directory and that you have read manually. * * Please note, that if you want to create an entry object that represents * some already existing entry, you should use {@link createExisting()}. * * The method should be called statically: $entry = Net_LDAP2_Entry::createConnected(); * * @param Net_LDAP2 $ldap Net_LDA2 object * @param resource $entry PHP LDAP entry resource * * @static * @return Net_LDAP2_Entry|Net_LDAP2_Error */ public static function createConnected($ldap, $entry) { if (!$ldap instanceof Net_LDAP2) { return PEAR::raiseError("Unable to create connected entry: Parameter \$ldap needs to be a Net_LDAP2 object!"); } if (!is_resource($entry)) { return PEAR::raiseError("Unable to create connected entry: Parameter \$entry needs to be a ldap entry resource!"); } $entry = new Net_LDAP2_Entry($ldap, $entry); return $entry; } /** * Creates an Net_LDAP2_Entry object that is considered already existing * * Use this method, if you want to modify an already existing entry * without fetching it first. * In most cases however, it is better to fetch the entry via Net_LDAP2->getEntry()! * * Please note that you should take care if you construct entries manually with this * because you may get weird synchronisation problems. * The attributes and values as well as the entry itself are considered existent * which may produce errors if you try to modify an entry which doesn't really exist * or if you try to overwrite some attribute with an value already present. * * This method is equal to calling createFresh() and after that markAsNew(FALSE). * * The method should be called statically: $entry = Net_LDAP2_Entry::createExisting(); * * The attributes parameter is as following: * * $attrs = array( 'attribute1' => array('value1', 'value2'), * 'attribute2' => 'single value' * ); * * * @param string $dn DN of the Entry * @param array $attrs Attributes of the entry * * @static * @return Net_LDAP2_Entry|Net_LDAP2_Error */ public static function createExisting($dn, $attrs = array()) { if (!is_array($attrs)) { return PEAR::raiseError("Unable to create entry object: Parameter \$attrs needs to be an array!"); } $entry = Net_LDAP2_Entry::createFresh($dn, $attrs); if ($entry instanceof Net_LDAP2_Error) { return $entry; } else { $entry->markAsNew(false); return $entry; } } /** * Get or set the distinguished name of the entry * * If called without an argument the current (or the new DN if set) DN gets returned. * If you provide an DN, this entry is moved to the new location specified if a DN existed. * If the DN was not set, the DN gets initialized. Call {@link update()} to actually create * the new Entry in the directory. * To fetch the current active DN after setting a new DN but before an update(), you can use * {@link currentDN()} to retrieve the DN that is currently active. * * Please note that special characters (eg german umlauts) should be encoded using utf8_encode(). * You may use {@link Net_LDAP2_Util::canonical_dn()} for properly encoding of the DN. * * @param string $dn New distinguished name * * @access public * @return string|true Distinguished name (or true if a new DN was provided) */ public function dn($dn = null) { if (false == is_null($dn)) { if (is_null($this->_dn) ) { $this->_dn = $dn; } else { $this->_newdn = $dn; } return true; } return (isset($this->_newdn) ? $this->_newdn : $this->currentDN()); } /** * Renames or moves the entry * * This is just a convinience alias to {@link dn()} * to make your code more meaningful. * * @param string $newdn The new DN * * @return true */ public function move($newdn) { return $this->dn($newdn); } /** * Sets the internal attributes array * * This fetches the values for the attributes from the server. * The attribute Syntax will be checked so binary attributes will be returned * as binary values. * * Attributes may be passed directly via the $attributes parameter to setup this * entry manually. This overrides attribute fetching from the server. * * @param array $attributes Attributes to set for this entry * * @access protected * @return void */ protected function setAttributes($attributes = null) { /* * fetch attributes from the server */ if (is_null($attributes) && is_resource($this->_entry) && is_resource($this->_link)) { // fetch schema if ($this->_ldap instanceof Net_LDAP2) { $schema = $this->_ldap->schema(); } // fetch attributes $attributes = array(); do { if (empty($attr)) { $ber = null; $attr = @ldap_first_attribute($this->_link, $this->_entry, $ber); } else { $attr = @ldap_next_attribute($this->_link, $this->_entry, $ber); } if ($attr) { $func = 'ldap_get_values'; // standard function to fetch value // Try to get binary values as binary data if ($schema instanceof Net_LDAP2_Schema) { if ($schema->isBinary($attr)) { $func = 'ldap_get_values_len'; } } // fetch attribute value (needs error checking?) $attributes[$attr] = $func($this->_link, $this->_entry, $attr); } } while ($attr); } /* * set attribute data directly, if passed */ if (is_array($attributes) && count($attributes) > 0) { if (isset($attributes["count"]) && is_numeric($attributes["count"])) { unset($attributes["count"]); } foreach ($attributes as $k => $v) { // attribute names should not be numeric if (is_numeric($k)) { continue; } // map generic attribute name to real one $this->_map[strtolower($k)] = $k; // attribute values should be in an array if (false == is_array($v)) { $v = array($v); } // remove the value count (comes from ldap server) if (isset($v["count"])) { unset($v["count"]); } $this->_attributes[$k] = $v; } } // save a copy for later use $this->_original = $this->_attributes; } /** * Get the values of all attributes in a hash * * The returned hash has the form * array('attributename' => 'single value', * 'attributename' => array('value1', value2', value3')) * Only attributes present at the entry will be returned. * * @access public * @return array Hash of all attributes with their values */ public function getValues() { $attrs = array(); foreach ($this->_attributes as $attr => $value) { $attrs[$attr] = $this->getValue($attr); } return $attrs; } /** * Get the value of a specific attribute * * The first parameter is the name of the attribute * The second parameter influences the way the value is returned: * 'single': only the first value is returned as string * 'all': all values are returned in an array * 'default': in all other cases an attribute value with a single value is * returned as string, if it has multiple values it is returned * as an array * * If the attribute is not set at this entry (no value or not defined in * schema), "false" is returned when $option is 'single', an empty string if * 'default', and an empty array when 'all'. * * You may use Net_LDAP2_Schema->checkAttribute() to see if the attribute * is defined for the objectClasses of this entry. * * @param string $attr Attribute name * @param string $option Option * * @access public * @return string|array */ public function getValue($attr, $option = null) { $attr = $this->getAttrName($attr); // return depending on set $options if (!array_key_exists($attr, $this->_attributes)) { // attribute not set switch ($option) { case 'single': $value = false; break; case 'all': $value = array(); break; default: $value = ''; } } else { // attribute present switch ($option) { case 'single': $value = $this->_attributes[$attr][0]; break; case 'all': $value = $this->_attributes[$attr]; break; default: $value = $this->_attributes[$attr]; if (count($value) == 1) { $value = array_shift($value); } } } return $value; } /** * Alias function of getValue for perl-ldap interface * * @see getValue() * @return string|array|PEAR_Error */ public function get_value() { $args = func_get_args(); return call_user_func_array(array( $this, 'getValue' ), $args); } /** * Returns an array of attributes names * * @access public * @return array Array of attribute names */ public function attributes() { return array_keys($this->_attributes); } /** * Returns whether an attribute exists or not * * @param string $attr Attribute name * * @access public * @return boolean */ public function exists($attr) { $attr = $this->getAttrName($attr); return array_key_exists($attr, $this->_attributes); } /** * Adds a new attribute or a new value to an existing attribute * * The paramter has to be an array of the form: * array('attributename' => 'single value', * 'attributename' => array('value1', 'value2)) * When the attribute already exists the values will be added, else the * attribute will be created. These changes are local to the entry and do * not affect the entry on the server until update() is called. * * Note, that you can add values of attributes that you haven't selected, but if * you do so, {@link getValue()} and {@link getValues()} will only return the * values you added, _NOT_ all values present on the server. To avoid this, just refetch * the entry after calling {@link update()} or select the attribute. * * @param array $attr Attributes to add * * @access public * @return true|Net_LDAP2_Error */ public function add($attr = array()) { if (false == is_array($attr)) { return PEAR::raiseError("Parameter must be an array"); } if ($this->isNew()) { $this->setAttributes($attr); } foreach ($attr as $k => $v) { $k = $this->getAttrName($k); if (false == is_array($v)) { // Do not add empty values if ($v == null) { continue; } else { $v = array($v); } } // add new values to existing attribute or add new attribute if ($this->exists($k)) { $this->_attributes[$k] = array_unique(array_merge($this->_attributes[$k], $v)); } else { $this->_map[strtolower($k)] = $k; $this->_attributes[$k] = $v; } // save changes for update() if (!isset($this->_changes["add"][$k])) { $this->_changes["add"][$k] = array(); } $this->_changes["add"][$k] = array_unique(array_merge($this->_changes["add"][$k], $v)); } $return = true; return $return; } /** * Deletes an whole attribute or a value or the whole entry * * The parameter can be one of the following: * * "attributename" - The attribute as a whole will be deleted * array("attributename1", "attributename2) - All given attributes will be * deleted * array("attributename" => "value") - The value will be deleted * array("attributename" => array("value1", "value2") - The given values * will be deleted * If $attr is null or omitted , then the whole Entry will be deleted! * * These changes are local to the entry and do * not affect the entry on the server until {@link update()} is called. * * Please note that you must select the attribute (at $ldap->search() for example) * to be able to delete values of it, Otherwise {@link update()} will silently fail * and remove nothing. * * @param string|array $attr Attributes to delete (NULL or missing to delete whole entry) * * @access public * @return true */ public function delete($attr = null) { if (is_null($attr)) { $this->_delete = true; return true; } if (is_string($attr)) { $attr = array($attr); } // Make the assumption that attribute names cannot be numeric, // therefore this has to be a simple list of attribute names to delete if (is_numeric(key($attr))) { foreach ($attr as $name) { if (is_array($name)) { // someone mixed modes (list mode but specific values given!) $del_attr_name = array_search($name, $attr); $this->delete(array($del_attr_name => $name)); } else { // mark for update() if this attr was not marked before $name = $this->getAttrName($name); if ($this->exists($name)) { $this->_changes["delete"][$name] = null; unset($this->_attributes[$name]); } } } } else { // Here we have a hash with "attributename" => "value to delete" foreach ($attr as $name => $values) { if (is_int($name)) { // someone mixed modes and gave us just an attribute name $this->delete($values); } else { // mark for update() if this attr was not marked before; // this time it must consider the selected values also $name = $this->getAttrName($name); if ($this->exists($name)) { if (false == is_array($values)) { $values = array($values); } // save values to be deleted if (empty($this->_changes["delete"][$name])) { $this->_changes["delete"][$name] = array(); } $this->_changes["delete"][$name] = array_unique(array_merge($this->_changes["delete"][$name], $values)); foreach ($values as $value) { // find the key for the value that should be deleted $key = array_search($value, $this->_attributes[$name]); if (false !== $key) { // delete the value unset($this->_attributes[$name][$key]); } } } } } } $return = true; return $return; } /** * Replaces attributes or its values * * The parameter has to an array of the following form: * array("attributename" => "single value", * "attribute2name" => array("value1", "value2"), * "deleteme1" => null, * "deleteme2" => "") * If the attribute does not yet exist it will be added instead (see also $force). * If the attribue value is null, the attribute will de deleted. * * These changes are local to the entry and do * not affect the entry on the server until {@link update()} is called. * * In some cases you are not allowed to read the attributes value (for * example the ActiveDirectory attribute unicodePwd) but are allowed to * replace the value. In this case replace() would assume that the attribute * is not in the directory yet and tries to add it which will result in an * LDAP_TYPE_OR_VALUE_EXISTS error. * To force replace mode instead of add, you can set $force to true. * * @param array $attr Attributes to replace * @param bool $force Force replacing mode in case we can't read the attr value but are allowed to replace it * * @access public * @return true|Net_LDAP2_Error */ public function replace($attr = array(), $force = false) { if (false == is_array($attr)) { return PEAR::raiseError("Parameter must be an array"); } foreach ($attr as $k => $v) { $k = $this->getAttrName($k); if (false == is_array($v)) { // delete attributes with empty values; treat ints as string if (is_int($v)) { $v = "$v"; } if ($v == null) { $this->delete($k); continue; } else { $v = array($v); } } // existing attributes will get replaced if ($this->exists($k) || $force) { $this->_changes["replace"][$k] = $v; $this->_attributes[$k] = $v; } else { // new ones just get added $this->add(array($k => $v)); } } $return = true; return $return; } /** * Update the entry on the directory server * * This will evaluate all changes made so far and send them * to the directory server. * Please note, that if you make changes to objectclasses wich * have mandatory attributes set, update() will currently fail. * Remove the entry from the server and readd it as new in such cases. * This also will deal with problems with setting structural object classes. * * @param Net_LDAP2 $ldap If passed, a call to setLDAP() is issued prior update, thus switching the LDAP-server. This is for perl-ldap interface compliance * * @access public * @return true|Net_LDAP2_Error * @todo Entry rename with a DN containing special characters needs testing! */ public function update($ldap = null) { if ($ldap) { $msg = $this->setLDAP($ldap); if (Net_LDAP2::isError($msg)) { return PEAR::raiseError('You passed an invalid $ldap variable to update()'); } } // ensure we have a valid LDAP object $ldap = $this->getLDAP(); if (!$ldap instanceof Net_LDAP2) { return PEAR::raiseError("The entries LDAP object is not valid"); } // Get and check link $link = $ldap->getLink(); if (!is_resource($link)) { return PEAR::raiseError("Could not update entry: internal LDAP link is invalid"); } /* * Delete the entry */ if (true === $this->_delete) { return $ldap->delete($this); } /* * New entry */ if (true === $this->_new) { $msg = $ldap->add($this); if (Net_LDAP2::isError($msg)) { return $msg; } $this->_new = false; $this->_changes['add'] = array(); $this->_changes['delete'] = array(); $this->_changes['replace'] = array(); $this->_original = $this->_attributes; // In case the "new" entry was moved after creation, we must // adjust the internal DNs as the entry was already created // with the most current DN. if (false == is_null($this->_newdn)) { $this->_dn = $this->_newdn; $this->_newdn = null; } $return = true; return $return; } /* * Rename/move entry */ if (false == is_null($this->_newdn)) { if ($ldap->getLDAPVersion() !== 3) { return PEAR::raiseError("Renaming/Moving an entry is only supported in LDAPv3"); } // make dn relative to parent (needed for ldap rename) $parent = Net_LDAP2_Util::ldap_explode_dn($this->_newdn, array('casefolding' => 'none', 'reverse' => false, 'onlyvalues' => false)); if (Net_LDAP2::isError($parent)) { return $parent; } $child = array_shift($parent); // maybe the dn consist of a multivalued RDN, we must build the dn in this case // because the $child-RDN is an array! if (is_array($child)) { $child = Net_LDAP2_Util::canonical_dn($child); } $parent = Net_LDAP2_Util::canonical_dn($parent); // rename/move if (false == @ldap_rename($link, $this->_dn, $child, $parent, false)) { return PEAR::raiseError("Entry not renamed: " . @ldap_error($link), @ldap_errno($link)); } // reflect changes to local copy $this->_dn = $this->_newdn; $this->_newdn = null; } /* * Retrieve a entry that has all attributes we need so that the list of changes to build is created accurately */ $fullEntry = $ldap->getEntry( $this->dn() ); if ( Net_LDAP2::isError($fullEntry) ) { return PEAR::raiseError("Could not retrieve a full set of attributes to reconcile changes with"); } $modifications = array(); // ADD foreach ($this->_changes["add"] as $attr => $value) { // if attribute exists, we need to combine old and new values if ($fullEntry->exists($attr)) { $currentValue = $fullEntry->getValue($attr, "all"); $value = array_merge( $currentValue, $value ); } $modifications[$attr] = $value; } // DELETE foreach ($this->_changes["delete"] as $attr => $value) { // In LDAPv3 you need to specify the old values for deleting if (is_null($value) && $ldap->getLDAPVersion() === 3) { $value = $fullEntry->getValue($attr); } if (!is_array($value)) { $value = array($value); } // Find out what is missing from $value and exclude it $currentValue = isset($modifications[$attr]) ? $modifications[$attr] : $fullEntry->getValue($attr, "all"); $modifications[$attr] = array_values( array_diff( $currentValue, $value ) ); } // REPLACE foreach ($this->_changes["replace"] as $attr => $value) { $modifications[$attr] = $value; } // COMMIT if (false === @ldap_modify($link, $this->dn(), $modifications)) { return PEAR::raiseError("Could not modify the entry: " . @ldap_error($link), @ldap_errno($link)); } // all went well, so _original (server) becomes _attributes (local copy), reset _changes too... $this->_changes['add'] = array(); $this->_changes['delete'] = array(); $this->_changes['replace'] = array(); $this->_original = $this->_attributes; $return = true; return $return; } /** * Returns the right attribute name * * @param string $attr Name of attribute * * @access protected * @return string The right name of the attribute */ protected function getAttrName($attr) { $name = strtolower($attr); if (array_key_exists($name, $this->_map)) { $attr = $this->_map[$name]; } return $attr; } /** * Returns a reference to the LDAP-Object of this entry * * @access public * @return Net_LDAP2|Net_LDAP2_Error Reference to the Net_LDAP2 Object (the connection) or Net_LDAP2_Error */ public function getLDAP() { if (!$this->_ldap instanceof Net_LDAP2) { $err = new PEAR_Error('LDAP is not a valid Net_LDAP2 object'); return $err; } else { return $this->_ldap; } } /** * Sets a reference to the LDAP-Object of this entry * * After setting a Net_LDAP2 object, calling update() will use that object for * updating directory contents. Use this to dynamicly switch directorys. * * @param Net_LDAP2 $ldap Net_LDAP2 object that this entry should be connected to * * @access public * @return true|Net_LDAP2_Error */ public function setLDAP($ldap) { if (!$ldap instanceof Net_LDAP2) { return PEAR::raiseError("LDAP is not a valid Net_LDAP2 object"); } else { $this->_ldap = $ldap; return true; } } /** * Marks the entry as new/existing. * * If an Entry is marked as new, it will be added to the directory * when calling {@link update()}. * If the entry is marked as old ($mark = false), then the entry is * assumed to be present in the directory server wich results in * modification when calling {@link update()}. * * @param boolean $mark Value to set, defaults to "true" * * @return void */ public function markAsNew($mark = true) { $this->_new = ($mark)? true : false; } /** * Applies a regular expression onto a single- or multivalued attribute (like preg_match()) * * This method behaves like PHPs preg_match() but with some exceptions. * If you want to retrieve match information, then you MUST pass the * $matches parameter via reference! otherwise you will get no matches. * Since it is possible to have multi valued attributes the $matches * array will have a additionally numerical dimension (one for each value): * * $matches = array( * 0 => array (usual preg_match() returnarray), * 1 => array (usual preg_match() returnarray) * ) * * Please note, that $matches will be initialized to an empty array inside. * * Usage example: * * $result = $entry->preg_match('/089(\d+)/', 'telephoneNumber', $matches); * if ( $result === true ){ * echo "First match: ".$matches[0][1]; // Match of value 1, content of first bracket * } else { * if ( Net_LDAP2::isError($result) ) { * echo "Error: ".$result->getMessage(); * } else { * echo "No match found."; * } * } * * * Please note that it is important to test for an Net_LDAP2_Error, because objects are * evaluating to true by default, thus if an error occured, and you only check using "==" then * you get misleading results. Use the "identical" (===) operator to test for matches to * avoid this as shown above. * * @param string $regex The regular expression * @param string $attr_name The attribute to search in * @param array $matches (optional, PASS BY REFERENCE!) Array to store matches in * * @return boolean|Net_LDAP2_Error TRUE, if we had a match in one of the values, otherwise false. Net_LDAP2_Error in case something went wrong */ public function pregMatch($regex, $attr_name, $matches = array()) { $matches = array(); // fetch attribute values $attr = $this->getValue($attr_name, 'all'); // perform preg_match() on all values $match = false; foreach ($attr as $thisvalue) { $matches_int = array(); if (preg_match($regex, $thisvalue, $matches_int)) { $match = true; array_push($matches, $matches_int); // store matches in reference } } return $match; } /** * Alias of {@link pregMatch()} for compatibility to Net_LDAP 1 * * @see pregMatch() * @return boolean|Net_LDAP2_Error */ public function preg_match() { $args = func_get_args(); return call_user_func_array(array( $this, 'pregMatch' ), $args); } /** * Tells if the entry is consiedered as new (not present in the server) * * Please note, that this doesn't tell you if the entry is present on the server. * Use {@link Net_LDAP2::dnExists()} to see if an entry is already there. * * @return boolean */ public function isNew() { return $this->_new; } /** * Is this entry going to be deleted once update() is called? * * @return boolean */ public function willBeDeleted() { return $this->_delete; } /** * Is this entry going to be moved once update() is called? * * @return boolean */ public function willBeMoved() { return ($this->dn() !== $this->currentDN()); } /** * Returns always the original DN * * If an entry will be moved but {@link update()} was not called, * {@link dn()} will return the new DN. This method however, returns * always the current active DN. * * @return string */ public function currentDN() { return $this->_dn; } /** * Returns the attribute changes to be carried out once update() is called * * @return array */ public function getChanges() { return $this->_changes; } } ?> * @copyright 2009 Benedikt Hallinger * @license http://www.gnu.org/licenses/lgpl-3.0.txt LGPLv3 * @version SVN: $Id$ * @link http://pear.php.net/package/Net_LDAP2/ */ /** * Includes */ require_once 'PEAR.php'; require_once 'Net/LDAP2/Util.php'; require_once 'Net/LDAP2/Entry.php'; /** * Object representation of a part of a LDAP filter. * * This Class is not completely compatible to the PERL interface! * * The purpose of this class is, that users can easily build LDAP filters * without having to worry about right escaping etc. * A Filter is built using several independent filter objects * which are combined afterwards. This object works in two * modes, depending how the object is created. * If the object is created using the {@link create()} method, then this is a leaf-object. * If the object is created using the {@link combine()} method, then this is a container object. * * LDAP filters are defined in RFC-2254 and can be found under * {@link http://www.ietf.org/rfc/rfc2254.txt} * * Here a quick copy&paste example: * * $filter0 = Net_LDAP2_Filter::create('stars', 'equals', '***'); * $filter_not0 = Net_LDAP2_Filter::combine('not', $filter0); * * $filter1 = Net_LDAP2_Filter::create('gn', 'begins', 'bar'); * $filter2 = Net_LDAP2_Filter::create('gn', 'ends', 'baz'); * $filter_comp = Net_LDAP2_Filter::combine('or',array($filter_not0, $filter1, $filter2)); * * echo $filter_comp->asString(); * // This will output: (|(!(stars=\0x5c0x2a\0x5c0x2a\0x5c0x2a))(gn=bar*)(gn=*baz)) * // The stars in $filter0 are treaten as real stars unless you disable escaping. * * * @category Net * @package Net_LDAP2 * @author Benedikt Hallinger * @license http://www.gnu.org/copyleft/lesser.html LGPL * @link http://pear.php.net/package/Net_LDAP2/ */ class Net_LDAP2_Filter extends PEAR { /** * Storage for combination of filters * * This variable holds a array of filter objects * that should be combined by this filter object. * * @access protected * @var array */ protected $_subfilters = array(); /** * Match of this filter * * If this is a leaf filter, then a matching rule is stored, * if it is a container, then it is a logical operator * * @access protected * @var string */ protected $_match; /** * Single filter * * If we operate in leaf filter mode, * then the constructing method stores * the filter representation here * * @acces private * @var string */ protected $_filter; /** * Create a new Net_LDAP2_Filter object and parse $filter. * * This is for PERL Net::LDAP interface. * Construction of Net_LDAP2_Filter objects should happen through either * {@link create()} or {@link combine()} which give you more control. * However, you may use the perl iterface if you already have generated filters. * * @param string $filter LDAP filter string * * @see parse() */ public function __construct($filter = false) { // The optional parameter must remain here, because otherwise create() crashes if (false !== $filter) { $filter_o = self::parse($filter); if (PEAR::isError($filter_o)) { $this->_filter = $filter_o; // assign error, so asString() can report it } else { $this->_filter = $filter_o->asString(); } } } /** * Constructor of a new part of a LDAP filter. * * The following matching rules exists: * - equals: One of the attributes values is exactly $value * Please note that case sensitiviness is depends on the * attributes syntax configured in the server. * - begins: One of the attributes values must begin with $value * - ends: One of the attributes values must end with $value * - contains: One of the attributes values must contain $value * - present | any: The attribute can contain any value but must be existent * - greater: The attributes value is greater than $value * - less: The attributes value is less than $value * - greaterOrEqual: The attributes value is greater or equal than $value * - lessOrEqual: The attributes value is less or equal than $value * - approx: One of the attributes values is similar to $value * * Negation ("not") can be done by prepending the above operators with the * "not" or "!" keyword, see example below. * * If $escape is set to true (default) then $value will be escaped * properly. If it is set to false then $value will be treaten as raw filter value string. * You should escape yourself using {@link Net_LDAP2_Util::escape_filter_value()}! * * Examples: * * // This will find entries that contain an attribute "sn" that ends with "foobar": * $filter = Net_LDAP2_Filter::create('sn', 'ends', 'foobar'); * * // This will find entries that contain an attribute "sn" that has any value set: * $filter = Net_LDAP2_Filter::create('sn', 'any'); * * // This will build a negated equals filter: * $filter = Net_LDAP2_Filter::create('sn', 'not equals', 'foobar'); * * * @param string $attr_name Name of the attribute the filter should apply to * @param string $match Matching rule (equals, begins, ends, contains, greater, less, greaterOrEqual, lessOrEqual, approx, any) * @param string $value (optional) if given, then this is used as a filter * @param boolean $escape Should $value be escaped? (default: yes, see {@link Net_LDAP2_Util::escape_filter_value()} for detailed information) * * @return Net_LDAP2_Filter|Net_LDAP2_Error */ public static function create($attr_name, $match, $value = '', $escape = true) { $leaf_filter = new Net_LDAP2_Filter(); if ($escape) { $array = Net_LDAP2_Util::escape_filter_value(array($value)); $value = $array[0]; } $match = strtolower($match); // detect negation $neg_matches = array(); $negate_filter = false; if (preg_match('/^(?:not|!)[\s_-](.+)/', $match, $neg_matches)) { $negate_filter = true; $match = $neg_matches[1]; } // build basic filter switch ($match) { case 'equals': case '=': case '==': $leaf_filter->_filter = '(' . $attr_name . '=' . $value . ')'; break; case 'begins': $leaf_filter->_filter = '(' . $attr_name . '=' . $value . '*)'; break; case 'ends': $leaf_filter->_filter = '(' . $attr_name . '=*' . $value . ')'; break; case 'contains': $leaf_filter->_filter = '(' . $attr_name . '=*' . $value . '*)'; break; case 'greater': case '>': $leaf_filter->_filter = '(' . $attr_name . '>' . $value . ')'; break; case 'less': case '<': $leaf_filter->_filter = '(' . $attr_name . '<' . $value . ')'; break; case 'greaterorequal': case '>=': $leaf_filter->_filter = '(' . $attr_name . '>=' . $value . ')'; break; case 'lessorequal': case '<=': $leaf_filter->_filter = '(' . $attr_name . '<=' . $value . ')'; break; case 'approx': case '~=': $leaf_filter->_filter = '(' . $attr_name . '~=' . $value . ')'; break; case 'any': case 'present': // alias that may improve user code readability $leaf_filter->_filter = '(' . $attr_name . '=*)'; break; default: return PEAR::raiseError('Net_LDAP2_Filter create error: matching rule "' . $match . '" not known!'); } // negate if requested if ($negate_filter) { $leaf_filter = Net_LDAP2_Filter::combine('!', $leaf_filter); } return $leaf_filter; } /** * Combine two or more filter objects using a logical operator * * This static method combines two or more filter objects and returns one single * filter object that contains all the others. * Call this method statically: $filter = Net_LDAP2_Filter::combine('or', array($filter1, $filter2)) * If the array contains filter strings instead of filter objects, we will try to parse them. * * @param string $log_op The locical operator. May be "and", "or", "not" or the subsequent logical equivalents "&", "|", "!" * @param array|Net_LDAP2_Filter $filters array with Net_LDAP2_Filter objects * * @return Net_LDAP2_Filter|Net_LDAP2_Error * @static */ public static function &combine($log_op, $filters) { if (PEAR::isError($filters)) { return $filters; } // substitude named operators to logical operators if ($log_op == 'and') $log_op = '&'; if ($log_op == 'or') $log_op = '|'; if ($log_op == 'not') $log_op = '!'; // tests for sane operation if ($log_op == '!') { // Not-combination, here we only accept one filter object or filter string if ($filters instanceof Net_LDAP2_Filter) { $filters = array($filters); // force array } elseif (is_string($filters)) { $filter_o = self::parse($filters); if (PEAR::isError($filter_o)) { $err = PEAR::raiseError('Net_LDAP2_Filter combine error: '.$filter_o->getMessage()); return $err; } else { $filters = array($filter_o); } } elseif (is_array($filters)) { if (count($filters) != 1) { $err = PEAR::raiseError('Net_LDAP2_Filter combine error: operator is "not" but $filter is an array!'); return $err; } elseif (!($filters[0] instanceof Net_LDAP2_Filter)) { $err = PEAR::raiseError('Net_LDAP2_Filter combine error: operator is "not" but $filter is not a valid Net_LDAP2_Filter nor a filter string!'); return $err; } } else { $err = PEAR::raiseError('Net_LDAP2_Filter combine error: operator is "not" but $filter is not a valid Net_LDAP2_Filter nor a filter string!'); return $err; } } elseif ($log_op == '&' || $log_op == '|') { if (!is_array($filters) || count($filters) < 2) { $err = PEAR::raiseError('Net_LDAP2_Filter combine error: parameter $filters is not an array or contains less than two Net_LDAP2_Filter objects!'); return $err; } } else { $err = PEAR::raiseError('Net_LDAP2_Filter combine error: logical operator is not known!'); return $err; } $combined_filter = new Net_LDAP2_Filter(); foreach ($filters as $key => $testfilter) { // check for errors if (PEAR::isError($testfilter)) { return $testfilter; } elseif (is_string($testfilter)) { // string found, try to parse into an filter object $filter_o = self::parse($testfilter); if (PEAR::isError($filter_o)) { return $filter_o; } else { $filters[$key] = $filter_o; } } elseif (!$testfilter instanceof Net_LDAP2_Filter) { $err = PEAR::raiseError('Net_LDAP2_Filter combine error: invalid object passed in array $filters!'); return $err; } } $combined_filter->_subfilters = $filters; $combined_filter->_match = $log_op; return $combined_filter; } /** * Parse FILTER into a Net_LDAP2_Filter object * * This parses an filter string into Net_LDAP2_Filter objects. * * @param string $FILTER The filter string * * @access static * @return Net_LDAP2_Filter|Net_LDAP2_Error * @todo Leaf-mode: Do we need to escape at all? what about *-chars?check for the need of encoding values, tackle problems (see code comments) */ public static function parse($FILTER) { if (preg_match('/^\((.+?)\)$/', $FILTER, $matches)) { // 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('/(?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) { // only one subfilter found } elseif (count($subfilters) > 1) { // several subfilters found if ($log_op == "!") { return PEAR::raiseError("Filter parsing error: invalid filter syntax - NOT operator detected but several arguments given!"); } } else { // this should not happen unless the user specified a wrong filter return PEAR::raiseError("Filter parsing error: invalid filter syntax - got operator '$log_op' but no argument!"); } // Now parse the subfilters into objects and combine them using the operator $subfilters_o = array(); foreach ($subfilters as $s_s) { $o = self::parse($s_s); if (PEAR::isError($o)) { return $o; } else { array_push($subfilters_o, self::parse($s_s)); } } $filter_o = self::combine($log_op, $subfilters_o); return $filter_o; } else { // 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], ')(')) { return PEAR::raiseError("Filter parsing error: invalid filter syntax - multiple leaf components detected!"); } else { $filter_parts = Net_LDAP2_Util::split_attribute_string($matches[1], true, true); if (count($filter_parts) != 3) { return PEAR::raiseError("Filter parsing error: invalid filter syntax - unknown matching rule used"); } else { $filter_o = new Net_LDAP2_Filter(); // [TODO]: Do we need to escape at all? what about *-chars user provide and that should remain special? // I think, those prevent escaping! We need to check against PERL Net::LDAP! // $value_arr = Net_LDAP2_Util::escape_filter_value(array($filter_parts[2])); // $value = $value_arr[0]; $value = $filter_parts[2]; $filter_o->_filter = '('.$filter_parts[0].$filter_parts[1].$value.')'; return $filter_o; } } } } else { // ERROR: Filter components must be enclosed in round brackets return PEAR::raiseError("Filter parsing error: invalid filter syntax - filter components must be enclosed in round brackets"); } } /** * Get the string representation of this filter * * This method runs through all filter objects and creates * the string representation of the filter. If this * filter object is a leaf filter, then it will return * the string representation of this filter. * * @return string|Net_LDAP2_Error */ public function asString() { if ($this->isLeaf()) { $return = $this->_filter; } else { $return = ''; foreach ($this->_subfilters as $filter) { $return = $return.$filter->asString(); } $return = '(' . $this->_match . $return . ')'; } return $return; } /** * Alias for perl interface as_string() * * @see asString() * @return string|Net_LDAP2_Error */ public function as_string() { return $this->asString(); } /** * Print the text representation of the filter to FH, or the currently selected output handle if FH is not given * * This method is only for compatibility to the perl interface. * However, the original method was called "print" but due to PHP language restrictions, * we can't have a print() method. * * @param resource $FH (optional) A filehandle resource * * @return true|Net_LDAP2_Error */ public function printMe($FH = false) { if (!is_resource($FH)) { if (PEAR::isError($FH)) { return $FH; } $filter_str = $this->asString(); if (PEAR::isError($filter_str)) { return $filter_str; } else { print($filter_str); } } else { $filter_str = $this->asString(); if (PEAR::isError($filter_str)) { return $filter_str; } else { $res = @fwrite($FH, $this->asString()); if ($res == false) { return PEAR::raiseError("Unable to write filter string to filehandle \$FH!"); } } } return true; } /** * This can be used to escape a string to provide a valid LDAP-Filter. * * LDAP will only recognise certain characters as the * character istself if they are properly escaped. This is * what this method does. * The method can be called statically, so you can use it outside * for your own purposes (eg for escaping only parts of strings) * * In fact, this is just a shorthand to {@link Net_LDAP2_Util::escape_filter_value()}. * For upward compatibiliy reasons you are strongly encouraged to use the escape * methods provided by the Net_LDAP2_Util class. * * @param string $value Any string who should be escaped * * @static * @return string The string $string, but escaped * @deprecated Do not use this method anymore, instead use Net_LDAP2_Util::escape_filter_value() directly */ public static function escape($value) { $return = Net_LDAP2_Util::escape_filter_value(array($value)); return $return[0]; } /** * Is this a container or a leaf filter object? * * @access protected * @return boolean */ protected function isLeaf() { if (count($this->_subfilters) > 0) { return false; // Container! } else { return true; // Leaf! } } /** * Filter entries using this filter or see if a filter matches * * @todo Currently slow and naive implementation with preg_match, could be optimized (esp. begins, ends filters etc) * @todo Currently only "="-based matches (equals, begins, ends, contains, any) implemented; Implement all the stuff! * @todo Implement expert code with schema checks in case $entry is connected to a directory * @param array|Net_LDAP2_Entry The entry (or array with entries) to check * @param array If given, the array will be appended with entries who matched the filter. Return value is true if any entry matched. * @return int|Net_LDAP2_Error Returns the number of matched entries or error */ function matches(&$entries, &$results=array()) { $numOfMatches = 0; if (!is_array($entries)) { $all_entries = array(&$entries); } else { $all_entries = &$entries; } foreach ($all_entries as $entry) { // look at the current entry and see if filter matches $entry_matched = false; // if this is not a single component, do calculate all subfilters, // then assert the partial results with the given combination modifier if (!$this->isLeaf()) { // get partial results from subfilters $partial_results = array(); foreach ($this->_subfilters as $filter) { $partial_results[] = $filter->matches($entry); } // evaluate partial results using this filters combination rule switch ($this->_match) { case '!': // result is the neagtive result of the assertion $entry_matched = !$partial_results[0]; break; case '&': // all partial results have to be boolean-true $entry_matched = !in_array(false, $partial_results); break; case '|': // at least one partial result has to be true $entry_matched = in_array(true, $partial_results); break; } } else { // Leaf filter: assert given entry // [TODO]: Could be optimized to avoid preg_match especially with "ends", "begins" etc // Translate the LDAP-match to some preg_match expression and evaluate it list($attribute, $match, $assertValue) = $this->getComponents(); switch ($match) { case '=': $regexp = '/^'.str_replace('*', '.*', $assertValue).'$/i'; // not case sensitive unless specified by schema $entry_matched = $entry->pregMatch($regexp, $attribute); break; // ------------------------------------- // [TODO]: implement <, >, <=, >= and =~ // ------------------------------------- default: $err = PEAR::raiseError("Net_LDAP2_Filter match error: unsupported match rule '$match'!"); return $err; } } // process filter matching result if ($entry_matched) { $numOfMatches++; $results[] = $entry; } } return $numOfMatches; } /** * Retrieve this leaf-filters attribute, match and value component. * * For leaf filters, this returns array(attr, match, value). * Match is be the logical operator, not the text representation, * eg "=" instead of "equals". Note that some operators are really * a combination of operator+value with wildcard, like * "begins": That will return "=" with the value "value*"! * * For non-leaf filters this will drop an error. * * @todo $this->_match is not always available and thus not usable here; it would be great if it would set in the factory methods and constructor. * @return array|Net_LDAP2_Error */ function getComponents() { if ($this->isLeaf()) { $raw_filter = preg_replace('/^\(|\)$/', '', $this->_filter); $parts = Net_LDAP2_Util::split_attribute_string($raw_filter, true, true); if (count($parts) != 3) { return PEAR::raiseError("Net_LDAP2_Filter getComponents() error: invalid filter syntax - unknown matching rule used"); } else { return $parts; } } else { return PEAR::raiseError('Net_LDAP2_Filter getComponents() call is invalid for non-leaf filters!'); } } } ?> * @copyright 2009 Benedikt Hallinger * @license http://www.gnu.org/licenses/lgpl-3.0.txt LGPLv3 * @version SVN: $Id$ * @link http://pear.php.net/package/Net_LDAP2/ */ /** * Includes */ require_once 'PEAR.php'; /** * Utility Class for Net_LDAP2 * * This class servers some functionality to the other classes of Net_LDAP2 but most of * the methods can be used separately as well. * * @category Net * @package Net_LDAP2 * @author Benedikt Hallinger * @license http://www.gnu.org/copyleft/lesser.html LGPL * @link http://pear.php.net/package/Net_LDAP22/ */ class Net_LDAP2_Util extends PEAR { /** * Constructor * * @access public */ public function __construct() { // We do nothing here, since all methods can be called statically. // In Net_LDAP <= 0.7, we needed a instance of Util, because // it was possible to do utf8 encoding and decoding, but this // has been moved to the LDAP class. The constructor remains only // here to document the downward compatibility of creating an instance. } /** * Explodes the given DN into its elements * * {@link http://www.ietf.org/rfc/rfc2253.txt RFC 2253} says, a Distinguished Name is a sequence * of Relative Distinguished Names (RDNs), which themselves * are sets of Attributes. For each RDN a array is constructed where the RDN part is stored. * * For example, the DN 'OU=Sales+CN=J. Smith,DC=example,DC=net' is exploded to: * array( [0] => array([0] => 'OU=Sales', [1] => 'CN=J. Smith'), [2] => 'DC=example', [3] => 'DC=net' ) * * [NOT IMPLEMENTED] DNs might also contain values, which are the bytes of the BER encoding of * the X.500 AttributeValue rather than some LDAP string syntax. These values are hex-encoded * and prefixed with a #. To distinguish such BER values, ldap_explode_dn uses references to * the actual values, e.g. '1.3.6.1.4.1.1466.0=#04024869,DC=example,DC=com' is exploded to: * [ { '1.3.6.1.4.1.1466.0' => "\004\002Hi" }, { 'DC' => 'example' }, { 'DC' => 'com' } ]; * See {@link http://www.vijaymukhi.com/vmis/berldap.htm} for more information on BER. * * It also performs the following operations on the given DN: * - Unescape "\" followed by ",", "+", """, "\", "<", ">", ";", "#", "=", " ", or a hexpair * and strings beginning with "#". * - Removes the leading 'OID.' characters if the type is an OID instead of a name. * - If an RDN contains multiple parts, the parts are re-ordered so that the attribute type names are in alphabetical order. * * OPTIONS is a list of name/value pairs, valid options are: * casefold Controls case folding of attribute types names. * Attribute values are not affected by this option. * The default is to uppercase. Valid values are: * lower Lowercase attribute types names. * upper Uppercase attribute type names. This is the default. * none Do not change attribute type names. * reverse If TRUE, the RDN sequence is reversed. * onlyvalues If TRUE, then only attributes values are returned ('foo' instead of 'cn=foo') * * @param string $dn The DN that should be exploded * @param array $options Options to use * * @static * @return array Parts of the exploded DN * @todo implement BER */ public static function ldap_explode_dn($dn, $options = array('casefold' => 'upper')) { if (!isset($options['onlyvalues'])) $options['onlyvalues'] = false; if (!isset($options['reverse'])) $options['reverse'] = false; if (!isset($options['casefold'])) $options['casefold'] = 'upper'; // Escaping of DN and stripping of "OID." $dn = self::canonical_dn($dn, array('casefold' => $options['casefold'])); // splitting the DN $dn_array = preg_split('/(?<=[^\\\\]),/', $dn); // clear wrong splitting (possibly we have split too much) // /!\ Not clear, if this is neccessary here //$dn_array = self::correct_dn_splitting($dn_array, ','); // construct subarrays for multivalued RDNs and unescape DN value // also convert to output format and apply casefolding foreach ($dn_array as $key => $value) { $value_u = self::unescape_dn_value($value); $rdns = self::split_rdn_multival($value_u[0]); if (count($rdns) > 1) { // MV RDN! foreach ($rdns as $subrdn_k => $subrdn_v) { // Casefolding if ($options['casefold'] == 'upper') { $subrdn_v = preg_replace_callback( "/^\w+=/", function ($matches) { return strtoupper($matches[0]); }, $subrdn_v ); } else if ($options['casefold'] == 'lower') { $subrdn_v = preg_replace_callback( "/^\w+=/", function ($matches) { return strtolower($matches[0]); }, $subrdn_v ); } if ($options['onlyvalues']) { preg_match('/(.+?)(?", ";", "#", "=" with a special meaning in RFC 2252 * are preceeded by ba backslash. Control characters with an ASCII code < 32 are represented as \hexpair. * Finally all leading and trailing spaces are converted to sequences of \20. * * @param array $values An array containing the DN values that should be escaped * * @static * @return array The array $values, but escaped */ public static function escape_dn_value($values = array()) { // Parameter validation if (!is_array($values)) { $values = array($values); } foreach ($values as $key => $val) { // Escaping of filter meta characters $val = str_replace('\\', '\\\\', $val); $val = str_replace(',', '\,', $val); $val = str_replace('+', '\+', $val); $val = str_replace('"', '\"', $val); $val = str_replace('<', '\<', $val); $val = str_replace('>', '\>', $val); $val = str_replace(';', '\;', $val); $val = str_replace('#', '\#', $val); $val = str_replace('=', '\=', $val); // ASCII < 32 escaping $val = self::asc2hex32($val); // Convert all leading and trailing spaces to sequences of \20. if (preg_match('/^(\s*)(.+?)(\s*)$/', $val, $matches)) { $val = $matches[2]; for ($i = 0; $i < strlen($matches[1]); $i++) { $val = '\20'.$val; } for ($i = 0; $i < strlen($matches[3]); $i++) { $val = $val.'\20'; } } if (null === $val) $val = '\0'; // apply escaped "null" if string is empty $values[$key] = $val; } return $values; } /** * Undoes the conversion done by escape_dn_value(). * * Any escape sequence starting with a baskslash - hexpair or special character - * will be transformed back to the corresponding character. * * @param array $values Array of DN Values * * @return array Same as $values, but unescaped * @static */ public static function unescape_dn_value($values = array()) { // Parameter validation if (!is_array($values)) { $values = array($values); } foreach ($values as $key => $val) { // strip slashes from special chars $val = str_replace('\\\\', '\\', $val); $val = str_replace('\,', ',', $val); $val = str_replace('\+', '+', $val); $val = str_replace('\"', '"', $val); $val = str_replace('\<', '<', $val); $val = str_replace('\>', '>', $val); $val = str_replace('\;', ';', $val); $val = str_replace('\#', '#', $val); $val = str_replace('\=', '=', $val); // Translate hex code into ascii $values[$key] = self::hex2asc($val); } return $values; } /** * Returns the given DN in a canonical form * * Returns false if DN is not a valid Distinguished Name. * DN can either be a string or an array * as returned by ldap_explode_dn, which is useful when constructing a DN. * The DN array may have be indexed (each array value is a OCL=VALUE pair) * or associative (array key is OCL and value is VALUE). * * It performs the following operations on the given DN: * - Removes the leading 'OID.' characters if the type is an OID instead of a name. * - Escapes all RFC 2253 special characters (",", "+", """, "\", "<", ">", ";", "#", "="), slashes ("/"), and any other character where the ASCII code is < 32 as \hexpair. * - Converts all leading and trailing spaces in values to be \20. * - If an RDN contains multiple parts, the parts are re-ordered so that the attribute type names are in alphabetical order. * * OPTIONS is a list of name/value pairs, valid options are: * casefold Controls case folding of attribute type names. * Attribute values are not affected by this option. The default is to uppercase. * Valid values are: * lower Lowercase attribute type names. * upper Uppercase attribute type names. This is the default. * none Do not change attribute type names. * [NOT IMPLEMENTED] mbcescape If TRUE, characters that are encoded as a multi-octet UTF-8 sequence will be escaped as \(hexpair){2,*}. * reverse If TRUE, the RDN sequence is reversed. * separator Separator to use between RDNs. Defaults to comma (','). * * Note: The empty string "" is a valid DN, so be sure not to do a "$can_dn == false" test, * because an empty string evaluates to false. Use the "===" operator instead. * * @param array|string $dn The DN * @param array $options Options to use * * @static * @return false|string The canonical DN or FALSE * @todo implement option mbcescape */ public static function canonical_dn($dn, $options = array('casefold' => 'upper', 'separator' => ',')) { if ($dn === '') return $dn; // empty DN is valid! // options check if (!isset($options['reverse'])) { $options['reverse'] = false; } else { $options['reverse'] = true; } if (!isset($options['casefold'])) $options['casefold'] = 'upper'; if (!isset($options['separator'])) $options['separator'] = ','; if (!is_array($dn)) { // It is not clear to me if the perl implementation splits by the user defined // separator or if it just uses this separator to construct the new DN $dn = preg_split('/(?<=[^\\\\])'.$options['separator'].'/', $dn); // clear wrong splitting (possibly we have split too much) $dn = self::correct_dn_splitting($dn, $options['separator']); } else { // Is array, check, if the array is indexed or associative $assoc = false; foreach ($dn as $dn_key => $dn_part) { if (!is_int($dn_key)) { $assoc = true; } } // convert to indexed, if associative array detected if ($assoc) { $newdn = array(); foreach ($dn as $dn_key => $dn_part) { if (is_array($dn_part)) { ksort($dn_part, SORT_STRING); // we assume here, that the rdn parts are also associative $newdn[] = $dn_part; // copy array as-is, so we can resolve it later } else { $newdn[] = $dn_key.'='.$dn_part; } } $dn =& $newdn; } } // Escaping and casefolding foreach ($dn as $pos => $dnval) { if (is_array($dnval)) { // subarray detected, this means very surely, that we had // a multivalued dn part, which must be resolved $dnval_new = ''; foreach ($dnval as $subkey => $subval) { // build RDN part if (!is_int($subkey)) { $subval = $subkey.'='.$subval; } $subval_processed = self::canonical_dn($subval); if (false === $subval_processed) return false; $dnval_new .= $subval_processed.'+'; } $dn[$pos] = substr($dnval_new, 0, -1); // store RDN part, strip last plus } else { // try to split multivalued RDNS into array $rdns = self::split_rdn_multival($dnval); if (count($rdns) > 1) { // Multivalued RDN was detected! // The RDN value is expected to be correctly split by split_rdn_multival(). // It's time to sort the RDN and build the DN! $rdn_string = ''; sort($rdns, SORT_STRING); // Sort RDN keys alphabetically foreach ($rdns as $rdn) { $subval_processed = self::canonical_dn($rdn); if (false === $subval_processed) return false; $rdn_string .= $subval_processed.'+'; } $dn[$pos] = substr($rdn_string, 0, -1); // store RDN part, strip last plus } else { // no multivalued RDN! // split at first unescaped "=" $dn_comp = preg_split('/(?<=[^\\\\])=/', $rdns[0], 2); $ocl = ltrim($dn_comp[0]); // trim left whitespaces 'cause of "cn=foo, l=bar" syntax (whitespace after comma) $val = $dn_comp[1]; // strip 'OID.', otherwise apply casefolding and escaping if (substr(strtolower($ocl), 0, 4) == 'oid.') { $ocl = substr($ocl, 4); } else { if ($options['casefold'] == 'upper') $ocl = strtoupper($ocl); if ($options['casefold'] == 'lower') $ocl = strtolower($ocl); $ocl = self::escape_dn_value(array($ocl)); $ocl = $ocl[0]; } // escaping of dn-value $val = self::escape_dn_value(array($val)); $val = str_replace('/', '\/', $val[0]); $dn[$pos] = $ocl.'='.$val; } } } if ($options['reverse']) $dn = array_reverse($dn); return implode($options['separator'], $dn); } /** * Escapes the given VALUES according to RFC 2254 so that they can be safely used in LDAP filters. * * Any control characters with an ACII code < 32 as well as the characters with special meaning in * LDAP filters "*", "(", ")", and "\" (the backslash) are converted into the representation of a * backslash followed by two hex digits representing the hexadecimal value of the character. * * @param array $values Array of values to escape * * @static * @return array Array $values, but escaped */ public static function escape_filter_value($values = array()) { // Parameter validation if (!is_array($values)) { $values = array($values); } foreach ($values as $key => $val) { // Escaping of filter meta characters $val = str_replace('\\', '\5c', $val); $val = str_replace('*', '\2a', $val); $val = str_replace('(', '\28', $val); $val = str_replace(')', '\29', $val); // ASCII < 32 escaping $val = self::asc2hex32($val); if (null === $val) $val = '\0'; // apply escaped "null" if string is empty $values[$key] = $val; } return $values; } /** * Undoes the conversion done by {@link escape_filter_value()}. * * Converts any sequences of a backslash followed by two hex digits into the corresponding character. * * @param array $values Array of values to escape * * @static * @return array Array $values, but unescaped */ public static function unescape_filter_value($values = array()) { // Parameter validation if (!is_array($values)) { $values = array($values); } foreach ($values as $key => $value) { // Translate hex code into ascii $values[$key] = self::hex2asc($value); } return $values; } /** * Converts all ASCII chars < 32 to "\HEX" * * @param string $string String to convert * * @static * @return string */ public static function asc2hex32($string) { for ($i = 0; $i < strlen($string); $i++) { $char = substr($string, $i, 1); if (ord($char) < 32) { $hex = dechex(ord($char)); if (strlen($hex) == 1) $hex = '0'.$hex; $string = str_replace($char, '\\'.$hex, $string); } } return $string; } /** * Converts all Hex expressions ("\HEX") to their original ASCII characters * * @param string $string String to convert * * @static * @author beni@php.net, heavily based on work from DavidSmith@byu.net * @return string */ public static function hex2asc($string) { $string = preg_replace_callback( "/\\\[0-9A-Fa-f]{2}/", function ($matches) { return chr(hexdec($matches[0])); }, $string ); return $string; } /** * Split an multivalued RDN value into an Array * * A RDN can contain multiple values, spearated by a plus sign. * This function returns each separate ocl=value pair of the RDN part. * * If no multivalued RDN is detected, an array containing only * the original rdn part is returned. * * For example, the multivalued RDN 'OU=Sales+CN=J. Smith' is exploded to: * array([0] => 'OU=Sales', [1] => 'CN=J. Smith') * * The method trys to be smart if it encounters unescaped "+" characters, but may fail, * so ensure escaped "+"es in attr names and attr values. * * [BUG] If you have a multivalued RDN with unescaped plus characters * and there is a unescaped plus sign at the end of an value followed by an * attribute name containing an unescaped plus, then you will get wrong splitting: * $rdn = 'OU=Sales+C+N=J. Smith'; * returns: * array('OU=Sales+C', 'N=J. Smith'); * The "C+" is treaten as value of the first pair instead as attr name of the second pair. * To prevent this, escape correctly. * * @param string $rdn Part of an (multivalued) escaped RDN (eg. ou=foo OR ou=foo+cn=bar) * * @static * @return array Array with the components of the multivalued RDN or Error */ public static function split_rdn_multival($rdn) { $rdns = preg_split('/(?, <, >=, <=, ~=). * * @param string $attr Attribute and Value Syntax ("foo=bar") * @param boolean $extended If set to true, also filter-assertion delimeter will be matched * @param boolean $withDelim If set to true, the return array contains the delimeter at index 1, putting the value to index 2 * * @return array Indexed array: 0=attribute name, 1=attribute value OR ($withDelim=true): 0=attr, 1=delimeter, 2=value */ public static function split_attribute_string($attr, $extended=false, $withDelim=false) { if ($withDelim) $withDelim = PREG_SPLIT_DELIM_CAPTURE; if (!$extended) { return preg_split('/(?=|<=|>|<|~=|=)/', $attr, 2, $withDelim); } } /** * Corrects splitting of dn parts * * @param array $dn Raw DN array * @param array $separator Separator that was used when splitting * * @return array Corrected array * @access protected */ protected static function correct_dn_splitting($dn = array(), $separator = ',') { foreach ($dn as $key => $dn_value) { $dn_value = $dn[$key]; // refresh value (foreach caches!) // if the dn_value is not in attr=value format, then we had an // unescaped separator character inside the attr name or the value. // We assume, that it was the attribute value. // [TODO] To solve this, we might ask the schema. Keep in mind, that UTIL class // must remain independent from the other classes or connections. if (!preg_match('/.+(? * @author Benedikt Hallinger * @copyright 2009 Jan Wagner, Benedikt Hallinger * @license http://www.gnu.org/licenses/lgpl-3.0.txt LGPLv3 * @version SVN: $Id$ * @link http://pear.php.net/package/Net_LDAP2/ * @todo see the comment at the end of the file */ /** * Includes */ require_once 'PEAR.php'; /** * Syntax definitions * * Please don't forget to add binary attributes to isBinary() below * to support proper value fetching from Net_LDAP2_Entry */ define('NET_LDAP2_SYNTAX_BOOLEAN', '1.3.6.1.4.1.1466.115.121.1.7'); define('NET_LDAP2_SYNTAX_DIRECTORY_STRING', '1.3.6.1.4.1.1466.115.121.1.15'); define('NET_LDAP2_SYNTAX_DISTINGUISHED_NAME', '1.3.6.1.4.1.1466.115.121.1.12'); define('NET_LDAP2_SYNTAX_INTEGER', '1.3.6.1.4.1.1466.115.121.1.27'); define('NET_LDAP2_SYNTAX_JPEG', '1.3.6.1.4.1.1466.115.121.1.28'); define('NET_LDAP2_SYNTAX_NUMERIC_STRING', '1.3.6.1.4.1.1466.115.121.1.36'); define('NET_LDAP2_SYNTAX_OID', '1.3.6.1.4.1.1466.115.121.1.38'); define('NET_LDAP2_SYNTAX_OCTET_STRING', '1.3.6.1.4.1.1466.115.121.1.40'); /** * Load an LDAP Schema and provide information * * This class takes a Subschema entry, parses this information * and makes it available in an array. Most of the code has been * inspired by perl-ldap( http://perl-ldap.sourceforge.net). * You will find portions of their implementation in here. * * @category Net * @package Net_LDAP2 * @author Jan Wagner * @author Benedikt Hallinger * @license http://www.gnu.org/copyleft/lesser.html LGPL * @link http://pear.php.net/package/Net_LDAP22/ */ class Net_LDAP2_Schema extends PEAR { /** * Map of entry types to ldap attributes of subschema entry * * @access public * @var array */ public $types = array( 'attribute' => 'attributeTypes', 'ditcontentrule' => 'dITContentRules', 'ditstructurerule' => 'dITStructureRules', 'matchingrule' => 'matchingRules', 'matchingruleuse' => 'matchingRuleUse', 'nameform' => 'nameForms', 'objectclass' => 'objectClasses', 'syntax' => 'ldapSyntaxes' ); /** * Array of entries belonging to this type * * @access protected * @var array */ protected $_attributeTypes = array(); protected $_matchingRules = array(); protected $_matchingRuleUse = array(); protected $_ldapSyntaxes = array(); protected $_objectClasses = array(); protected $_dITContentRules = array(); protected $_dITStructureRules = array(); protected $_nameForms = array(); /** * hash of all fetched oids * * @access protected * @var array */ protected $_oids = array(); /** * Tells if the schema is initialized * * @access protected * @var boolean * @see parse(), get() */ protected $_initialized = false; /** * Constructor of the class * * @access protected */ public function __construct() { parent::__construct('Net_LDAP2_Error'); // default error class } /** * Fetch the Schema from an LDAP connection * * @param Net_LDAP2 $ldap LDAP connection * @param string $dn (optional) Subschema entry dn * * @access public * @return Net_LDAP2_Schema|NET_LDAP2_Error */ public static function fetch($ldap, $dn = null) { if (!$ldap instanceof Net_LDAP2) { return PEAR::raiseError("Unable to fetch Schema: Parameter \$ldap must be a Net_LDAP2 object!"); } $schema_o = new Net_LDAP2_Schema(); if (is_null($dn)) { // get the subschema entry via root dse $dse = $ldap->rootDSE(array('subschemaSubentry')); if (false == Net_LDAP2::isError($dse)) { $base = $dse->getValue('subschemaSubentry', 'single'); if (!Net_LDAP2::isError($base)) { $dn = $base; } } } // Support for buggy LDAP servers (e.g. Siemens DirX 6.x) that incorrectly // call this entry subSchemaSubentry instead of subschemaSubentry. // Note the correct case/spelling as per RFC 2251. if (is_null($dn)) { // get the subschema entry via root dse $dse = $ldap->rootDSE(array('subSchemaSubentry')); if (false == Net_LDAP2::isError($dse)) { $base = $dse->getValue('subSchemaSubentry', 'single'); if (!Net_LDAP2::isError($base)) { $dn = $base; } } } // Final fallback case where there is no subschemaSubentry attribute // in the root DSE (this is a bug for an LDAP v3 server so report this // to your LDAP vendor if you get this far). if (is_null($dn)) { $dn = 'cn=Subschema'; } // fetch the subschema entry $result = $ldap->search($dn, '(objectClass=*)', array('attributes' => array_values($schema_o->types), 'scope' => 'base')); if (Net_LDAP2::isError($result)) { return PEAR::raiseError('Could not fetch Subschema entry: '.$result->getMessage()); } $entry = $result->shiftEntry(); if (!$entry instanceof Net_LDAP2_Entry) { if ($entry instanceof Net_LDAP2_Error) { return PEAR::raiseError('Could not fetch Subschema entry: '.$entry->getMessage()); } else { return PEAR::raiseError('Could not fetch Subschema entry (search returned '.$result->count().' entries. Check parameter \'basedn\')'); } } $schema_o->parse($entry); return $schema_o; } /** * Return a hash of entries for the given type * * Returns a hash of entry for the givene type. Types may be: * objectclasses, attributes, ditcontentrules, ditstructurerules, matchingrules, * matchingruleuses, nameforms, syntaxes * * @param string $type Type to fetch * * @access public * @return array|Net_LDAP2_Error Array or Net_LDAP2_Error */ public function &getAll($type) { $map = array('objectclasses' => &$this->_objectClasses, 'attributes' => &$this->_attributeTypes, 'ditcontentrules' => &$this->_dITContentRules, 'ditstructurerules' => &$this->_dITStructureRules, 'matchingrules' => &$this->_matchingRules, 'matchingruleuses' => &$this->_matchingRuleUse, 'nameforms' => &$this->_nameForms, 'syntaxes' => &$this->_ldapSyntaxes ); $key = strtolower($type); $ret = ((key_exists($key, $map)) ? $map[$key] : PEAR::raiseError("Unknown type $type")); return $ret; } /** * Return a specific entry * * @param string $type Type of name * @param string $name Name or OID to fetch * * @access public * @return mixed Entry or Net_LDAP2_Error */ public function &get($type, $name) { if ($this->_initialized) { $type = strtolower($type); if (false == key_exists($type, $this->types)) { return PEAR::raiseError("No such type $type"); } $name = strtolower($name); $type_var = &$this->{'_' . $this->types[$type]}; if (key_exists($name, $type_var)) { return $type_var[$name]; } elseif (key_exists($name, $this->_oids) && $this->_oids[$name]['type'] == $type) { return $this->_oids[$name]; } else { return PEAR::raiseError("Could not find $type $name"); } } else { $return = null; return $return; } } /** * Fetches attributes that MAY be present in the given objectclass * * @param string $oc Name or OID of objectclass * * @access public * @return array|Net_LDAP2_Error Array with attributes or Net_LDAP2_Error */ public function may($oc) { return $this->_getAttr($oc, 'may'); } /** * Fetches attributes that MUST be present in the given objectclass * * @param string $oc Name or OID of objectclass * * @access public * @return array|Net_LDAP2_Error Array with attributes or Net_LDAP2_Error */ public function must($oc) { return $this->_getAttr($oc, 'must'); } /** * Fetches the given attribute from the given objectclass * * @param string $oc Name or OID of objectclass * @param string $attr Name of attribute to fetch * * @access protected * @return array|Net_LDAP2_Error The attribute or Net_LDAP2_Error */ protected function _getAttr($oc, $attr) { $oc = strtolower($oc); if (key_exists($oc, $this->_objectClasses) && key_exists($attr, $this->_objectClasses[$oc])) { return $this->_objectClasses[$oc][$attr]; } elseif (key_exists($oc, $this->_oids) && $this->_oids[$oc]['type'] == 'objectclass' && key_exists($attr, $this->_oids[$oc])) { return $this->_oids[$oc][$attr]; } else { return PEAR::raiseError("Could not find $attr attributes for $oc "); } } /** * Returns the name(s) of the immediate superclass(es) * * @param string $oc Name or OID of objectclass * * @access public * @return array|Net_LDAP2_Error Array of names or Net_LDAP2_Error */ public function superclass($oc) { $o = $this->get('objectclass', $oc); if (Net_LDAP2::isError($o)) { return $o; } return (key_exists('sup', $o) ? $o['sup'] : array()); } /** * Parses the schema of the given Subschema entry * * @param Net_LDAP2_Entry &$entry Subschema entry * * @access public * @return void */ public function parse(&$entry) { foreach ($this->types as $type => $attr) { // initialize map type to entry $type_var = '_' . $attr; $this->{$type_var} = array(); // get values for this type if ($entry->exists($attr)) { $values = $entry->getValue($attr); if (is_array($values)) { foreach ($values as $value) { unset($schema_entry); // this was a real mess without it // get the schema entry $schema_entry = $this->_parse_entry($value); // set the type $schema_entry['type'] = $type; // save a ref in $_oids $this->_oids[$schema_entry['oid']] = &$schema_entry; // save refs for all names in type map $names = $schema_entry['aliases']; array_push($names, $schema_entry['name']); foreach ($names as $name) { $this->{$type_var}[strtolower($name)] = &$schema_entry; } } } } } $this->_initialized = true; } /** * Parses an attribute value into a schema entry * * @param string $value Attribute value * * @access protected * @return array|false Schema entry array or false */ protected function &_parse_entry($value) { // tokens that have no value associated $noValue = array('single-value', 'obsolete', 'collective', 'no-user-modification', 'abstract', 'structural', 'auxiliary'); // tokens that can have multiple values $multiValue = array('must', 'may', 'sup'); $schema_entry = array('aliases' => array()); // initilization $tokens = $this->_tokenize($value); // get an array of tokens // remove surrounding brackets if ($tokens[0] == '(') array_shift($tokens); if ($tokens[count($tokens) - 1] == ')') array_pop($tokens); // -1 doesnt work on arrays :-( $schema_entry['oid'] = array_shift($tokens); // first token is the oid // cycle over the tokens until none are left while (count($tokens) > 0) { $token = strtolower(array_shift($tokens)); if (in_array($token, $noValue)) { $schema_entry[$token] = 1; // single value token } else { // this one follows a string or a list if it is multivalued if (($schema_entry[$token] = array_shift($tokens)) == '(') { // this creates the list of values and cycles through the tokens // until the end of the list is reached ')' $schema_entry[$token] = array(); while ($tmp = array_shift($tokens)) { if ($tmp == ')') break; if ($tmp != '$') array_push($schema_entry[$token], $tmp); } } // create a array if the value should be multivalued but was not if (in_array($token, $multiValue) && !is_array($schema_entry[$token])) { $schema_entry[$token] = array($schema_entry[$token]); } } } // get max length from syntax if (key_exists('syntax', $schema_entry)) { if (preg_match('/{(\d+)}/', $schema_entry['syntax'], $matches)) { $schema_entry['max_length'] = $matches[1]; } } // force a name if (empty($schema_entry['name'])) { $schema_entry['name'] = $schema_entry['oid']; } // make one name the default and put the other ones into aliases if (is_array($schema_entry['name'])) { $aliases = $schema_entry['name']; $schema_entry['name'] = array_shift($aliases); $schema_entry['aliases'] = $aliases; } return $schema_entry; } /** * Tokenizes the given value into an array of tokens * * @param string $value String to parse * * @access protected * @return array Array of tokens */ protected function _tokenize($value) { $tokens = array(); // array of tokens $matches = array(); // matches[0] full pattern match, [1,2,3] subpatterns // this one is taken from perl-ldap, modified for php $pattern = "/\s* (?:([()]) | ([^'\s()]+) | '((?:[^']+|'[^\s)])*)') \s*/x"; /** * This one matches one big pattern wherin only one of the three subpatterns matched * We are interested in the subpatterns that matched. If it matched its value will be * non-empty and so it is a token. Tokens may be round brackets, a string, or a string * enclosed by ' */ preg_match_all($pattern, $value, $matches); for ($i = 0; $i < count($matches[0]); $i++) { // number of tokens (full pattern match) for ($j = 1; $j < 4; $j++) { // each subpattern if (null != trim($matches[$j][$i])) { // pattern match in this subpattern $tokens[$i] = trim($matches[$j][$i]); // this is the token } } } return $tokens; } /** * Returns wether a attribute syntax is binary or not * * This method gets used by Net_LDAP2_Entry to decide which * PHP function needs to be used to fetch the value in the * proper format (e.g. binary or string) * * @param string $attribute The name of the attribute (eg.: 'sn') * * @access public * @return boolean */ public function isBinary($attribute) { $return = false; // default to false // This list contains all syntax that should be treaten as // containing binary values // The Syntax Definitons go into constants at the top of this page $syntax_binary = array( NET_LDAP2_SYNTAX_OCTET_STRING, NET_LDAP2_SYNTAX_JPEG ); // Check Syntax $attr_s = $this->get('attribute', $attribute); if (Net_LDAP2::isError($attr_s)) { // Attribute not found in schema $return = false; // consider attr not binary } elseif (isset($attr_s['syntax']) && in_array($attr_s['syntax'], $syntax_binary)) { // Syntax is defined as binary in schema $return = true; } else { // Syntax not defined as binary, or not found // if attribute is a subtype, check superior attribute syntaxes if (isset($attr_s['sup'])) { foreach ($attr_s['sup'] as $superattr) { $return = $this->isBinary($superattr); if ($return) { break; // stop checking parents since we are binary } } } } return $return; } /** * See if an schema element exists * * @param string $type Type of name, see get() * @param string $name Name or OID * * @return boolean */ public function exists($type, $name) { $entry = $this->get($type, $name); if ($entry instanceof Net_LDAP2_ERROR) { return false; } else { return true; } } /** * See if an attribute is defined in the schema * * @param string $attribute Name or OID of the attribute * @return boolean */ public function attributeExists($attribute) { return $this->exists('attribute', $attribute); } /** * See if an objectClass is defined in the schema * * @param string $ocl Name or OID of the objectClass * @return boolean */ public function objectClassExists($ocl) { return $this->exists('objectclass', $ocl); } /** * See to which ObjectClasses an attribute is assigned * * The objectclasses are sorted into the keys 'may' and 'must'. * * @param string $attribute Name or OID of the attribute * * @return array|Net_LDAP2_Error Associative array with OCL names or Error */ public function getAssignedOCLs($attribute) { $may = array(); $must = array(); // Test if the attribute type is defined in the schema, // if so, retrieve real name for lookups $attr_entry = $this->get('attribute', $attribute); if ($attr_entry instanceof Net_LDAP2_ERROR) { return PEAR::raiseError("Attribute $attribute not defined in schema: ".$attr_entry->getMessage()); } else { $attribute = $attr_entry['name']; } // We need to get all defined OCLs for this. $ocls = $this->getAll('objectclasses'); foreach ($ocls as $ocl => $ocl_data) { // Fetch the may and must attrs and see if our searched attr is contained. // If so, record it in the corresponding array. $ocl_may_attrs = $this->may($ocl); $ocl_must_attrs = $this->must($ocl); if (is_array($ocl_may_attrs) && in_array($attribute, $ocl_may_attrs)) { array_push($may, $ocl_data['name']); } if (is_array($ocl_must_attrs) && in_array($attribute, $ocl_must_attrs)) { array_push($must, $ocl_data['name']); } } return array('may' => $may, 'must' => $must); } /** * See if an attribute is available in a set of objectClasses * * @param string $attribute Attribute name or OID * @param array $ocls Names of OCLs to check for * * @return boolean TRUE, if the attribute is defined for at least one of the OCLs */ public function checkAttribute($attribute, $ocls) { foreach ($ocls as $ocl) { $ocl_entry = $this->get('objectclass', $ocl); $ocl_may_attrs = $this->may($ocl); $ocl_must_attrs = $this->must($ocl); if (is_array($ocl_may_attrs) && in_array($attribute, $ocl_may_attrs)) { return true; } if (is_array($ocl_must_attrs) && in_array($attribute, $ocl_must_attrs)) { return true; } } return false; // no ocl for the ocls found. } } ?> * @copyright 2009 Benedikt Hallinger * @license http://www.gnu.org/licenses/lgpl-3.0.txt LGPLv3 * @version SVN: $Id$ * @link http://pear.php.net/package/Net_LDAP2/ */ /** * A simple file based schema cacher with cache aging. * * Once the cache is too old, the loadSchema() method will return false, so * Net_LDAP2 will fetch a fresh object from the LDAP server that will * overwrite the current (outdated) old cache. */ class Net_LDAP2_SimpleFileSchemaCache implements Net_LDAP2_SchemaCache { /** * Internal config of this cache * * @see Net_LDAP2_SimpleFileSchemaCache() * @var array */ protected $config = array( 'path' => '/tmp/Net_LDAP_Schema.cache', 'max_age' => 1200 ); /** * Initialize the simple cache * * Config is as following: * path Complete path to the cache file. * max_age Maximum age of cache in seconds, 0 means "endlessly". * * @param array $cfg Config array */ public function __construct($cfg) { foreach ($cfg as $key => $value) { if (array_key_exists($key, $this->config)) { if (gettype($this->config[$key]) != gettype($value)) { $this->getCore()->dropFatalError(__CLASS__.": Could not set config! Key $key does not match type ".gettype($this->config[$key])."!"); } $this->config[$key] = $value; } else { $this->getCore()->dropFatalError(__CLASS__.": Could not set config! Key $key is not defined!"); } } } /** * Return the schema object from the cache * * If file is existent and cache has not expired yet, * then the cache is deserialized and returned. * * @return Net_LDAP2_Schema|Net_LDAP2_Error|false */ public function loadSchema() { $return = false; // Net_LDAP2 will load schema from LDAP if (file_exists($this->config['path'])) { $cache_maxage = filemtime($this->config['path']) + $this->config['max_age']; if (time() <= $cache_maxage || $this->config['max_age'] == 0) { $return = unserialize(file_get_contents($this->config['path'])); } } return $return; } /** * Store a schema object in the cache * * This method will be called, if Net_LDAP2 has fetched a fresh * schema object from LDAP and wants to init or refresh the cache. * * To invalidate the cache and cause Net_LDAP2 to refresh the cache, * you can call this method with null or false as value. * The next call to $ldap->schema() will then refresh the caches object. * * @param mixed $schema The object that should be cached * @return true|Net_LDAP2_Error|false */ public function storeSchema($schema) { file_put_contents($this->config['path'], serialize($schema)); return true; } } %tR SJlk`*GBMB