<?php

use NSL\Persistent\Persistent;

require_once NSL_PATH . '/includes/auth.php';


abstract class NextendSocialOauth2 extends NextendSocialAuth {

    const CSRF_LENGTH = 32;

    protected $state = false;

    protected $client_id;
    protected $client_secret;
    protected $redirect_uri;

    protected $endpointAuthorization;
    protected $endpointAccessToken;
    protected $endpointRestAPI;

    protected $defaultRestParams = array();

    protected $scopes = array();

    public function checkError() {
        if (isset($_REQUEST['error']) && isset($_REQUEST['error_description'])) {
            if ($this->validateState()) {
                throw new NSLSanitizedRequestErrorMessageException($_REQUEST['error'] . ': ' . htmlspecialchars_decode($_REQUEST['error_description']));
            }
        }
    }

    public function getTestUrl() {
        return $this->endpointAccessToken;
    }

    public function hasAuthenticateData() {
        return isset($_REQUEST['code']);
    }

    /**
     * @param string $client_id
     */
    public function setClientId($client_id) {
        $this->client_id = $client_id;
    }

    /**
     * @param string $client_secret
     */
    public function setClientSecret($client_secret) {
        $this->client_secret = $client_secret;
    }

    /**
     * @param string $redirect_uri
     */
    public function setRedirectUri($redirect_uri) {
        $this->redirect_uri = $redirect_uri;
    }

    public function getEndpointAuthorization() {
        return $this->endpointAuthorization;
    }

    /*
     * Adds response_type, client_id, redirect_uri and state as query parameter in the Authorization Url.
     * client_id can be found in the App when you create one
     * redirect_uri is the url you wish to be redirected after you entered you login credentials
     * state is a randomly generated string
     */
    public function createAuthUrl() {

        $args = array(
            'response_type' => 'code',
            'client_id'     => urlencode($this->client_id),
            'redirect_uri'  => urlencode($this->redirect_uri),
            'state'         => urlencode($this->getState())
        );

        $scopes = apply_filters('nsl_' . $this->providerID . '_scopes', $this->scopes);
        if (count($scopes)) {
            $args['scope'] = urlencode($this->formatScopes($scopes));
        }

        $args = apply_filters('nsl_' . $this->providerID . '_auth_url_args', $args);

        return add_query_arg($args, $this->getEndpointAuthorization());
    }

    /**
     * @param $scopes
     * Connects an array of scopes with whitespace.
     *
     * @return string
     */
    protected function formatScopes($scopes) {
        return implode(' ', array_unique($scopes));
    }

    /**
     * @return bool|false|string
     * If the code that was sent by the selected provider and the state is valid,
     * we can make a request for an accessToken with wp_remote_post().
     * The result contains HTTP headers and content.
     *
     * Returns the accessToken with which we can make certain requests for their user profile data.
     * @throws Exception
     */
    public function authenticate() {

        if (isset($_GET['code'])) {
            if (!$this->validateState()) {
                throw  new Exception('Unable to validate CSRF state');
            }

            $http_args = array(
                'timeout'    => 15,
                'user-agent' => 'WordPress',
                'body'       => array(
                    'grant_type'    => 'authorization_code',
                    'code'          => $_GET['code'],
                    'redirect_uri'  => $this->redirect_uri,
                    'client_id'     => $this->client_id,
                    'client_secret' => $this->client_secret
                )
            );

            $request = wp_remote_post($this->endpointAccessToken, $this->extendAuthenticateHttpArgs($this->extendAllHttpArgs($http_args)));

            if (is_wp_error($request)) {

                throw new NSLSanitizedRequestErrorMessageException($request->get_error_message());
            } else if (wp_remote_retrieve_response_code($request) !== 200) {

                $this->errorFromResponse(json_decode(wp_remote_retrieve_body($request), true));
            }

            $accessTokenData = json_decode(wp_remote_retrieve_body($request), true);

            if (!is_array($accessTokenData)) {
                throw new NSLSanitizedRequestErrorMessageException(sprintf(__('Unexpected response: %s', 'nextend-facebook-connect'), wp_remote_retrieve_body($request)));
            }

            $accessTokenData = $this->extendAccessTokenData($accessTokenData);

            $accessTokenData['created'] = time();

            $this->access_token_data = $accessTokenData;

            return wp_json_encode($accessTokenData);
        }

        return false;
    }

    /**
     * @param $response
     *
     * @throws Exception
     */
    protected function errorFromResponse($response) {
        if (isset($response['error'])) {
            throw new NSLSanitizedRequestErrorMessageException($response['error'] . ': ' . $response['error_description']);
        }
    }

    public function deleteLoginPersistentData() {
        Persistent::delete($this->providerID . '_state');
    }

    /**
     * If the stored state is the same as the state we have received from the remote Provider, it is valid.
     *
     * @return bool
     */
    protected function validateState() {
        $this->state = Persistent::get($this->providerID . '_state');
        if ($this->state === false) {
            return false;
        }

        if (empty($_GET['state'])) {
            return false;
        }

        if ($_GET['state'] == $this->state) {
            return true;
        }

        return false;
    }

    /**
     * Returns the stored state for the current provider.
     *
     * @return bool|mixed|null|string
     */
    protected function getState() {
        $this->state = Persistent::get($this->providerID . '_state');
        if ($this->state === null) {
            $this->state = $this->generateRandomState();

            Persistent::set($this->providerID . '_state', $this->state);
        }

        return $this->state;
    }

    /**
     * Generates a random string, which will be needed for the remote provider.
     * It will be stored for a time.
     *
     * @return bool|string
     */
    protected function generateRandomState() {

        if (function_exists('random_bytes')) {
            return $this->bytesToString(random_bytes(self::CSRF_LENGTH));
        }

        if (function_exists('mcrypt_create_iv')) {
            /** @noinspection PhpDeprecationInspection */
            $binaryString = mcrypt_create_iv(self::CSRF_LENGTH, MCRYPT_DEV_URANDOM);

            if ($binaryString !== false) {
                return $this->bytesToString($binaryString);
            }
        }

        if (function_exists('openssl_random_pseudo_bytes')) {
            $wasCryptographicallyStrong = false;

            $binaryString = openssl_random_pseudo_bytes(self::CSRF_LENGTH, $wasCryptographicallyStrong);

            if ($binaryString !== false && $wasCryptographicallyStrong === true) {
                return $this->bytesToString($binaryString);
            }
        }

        return $this->randomStr(self::CSRF_LENGTH);
    }

    private function bytesToString($binaryString) {
        return substr(bin2hex($binaryString), 0, self::CSRF_LENGTH);
    }

    private function randomStr($length, $keyspace = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ') {
        $str = '';
        $max = strlen($keyspace) - 1;
        for ($i = 0; $i < $length; ++$i) {
            $str .= $keyspace[random_int(0, $max)];
        }

        return $str;
    }

    /**
     * @param       $path
     * @param array $data
     * @param       $endpoint
     *
     * @return array
     * @throws Exception
     */
    public function get($path, $data = array(), $endpoint = false) {

        $http_args = array(
            'timeout'    => 15,
            'user-agent' => 'WordPress',
            'body'       => array_merge($this->defaultRestParams, $data)
        );
        if (!$endpoint) {
            $endpoint = $this->endpointRestAPI;
        }
        $request = wp_remote_get($endpoint . $path, $this->extendHttpArgs($this->extendAllHttpArgs($http_args)));

        if (is_wp_error($request)) {

            throw new NSLSanitizedRequestErrorMessageException($request->get_error_message());
        } else if (wp_remote_retrieve_response_code($request) !== 200) {

            $this->errorFromResponse(json_decode(wp_remote_retrieve_body($request), true));
        }

        $result = json_decode(wp_remote_retrieve_body($request), true);

        if (!is_array($result)) {
            throw new NSLSanitizedRequestErrorMessageException(sprintf(__('Unexpected response: %s', 'nextend-facebook-connect'), wp_remote_retrieve_body($request)));
        }

        return $result;
    }

    /**
     * @param       $path
     * @param array $data
     * @param       $endpoint
     *
     * @return array
     * @throws Exception
     */
    public function post($path, $data = array(), $endpoint = false) {

        $http_args = array(
            'timeout'    => 15,
            'user-agent' => 'WordPress',
            'body'       => array_merge($this->defaultRestParams, $data)
        );
        if (!$endpoint) {
            $endpoint = $this->endpointRestAPI;
        }


        $request = wp_remote_post($endpoint . $path, $this->extendHttpArgs($this->extendAllHttpArgs($http_args)));

        if (is_wp_error($request)) {

            throw new NSLSanitizedRequestErrorMessageException($request->get_error_message());
        } else if (wp_remote_retrieve_response_code($request) !== 200) {
            $this->errorFromResponse(json_decode(wp_remote_retrieve_body($request), true));
        }

        $result = json_decode(wp_remote_retrieve_body($request), true);

        if (!is_array($result)) {
            throw new NSLSanitizedRequestErrorMessageException(sprintf(__('Unexpected response: %s', 'nextend-facebook-connect'), wp_remote_retrieve_body($request)));
        }

        return $result;
    }

    /**
     * @param $http_args
     * Puts additional data into the http header.
     * Used for getting access to the resources with a bearer token.
     *
     * @return mixed
     */
    protected function extendHttpArgs($http_args) {
        if (isset($this->access_token_data['access_token'])) {
            $http_args['headers'] = array(
                'Authorization' => 'Bearer ' . $this->access_token_data['access_token']
            );
        }

        return $http_args;
    }

    protected function extendAllHttpArgs($http_args) {

        return $http_args;
    }


    /**
     * @param $http_args
     *
     * Can be used for adding additional data into the authentication request arguments only.
     *
     * @return mixed
     */
    protected function extendAuthenticateHttpArgs($http_args) {

        return $http_args;
    }

    /**
     * @param $access_token_data
     *
     *  Can be used for adding additional data into the access_token_data array or modifying its structure
     *  if the format is wrong.
     *
     * @return mixed
     */
    protected function extendAccessTokenData($access_token_data) {

        return $access_token_data;
    }
}