<?php

namespace WPLEClient;

use WPLE_Trait;
use WPLEClient\Exceptions\LEConnectorException;
/**
 * LetsEncrypt Connector class, containing the functions necessary to sign with JSON Web Key and Key ID, and perform GET, POST and HEAD requests.
 *
 * PHP version 5.2.0
 *
 * MIT License
 *
 * Copyright (c) 2018 Youri van Weegberg
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 * @author     Youri van Weegberg <youri@yourivw.nl>
 * @copyright  2018 Youri van Weegberg
 * @license    https://opensource.org/licenses/mit-license.php  MIT License
 * @link       https://github.com/yourivw/LEClient
 * @since      Class available since Release 1.0.0
 */
class LEConnector {
    public $baseURL;

    public $accountKeys;

    private $nonce;

    public $keyChange;

    public $newAccount;

    public $newNonce;

    public $newOrder;

    public $revokeCert;

    public $accountURL;

    public $accountDeactivated = false;

    private $log;

    private $sourceIp = false;

    /**
     * Initiates the LetsEncrypt Connector class.
     *
     * @param int 		$log			The level of logging. Defaults to no logging. LOG_OFF, LOG_STATUS, LOG_DEBUG accepted.
     * @param string	$baseURL 		The LetsEncrypt server URL to make requests to.
     * @param array		$accountKeys 	Array containing location of account keys files.
     */
    public function __construct(
        $log,
        $baseURL,
        $accountKeys,
        $sourceIp = false
    ) {
        foreach ( $accountKeys as $id => $pky ) {
            $accountKeys[$id] = str_ireplace( ABSPATH . ABSPATH, ABSPATH, $pky );
        }
        $this->baseURL = $baseURL;
        $this->accountKeys = $accountKeys;
        $this->log = $log;
        $this->sourceIp = $sourceIp;
        $this->getLEDirectory();
        $this->getNewNonce();
    }

    /**
     * Requests the LetsEncrypt Directory and stores the necessary URLs in this LetsEncrypt Connector instance.
     */
    private function getLEDirectory() {
        $req = $this->get( '/directory' );
        $this->keyChange = $req['body']['keyChange'];
        $this->newAccount = $req['body']['newAccount'];
        $this->newNonce = $req['body']['newNonce'];
        $this->newOrder = $req['body']['newOrder'];
        $this->revokeCert = $req['body']['revokeCert'];
    }

    /**
     * Requests a new nonce from the LetsEncrypt server and stores it in this LetsEncrypt Connector instance.
     */
    private function getNewNonce() {
        if ( $this->head( $this->newNonce )['status'] !== 200 ) {
            throw LEConnectorException::NoNewNonceException();
        }
    }

    /**
     * Makes a Curl request.
     *
     * @param string	$method	The HTTP method to use. Accepting GET, POST and HEAD requests.
     * @param string 	$URL 	The URL or partial URL to make the request to. If it is partial, the baseURL will be prepended.
     * @param object 	$data  	The body to attach to a POST request. Expected as a JSON encoded string.
     *
     * @return array 	Returns an array with the keys 'request', 'header', 'status' and 'body'.
     */
    private function request( $method, $URL, $data = null ) {
        if ( $this->accountDeactivated ) {
            throw LEConnectorException::AccountDeactivatedException();
        }
        $headers = array('Accept: application/json', 'Content-Type: application/jose+json');
        $requestURL = ( preg_match( '~^http~', $URL ) ? $URL : $this->baseURL . $URL );
        $handle = curl_init();
        curl_setopt( $handle, CURLOPT_URL, $requestURL );
        curl_setopt( $handle, CURLOPT_HTTPHEADER, $headers );
        curl_setopt( $handle, CURLOPT_RETURNTRANSFER, true );
        curl_setopt( $handle, CURLOPT_HEADER, true );
        curl_setopt( $handle, CURLOPT_SSL_VERIFYPEER, false );
        // }
        switch ( $method ) {
            case 'GET':
                break;
            case 'POST':
                curl_setopt( $handle, CURLOPT_POST, true );
                curl_setopt( $handle, CURLOPT_POSTFIELDS, $data );
                break;
            case 'HEAD':
                curl_setopt( $handle, CURLOPT_CUSTOMREQUEST, 'HEAD' );
                curl_setopt( $handle, CURLOPT_NOBODY, true );
                break;
            default:
                throw LEConnectorException::MethodNotSupportedException( esc_html( $method ) );
                break;
        }
        $response = curl_exec( $handle );
        if ( curl_errno( $handle ) ) {
            throw LEConnectorException::CurlErrorException( esc_html( curl_error( $handle ) ) );
        }
        $headerSize = curl_getinfo( $handle, CURLINFO_HEADER_SIZE );
        $statusCode = curl_getinfo( $handle, CURLINFO_HTTP_CODE );
        $header = substr( $response, 0, $headerSize );
        $body = substr( $response, $headerSize );
        $jsonbody = json_decode( $body, true );
        $jsonresponse = array(
            'sourceip' => $this->sourceIp,
            'request'  => $method . ' ' . $requestURL,
            'header'   => $header,
            'status'   => $statusCode,
            'body'     => ( $jsonbody === null ? $body : $jsonbody ),
        );
        if ( $this->log instanceof \Psr\Log\LoggerInterface ) {
            ///$this->log->debug($method . ' response received', $jsonresponse);
        } elseif ( $this->log >= LEClient::LOG_DEBUG ) {
            LEFunctions::log( $jsonresponse );
        } elseif ( stripos( $requestURL, 'authz' ) && $method == 'POST' ) {
            $res_data = $jsonresponse['body'];
            if ( array_key_exists( 'status', $res_data ) && $res_data['status'] == 'invalid' ) {
                if ( isset( $res_data['challenges'][0]['error'] ) ) {
                    LEFunctions::log( 'Authorization Error: ' . json_encode( $res_data['challenges'][0]['error'] ) );
                }
            }
        }
        if ( preg_match( '~Replay\\-Nonce: (\\S+)~i', $header, $matches ) ) {
            $this->nonce = trim( $matches[1] );
        } else {
            if ( $method == 'POST' ) {
                $this->getNewNonce();
            }
            // Not expecting a new nonce with GET and HEAD requests.
        }
        if ( ($method == 'POST' or $method == 'GET') and $statusCode !== 200 and $statusCode !== 201 or $method == 'HEAD' and $statusCode !== 200 ) {
            //Ex: Invalid response: 429 (Rate limit for \'/directory\' reached)
            // if ($this->sourceIp !== false && get_option('wple_sourceip_enable') === false) {
            //   LEFunctions::log("Invalid response without source IP: " . $jsonresponse);
            //   update_option('wple_sourceip_enable', true);
            //   $this->request($method, $URL, $data); //re-call once
            // } else {
            throw LEConnectorException::InvalidResponseException( $jsonresponse );
            // }
        }
        return $jsonresponse;
    }

    /**
     * Makes a GET request.
     *
     * @param string	$url 	The URL or partial URL to make the request to. If it is partial, the baseURL will be prepended.
     *
     * @return array 	Returns an array with the keys 'request', 'header', 'status' and 'body'.
     */
    public function get( $url ) {
        return $this->request( 'GET', $url );
    }

    /**
     * Makes a POST request.
     *
     * @param string 	$url	The URL or partial URL to make the request to. If it is partial, the baseURL will be prepended.
     * @param object 	$data	The body to attach to a POST request. Expected as a json string.
     *
     * @return array 	Returns an array with the keys 'request', 'header', 'status' and 'body'.
     */
    public function post( $url, $data = null ) {
        return $this->request( 'POST', $url, $data );
    }

    /**
     * Makes a HEAD request.
     *
     * @param string 	$url	The URL or partial URL to make the request to. If it is partial, the baseURL will be prepended.
     *
     * @return array	Returns an array with the keys 'request', 'header', 'status' and 'body'.
     */
    public function head( $url ) {
        return $this->request( 'HEAD', $url );
    }

    /**
     * Generates a JSON Web Key signature to attach to the request.
     *
     * @param array 	$payload		The payload to add to the signature.
     * @param string	$url 			The URL to use in the signature.
     * @param string 	$privateKeyFile The private key to sign the request with. Defaults to 'private.pem'. Defaults to accountKeys[private_key].
     *
     * @return string	Returns a JSON encoded string containing the signature.
     */
    public function signRequestJWK( $payload, $url, $privateKeyFile = '' ) {
        if ( $privateKeyFile == '' ) {
            $privateKeyFile = $this->accountKeys['private_key'];
        }
        if ( !file_exists( $privateKeyFile ) ) {
            WPLE_Trait::wple_logger(
                "Could not create key files due to file permission issues. Please check if public_html/keys/ folder permission is set to 0755. You can still generate premium SSL certificate in Annual <b>PRO</b> Plan without these requirements.",
                "error",
                "a",
                true
            );
        }
        $privateKey = openssl_pkey_get_private( file_get_contents( $privateKeyFile ) );
        $details = openssl_pkey_get_details( $privateKey );
        $protected = array(
            "alg"   => "RS256",
            "jwk"   => array(
                "kty" => "RSA",
                "n"   => LEFunctions::Base64UrlSafeEncode( $details["rsa"]["n"] ),
                "e"   => LEFunctions::Base64UrlSafeEncode( $details["rsa"]["e"] ),
            ),
            "nonce" => $this->nonce,
            "url"   => $url,
        );
        $payload64 = LEFunctions::Base64UrlSafeEncode( str_replace( '\\/', '/', ( is_array( $payload ) ? json_encode( $payload ) : $payload ) ) );
        $protected64 = LEFunctions::Base64UrlSafeEncode( json_encode( $protected ) );
        openssl_sign(
            $protected64 . '.' . $payload64,
            $signed,
            $privateKey,
            "SHA256"
        );
        $signed64 = LEFunctions::Base64UrlSafeEncode( $signed );
        $data = array(
            'protected' => $protected64,
            'payload'   => $payload64,
            'signature' => $signed64,
        );
        return json_encode( $data );
    }

    /**
     * Generates a Key ID signature to attach to the request.
     *
     * @param array 	$payload		The payload to add to the signature.
     * @param string	$kid			The Key ID to use in the signature.
     * @param string	$url 			The URL to use in the signature.
     * @param string 	$privateKeyFile The private key to sign the request with. Defaults to 'private.pem'. Defaults to accountKeys[private_key].
     *
     * @return string	Returns a JSON encoded string containing the signature.
     */
    public function signRequestKid(
        $payload,
        $kid,
        $url,
        $privateKeyFile = ''
    ) {
        if ( $privateKeyFile == '' ) {
            $privateKeyFile = $this->accountKeys['private_key'];
        }
        $privateKey = openssl_pkey_get_private( file_get_contents( $privateKeyFile ) );
        $details = openssl_pkey_get_details( $privateKey );
        $protected = array(
            "alg"   => "RS256",
            "kid"   => $kid,
            "nonce" => $this->nonce,
            "url"   => $url,
        );
        $payload64 = LEFunctions::Base64UrlSafeEncode( str_replace( '\\/', '/', ( is_array( $payload ) ? json_encode( $payload ) : $payload ) ) );
        $protected64 = LEFunctions::Base64UrlSafeEncode( json_encode( $protected ) );
        openssl_sign(
            $protected64 . '.' . $payload64,
            $signed,
            $privateKey,
            "SHA256"
        );
        $signed64 = LEFunctions::Base64UrlSafeEncode( $signed );
        $data = array(
            'protected' => $protected64,
            'payload'   => $payload64,
            'signature' => $signed64,
        );
        return json_encode( $data );
    }

}
