<?php
/**
 *  AutoLogin.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("PEAR.php");
require_once("HTTP/Request.php");

define('ALCOOKIE', 'secioss_autologin');
define('PWD_LEN', 12);
if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
    define('WINPWDCMD', '/opt/secioss/sbin/chwinpasswd');
} else {
    define('WINPWDCMD', 'perl /secioss/sbin/chwinpasswd');
}

/**
 *  AutoLogin
 *
 *  @package    Auth
 *  @author     Kaoru Sekiguchi <sekiguchi.kaoru@secioss.co.jp>
 *  @copyright  2007 SECIOSS CORPORATION
 *  @version    CVS: $Id$
 */
class AutoLogin
{
    /**
     * ユーザ名
     * @var array
     */
    var $username = null;

    /**
     * アプリケーション名
     * @var array
     */
    var $app = null;

    /**
     * ログイン時にリダイレクトするURL
     * @var array
     */
    var $url = null;

    /**
     * POSTするユーザ名のname
     * @var array
     */
    var $postUser = null;

    /**
     * POSTするパスワードのname
     * @var array
     */
    var $postPass = null;

    /**
     * POSTするデータ
     * @var array
     */
    var $postData = null;

    var $recirect = null;

    var $loginid = null;

    var $password = null;

    var $domain = null;

    var $sessioncookie = null;

    var $options = null;

    // {{{ AutoLogin
    /**
     *  AutoLoginクラスのコンストラクタ
     *
     *  @access public
     *  @param  string  $storageDriver  ストレージドライバの型
     *  @param  mixed   $options        ストレージドライバの設定
     *  @param  string  $username       ユーザ名
     *  @param  mixed   $app            アプリケーション名
     *  @param  string  $domain         ドメイン
     *  @param  string  $url            ログイン時にリダイレクトするURL
     *  @param  string  $postUser       POSTするユーザ名のname
     *  @param  string  $postPass       POSTするパスワードのname
     *  @param  mixed   $postData       POSTするデータ
     *  @param  mixed   $cookie         送信するcookie
     *  @param  mixed   $content        受信したコンテンツに対する設定
     *  @param  mixed   $debug          デバッグ情報を出力するファイル
     *  @return mixed   0:正常終了 PEAR_Error:エラー
     */
    function AutoLogin($storageDriver, $options, $app = null, $url = null, $postUser = null , $postPass = null, $postData = null, $cookie = null, $content = null, $debug = null)
    {
        if (isset($_SERVER['HTTP_HOST'])) {
            $host = $_SERVER['HTTP_HOST'];
        } else if (isset($_SERVER['SERVER_NAME'])) {
            $host = $_SERVER['SERVER_NAME'];
        }

        $this->options = $options;
        $this->app = $app;

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

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

        if (isset($_GET['url_back'])) {
            $url['back'] = $_GET['url_back'];
        }
        if (!isset($url['fatal'])) {
            $url['fatal'] = "/error/HTTP_INTERNAL_SERVER_ERROR.html.var";
        }
        if (!isset($url['password'])) {
            $url['password'] = "/user/password.php";
        }
        $this->url = $url;

        $this->postUser = $postUser;
        $this->postPass = $postPass;
        if (is_array($postData)) {
            $this->postData = $postData;
        } else {
            $this->postData = array();
        }
        if (is_array($cookie)) {
            $this->cookie = $cookie;
        } else {
            $this->cookie = array();
        }

        $this->content = $content;
        if (isset($this->content['check'])) {
            $this->content['check'] = preg_quote($this->content['check'], "/");
        }

        $this->debug = $debug;

        $storage = $this->_factory($storageDriver, $options);
        if (PEAR::isError($storage)) {
            return $storage;
        }

        $this->storage = $storage;

        return 0;
    }
    // }}}

    // {{{ set()
    /**
     * 値を設定する
     *
     * @access public
     * @return boolean  true:正常終了 PEAR_Error:エラー
     */
    function set($key, $value)
    {
        return $this->storage->set($key, $value);
    }
    // }}}

    // {{{ setUser()
    /**
     * ユーザ情報を取得する
     *
     * @access public
     * @return boolean  true:正常終了 PEAR_Error:エラー
     */
    function setUser($username, $auth = false)
    {
        $this->username = $username;

        $rc = $this->storage->fetchData($username);
        if (PEAR::isError($rc)) {
            if (!$auth) {
                $this->redirect('fatal');
            }
            return $rc;
        }

        return true;
    }
    // }}}

    // {{{ start()
    /**
     * 自動ログインを行う
     *
     * @access public
     * @return boolean  true:正常終了 PEAR_Error:エラー
     */
    function start()
    {
        if (isset($_SERVER['REMOTE_USER_LOGGEDIN'])) {
            $loggedin = true;
            if (isset($this->url['session'])) {
                $loggedin = false;
                $req =& new HTTP_Request($this->url['session']);
                $req->addHeader('User-Agent', $_SERVER['HTTP_USER_AGENT']);
                $req->setMethod(HTTP_REQUEST_METHOD_GET);
                foreach ($_COOKIE as $key => $value) {
                    $req->addCookie($key, $value);
                }
                $res = $req->sendRequest();
                if (!PEAR::isError($res)) {
                    $response = $req->getResponseCode();
                    if ($response < 300) {
                        $body = $req->getResponseBody();
                        if (!isset($this->content['sessionfail']) || !preg_match('/'.$this->content['sessionfail'].'/', $body)) {
                            $loggedin = true;
                        }
                    }
                }
            }
            if ($loggedin) {
                $back = isset($this->url['back']) ? $this->url['back'] : $this->url['login'];
                header('Location: '.$back);
                return true;
            }
        }

        if (PEAR::isError($rc = $this->login())) {
            $querystr = null;
            if ($this->app) {
                $querystr = 'ssoapp='.$this->app;
            }
            $errcode = $rc->getCode();
            switch ($errcode) {
              case AUTO_LOGIN_NO_PASSWD:
                if ($querystr) {
                    $querystr = "$querystr&msg=USS_NML_002";
                } else {
                    $querystr = "msg=USS_NML_002";
                }
                $this->redirect('password', $querystr);
                break;
              case AUTO_LOGIN_BAD_PASSWD:
                if ($querystr) {
                    $querystr = "$querystr&msg=USS_NML_003";
                } else {
                    $querystr = "msg=USS_NML_003";
                }
                $this->redirect('password', $querystr);
                break;
              default:
                $this->redirect('fatal', $querystr);
                break;
            }

            return $rc;
        }

        return true;
    }
    // }}}

    // {{{ login()
    /**
     * ログインを実行後、リダイレクトする
     *
     * @access public
     * @return mixed    true:正常終了 PEAR_Error:エラー
     */
    function login()
    {
        if (isset($_GET['url_back']) && isset($this->url['check_url_back']) && preg_match('/^https?:\/\//', $_GET['url_back']) && !preg_match('#'.$this->url['check_url_back'].'#', $_GET['url_back'])) {
            return PEAR::raiseError("Bad back url", AUTO_LOGIN_ERROR);
        }
        $loginid = $this->storage->getLoginId();
        if (!$loginid) {
            return PEAR::raiseError("No login id", AUTO_LOGIN_ERROR);
        }
        $this->loginid = $loginid;

        if ($this->otp) {
            $password = $this->random(PWD_LEN);
        } else {
            $password = $this->storage->getPassword($this->app);
            if (PEAR::isError($password)) {
                return $password;
            } else if (!$password) {
                return PEAR::raiseError("No password", AUTO_LOGIN_NO_PASSWD);
            }
            if (isset($this->options['pwsalt']) && $this->options['pwsalt']) {
                $password .= $this->options['pwsalt'];
                if (isset($this->options['pwmaxlen']) && $this->options['pwmaxlen']) {
                    $password = substr($password, 0, $this->options['pwmaxlen']);
                }
            }
        }
        $this->password = $password;

        if ($this->otp || (isset($this->options['sync']) && $this->options['sync'])) {
            $rc = $this->storage->setPassword($password);
            if (PEAR::isError($rc)) {
                return $rc;
            }
        }

        if (isset($_SERVER['HTTP_X_FORWARDED_FOR']) && $_SERVER['HTTP_X_FORWARDED_FOR']) {
            $ipaddr = $_SERVER['HTTP_X_FORWARDED_FOR'];
        } else {
            $ipaddr = $_SERVER['REMOTE_ADDR'];
        }

        $querystr = '';
        foreach($_GET as $param => $value) {
            if (!isset($this->postData[$param])) {
                $querystr = $querystr ? $querystr.'&'."$param=$value" : "$param=$value";
            }
        }

        $domain = $this->domain;

        if ($this->sessioncookie) {
            $now = time();
            $hmac = hash_hmac('sha1', $now, $this->storage->getSecretKey());
            if (!setcookie($this->sessioncookie, "$now\t$hmac", 0, '/', $domain)) {
                return PEAR::raiseError("Can't set cookie ".$this->sessioncookie, AUTO_LOGIN_ERROR);
            }
        }

        if ($this->redirect == 'post') {
            $this->_postRedirect($this->url['login'], $loginid, $password);
            return true;
        }

        $login_url = preg_split('/, */', $this->url['login']);
        $url = array_shift($login_url);

        $login = false;
        $authenticated = false;
        $oldurl = $url;
        $body = '';
        $cookies = array();
        foreach ($_COOKIE as $key => $value) {
            if ($key == ALCOOKIE) continue;
            $cookies[$key] = array('name' => $key, 'value' => $value);
        }
        foreach ($this->cookie as $key => $value) {
            $value = $this->_parsePostData($value);
            $cookies[$key] = array('name' => $key, 'value' => $value);
        }

        for ($i = 0; $i < 10; $i++) {
            $req =& new HTTP_Request($url);
            $req->addHeader('User-Agent', $_SERVER['HTTP_USER_AGENT']);
            if ($login && !$authenticated) {
                $req->setMethod(HTTP_REQUEST_METHOD_POST);

                $req->addHeader('Referer', isset($this->url['referer']) ? $this->url['referer'] : $oldurl);
                $req->addHeader('X-Forwarded-For', $ipaddr);
                if ($this->postUser) {
                    $req->addPostData($this->postUser, $loginid);
                }
                if ($this->postPass) {
                    $req->addPostData($this->postPass, $password);
                }
                if ($body && preg_match_all('/<input[^>]*type="hidden"[^>]*name="([^">]+)"[^>]*value="([^">]*)"/i', $body, $matches)) {
                    for ($i = 0; $i < count($matches[0]); $i++) {
                        if ($matches[1][$i] == $this->postUser || $matches[1][$i] == $this->postPass) {
                            continue;
                        }
                        $req->addPostData($matches[1][$i], $matches[2][$i]);
                    }
                }
                if ($body && preg_match_all('/<input[^>]*name="([^">]+)"[^>]*type="hidden"[^>]*value="([^">]*)"/i', $body, $matches)) {
                    for ($i = 0; $i < count($matches[0]); $i++) {
                        if ($matches[1][$i] == $this->postUser || $matches[1][$i] == $this->postPass) {
                            continue;
                        }
                        $req->addPostData($matches[1][$i], $matches[2][$i]);
                    }
                }
                foreach ($this->postData as $key => $value) {
                    if ($key == 'query_string' && $value == 'all') {
                        foreach($_GET as $query_key => $query_val) {
                            $req->addPostData($query_key, $query_val);
                        }
                        continue;
                    }

                    $value = $this->_parsePostData($value);
                    $req->addPostData($key, $value);
                }
                $authenticated = true;
            } else {
                $req->setMethod(HTTP_REQUEST_METHOD_GET);
                $req->addHeader('Referer', isset($this->url['referer']) ? $this->url['referer'] : $oldurl);
                $req->addHeader('X-Forwarded-For', $ipaddr);
            }

            foreach ($cookies as $key => $cookie) {
                $req->addCookie($cookie['name'], $cookie['value']);
            }

            for ($i = 0; $i < 5; $i++) {
                $res = $req->sendRequest();
                if (PEAR::isError($res)) {
                    return PEAR::raiseError("Send request failure(".$res->getMessage().")", AUTO_LOGIN_ERROR);
                }

                $response = $req->getResponseCode();
                if ($response >= 500) {
                    if (isset($this->content['return_error']) && $this->content['return_error']) {
                        if (isset($this->url['back'])) {
                            $this->url['back'] = null;
                        }
                    } else {
                        return PEAR::raiseError("Bad response code($response)", AUTO_LOGIN_ERROR);
                    }
                } else if ($response) {
                    break;
                }
            }

            $rescookies = $req->getResponseCookies();
            if (is_array($rescookies)) {
                foreach ($rescookies as $rescookie) {
                    $cookies[$rescookie['name']] = $rescookie;
                }
            }

            $body = $req->getResponseBody();
            $headers = $req->getResponseHeader();

            if ($this->debug) {
                file_put_contents($this->debug, date("M d H:i:s Y")." $url $response\n", FILE_APPEND);
                file_put_contents($this->debug, $headers, FILE_APPEND);
                file_put_contents($this->debug, "\n$body\n\n", FILE_APPEND);
            }

            $oldurl = $url;
            if (!$login && isset($headers['location'])) {
                $url = $this->_constructUrl($headers['location'], $url);
            } else if (!$login && preg_match('/window\.location\.href = "([^"]+)"/', $body, $matches)) {
                $url = $this->_constructUrl($matches[1], $url);
            } else if ($login) {
                if (isset($this->content['authfail']) &&
                    preg_match('/'.$this->content['authfail'].'/', $body)) {
                    return PEAR::raiseError("Authentication failure", AUTO_LOGIN_BAD_PASSWD);
                }
                if (isset($this->content['check']) &&
                    !preg_match('/'.$this->content['check'].'/', $body)) {
                    if (isset($this->content['return_error']) && $this->content['return_error']) {
                        if (isset($this->url['back'])) {
                            $this->url['back'] = null;
                        }
                    } else {
                        return PEAR::raiseError("Bad content", AUTO_LOGIN_ERROR);
                    }
                }
                break;
            } else if (preg_match('/<form .*action="([^"]+)".*>.*name="'.$this->postUser.($this->postPass ? '".*name="'.$this->postPass.'"' : '').'/si', $body, $matches)) {
                $url = $this->_constructUrl($matches[1], $url);
                if (preg_match('/\?/', $url)) {
                    $url = $url.'&'.$querystr;
                } else {
                    $url = $url.'?'.$querystr;
                }
                $login = true;
            } else {
                if (count($login_url)) {
                    $url = array_shift($login_url);
                } else {
                    $login = true;
                }
            }
        }

        $appcookies = isset($_COOKIE[ALCOOKIE]) ? preg_split('/#/', $_COOKIE[ALCOOKIE]) : array();
        if (is_array($cookies)) {
            foreach ($cookies as $key => $cookie) {
                if ($cookie['name'] == 'auth_tkt') {
                    continue;
                }
                if (preg_match('/[,; \t\r\n]/', $cookie['value'])) {
                    $cookie['value'] = base64_encode($cookie['value']);
                }
                if (isset($cookie['path'])) {
                    if (isset($this->options['rewrite_cookiepath']) && $this->options['rewrite_cookiepath']) {
                        list($match, $substitute) = preg_split('/, */', $this->options['rewrite_cookiepath']);
                        $cookiepath = preg_replace('#^'.$match.'$#', $substitute, $cookie['path']);
                    } else if (!isset($this->url['back']) || preg_match('#^'.$cookie['path'].'#', preg_replace('#^https?://[^/]+#', '', $this->url['back']))) {
                        $cookiepath = $cookie['path'];
                    } else {
                        $cookiepath = '/';
                    }
                } else {
                    $cookiepath = '/';
                }
                if (!setrawcookie($cookie['name'], $cookie['value'], isset($cookie['expires']) ? strtotime($cookie['expires']) : 0, $cookiepath, $domain, isset($cookie['secure']) ? $cookie['secure'] : false)) {
                    return PEAR::raiseError("Can't set cookie ".$cookie['name'], AUTO_LOGIN_ERROR);
                }
                if (isset($this->options['cookiename']) && $this->options['cookiename'] == $cookie['name']) {
                    array_push($appcookies, $cookie['name']."=$cookiepath");
                }
            }
        }
        if (count($appcookies)) {
            setcookie(ALCOOKIE, join('#', $appcookies), 0, null, $domain);
        }

        if (isset($this->url['back'])) {
            $back = $this->url['back'];
            if (preg_match('/%{([^}]+)}/', $back, $matches) && isset($_GET[$matches[1]])) {
                $back = preg_replace('/%{'.$matches[1].'}/', $_GET[$matches[1]], $back);
            }
            header('Location: '.$back);
        } else if (isset($this->url['webdav'])) {
            $req->reset($this->url['webdav']);
            $req->setMethod(HTTP_REQUEST_METHOD_OPTIONS);
            $req->sendRequest();
            print $req->getResponseBody();
        } else if (isset($headers['location'])) {
            header('Location: '.$this->_constructUrl($headers['location'], $url));
        } else {
            print $body;
        }
        return true;
    }
    // }}}

    // {{{ auth()
    /**
     *  ユーザの認証を行う
     *
     *  @access public
     *  @param  string   パスワード
     *  @return string   0:成功 1:失敗
     */
    function auth($password)
    {
        $rc = $this->storage->auth($password);
        if (!$rc) {
            $status = $this->storage->getStatus();
            if ($status && $status != 'active') {
                $rc = 2;
            }
        }
        if (!$rc) {
            $lockout = $this->storage->getPwdLockout();
            if ($lockout) {
                $rc = 3;
            }
        }

        return $rc;
    }
    // }}}

    // {{{ changePasswd()
    /**
     * パスワードを変更する
     *
     * @access public
     * @param  string   $password       パスワード
     * @return mixed    true:正常終了 PEAR_Error:エラー
     */
    function changePasswd($password = null, $init = false, $oldpasswd = null, $euser = null)
    {
        $rc = 0;
        $pwstrength = true;
        $random = $this->random(PWD_LEN);
        $winpasswd = isset($this->options['winpasswd']) && $this->options['winpasswd'] ? true : false;

        $object = $this->storage;
        $prop = $object->getProp();
        if ($winpasswd) {
            exec(WINPWDCMD.' '.$object->id.' '.base64_encode($object->hashPasswd($password)).' '.base64_encode($object->hashPasswd($oldpasswd)), $output, $errcode);
            if ($errcode) {
                if ($errcode == 53) {
                    return PEAR::raiseError("Password is invalid", AUTO_LOGIN_INVALID_VALUE);
                } else {
                    return PEAR::raiseError(WINPWDCMD, AUTO_LOGIN_ERROR);
                }
            }
        } else {
            if (!$this->app && $init == false) {
                $rc = $this->storage->validatePasswd($password);
            }
            if ($rc == AUTO_LOGIN_WEAK_PASSWD_WARN) {
                $pwstrength = false;
            } else if ($rc) {
                switch($rc) {
                  case AUTO_LOGIN_INVALID_VALUE:
                    $message = "Invalid password";
                    break;
                  case AUTO_LOGIN_MIN_AGE:
                    $message = "Minimum time doesn't elapse between modification to the password";
                    break;
                  case AUTO_LOGIN_IN_HISTORY:
                    $message = "Password is in th history";
                    break;
                  case AUTO_LOGIN_WEAK_PASSWD:
                    $message = $this->storage->error;
                    break;
                  default:
                    $message = "Internal error";
                }
                return PEAR::raiseError($message, $rc);
            }

            $rc = $this->storage->setPassword($password, $this->app, $init, $random);
            if (PEAR::isError($rc)) {
                return $rc;
            }
        }

        if (isset($this->options['sync']) && $this->options['sync']) {
            $passwdfile = '/opt/secioss/var/lib/passwd/passwd.log';
            $lock_create = file_exists("$passwdfile.lock") ? false : true;
            $lp = fopen("$passwdfile.lock", 'w');
            if (!$lp) {
                return PEAR::raiseError("Can't open $passwdfile.lock", AUTO_LOGIN_ERROR);
            }
            flock($lp, LOCK_EX);

            $file_create = file_exists($passwdfile) ? false : true;
            if (file_put_contents($passwdfile, $this->username.",$password,$random,".($euser ? urlencode($euser) : '')."\n", FILE_APPEND) === false) {
                return PEAR::raiseError("Can't write $passwdfile", AUTO_LOGIN_ERROR);
            }
            if ($lock_create) {
                chmod("$passwdfile.lock", 0660);
            }
            if ($file_create) {
                chmod($passwdfile, 0660);
            }

            fclose($lp);
        } else if (isset($this->options['command']) && $this->options['command']) {
            $commands = $this->options['command'];
            if (!is_array($commands)) {
                $commands = array($commands);
            }

            $complete = 0;
            for ($i = 0; $i < count($commands); $i++) {
                $command = $this->_parseCommand($commands[$i], $password, $oldpasswd);
                exec($command, $output, $errcode);
                if ($errcode) {
                    if (!$winpasswd && isset($this->options['rollback']) && $this->options['rollback']) {
                        for ($j = 0; $j < $complete; $j++) {
                            $command = $this->_parseCommand($commands[$i], $oldpasswd, $password);
                            exec($command, $output, $errcode);
                        }
                        $pwdhistoryattr = $object->options['pwdhistoryattr'];
                        if ($pwdhistoryattr && !isset($prop[$pwdhistoryattr])) {
                            $prop[$pwdhistoryattr] = '';
                        }
                        $object->setProp($prop);
                    }
                    return PEAR::raiseError("Changing password failed: ".$commands[$i]." failed($errcode)", AUTO_LOGIN_ERROR);
                }
                $complete++;
            }
        }

        if ($rc && $pwstrength == false) {
            return PEAR::raiseError($this->storage->error, AUTO_LOGIN_WEAK_PASSWD_WARN);
        }

        return $rc;
    }

    function deletePasswd()
    {
        return $this->storage->deletePassword($this->app);
    }

    // {{{ updateSecret()
    /**
     * added by okazaki 20090710
     * シークレットを設定する
     *
     * @access public
     * @param  string   $secret       シークレット
     * @return mixed    true:正常終了 PEAR_Error:エラー
     */
    function updateSecret($secret, $pin = null, $deviceid = null, $device = null, $otplen = null, $timewindow = null)
    {
        $rc = 0;

        if (!$this->app) {
            $rc = $this->storage->validateSecret($secret, $pin);
        }
        if ($rc) {
            switch($rc) {
              case AUTO_LOGIN_INVALID_VALUE:
                $message = "Invalid secret or PIN";
                break;
            }
            return PEAR::raiseError($message, $rc);
        }
        if ($otplen && $otplen != 6 && $otplen != 8) {
            return PEAR::raiseError("Invalid otp length", AUTO_LOGIN_INVALID_VALUE);
        }
        if ($timewindow && $timewindow != 30 && $timewindow != 60) {
            return PEAR::raiseError("Invalid time window", AUTO_LOGIN_INVALID_VALUE);
        }

        return $this->storage->setSecret($secret, $pin, $deviceid, $device, $otplen, $timewindow);
    }

    function getTenants($tenant = null)
    {
        return $this->storage->getTenants($tenant);
    }

    // {{{ _factory()
    /**
     * ストレージドライバのオブジェクトを返す
     *
     * @access private
     * @static
     * @param  string $driver  ストレージクラスの型
     * @param  string $options ストレージクラスの設定
     * @return object Object   Storageオブジェクト 
     */
    function _factory($driver, $options = '')
    {
        if (isset($options['redirect'])) {
            $this->redirect = $options['redirect'];
        } else {
            $this->redirect = 'header';
        }
        if (isset($options['otp'])) {
            $this->otp = $options['otp'];
        } else {
            $this->otp = false;
        }

        $storage_path = 'AutoLogin/Container/' . $driver . '.php';
        $storage_class = 'AutoLogin_Container_' . $driver;

        require_once $storage_path;

        return new $storage_class($options);
    }
    // }}}

    // {{{ redirect()
    /**
     * 指定した画面にリダイレクトする
     *
     * @access public
     * @return void
     */
    function redirect($dst, $querystr = null)
    {
        if (isset($this->url[$dst])) {
            $url = $this->url[$dst];
            if (preg_match('/%{([^}]+)}/', $url, $matches) && isset($_GET[$matches[1]])) {
                $url = preg_replace('/%{'.$matches[1].'}/', $_GET[$matches[1]], $url);
            }

            if (isset($this->url['back'])) {
                $back = $this->url['back'];
                if (preg_match('/%{([^}]+)}/', $back, $matches) && isset($_GET[$matches[1]])) {
                    $back = preg_replace('/%{'.$matches[1].'}/', $_GET[$matches[1]], $back);
                }
            } else {
                if (isset($_SERVER['REQUEST_URI'])) {
                    $back = $_SERVER['REQUEST_URI'];
                } else {
                    $back = $_SERVER['SCRIPT_NAME'];
                    if (isset($_SERVER['QUERY_STRING'])) {
                        $back .= '?'.$_SERVER['QUERY_STRING'];
                    }
                }
            }
            if (!preg_match('/^http/', $back)) {
                $port = $_SERVER['SERVER_PORT'];
                $back = ($port == 443 ? 'https' : 'http').'://'.$_SERVER['SERVER_NAME'].($port != 443 && $port != 80 ? ":$port" : "").$back;
            }
            $back = urlencode($back);
            if ($querystr) {
                $back = "back=$back&$querystr";
            } else {
                $back = "back=$back";
            }
            if (preg_match('/\?/', $url)) {
                $back = "&$back";
            } else {
                $back = "?$back";
            }

            header('Location: '.$url.$back);
        }
    }
    // }}}

    function _postRedirect($url, $loginid = null, $password = null)
    {
        if (preg_match('/\?.+/', $url)) {
            $url .= '&post_redirect=on';
        } else {
            $url .= '?post_redirect=on';
        }

        header('Content-Type: text/html; charset=UTF-8');
        header('Pragma: no-cache');
        header('Cache-Control: no-cache');
        header('Expires: Thu, 01 Des 1994 16:00:00 GMT');

        print('<html>
<body onLoad="document.redirect.submit()">
<form name="redirect" method="POST" action="'.$url.'">');

        if ($this->postUser) {
            print('<input type="hidden" name="'.$this->postUser.'" value="'.$loginid.'">');
        }
        if ($this->postPass) {
            print('<input type="hidden" name="'.$this->postPass.'" value="'.$password.'">');
        }
        foreach ($this->postData as $key => $value) {
            $value = $this->_parsePostData($value);
            print('<input type="hidden" name="'.$key.'" value="'.$value.'">');
        }

        print('</form>
</body>
</html>');

        return true;
    }

    function _parsePostData($value)
    {
        if (preg_match('/%{([^}]+)}/', $value, $matches)) {
            if (!isset($_GET[$matches[1]])) {
                return '';
            }
            $value = preg_replace('/%{'.$matches[1].'}/', $_GET[$matches[1]], $value);
        }

        if (preg_match('/\${loginid}/', $value)) {
            $value = preg_replace('/\${loginid}/', $this->loginid, $value);
        }

        if (preg_match('/\${password}/', $value)) {
            $value = preg_replace('/\${password}/', $this->password, $value);
        }

        if (preg_match('/&{([^}]+)}/', $value, $matches)) {
            for ($i = 1; $i < count($matches); $i++) {
                if (preg_match('/([^\(]+)\(([^\)]*)\)/', $matches[$i], $elts)) {
                    $output = $elts[1]($elts[2]);
                    $value = preg_replace('/&{'.addcslashes($matches[$i], '()').'}/', $output, $value);
                } else {
                    return '';
                }
            }
        }

        return $value;
    }

    function _constructUrl($url, $oldurl)
    {
        if (preg_match('/^http/', $url)) {
            return $url;
        } else if (preg_match('/^\//', $url)) {
            return preg_replace('/^(https?:\/\/[^\/]+).*$/', '$1', $oldurl).$url;
        } else {
            return preg_replace('/[^\/]+$/', '', $oldurl).$url;
        }
    }

    function _parseCommand($command, $password, $oldpasswd)
    {
        $object = $this->storage;

        $command = str_replace('\'', '"', $command);
        $command = preg_replace(array('/%u/', '/%p/', '/%o/', '/%P/', '/%O/'),
            array($this->username, $password, $oldpasswd, $object->hashPasswd($password), $object->hashPasswd($oldpasswd)),
                $command);

        if (preg_match_all('/%{([^}]+)}/', $command, $matches)) {
            for ($i = 0; $i < count($matches[1]); $i++) {
                $values = $object->get($matches[1][$i]);
                if (is_array($values)) {
                    $values = join('+', $values);
                }
                $values = preg_replace('/(["`\\\\])/', '\\\\$1', $values);
                $values = str_replace('$', '\\\$', $values);
                $command = preg_replace('/%{'.$matches[1][$i].'}/', $values, $command);
            }
        }

        return $command;
    }

    function random($len)
    {
        $token = array_merge(range('0', '9'), range('a', 'z'), range('A', 'Z'));

        for ($i = 0; $i <10; $i++) {
            $str = '';
            for ($j = 0; $j < $len; $j++) {
                $str = $str.$token[rand(0, count($token) - 1)];
            }
            if (preg_match('/[a-zA-Z]/', $str) && preg_match('/[0-9]/', $str)) {
                break;
            }
        }

        return $str;
    }
}
?>
