<?php

namespace dokuwiki;

/**
 * Minimal JWT implementation
 */
class JWT
{
    protected $user;
    protected $issued;
    protected $secret;

    /**
     * Create a new JWT object
     *
     * Use validate() or create() to create a new instance
     *
     * @param string $user
     * @param int $issued
     */
    protected function __construct($user, $issued)
    {
        $this->user = $user;
        $this->issued = $issued;
    }

    /**
     * Load the cookiesalt as secret
     *
     * @return string
     */
    protected static function getSecret()
    {
        return auth_cookiesalt(false, true);
    }

    /**
     * Create a new instance from a token
     *
     * @param $token
     * @return self
     * @throws \Exception
     */
    public static function validate($token)
    {
        [$header, $payload, $signature] = sexplode('.', $token, 3, '');
        $signature = base64_decode($signature);

        if (!hash_equals($signature, hash_hmac('sha256', "$header.$payload", self::getSecret(), true))) {
            throw new \Exception('Invalid JWT signature');
        }

        try {
            $header = json_decode(base64_decode($header), true, 512, JSON_THROW_ON_ERROR);
            $payload = json_decode(base64_decode($payload), true, 512, JSON_THROW_ON_ERROR);
        } catch (\Exception $e) {
            throw new \Exception('Invalid JWT', $e->getCode(), $e);
        }

        if (!$header || !$payload || !$signature) {
            throw new \Exception('Invalid JWT');
        }

        if ($header['alg'] !== 'HS256') {
            throw new \Exception('Unsupported JWT algorithm');
        }
        if ($header['typ'] !== 'JWT') {
            throw new \Exception('Unsupported JWT type');
        }
        if ($payload['iss'] !== 'dokuwiki') {
            throw new \Exception('Unsupported JWT issuer');
        }
        if (isset($payload['exp']) && $payload['exp'] < time()) {
            throw new \Exception('JWT expired');
        }

        $user = $payload['sub'];
        $file = self::getStorageFile($user);
        if (!file_exists($file)) {
            throw new \Exception('JWT not found, maybe it expired?');
        }

        if (file_get_contents($file) !== $token) {
            throw new \Exception('JWT invalid, maybe it expired?');
        }

        return new self($user, $payload['iat']);
    }

    /**
     * Create a new instance from a user
     *
     * Loads an existing token if available
     *
     * @param $user
     * @return self
     */
    public static function fromUser($user)
    {
        $file = self::getStorageFile($user);

        if (file_exists($file)) {
            try {
                return self::validate(io_readFile($file));
            } catch (\Exception $ignored) {
            }
        }

        $token = new self($user, time());
        $token->save();
        return $token;
    }


    /**
     * Get the JWT token for this instance
     *
     * @return string
     */
    public function getToken()
    {
        $header = [
            'alg' => 'HS256',
            'typ' => 'JWT',
        ];
        $header = base64_encode(json_encode($header));

        $payload = [
            'iss' => 'dokuwiki',
            'sub' => $this->user,
            'iat' => $this->issued,
        ];
        $payload = base64_encode(json_encode($payload, JSON_THROW_ON_ERROR));

        $signature = hash_hmac('sha256', "$header.$payload", self::getSecret(), true);
        $signature = base64_encode($signature);
        return "$header.$payload.$signature";
    }

    /**
     * Save the token for the user
     *
     * Resets the issued timestamp
     */
    public function save()
    {
        $this->issued = time();
        io_saveFile(self::getStorageFile($this->user), $this->getToken());
    }

    /**
     * Get the user of this token
     *
     * @return string
     */
    public function getUser()
    {
        return $this->user;
    }

    /**
     * Get the issued timestamp of this token
     *
     * @return int
     */
    public function getIssued()
    {
        return $this->issued;
    }

    /**
     * Get the storage file for this token
     *
     * Tokens are stored to be able to invalidate them
     *
     * @param string $user The user the token is for
     * @return string
     */
    public static function getStorageFile($user)
    {
        return getCacheName($user, '.token');
    }
}
