<?php

/**
 * @uri /oauth2/token
 */

class Oauth extends CfBaseResource
{
    /**
     * @throws ResponseException
     * @throws Exception
     */
    public function post($request)
    {
        $response = new Response($request);
        $data = Utils::getValidJsonData($request->data);
        $headers = $this->_getallheaders();
        $isTwoFaEnforced = (CfSettings::getInstance())->getSetting(SettingsHelper::ENFORCE_2FA);

        $CfFailedAttempts = new CfFailedAttempts(ipAddress: CfFailedAttemptsIdentifier::get($headers, $_SERVER), action: CfFailedAttempts::AUTHENTICATION_ACTION);
        $timeout = $CfFailedAttempts->timeOutBeforeNextAttempt();

        if ($timeout !== null) {
            $response->code = Response::TOO_MANY_REQUESTS;
            $response->body = "We have detected multiple unsuccessful login attempts, please try again after $timeout seconds.";
            return $response;
        }

        $tokenLifeTime = (isset($data->life_time) && is_int($data->life_time)) ? $data->life_time : OAUTH_ACCESSTOKEN_TIME;
        $server = Utils::getOauthServer($tokenLifeTime);
        $accessTokenStorage = $server->getStorage('access_token');
        $oAuthResponse = $server->handleTokenRequest(OAuth2\Request::createFromGlobals());
        $response->body = $oAuthResponse->getResponseBody('json');
        $response->code = $oAuthResponse->getStatusCode();

        foreach ($oAuthResponse->getHttpHeaders() as $key => $value) {
            $response->addHeader($key, $value);
        }

        if ($response->code === Response::OK && !property_exists($data, 'username')) {
            // initial request after session expires? So OK to just move on?
            return $response;
        }

        if ($response->code === Response::OK && (new CfUsers($data->username))->isPasswordExpired()) {
            $response->code = Response::FORBIDDEN;
            $response->body = "Your password has expired. Please contact the administrator to request a new password.";
            return $response;
        } elseif ($response->code == Response::UNAUTHORIZED) {
            try {
                $CfFailedAttempts->logFailedAttempt();
            } catch (PDOException $exception) {
                $response->code = Response::INTERNALSERVERERROR;
                $response->body = 'An internal server error occurred. Please contact your server administrator.';
                syslog(LOG_ERR, $exception->getTraceAsString());
                return $response;
            }
        } elseif ($response->code == Response::OK) {
            $oauthResponse = json_decode($response->body);
            $accessTokenExists = isset($oauthResponse->access_token);
            $cfUser = new CfUsers($data->username);

            if ($cfUser->isUserLockedByTwoFaEnforcement($isTwoFaEnforced)) {
                $accessTokenStorage->unsetAccessToken([$oauthResponse->access_token]);
                throw new ResponseException('Your account has been locked. Please contact the administrator.', Response::FORBIDDEN);
            }

            // set fist login after 2FA enforced timestamp if enforce 2FA settings enabled and user does not have it
            if ($isTwoFaEnforced && !$cfUser->getFirstLoginAfter2faEnforced()) {
                $cfUser->setFirstLoginAfter2faEnforced();
            }

            // $this->_getallheaders() does not return Authorization token,
            // this means that 2FA verification will not pay attention to access token if it is provided
            try {
                TwoFaHelper::verifyRequest($cfUser, $headers);
            } catch (Exception $exception) {
                // if the request contains two FA code and it's wrong, then mark the login attempts as failed
                if (isset($headers[TwoFaHelper::TWO_FA_TOKEN_HEADER]) && !empty($headers[TwoFaHelper::TWO_FA_TOKEN_HEADER])) {
                    $CfFailedAttempts->logFailedAttempt();
                }

                // if 2FA is not valid, then unset issued access token
                if ($accessTokenExists) {
                    $accessTokenStorage->unsetAccessToken([$oauthResponse->access_token]);
                }
                throw $exception;
            }
            if ($accessTokenExists) {
                TwoFaHelper::updateAccessTokeVerificationTime($oauthResponse->access_token);
            }
        }

        return $response;
    }

    public function get($request)
    {
        $response = new Response($request);
        $this->accessRequired();

        $permissions = (new CfRBAC())->getPermissionsByRoles((new CfUsers($this->username))->getUserRoles());
        $server = Utils::getOauthServer();
        $accessTokenStorage = $server->getStorage('access_token');
        $tokens = RbacAccessService::isActionAllowed('all-users-tokens.get', $permissions) ?
            $accessTokenStorage->getActiveTokens() :
            $accessTokenStorage->getActiveTokensByUserId($this->username);
        // decode the json from PGSQL else we will encode it twice when encoding to output
        array_walk($tokens, function (&$value, $index) {
            $value['details'] = json_decode($value['details'], true);
        });
        $response->code = Response::OK;
        $response->body = json_encode($tokens);
        return $response;
    }
}

/**
 * @uri /oauth2/token/revoke
 */
class OauthRevoke extends CfBaseResource
{
    public function post($request)
    {
        $this->accessRequired();
        $response = new Response($request);
        $data = Utils::getValidJsonData($request->data);
        $deleteToken = $data->tokens;
        $permissions = (new CfRBAC())->getPermissionsByRoles((new CfUsers($this->username))->getUserRoles());

        if (empty($deleteToken)) {
            throw new Exception("Error Processing Request. tokens parameter is missing or empty", 1);
        }

        syslog(LOG_DEBUG, 'DELETING ACCESS TOKEN:: ' . implode(',', $deleteToken));
        $server = Utils::getOauthServer();
        $accessTokenStorage = $server->getStorage('access_token');

        if (RbacAccessService::isActionAllowed('all-users-tokens.revoke', $permissions)) {
            $accessTokenStorage->unsetAccessToken($deleteToken);
        } else {
            // if a user does not have permissions to revoke all users tokens then pass its username as the second parameter
            // which will allow to delete only current user tokens
            $accessTokenStorage->unsetAccessToken($deleteToken, $this->username);
        }

        $response->code = Response::OK;
        $response->body = null;
        return $response;
    }
}

/**
 * @uri /oauth2/external-users-tokens/revoke
 */
class OauthExternalUsersRevoke extends CfProtectedResource
{
    /**
     * @rbacAlias all-users-tokens.revoke
     */
    public function post($request)
    {
        $response = new Response($request);

        $server = Utils::getOauthServer();
        $accessTokenStorage = $server->getStorage('access_token');
        $accessTokenStorage->removeExternalUsersTokens();

        $response->code = Response::OK;
        return $response;
    }
}
