<?php
// namespace administrator\components\com_jmap\framework\http\transport;
/**
 * @package JMAP::FRAMEWORK::components::com_jmap
 * @subpackage framework
 * @subpackage http
 * @subpackage transport
 * @author Joomla! Extensions Store
 * @copyright (C) 2021 - Joomla! Extensions Store
 * @license GNU/GPLv2 http://www.gnu.org/licenses/gpl-2.0.html
 */
defined ( '_JEXEC' ) or die ( 'Restricted access' );

/**
 * HTTP transport class for using curl adapter
 *
 * @package JMAP::FRAMEWORK::components::com_jmap
 * @subpackage framework
 * @subpackage http
 * @subpackage transport
 * @since 3.0
 */
class JMapHttpTransportCurl implements JMapHttpTransport {
	/**
	 *
	 * @var JRegistry The client options.
	 * @since 11.3
	 */
	protected $options;
	
	/**
	 * Accept the GZip encoding
	 *
	 * @access protected
	 * @var $acceptGZip
	 */
	protected $acceptGZip;
	
	/**
	 * Method to get a response object from a server response.
	 *
	 * @param string $content
	 *        	The complete server response, including headers
	 *        	as a string if the response has no errors.
	 * @param array $info
	 *        	The cURL request information.
	 *
	 * @return JHttpResponse
	 *
	 * @since 11.3
	 * @throws UnexpectedValueException
	 */
	protected function getResponse($content, $info) {
		// Create the response object.
		$return = new JHttpResponse ();
	
		// Get the number of redirects that occurred.
		$redirects = isset ( $info ['redirect_count'] ) ? $info ['redirect_count'] : 0;
	
		/*
		 * Split the response into headers and body. If cURL encountered redirects, the headers for the redirected requests will also be included. So we split the response into header + body + the number of redirects and only use the last two sections which should be the last set of headers and the actual body.
		*/
		$response = explode ( "\r\n\r\n", $content, 2 + $redirects );
	
		// Set the body for the response.
		$return->body = array_pop ( $response );
	
		// Get the last set of response headers as an array.
		$headers = explode ( "\r\n", array_pop ( $response ) );
	
		// Get the response code from the first offset of the response headers.
		preg_match ( '/[0-9]{3}/', array_shift ( $headers ), $matches );
	
		$code = count ( $matches ) ? $matches [0] : null;
	
		if (is_numeric ( $code )) {
			$return->code = ( int ) $code;
		}
		// No valid response code was detected.
		else {
			if(!$this->acceptGZip) {
				throw new UnexpectedValueException ( 'No HTTP response code found.', 'error' );
			}
		}
	
		// Add the response headers to the response object.
		foreach ( $headers as $header ) {
			$pos = strpos ( $header, ':' );
			$return->headers [trim ( substr ( $header, 0, $pos ) )] = trim ( substr ( $header, ($pos + 1) ) );
		}
	
		return $return;
	}
	
	/**
	 * Method to check if HTTP transport cURL is available for use
	 *
	 * @return boolean true if available, else false
	 *
	 * @since 12.1
	 */
	public static function isSupported() {
		return function_exists ( 'curl_version' ) && curl_version ();
	}
	
	/**
	 * Send a request to the server and return a JHttpResponse object with the response.
	 *
	 * @param string $method
	 *        	The HTTP method for sending the request.
	 * @param JUri $uri
	 *        	The URI to the resource to request.
	 * @param mixed $data
	 *        	Either an associative array or a string to be sent with the request.
	 * @param array $headers
	 *        	An array of request headers to send with the request.
	 * @param integer $timeout
	 *        	Read timeout in seconds.
	 * @param string $userAgent
	 *        	The optional user agent string to send with the request.
	 *        	
	 * @return JHttpResponse
	 *
	 * @since 11.3
	 * @throws RuntimeException
	 */
	public function request($method, JUri $uri, $data = null, array $headers = null, $timeout = 60, $userAgent = null) {
		// Setup the cURL handle.
		$ch = curl_init ();
		
		// Set the request method.
		$options [CURLOPT_CUSTOMREQUEST] = strtoupper ( $method );
		
		// Don't wait for body when $method is HEAD
		$options [CURLOPT_NOBODY] = ($method === 'HEAD');
		
		// If data exists let's encode it and make sure our Content-type header is set.
		if (isset ( $data )) {
			// If the data is a scalar value simply add it to the cURL post fields.
			if (is_scalar ( $data ) || (isset ( $headers ['Content-Type'] ) && strpos ( $headers ['Content-Type'], 'multipart/form-data' ) === 0)) {
				$options [CURLOPT_POSTFIELDS] = $data;
			} 			

			// Otherwise we need to encode the value first.
			else {
				$options [CURLOPT_POSTFIELDS] = http_build_query ( $data );
			}
			
			if (! isset ( $headers ['Content-Type'] )) {
				$headers ['Content-Type'] = 'application/x-www-form-urlencoded; charset=utf-8';
			}
			
			// Add the relevant headers.
			if (is_scalar ( $options [CURLOPT_POSTFIELDS] )) {
				$headers ['Content-Length'] = strlen ( $options [CURLOPT_POSTFIELDS] );
			}
		}
		
		// Build the headers string for the request.
		$headerArray = array ();
		
		if (isset ( $headers )) {
			foreach ( $headers as $key => $value ) {
				$headerArray [] = $key . ': ' . $value;
			}
			
			// Add the headers string into the stream context options array.
			$options [CURLOPT_HTTPHEADER] = $headerArray;
		}
		
		// If an explicit timeout is given user it.
		if (isset ( $timeout )) {
			$options [CURLOPT_TIMEOUT] = ( int ) $timeout;
			$options [CURLOPT_CONNECTTIMEOUT] = ( int ) $timeout;
		}
		
		// If an explicit user agent is given use it.
		if (isset ( $userAgent )) {
			$options [CURLOPT_USERAGENT] = $userAgent;
		}
		
		// Set the request URL.
		$options [CURLOPT_URL] = ( string ) $uri;
		
		// Accept gzip encoding
		if($this->acceptGZip) {
			$options [CURLOPT_ENCODING] = 'gzip';
		}
		
		// We want our headers. :-)
		$options [CURLOPT_HEADER] = true;
		
		// Return it... echoing it would be tacky.
		$options [CURLOPT_RETURNTRANSFER] = true;
		
		// Override the Expect header to prevent cURL from confusing itself in its own stupidity.
		// Link: http://the-stickman.com/web-development/php-and-curl-disabling-100-continue-header/
		$options [CURLOPT_HTTPHEADER] [] = 'Expect:';
		
		// Follow redirects.
		if(!ini_get('open_basedir') || $this->acceptGZip) {
			$options [CURLOPT_FOLLOWLOCATION] = true;
		}
		
		// Set the cURL options.
		curl_setopt_array ( $ch, $options );
		
		// Execute the request and close the connection.
		$content = curl_exec ( $ch );
		
		// Check if the content is a string. If it is not, it must be an error.
		if (! is_string ( $content ) && !$this->acceptGZip) {
			$message = curl_error ( $ch );
			
			if (empty ( $message )) {
				// Error but nothing from cURL? Create our own
				$message = 'No HTTP response received';
			}
			
			throw new RuntimeException ( $message );
		}
		
		// Get the request information.
		$info = curl_getinfo ( $ch );
		
		// Close the connection.
		curl_close ( $ch );
		
		return $this->getResponse ( $content, $info );
	}
	
	/**
	 * Constructor.
	 * CURLOPT_FOLLOWLOCATION must be disabled when open_basedir or safe_mode are enabled.
	 *
	 * @param bool $acceptGZip
	 *
	 * @see http://www.php.net/manual/en/function.curl-setopt.php
	 * @since 11.3
	 * @throws RuntimeException
	 */
	public function __construct($acceptGZip = false) {
		if (! function_exists ( 'curl_init' ) || ! is_callable ( 'curl_init' )) {
			throw new RuntimeException ( 'Cannot use a cURL transport when curl_init() is not available.' );
		}
		
		$this->acceptGZip = $acceptGZip;
	}
}
