<?php
/**
 *  LDAP.php
 *
 *  PHP versions 4 and 5
 *
 *  @package    Auth
 *  @author     Kaoru Sekiguchi <sekiguchi.kaoru@secioss.co.jp>
 *  @copyright  2007 SECIOSS CORPORATION
 *  @version    CVS: $Id$
 */
require_once('Secioss/AutoLogin/Container.php');

define('PASSWORD_SYNTAX', '2.5.4.35');

/**
 *  AutoLogin_Container_LDAP
 *
 *  @package    Auth
 *  @author     Kaoru Sekiguchi <sekiguchi.kaoru@secioss.co.jp>
 *  @copyright  2007 SECIOSS CORPORATION
 *  @version    CVS: $Id$
 */
class AutoLogin_Container_LDAP extends AutoLogin_Container
{
    // {{{ AutoLogin_Container_LDAP
    /**
     *  AutoLogin_Container_LDAPクラスのコンストラクタ
     *
     *  @access public
     *  @param  mixed   $options        LDAPの設定
     *  @return mixed   0:正常終了 PEAR_Error:エラー
     */
    function AutoLogin_Container_LDAP($options)
    {
        parent::AutoLogin_Container($options);
        if ($this->options['pwhash'] == 'AD') {
            $this->options['basedn'] = mb_convert_encoding($this->options['basedn'], 'SJIS');
        }

        $rc = 0;
        if (strtoupper(substr(PHP_OS, 0, 3)) != 'WIN') {
            $rc = $this->_connect();
            register_shutdown_function(array(&$this, '_disconnect'));
        }

        return $rc;
    }
    // }}}

    // {{{ _connect()
    /**
     * LDAPサーバに接続する
     *
     * @access private
     * @return mixed   0:正常終了 PEAR_Error:エラー
     */
    function _connect()
    {
        $this->conn = @ldap_connect($this->options['uri']);
        if (!@ldap_bind($this->conn, $this->options['binddn'], $this->options['bindpw'])) {
            return PEAR::raiseError(ldap_error($this->conn), ldap_errno($this->conn));
        }

        return 0;
    }
    // }}}

    // {{{ _disconnect()
    /**
     * LDAPサーバへの接続を切断する
     *
     * @access private
     */
    function _disconnect()
    {
        @ldap_unbind($this->conn);
    }

    // {{{ fetchData()
    /**
     * LDAPサーバからユーザ情報を取得する
     *
     * @param  string   ユーザ名
     * @return mixed    true:正常終了 PEAR_Error:エラー
     */
    function fetchData($username)
    {
        if (!$username || strlen($username) > 255) {
            return PEAR::raiseError("Invalid user id", AUTO_LOGIN_INVALID_VALUE);
        }

        $tenant = '';
        if (preg_match('/^[^@]+@(.*)$/', $username, $matches)) {
            $tenant = $matches[1];
            $tenant = preg_replace('/([*()])/', '\\$1', $tenant);
        }

        if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') {
            $this->_connect();
        }

        $filter = "(".$this->options['userattr']."=".preg_replace('/([*()])/', '\\$1', $username).")";
        if ($this->options['userfilter']) {
            $filter = '(&'.$filter.$this->options['userfilter'].')';
        }
        $res = @ldap_search($this->conn, $this->options['basedn'], $filter);
        if ($res == false) {
            return PEAR::raiseError(ldap_error($this->conn), ldap_errno($this->conn));
        }

        $num = ldap_count_entries($this->conn, $res);

        if ($num == 0 ) {
            return PEAR::raiseError("User doesn't exist", AUTO_LOGIN_NO_USER);
        } else if ($num != 1) {
            return PEAR::raiseError("User isn't unique", AUTO_LOGIN_ERROR);
        }

        $entries = ldap_get_entries($this->conn, $res);
        $entry = $entries[0];

        if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') {
            $this->_disconnect();
        }

        $this->id = $entry['dn'];

        $this->prop = array();
        for ($i = 0; $i < $entry['count']; $i++) {
            $key = $entry[$i];

            if ($entry[$key]['count'] == 1) {
                $this->prop[$key] = $entry[$key][0];
            } else {
                $v = array();
                for ($j = 0; $j < $entry[$key]['count']; $j++) {
                    $v[] = $entry[$key][$j];
                }
                $this->prop[$key] = $v;
            }
        }

        if (!isset($this->prop['seciossloginid'])) {
            $this->prop['seciossloginid'] = $username;
        }
        if (!isset($this->prop['seciosssystemid'])) {
            $this->prop['seciosssystemid'] = preg_replace('/@[^@]+$/', '', $username);
        }

        if (isset($this->prop[$this->options['loginattr']])) {
            $loginid = $this->prop[$this->options['loginattr']];
            if (is_array($loginid)) {
                $this->loginid = $loginid[0];
            } else {
                $this->loginid = $loginid;
            }
        }

        if (isset($this->prop[$this->options['statusattr']])) {
            $this->status = $this->prop[$this->options['statusattr']];
        }

        if (isset($this->prop[$this->options['pwdlockoutattr']])) {
            if ($this->prop[$this->options['pwdlockoutattr']] == 'TRUE') {;
                $this->pwdlockout = true;
            } else {
                $this->pwdlockout = false;
            }
        }

        if (!$tenant && preg_match('/[^,]+,[^,]+,'.$this->options['tenantattr'].'=([^,]+),'.$this->options['basedn'].'/i', $this->id, $matches)) {
            $tenant = $matches[1];
        }

        if ($this->options['pwdpolicydb']) {
            $rc = $this->_getPwdPolicy($tenant);
            if (PEAR::isError($rc)) {
                return $rc;
            }
        }

        return true;
    }
    // }}}

    // {{{ auth()
    /**
     *  ユーザの認証を行う
     *
     *  @access public
     *  @param  string   パスワード
     *  @return string   0:成功 1:失敗
     */
    function auth($password)
    {
        if (!$password) {
            return 1;
        }

        if (isset($this->prop[$this->options['pwdmustchangeattr']]) && $this->prop[$this->options['pwdmustchangeattr']] == 'TRUE') {
            if (isset($this->prop[$this->options['pwdhistoryattr']])) {
                $pwdhistory = $this->_to_array($this->prop[$this->options['pwdhistoryattr']]);
                if (!preg_match('/#([^#]+)$/', $pwdhistory[0], $matches)) {
                    return 1;
                }
                $oldpasswd = $matches[1];

                $hash = '';
                if (preg_match('/^{([^}]+)}/', $oldpasswd, $matches)) {
                    $hash = $matches[1];
                }

                if ($this->cmpPasswd($password, $oldpasswd, $hash)) {
                    return 0;
                }
            }
        }

        if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') {
            $this->_connect();
        }

        $rc = @ldap_bind($this->conn, $this->id, $password) ? 0 : 1;
        @ldap_bind($this->conn, $this->options['binddn'], $this->options['bindpw']);

        if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') {
            $this->_disconnect();
        }

        return $rc;
    }
    // }}}

    // {{{ getPassword()
    /**
     *  暗号化されているパスワードを復号化して取得する
     *
     *  @access public
     *  @return string   パスワード
     */
    function getPassword($app = null)
    {
        $password = '';

        if (isset($this->prop[$this->options['encryptpwdattr']])) {
            $encryptpwds = $this->_to_array($this->prop[$this->options['encryptpwdattr']]);

            for ($i = 0; $i < count($encryptpwds); $i++) {
                if ($app) {
                    if (preg_match("/^{".$app."}(.+)/i", $encryptpwds[$i], $matches)) {
                        $password = $this->decrypt($matches[1]);
                        break;
                    }
                }
                if (!preg_match("/^{[^}]+}/", $encryptpwds[$i])) {
                    $password = $this->decrypt($encryptpwds[$i]);
                }
            }
        }

        return $password;
    }
    // }}}

    function getTenants($tenant = null)
    {
        if ($tenant) {
            $filter = "(".$this->options['tenantattr']."=$tenant)";
        } else {
            $filter = "(".$this->options['tenantattr']."=*)";
        }
        $res = @ldap_list($this->conn, $this->options['basedn'], $filter, array($this->options['tenantattr'], 'seciossallowedfunction'));
        if ($res == false) {
            return PEAR::raiseError(ldap_error($this->conn), ldap_errno($this->conn));
        }

        $entries = ldap_get_entries($this->conn, $res);
        $tenants = array();
        for ($i = 0; $i < $entries['count']; $i++) {
            $tenants[$entries[$i][$this->options['tenantattr']][0]] = array('func' => isset($entries[$i]['seciossallowedfunction']) ? $entries[$i]['seciossallowedfunction'] : array());
        }

        return $tenants;
    }

    // {{{ setProp
    /**
     *  プロパティへのアクセサ(W)
     *
     *  @access public
     *  @param  array   $prop    プロパティ
     *  @return mixed   true:正常終了 PEAR_Error:エラー
     */
    function setProp($prop)
    {
        if (!$this->id) {
            return PEAR::raiseError("Must fetch data", AUTO_LOGIN_ERROR);
        }

        if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') {
            $this->_connect();
        }

        if (!@ldap_mod_replace($this->conn, $this->id, $prop)) {
            return PEAR::raiseError(ldap_error($this->conn), ldap_errno($this->conn));
        }

        if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') {
            $this->_disconnect();
        }

        return true;
    }
    // }}}

    // {{{ setPassword()
    /**
     *  パスワードを暗号化してLDAPに格納する
     *
     *  @access public
     *  @param  string   パスワード
     *  @return mixed    true:正常終了 PEAR_Error:エラー
     */
    function setPassword($password = null, $app = null, $init = false)
    {
        if (!$this->id) {
            return PEAR::raiseError("Must fetch data", AUTO_LOGIN_ERROR);
        }

        if (!$password || strlen($password) > 255) {
            return PEAR::raiseError("Invalid password", AUTO_LOGIN_INVALID_VALUE);
        }

        if ($this->options['encryptpwdattr']) {
            $encrypt = $this->encrypt($password);
            if (PEAR::isError($encrypt)) {
                return $encrypt;
            }

            $encryptpwds = array();
            if (isset($this->prop[$this->options['encryptpwdattr']])) {
                $encryptpwds = $this->_to_array($this->prop[$this->options['encryptpwdattr']]);
            }
        }

        if ($this->options['randompwdattr']) {
            $pwlen = strlen($password);
            $rndencrypt = $this->encrypt($this->random($pwlen));
            if (PEAR::isError($rndencrypt)) {
                return $rndencrypt;
            }

            $info[$this->options['randompwdattr']] = $rndencrypt;
        }

        if (!$app) {
            if ($this->options['pwdattr']) {
                $hash = $this->options['pwhash'];
                $tab = '';
                if ($hash == 'CRYPT' || $hash == 'MD5' || $hash == 'SHA') {
                    $tab = '{'.$hash.'}';
                }
                $info[$this->options['pwdattr']] = $tab.$this->hashPasswd($password);
            }

            if ($this->options['encryptpwdattr']) {
                $match = false;
                for ($i = 0; $i < count($encryptpwds); $i++) {
                    if (!preg_match("/^{[^}]+}/", $encryptpwds[$i])) {
                        $encryptpwds[$i] = $encrypt;
                        $match = true;
                    }
                }
                if (!$match) {
                    array_push($encryptpwds, $encrypt);
                }
                $info[$this->options['encryptpwdattr']] = $encryptpwds;
            }

            $oldpwdtime = '';
            if ($this->options['pwdtimeattr']) {
                $info[$this->options['pwdtimeattr']] = date("YmdHis")."Z";
                if (isset($this->prop[$this->options['pwdtimeattr']])) {
                    $oldpwdtime = $this->prop[$this->options['pwdtimeattr']];
                }
            }

            if ($this->options['pwdhistoryattr']) {
                $oldpasswd = isset($this->prop[$this->options['pwdattr']]) ? $this->prop[$this->options['pwdattr']] : '';
                $oldpwdtime = $oldpwdtime ? $oldpwdtime : '19700101000000Z';

                if ($oldpasswd) {
                    $pwdhistory = array();
                    $oldpasswd = $oldpwdtime.'#'.PASSWORD_SYNTAX.'#'.strlen($oldpasswd).'#'.$oldpasswd;
                    $pwinhistory = $this->options['pwinhistory'];
                    if ($pwinhistory) {
                        if (isset($this->prop[$this->options['pwdhistoryattr']])) {
                            $pwdhistory = $this->_to_array($this->prop[$this->options['pwdhistoryattr']]);
                        }

                        if (count($pwdhistory) >= $pwinhistory - 1) {
                            for ($i = 0; $i < count($pwdhistory) - $pwinhistory + 1; $i++) { 
                                array_pop($pwdhistory);
                            }
                        }
                    }
                    array_unshift($pwdhistory, $oldpasswd);

                    $info[$this->options['pwdhistoryattr']] = $pwdhistory;
                }
            }

            $info[$this->options['pwdmustchangeattr']] = $init ? 'TRUE' : 'FALSE';
            $info[$this->options['pwdexpwarnedattr']] = 'FALSE';
        } else {
            if ($this->options['encryptpwdattr']) {
                $match = false;
                for ($i = 0; $i < count($encryptpwds); $i++) {
                    if (preg_match("/^{".$app."}/i", $encryptpwds[$i])) {
                        $encryptpwds[$i] = "{".$app."}$encrypt";
                        $match = true;
                    }
                }
                if (!$match) {
                    array_push($encryptpwds, "{".$app."}$encrypt");
                }
                $info[$this->options['encryptpwdattr']] = $encryptpwds;
            }
        }

        if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') {
            $this->_connect();
        }

        if (!@ldap_mod_replace($this->conn, $this->id, $info)) {
            if (ldap_errno($this->conn) == 19) {
                return PEAR::raiseError("Invalid password", AUTO_LOGIN_INVALID_VALUE);
            } else {
                return PEAR::raiseError(ldap_error($this->conn), ldap_errno($this->conn));
            }
        }

        if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') {
            $this->_disconnect();
        }

        return true;
    }
    // }}}

    function deletePassword($app)
    {
        if (!$app) {
            return true;
        }

        if (isset($this->prop[$this->options['encryptpwdattr']])) {
            $encryptpwds = $this->_to_array($this->prop[$this->options['encryptpwdattr']]);

            for ($i = 0; $i < count($encryptpwds); $i++) {
                if (preg_match("/^{".$app."}/i", $encryptpwds[$i])) {
                    if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') {
                        $this->_connect();
                    }

                    if (!@ldap_mod_del($this->conn, $this->id, array($this->options['encryptpwdattr'] => $encryptpwds[$i]))) {
                        return PEAR::raiseError(ldap_error($this->conn), ldap_errno($this->conn));
                    }

                    if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') {
                        $this->_disconnect();
                    }

                    return true;
                }
            }
        }

        return true;
    }

    // {{{ getPwdChangedTime()
    /**
     *  パスワード変更時間を取得する
     *
     *  @access public
     *  @return string   パスワード変更時間（秒）
     */
    function getPwdChangedTime()
    {
        if (!$this->options['pwdtimeattr']) {
            return 0;
        }

        $pwdtime = $this->prop[$this->options['pwdtimeattr']];

        if (!$pwdtime) {
            return 0;
        }

        if (!preg_match('/^([0-9]{4})([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{2})Z$/', $pwdtime, $matches)){
            return 0;
        }

        return mktime($matches[4], $matches[5], $matches[6], $matches[2], $matches[3], $matches[1]);
    }
    // }}}

    // {{{ getPwdHistory()
    /**
     *  パスワード履歴を取得する
     *
     *  @access public
     *  @return mixed   パスワード履歴
     */
    function getPwdHistory()
    {
        if (!$this->options['pwdhistoryattr'] || !isset($this->prop[$this->options['pwdhistoryattr']])) {
            return array();
        }

        $pwdhistory = $this->_to_array($this->prop[$this->options['pwdhistoryattr']]);

        for ($i = 0; $i < count($pwdhistory); $i++) {
            $pwdhistory[$i] = preg_replace('/^[^#]+#[^#]+#[^#]+#/', '', $pwdhistory[$i]);
        }
        array_unshift($pwdhistory, $this->prop[$this->options['pwdattr']]);

        return $pwdhistory;
    }
    // }}}

    function isPwdMustChange()
    {
        if (isset($this->prop[$this->options['pwdmustchangeattr']]) &&
            $this->prop[$this->options['pwdmustchangeattr']] == 'TRUE') {
            return true;
        }
        return false;
    }

    // {{{ _getPwdPolicy($tenant)
    function _getPwdPolicy($tenant)
    {
        if (!$tenant) {
            return true;
        }

        if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') {
            $this->_connect();
        }

        $res = @ldap_search($this->conn, $this->options['basedn'], "(&(seciossPwdPolicyEnabled=TRUE)(".$this->options['tenantattr']."=$tenant))");
        if ($res == false) {
            return PEAR::raiseError(ldap_error($this->conn), ldap_errno($this->conn));
        }

        $num = ldap_count_entries($this->conn, $res);
        if ($num == 0 ) {
            return true;
        }

        $entries = ldap_get_entries($this->conn, $res);
        $pwdpolicy = $entries[0];

        if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') {
            $this->_disconnect();
        }

        for ($i = 0; $i < $pwdpolicy['count']; $i++) {
            $key = $pwdpolicy[$i];
            if ($pwdpolicy[$key]['count'] == 1) {
                $v = $pwdpolicy[$key][0];
            } else {
                $v = array();
                for ($j = 0; $j < $pwdpolicy[$key]['count']; $j++) {
                    $v[] = $pwdpolicy[$key][$j];
                }
            }
            if (!is_array($v) && preg_match('/^ *$/', $v)) {
                continue;
            }

            switch ($key) {
              case 'pwdminlength':
                $this->options['pwminlen'] = $v;
                break;
              case 'seciosspwdmaxlength':
                $this->options['pwmaxlen'] = $v;
                break;
              case 'seciosspwdallowedchars':
                $v = is_array($v) ? $v : array($v);
                $this->options['pwallow'] = $v;
                break;
              case 'seciosspwddeniedchars':
                $v = is_array($v) ? $v : array($v);
                $this->options['pwdeny'] = $v;
                break;
              case 'pwdminage':
                $this->options['pwminage'] = $v;
                break;
              case 'pwdinhistory':
                $this->options['pwinhistory'] = $v;
                break;
              case 'pwdcheckquality':
                $this->options['pwstrength'] = $v;
                break;
            }
        }

        return true;
    }
    // }}}

    // {{{ _setDefaults()
    /**
     * optionsにデフォルト値を設定する
     *
     * @access private
     */
    function _setDefaults()
    {
        parent::_setDefaults();

        $this->options['uri']         = 'ldap://localhost';
        $this->options['binddn']      = '';
        $this->options['bindpw']      = '';
        $this->options['basedn']      = '';
        $this->options['userattr']    = 'uid';
        $this->options['pwdattr']     = 'userpassword';
        $this->options['loginattr']   = 'uid';
        $this->options['statusattr']  = 'seciossaccountstatus';
        $this->options['encryptpwdattr'] = 'seciossencryptedpassword';
        $this->options['randompwdattr'] = 'seciossrandompassword';
        $this->options['pwdtimeattr'] = '';
        $this->options['tenantattr'] = 'o';
        $this->options['pwdhistoryattr'] = '';
        $this->options['pwdmustchangeattr'] = 'seciosspwdmustchange';
        $this->options['pwdexpwarnedattr'] = 'seciosspwdexpwarned';
        $this->options['pwdlockoutattr'] = 'seciosspwdlockout';
        $this->options['pwdpolicydb'] = '';
        $this->options['secretattr'] = 'seciossotpinitsecret';
        $this->options['userfilter'] = '';
    }
    // }}}

    // {{{ setSecret()
    /**
     *  added by okazaki 20090710
     *  シークレットを暗号化してLDAPに格納する
     *
     *  @access public
     *  @param  string   シークレット
     *  @return mixed    true:正常終了 PEAR_Error:エラー
     */
    function setSecret($secret, $pin = null, $deviceid = null, $device = null)
    {
        if (!$secret || strlen($secret) > 255) {
            return PEAR::raiseError("Invalid secret", AUTO_LOGIN_INVALID_VALUE);
        }

        $prop = array();
        if (array_search('seciossOtpUser', $this->prop['objectclass']) === false) {
            $prop['objectclass'] = $this->prop['objectclass'];
            array_push($prop['objectclass'], 'seciossOtpUser');
        }

        $encrypt = $this->encrypt($secret);
        if (PEAR::isError($encrypt)) {
            return $encrypt;
        }
        $prop[$this->options['secretattr'].($device ? ";x-dev-$device" : '')] = $encrypt;

        if ($pin) {
            $encrypt = $this->encrypt($pin);
            if (PEAR::isError($encrypt)) {
                return $encrypt;
            }
            $prop['seciossotpsecretpin'.($device ? ";x-dev-$device" : '')] = $encrypt;
        }
        if ($deviceid) {
            $attr = 'seciossdeviceid'.($device ? ";x-dev-$device" : '');
            if (isset($this->prop[$attr]) && !preg_match('/^ *$/', $this->prop[$attr])) {
                if ($this->prop[$attr] != $deviceid) {
                    return PEAR::raiseError("Invalid device id", AUTO_LOGIN_BAD_PASSWD);
                }
            } else {
                $prop[$attr] = $deviceid;
            }
        }

        return $this->setProp($prop);
    }
    // }}}

    // {{{ getSecret()
    /**
     *  added by okazaki 20090710
     *  暗号化されているシークレットを復号化して取得する
     *
     *  @access public
     *  @return string   シークレット
     */
    function getSecret($app = null)
    {
        $secret = '';

        if (isset($this->prop[$this->options['secretattr']])) {
            $encrypt = $this->prop[$this->options['secretattr']];
            $secret = $this->decrypt($encrypt);
        }

        return $secret;
    }
    // }}}

}

?>
