<?php

/**
 * Class represents records from table saved_pass
 * {autogenerated}
 * @property int $saved_pass_id PK
 * @property int $user_id FK user
 * @property string $format
 * @property string $pass
 * @property string|null $salt
 * @property timestamp $time
 * {/autogenerated}
 */

class SavedPass extends Am_Record
{
    function checkPassword($pass)
    {
        $formats = $this->getTable()->getPasswordFormats();
        $callback = $formats[$this->format];
        $salt = $this->salt ?: $this->pass;
        $arr = [$pass, & $salt, null];
        $hash = $callback ?
            call_user_func_array($callback, $arr) :
            SavedPassTable::crypt($pass, $this->format, $salt, null);
        return $pass && strlen($this->pass) && hash_equals($this->pass, $hash);
    }
}

/**
 * @extends Am_Table<SavedPass>
 * @method SavedPass[] findByUserId($value, $start = null, $count = null, $orderBy = null)
 */
class SavedPassTable extends Am_Table
{
    const PASSWORD_PLAIN = 'plain';
    const PASSWORD_MD5 = 'md5';
    const PASSWORD_SHA1 = 'sha1';
    const PASSWORD_CRYPT = 'crypt';
    const PASSWORD_MD5_MD5_PASS_SALT = 'md5(md5(pass).salt)';
    const PASSWORD_MD5_SALT_MD5_PASS = 'md5(salt.md5(pass))';
    const PASSWORD_SHA1_SHA1_PASS_SALT = 'sha1(sha1(pass).salt)';
    const PASSWORD_SHA256_SHA256_PASS_SALT = 'sha256(sha256(pass).salt)';
    const PASSWORD_PHPASS = 'phpass';
    const PASSWORD_PASSWORD_HASH = 'password_hash';
    const PASSWORD_WORDPRESS_BCRYPT = 'password_wordpress_bcrypt';

    protected $_key = 'saved_pass_id';
    protected $_table = '?_saved_pass';
    protected $_recordClass = 'SavedPass';

    /** @return SavedPass|null */
    function findSaved(User $user, $format)
    {
        if ($format === SavedPassTable::PASSWORD_PHPASS) {
            $row = [
                'saved_pass_id' => 0,
                'user_id' => $user->user_id,
                'format' => $format,
                'pass' => $user->pass,
                'salt' => null,
                'time' => null,
            ];
        } else {
            $row = $this->_db->selectRow(
                "SELECT * FROM ?_saved_pass WHERE user_id=?d AND format=?", $user->user_id, $format);
        }
        if ($row)
            return $this->createRecord()->fromRow($row);
    }

    function setPass(Am_Event_SetPassword $event)
    {
        $user = $event->getUser();
        $pass = $event->getPassword();
        $sql = [];
        ////
        $obj = $this->getDi()->savedPassRecord;
        $obj->format = self::PASSWORD_PHPASS;
        $obj->pass = $user->pass;
        $obj->salt = null;
        $obj->toggleFrozen(true);
        $event->addSaved($obj);
        ////
        foreach ($this->getPasswordFormats() as $format => $callback) {
            $salt = null;
            $arr = [$pass, & $salt, $user];
            $password = $callback ?
                call_user_func_array($callback, $arr) :
                self::crypt($pass, $format, $salt, $user);
            $sql[] = $this->_db->expandPlaceholders(
                [
                    "(?d, ?, ?, ?)",
                    $user->user_id, $format, $password, $salt
                ]
            );
            $obj = $this->getDi()->savedPassRecord;
            $obj->format = $format;
            $obj->pass = $password;
            $obj->salt = $salt;
            $obj->toggleFrozen(true);
            $event->addSaved($obj);
        }
        if ($sql)
            $this->_db->query("INSERT INTO ?_saved_pass
                (user_id, format, pass, salt)
                VALUES " . implode(",", $sql) .
                " ON DUPLICATE KEY UPDATE pass=VALUES(pass), salt=VALUES(salt)");
    }

    /** @return array of array(type, [optional]callback) */
    function getPasswordFormats()
    {
        $types = [
            self::PASSWORD_MD5_MD5_PASS_SALT => null,
            self::PASSWORD_CRYPT => null,
            self::PASSWORD_PASSWORD_HASH => null,
            self::PASSWORD_WORDPRESS_BCRYPT => null
        ];
        foreach ($this->getDi()->plugins_protect->loadEnabled()->getAllEnabled() as $pl) {
            $format = $pl->getPasswordFormat();
            if ($format === null)
                continue;

            if (array_key_exists($format, $types))
                continue;

            $types[$format] = [$pl, 'cryptPassword'];
        }
        return $this->getDi()->hook->filter($types, Am_Event::GET_PASSWORD_FORMATS);
    }

    static function crypt($pass, $methodConst, & $salt = null, ?User $user = null)
    {
        switch ($methodConst) {
            // accept all wordpress passwords here
            case self::PASSWORD_WORDPRESS_BCRYPT:
                if($salt && User::verifyPassword($pass, $salt))
                    return $salt;
                $password_to_hash = base64_encode( hash_hmac( 'sha384', trim( $pass ), 'wp-sha384', true ) );
                return '$wp' . password_hash( $password_to_hash, PASSWORD_BCRYPT);
            case self::PASSWORD_PHPASS:
                $ph = new PasswordHash(8, true);
                // validation request
                if ($salt && $ph->CheckPassword($pass, $salt))
                    return $salt;
                // new password
                return $ph->HashPassword($pass);
            case self::PASSWORD_PASSWORD_HASH :
                if($salt && password_verify($pass, $salt)){
                    return $salt;
                }
                return password_hash($pass, PASSWORD_DEFAULT);
            case self::PASSWORD_PLAIN: return $pass;
            case self::PASSWORD_MD5: return md5($pass);
            case self::PASSWORD_SHA1: return sha1($pass);
            case self::PASSWORD_CRYPT:
                if (empty($salt)) {
                    $salt = crypt($pass, '$1$'.Am_Di::getInstance()->security->randomString(8));
                    return $salt;
                }
                return crypt($pass, $salt);
            case self::PASSWORD_MD5_MD5_PASS_SALT:
                if (empty($salt))
                    $salt = Am_Di::getInstance()->security->randomString(3);
                return md5(md5($pass) . $salt);
            case self::PASSWORD_MD5_SALT_MD5_PASS:
                if (empty($salt))
                    $salt = Am_Di::getInstance()->security->randomString(3);
                return md5($salt . md5($pass));
            case self::PASSWORD_SHA1_SHA1_PASS_SALT:
                if (empty($salt))
                    $salt = Am_Di::getInstance()->security->randomString(3);
                return sha1(sha1($pass) . $salt);
            case self::PASSWORD_SHA256_SHA256_PASS_SALT:
                if (empty($salt))
                    $salt = Am_Di::getInstance()->security->randomString(3);
                return hash('sha256', hash('sha256', $pass) . $salt);
            default:
                throw new Am_Exception_InternalError("Unknown crypt method passed: [" . htmlentities($methodConst) . "]");
        }
    }
}