/library/Zend/Ldap.php
PHP | 759 lines | 553 code | 49 blank | 157 comment | 48 complexity | 1c6307f998b16f45fe51b2699561f527 MD5 | raw file
- <?php
- /**
- * Zend Framework
- *
- * LICENSE
- *
- * This source file is subject to the new BSD license that is bundled
- * with this package in the file LICENSE.txt.
- * It is also available through the world-wide-web at this URL:
- * http://framework.zend.com/license/new-bsd
- * If you did not receive a copy of the license and are unable to
- * obtain it through the world-wide-web, please send an email
- * to license@zend.com so we can send you a copy immediately.
- *
- * @category Zend
- * @package Zend_Ldap
- * @copyright Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
- * @license http://framework.zend.com/license/new-bsd New BSD License
- * @version $Id: Ldap.php 11765 2008-10-09 01:53:43Z miallen $
- */
- /**
- * @category Zend
- * @package Zend_Ldap
- * @copyright Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
- * @license http://framework.zend.com/license/new-bsd New BSD License
- */
- class Zend_Ldap
- {
- const ACCTNAME_FORM_DN = 1;
- const ACCTNAME_FORM_USERNAME = 2;
- const ACCTNAME_FORM_BACKSLASH = 3;
- const ACCTNAME_FORM_PRINCIPAL = 4;
- /**
- * String used with ldap_connect for error handling purposes.
- *
- * @var string
- */
- private $_connectString;
- /**
- * The raw LDAP extension resource.
- *
- * @var resource
- */
- protected $_resource = null;
- /**
- * @param string $str The string to escape.
- * @return string The escaped string
- */
- public static function filterEscape($str)
- {
- $ret = '';
- $len = strlen($str);
- for ($si = 0; $si < $len; $si++) {
- $ch = $str[$si];
- $ord = ord($ch);
- if ($ord < 0x20 || $ord > 0x7e || strstr('*()\/', $ch)) {
- $ch = '\\' . dechex($ord);
- }
- $ret .= $ch;
- }
- return $ret;
- }
- /**
- * @param string $dn The DN to parse
- * @param array $keys An optional array to receive DN keys (e.g. CN, OU, DC, ...)
- * @param array $vals An optional array to receive DN values
- * @return bool True if the DN was successfully parsed or false if the string is not a valid DN.
- */
- public static function explodeDn($dn, array &$keys = null, array &$vals = null)
- {
- /* This is a classic state machine parser. Each iteration of the
- * loop processes one character. State 1 collects the key. When equals (=)
- * is encountered the state changes to 2 where the value is collected
- * until a comma (,) or semicolon (;) is encountered after which we switch back
- * to state 1. If a backslash (\) is encountered, state 3 is used to collect the
- * following character without engaging the logic of other states.
- */
- $key = null;
- $slen = strlen($dn);
- $state = 1;
- $ko = $vo = 0;
- for ($di = 0; $di <= $slen; $di++) {
- $ch = $di == $slen ? 0 : $dn[$di];
- switch ($state) {
- case 1: // collect key
- if ($ch === '=') {
- $key = trim(substr($dn, $ko, $di - $ko));
- if ($keys !== null) {
- $keys[] = $key;
- }
- $state = 2;
- $vo = $di + 1;
- } else if ($ch === ',' || $ch === ';') {
- return false;
- }
- break;
- case 2: // collect value
- if ($ch === '\\') {
- $state = 3;
- } else if ($ch === ',' || $ch === ';' || $ch === 0) {
- if ($vals !== null) {
- $vals[] = trim(substr($dn, $vo, $di - $vo));
- }
- $state = 1;
- $ko = $di + 1;
- } else if ($ch === '=') {
- return false;
- }
- break;
- case 3: // escaped
- $state = 2;
- break;
- }
- }
- return $state === 1 && $ko > 0;
- }
- /**
- * @param array $options Options used in connecting, binding, etc.
- * @return void
- */
- public function __construct(array $options = array())
- {
- $this->setOptions($options);
- }
- /**
- * Sets the options used in connecting, binding, etc.
- *
- * Valid option keys:
- * host
- * port
- * useSsl
- * username
- * password
- * bindRequiresDn
- * baseDn
- * accountCanonicalForm
- * accountDomainName
- * accountDomainNameShort
- * accountFilterFormat
- * allowEmptyPassword
- * useStartTls
- * optRefferals
- *
- * @param array $options Options used in connecting, binding, etc.
- * @return Zend_Ldap Provides a fluent interface
- * @throws Zend_Ldap_Exception
- */
- public function setOptions(array $options)
- {
- $permittedOptions = array(
- 'host' => null,
- 'port' => null,
- 'useSsl' => null,
- 'username' => null,
- 'password' => null,
- 'bindRequiresDn' => null,
- 'baseDn' => null,
- 'accountCanonicalForm' => null,
- 'accountDomainName' => null,
- 'accountDomainNameShort' => null,
- 'accountFilterFormat' => null,
- 'allowEmptyPassword' => null,
- 'useStartTls' => null,
- 'optReferrals' => null,
- );
- $diff = array_diff_key($options, $permittedOptions);
- if ($diff) {
- list($key, $val) = each($diff);
- require_once 'Zend/Ldap/Exception.php';
- throw new Zend_Ldap_Exception(null, "Unknown Zend_Ldap option: $key");
- }
- foreach ($permittedOptions as $key => $val) {
- if (!array_key_exists($key, $options)) {
- $options[$key] = null;
- } else {
- /* Enforce typing. This eliminates issues like Zend_Config_Ini
- * returning '1' as a string (ZF-3163).
- */
- switch ($key) {
- case 'port':
- case 'accountCanonicalForm':
- $options[$key] = (int)$options[$key];
- break;
- case 'useSsl':
- case 'bindRequiresDn':
- case 'allowEmptyPassword':
- case 'useStartTls':
- case 'optReferrals':
- $val = $options[$key];
- $options[$key] = $val === true ||
- $val === '1' ||
- strcasecmp($val, 'true') == 0;
- break;
- }
- }
- }
- $this->_options = $options;
- return $this;
- }
- /**
- * @return array The current options.
- */
- public function getOptions()
- {
- return $this->_options;
- }
- /**
- * @return resource The raw LDAP extension resource.
- */
- public function getResource()
- {
- /**
- * @todo by reference?
- */
- return $this->_resource;
- }
- /**
- * @return string The hostname of the LDAP server being used to authenticate accounts
- */
- protected function _getHost()
- {
- return $this->_options['host'];
- }
- /**
- * @return int The port of the LDAP server or 0 to indicate that no port value is set
- */
- protected function _getPort()
- {
- if ($this->_options['port'])
- return $this->_options['port'];
- return 0;
- }
- /**
- * @return string The default acctname for binding
- */
- protected function _getUsername()
- {
- return $this->_options['username'];
- }
- /**
- * @return string The default password for binding
- */
- protected function _getPassword()
- {
- return $this->_options['password'];
- }
- /**
- * @return boolean The default SSL / TLS encrypted transport control
- */
- protected function _getUseSsl()
- {
- return $this->_options['useSsl'];
- }
- /**
- * @return string The default base DN under which objects of interest are located
- */
- protected function _getBaseDn()
- {
- return $this->_options['baseDn'];
- }
- /**
- * @return string Either ACCTNAME_FORM_BACKSLASH, ACCTNAME_FORM_PRINCIPAL or ACCTNAME_FORM_USERNAME indicating the form usernames should be canonicalized to.
- */
- protected function _getAccountCanonicalForm()
- {
- /* Account names should always be qualified with a domain. In some scenarios
- * using non-qualified account names can lead to security vulnerabilities. If
- * no account canonical form is specified, we guess based in what domain
- * names have been supplied.
- */
- $accountCanonicalForm = $this->_options['accountCanonicalForm'];
- if (!$accountCanonicalForm) {
- $accountDomainName = $this->_options['accountDomainName'];
- $accountDomainNameShort = $this->_options['accountDomainNameShort'];
- if ($accountDomainNameShort) {
- $accountCanonicalForm = Zend_Ldap::ACCTNAME_FORM_BACKSLASH;
- } else if ($accountDomainName) {
- $accountCanonicalForm = Zend_Ldap::ACCTNAME_FORM_PRINCIPAL;
- } else {
- $accountCanonicalForm = Zend_Ldap::ACCTNAME_FORM_USERNAME;
- }
- }
- return $accountCanonicalForm;
- }
- /**
- * @return string A format string for building an LDAP search filter to match an account
- */
- protected function _getAccountFilterFormat()
- {
- return $this->_options['accountFilterFormat'];
- }
- /**
- * @return string The LDAP search filter for matching directory accounts
- */
- protected function _getAccountFilter($acctname)
- {
- $this->_splitName($acctname, $dname, $aname);
- $accountFilterFormat = $this->_getAccountFilterFormat();
- $aname = Zend_Ldap::filterEscape($aname);
- if ($accountFilterFormat)
- return sprintf($accountFilterFormat, $aname);
- if (!$this->_options['bindRequiresDn']) {
- // is there a better way to detect this?
- return "(&(objectClass=user)(sAMAccountName=$aname))";
- }
- return "(&(objectClass=posixAccount)(uid=$aname))";
- }
- /**
- * @param string $name The name to split
- * @param string $dname The resulting domain name (this is an out parameter)
- * @param string $aname The resulting account name (this is an out parameter)
- */
- protected function _splitName($name, &$dname, &$aname)
- {
- $dname = NULL;
- $aname = $name;
- $pos = strpos($name, '@');
- if ($pos) {
- $dname = substr($name, $pos + 1);
- $aname = substr($name, 0, $pos);
- } else {
- $pos = strpos($name, '\\');
- if ($pos) {
- $dname = substr($name, 0, $pos);
- $aname = substr($name, $pos + 1);
- }
- }
- }
- /**
- * @param string $acctname The name of the account
- * @return string The DN of the specified account
- * @throws Zend_Ldap_Exception
- */
- protected function _getAccountDn($acctname)
- {
- if (Zend_Ldap::explodeDn($acctname))
- return $acctname;
- $acctname = $this->getCanonicalAccountName($acctname, Zend_Ldap::ACCTNAME_FORM_USERNAME);
- $acct = $this->_getAccount($acctname, array('dn'));
- return $acct['dn'];
- }
- /**
- * @param string $dname The domain name to check
- * @return bool
- */
- protected function _isPossibleAuthority($dname)
- {
- if ($dname === null)
- return true;
- $accountDomainName = $this->_options['accountDomainName'];
- $accountDomainNameShort = $this->_options['accountDomainNameShort'];
- if ($accountDomainName === null && $accountDomainNameShort === null)
- return true;
- if (strcasecmp($dname, $accountDomainName) == 0)
- return true;
- if (strcasecmp($dname, $accountDomainNameShort) == 0)
- return true;
- return false;
- }
- /**
- * @param string $acctname The name to canonicalize
- * @param int $type The desired form of canonicalization
- * @return string The canonicalized name in the desired form
- * @throws Zend_Ldap_Exception
- */
- public function getCanonicalAccountName($acctname, $form = 0)
- {
- $this->_splitName($acctname, $dname, $uname);
- if (!$this->_isPossibleAuthority($dname)) {
- /**
- * @see Zend_Ldap_Exception
- */
- require_once 'Zend/Ldap/Exception.php';
- throw new Zend_Ldap_Exception(null,
- "Binding domain is not an authority for user: $acctname",
- Zend_Ldap_Exception::LDAP_X_DOMAIN_MISMATCH);
- }
- if ($form === Zend_Ldap::ACCTNAME_FORM_DN)
- return $this->_getAccountDn($acctname);
- if (!$uname) {
- /**
- * @see Zend_Ldap_Exception
- */
- require_once 'Zend/Ldap/Exception.php';
- throw new Zend_Ldap_Exception(null, "Invalid account name syntax: $acctname");
- }
- $uname = strtolower($uname);
- if ($form === 0)
- $form = $this->_getAccountCanonicalForm();
- switch ($form) {
- case Zend_Ldap::ACCTNAME_FORM_USERNAME:
- return $uname;
- case Zend_Ldap::ACCTNAME_FORM_BACKSLASH:
- $accountDomainNameShort = $this->_options['accountDomainNameShort'];
- if (!$accountDomainNameShort) {
- /**
- * @see Zend_Ldap_Exception
- */
- require_once 'Zend/Ldap/Exception.php';
- throw new Zend_Ldap_Exception(null, 'Option required: accountDomainNameShort');
- }
- return "$accountDomainNameShort\\$uname";
- case Zend_Ldap::ACCTNAME_FORM_PRINCIPAL:
- $accountDomainName = $this->_options['accountDomainName'];
- if (!$accountDomainName) {
- /**
- * @see Zend_Ldap_Exception
- */
- require_once 'Zend/Ldap/Exception.php';
- throw new Zend_Ldap_Exception(null, 'Option required: accountDomainName');
- }
- return "$uname@$accountDomainName";
- default:
- /**
- * @see Zend_Ldap_Exception
- */
- require_once 'Zend/Ldap/Exception.php';
- throw new Zend_Ldap_Exception(null, "Unknown canonical name form: $form");
- }
- }
- /**
- * @param array $attrs An array of names of desired attributes
- * @return array An array of the attributes representing the account
- * @throws Zend_Ldap_Exception
- */
- private function _getAccount($acctname, array $attrs = null)
- {
- $baseDn = $this->_getBaseDn();
- if (!$baseDn) {
- /**
- * @see Zend_Ldap_Exception
- */
- require_once 'Zend/Ldap/Exception.php';
- throw new Zend_Ldap_Exception(null, 'Base DN not set');
- }
- $accountFilter = $this->_getAccountFilter($acctname);
- if (!$accountFilter) {
- /**
- * @see Zend_Ldap_Exception
- */
- require_once 'Zend/Ldap/Exception.php';
- throw new Zend_Ldap_Exception(null, 'Invalid account filter');
- }
- if (!is_resource($this->_resource))
- $this->bind();
- $resource = $this->_resource;
- $str = $accountFilter;
- $code = 0;
- /**
- * @todo break out search operation into simple function (private for now)
- */
- if (!extension_loaded('ldap')) {
- /**
- * @see Zend_Ldap_Exception
- */
- require_once 'Zend/Ldap/Exception.php';
- throw new Zend_Ldap_Exception(null, 'LDAP extension not loaded');
- }
- $result = @ldap_search($resource,
- $baseDn,
- $accountFilter,
- $attrs);
- if (is_resource($result) === true) {
- $count = @ldap_count_entries($resource, $result);
- if ($count == 1) {
- $entry = @ldap_first_entry($resource, $result);
- if ($entry) {
- $acct = array('dn' => @ldap_get_dn($resource, $entry));
- $name = @ldap_first_attribute($resource, $entry, $berptr);
- while ($name) {
- $data = @ldap_get_values_len($resource, $entry, $name);
- $acct[$name] = $data;
- $name = @ldap_next_attribute($resource, $entry, $berptr);
- }
- @ldap_free_result($result);
- return $acct;
- }
- } else if ($count == 0) {
- /**
- * @see Zend_Ldap_Exception
- */
- require_once 'Zend/Ldap/Exception.php';
- $code = Zend_Ldap_Exception::LDAP_NO_SUCH_OBJECT;
- } else {
- /**
- * @todo limit search to 1 record and remove some of this logic?
- */
- $resource = null;
- $str = "$accountFilter: Unexpected result count: $count";
- /**
- * @see Zend_Ldap_Exception
- */
- require_once 'Zend/Ldap/Exception.php';
- $code = Zend_Ldap_Exception::LDAP_OPERATIONS_ERROR;
- }
- @ldap_free_result($result);
- }
- /**
- * @see Zend_Ldap_Exception
- */
- require_once 'Zend/Ldap/Exception.php';
- throw new Zend_Ldap_Exception($resource, $str, $code);
- }
- /**
- * @return Zend_Ldap Provides a fluent interface
- */
- public function disconnect()
- {
- if (is_resource($this->_resource)) {
- if (!extension_loaded('ldap')) {
- /**
- * @see Zend_Ldap_Exception
- */
- require_once 'Zend/Ldap/Exception.php';
- throw new Zend_Ldap_Exception(null, 'LDAP extension not loaded');
- }
- @ldap_unbind($this->_resource);
- }
- $this->_resource = null;
- return $this;
- }
- /**
- * @param string $host The hostname of the LDAP server to connect to
- * @param int $port The port number of the LDAP server to connect to
- * @return Zend_Ldap Provides a fluent interface
- * @throws Zend_Ldap_Exception
- */
- public function connect($host = null, $port = 0, $useSsl = false)
- {
- if ($host === null)
- $host = $this->_getHost();
- if ($port === 0)
- $port = $this->_getPort();
- if ($useSsl === false)
- $useSsl = $this->_getUseSsl();
- if (!$host) {
- /**
- * @see Zend_Ldap_Exception
- */
- require_once 'Zend/Ldap/Exception.php';
- throw new Zend_Ldap_Exception(null, 'A host parameter is required');
- }
- /* To connect using SSL it seems the client tries to verify the server
- * certificate by default. One way to disable this behavior is to set
- * 'TLS_REQCERT never' in OpenLDAP's ldap.conf and restarting Apache. Or,
- * if you really care about the server's cert you can put a cert on the
- * web server.
- */
- $url = $useSsl ? "ldaps://$host" : "ldap://$host";
- if ($port) {
- $url .= ":$port";
- }
- /* Because ldap_connect doesn't really try to connect, any connect error
- * will actually occur during the ldap_bind call. Therefore, we save the
- * connect string here for reporting it in error handling in bind().
- */
- $this->_connectString = $url;
- $this->disconnect();
- if (!extension_loaded('ldap')) {
- /**
- * @see Zend_Ldap_Exception
- */
- require_once 'Zend/Ldap/Exception.php';
- throw new Zend_Ldap_Exception(null, 'LDAP extension not loaded');
- }
- /* Only OpenLDAP 2.2 + supports URLs so if SSL is not requested, just
- * use the old form.
- */
- $resource = $useSsl ? @ldap_connect($url) : @ldap_connect($host, $port);
- if (is_resource($resource) === true) {
- $this->_resource = $resource;
- $optReferrals = $this->_options['optReferrals'] ? 1 : 0;
- if (@ldap_set_option($resource, LDAP_OPT_PROTOCOL_VERSION, 3) &&
- @ldap_set_option($resource, LDAP_OPT_REFERRALS, $optReferrals)) {
- if ($useSsl ||
- $this->_options['useStartTls'] !== true ||
- @ldap_start_tls($resource)) {
- return $this;
- }
- }
- /**
- * @see Zend_Ldap_Exception
- */
- require_once 'Zend/Ldap/Exception.php';
- $zle = new Zend_Ldap_Exception($resource, "$host:$port");
- $this->disconnect();
- throw $zle;
- }
- /**
- * @see Zend_Ldap_Exception
- */
- require_once 'Zend/Ldap/Exception.php';
- throw new Zend_Ldap_Exception("Failed to connect to LDAP server: $host:$port");
- }
- /**
- * @param string $username The username for authenticating the bind
- * @param string $password The password for authenticating the bind
- * @return Zend_Ldap Provides a fluent interface
- * @throws Zend_Ldap_Exception
- */
- public function bind($username = null, $password = null)
- {
- $moreCreds = true;
- if ($username === null) {
- $username = $this->_getUsername();
- $password = $this->_getPassword();
- $moreCreds = false;
- }
- if ($username === NULL) {
- /* Perform anonymous bind
- */
- $password = NULL;
- } else {
- /* Check to make sure the username is in DN form.
- */
- if (!Zend_Ldap::explodeDn($username)) {
- if ($this->_options['bindRequiresDn']) {
- /* moreCreds stops an infinite loop if _getUsername does not
- * return a DN and the bind requires it
- */
- if ($moreCreds) {
- try {
- $username = $this->_getAccountDn($username);
- } catch (Zend_Ldap_Exception $zle) {
- /**
- * @todo Temporary measure to deal with exception thrown for ldap extension not loaded
- */
- if (strpos($zle->getMessage(), 'LDAP extension not loaded') !== false) {
- throw $zle;
- }
- // end temporary measure
- switch ($zle->getCode()) {
- case Zend_Ldap_Exception::LDAP_NO_SUCH_OBJECT:
- case Zend_Ldap_Exception::LDAP_X_DOMAIN_MISMATCH:
- throw $zle;
- }
- throw new Zend_Ldap_Exception(null,
- 'Failed to retrieve DN for account: ' . $zle->getMessage(),
- Zend_Ldap_Exception::LDAP_OPERATIONS_ERROR);
- }
- } else {
- /**
- * @see Zend_Ldap_Exception
- */
- require_once 'Zend/Ldap/Exception.php';
- throw new Zend_Ldap_Exception(null, 'Binding requires username in DN form');
- }
- } else {
- $username = $this->getCanonicalAccountName($username,
- Zend_Ldap::ACCTNAME_FORM_PRINCIPAL);
- }
- }
- }
- if (!is_resource($this->_resource))
- $this->connect();
- if ($username !== null &&
- $password === '' &&
- $this->_options['allowEmptyPassword'] !== true) {
- /**
- * @see Zend_Ldap_Exception
- */
- require_once 'Zend/Ldap/Exception.php';
- $zle = new Zend_Ldap_Exception(null,
- 'Empty password not allowed - see allowEmptyPassword option.');
- } else {
- if (@ldap_bind($this->_resource, $username, $password))
- return $this;
- $message = $username === null ? $this->_connectString : $username;
- /**
- * @see Zend_Ldap_Exception
- */
- require_once 'Zend/Ldap/Exception.php';
-
- switch (Zend_Ldap_Exception::getLdapCode($this)) {
- case Zend_Ldap_Exception::LDAP_SERVER_DOWN:
- /* If the error is related to establishing a connection rather than binding,
- * the connect string is more informative than the username.
- */
- $message = $this->_connectString;
- }
-
- $zle = new Zend_Ldap_Exception($this->_resource, $message);
- }
- $this->disconnect();
- throw $zle;
- }
- }