<?php
/**
* @version	$Id: curl_helper.php 15514 2012-08-15 11:07:24Z alex $
* @package	In-Portal
* @copyright	Copyright (C) 1997 - 2009 Intechnic. All rights reserved.
* @license      GNU/GPL
* In-Portal is Open Source software.
* This means that this software may have been modified pursuant
* the GNU General Public License, and as distributed it includes
* or is derivative of works licensed under the GNU General Public License
* or other free or open source software licenses.
* See http://www.in-portal.org/license for copyright notices and details.
*/

	defined('FULL_PATH') or die('restricted access!');

	class kCurlHelper extends kHelper {

		const REQUEST_METHOD_GET = 1;

		const REQUEST_METHOD_POST = 2;

		/**
		 * ID of database record of currently active curl request
		 *
		 * @var int
		 * @access protected
		 */
		protected $logId = 0;

		/**
		 * Connection to host
		 *
		 * @var resource
		 * @access protected
		 */
		protected $connectionID = NULL;

		/**
		 * Response waiting timeout in seconds
		 *
		 * @var int
		 * @access public
		 */
		public $timeout = 90;

		/**
		 * Follow to url, if redirect received instead of document (only works when open_basedir and safe mode is off)
		 *
		 * @var bool
		 * @access public
		 */
		public $followLocation = false;

		/**
		 * Last response received by Curl
		 *
		 * @var string
		 * @access public
		 */
		public $lastResponse = '';

		/**
		 * Last error code
		 *
		 * @var int
		 * @access public
		 */
		public $lastErrorCode = 0;

		/**
		 * Last error message
		 *
		 * @var string
		 * @access public
		 */
		public $lastErrorMsg = '';

		/**
		 * Most recent HTTP response code received
		 *
		 * @var int
		 * @access public
		 */
		public $lastHTTPCode = 0;

		/**
		 * Count of intermediate redirects performed to get actual content
		 *
		 * @var int
		 * @access protected
		 */
		protected $lastRedirectCount = 0;

		/**
		 * Default request method
		 *
		 * @var int
		 * @access protected
		 */
		protected $requestMethod = self::REQUEST_METHOD_GET;

		/**
		 * Data to be sent using curl
		 *
		 * @var string
		 * @access protected
		 */
		protected $requestData = '';

		/**
		 * Request headers (associative array)
		 *
		 * @var Array
		 * @access protected
		 */
		protected $requestHeaders = Array ();

		/**
		 * Response headers
		 *
		 * @var Array
		 * @access protected
		 */
		protected $responseHeaders = Array ();

		/**
		 * CURL options
		 *
		 * @var Array
		 * @access protected
		 */
		protected $options = Array ();

		/**
		 * Indicates debug mode status
		 *
		 * @var bool
		 * @access public
		 */
		public $debugMode = false;

		/**
		 * Creates an instance of kCurlHelper class
		 */
		public function __construct()
		{
			parent::__construct();

			$this->debugMode = kUtil::constOn('DBG_CURL');
		}

		/**
		 * Reset connection settings (not results) after connection was closed
		 *
		 * @access protected
		 */
		protected function _resetSettings()
		{
			$this->timeout = 90;
			$this->followLocation = false;
			$this->requestMethod = self::REQUEST_METHOD_GET;
			$this->requestData = '';
			$this->requestHeaders = Array ();
			$this->options = Array ();
		}

		/**
		 * Sets CURL options (adds to options set before)
		 *
		 * @param Array $options_hash
		 * @access public
		 */
		public function setOptions($options_hash)
		{
			$this->options = kUtil::array_merge_recursive($this->options, $options_hash);
		}

		/**
		 * Combines user-defined and default options before setting them to CURL
		 *
		 * @access protected
		 */
		protected function prepareOptions()
		{
			$default_options = Array (
				// customizable options
				CURLOPT_TIMEOUT => $this->timeout,

				// hardcoded options
				CURLOPT_RETURNTRANSFER => 1,
				CURLOPT_REFERER => PROTOCOL.SERVER_NAME,

				// don't verify SSL certificates
				CURLOPT_SSL_VERIFYPEER => false,
				CURLOPT_SSL_VERIFYHOST => false,
				CURLOPT_HTTPHEADER => Array ('Expect:'),
			);

			if ( isset($_SERVER['HTTP_USER_AGENT']) ) {
				$default_options[CURLOPT_USERAGENT] = $_SERVER['HTTP_USER_AGENT'];
			}

			if ($this->requestHeaders) {
				$default_options[CURLOPT_HTTPHEADER] = $this->prepareHeaders();
			}

			// if we have post data, then POST else use GET method instead
			if ($this->requestMethod == self::REQUEST_METHOD_POST) {
				$default_options[CURLOPT_POST] = 1;
				$default_options[CURLOPT_POSTFIELDS] = $this->requestData;
			}

//			$default_options[CURLOPT_HEADERFUNCTION] = Array(&$this, 'ParseHeader');

			$user_options = $this->options; // backup options, that user set directly
			$this->setOptions($default_options);
			$this->setOptions($user_options);
			$this->applyOptions();
		}

		/**
		 * Sets prepared options to CURL
		 *
		 * @access protected
		 */
		protected function applyOptions()
		{
			foreach ($this->options as $option_name => $option_value) {
				curl_setopt($this->connectionID, $option_name, $option_value);
			}
		}

		/**
		 * Parses headers from CURL request
		 *
		 * @param resource $ch
		 * @param string $header
		 * @return int
		 * @access protected
		 */
		protected function ParseHeader(&$ch, $header)
		{
			$this->responseHeaders[] = $header;

			return strlen($header);
		}

		/**
		 * Sets request data for next query
		 *
		 * @param mixed $data Array or string
		 */
		public function SetRequestData($data)
		{
			if ( is_array($data) ) {
				$params_str = '';
				$data = $this->Application->HttpQuery->_transformArrays($data);

				foreach ($data as $key => $value) {
					$params_str .= $key . '=' . urlencode($value) . '&';
				}

				$data = $params_str;
			}

			$this->requestData = $data;
		}

		/**
		 * Sets request data for next query and switches request method to POST
		 *
		 * @param mixed $data Array or string
		 * @access public
		 */
		public function SetPostData($data)
		{
			$this->requestMethod = self::REQUEST_METHOD_POST;
			$this->SetRequestData($data);
		}

		/**
		 * Sets request method to be used in next request
		 *
		 * @param int $request_method
		 */
		public function SetRequestMethod($request_method)
		{
			if ($request_method != self::REQUEST_METHOD_GET || $request_method != self::REQUEST_METHOD_POST) {
				throw new Exception('Method "' . __METHOD__ . '": Invalid $request_method parameter value');

				return ;
			}

			$this->requestMethod = $request_method;
		}

		/**
		 * Sets headers to be sent along with next query
		 *
		 * @param Array $headers
		 * @access public
		 */
		public function SetHeaders($headers)
		{
			$this->requestHeaders = array_merge($this->requestHeaders, $headers);
		}

		/**
		 * Returns compiled header to be used by curl
		 *
		 * @return Array
		 * @access protected
		 */
		protected function prepareHeaders()
		{
			$ret = Array ();

			foreach ($this->requestHeaders as $header_name => $header_value) {
				$ret[] = is_numeric($header_name) ? $header_value : $header_name . ': ' . $header_value;
			}

			return $ret;
		}

		/**
		 * Performs CURL request and returns it's result
		 *
		 * @param string $url
		 * @param bool $close_connection
		 * @param bool $log_status
		 * @param string $log_message
		 * @return string
		 * @access public
		 */
		public function Send($url, $close_connection = true, $log_status = NULL, $log_message = '')
		{
			if ( isset($log_status) ) {
				// override debug mode setting
				$this->debugMode = $log_status;
			}

			$request_url = $url;

			if ( $this->requestMethod == self::REQUEST_METHOD_GET && $this->requestData ) {
				$request_url .= (strpos($request_url, '?') !== false ? '&' : '?') . $this->requestData;
			}

			$this->connectionID = curl_init($request_url);

			if ( $this->debugMode ) {
				// collect page data
				$page_data = Array ();

				if ( $_GET ) {
					$page_data[] = '_GET:' . "\n" . print_r($_GET, true);
				}

				if ( $_POST ) {
					$page_data[] = '_POST:' . "\n" . print_r($_POST, true);
				}

				if ( $_COOKIE ) {
					$page_data[] = '_COOKIE:' . "\n" . print_r($_COOKIE, true);
				}

				// create log record
				$fields_hash = Array (
					'Message' => $log_message,
					'PageUrl' => $_SERVER['REQUEST_URI'],
					'RequestUrl' => $url,
					'PortalUserId' => $this->Application->RecallVar('user_id'),
					'SessionKey' => $this->Application->GetSID(),
					'IsAdmin' => $this->Application->isAdminUser ? 1 : 0,
					'PageData' => implode("\n", $page_data),
					'RequestData' => $this->requestData,
					'RequestDate' => adodb_mktime(),
				);

				$this->Conn->doInsert($fields_hash, TABLE_PREFIX . 'CurlLog');
				$this->logId = $this->Conn->getInsertID();
			}

			$this->responseHeaders = Array ();

			$this->prepareOptions();
			$this->lastResponse = $this->_sendRequest();

			$this->Finalize($close_connection);

			return $this->lastResponse;
		}

		/**
		 * Reads data from remote url
		 *
		 * @return string
		 * @access protected
		 */
		protected function _sendRequest()
		{
			curl_setopt($this->connectionID, CURLOPT_RETURNTRANSFER, true);

			if ( $this->followLocation ) {
				if ( $this->followLocationLimited() ) {
					return $this->_followLocationManually();
				}
				else {
					// no restrictions - let curl do automatic redirects
					curl_setopt($this->connectionID, CURLOPT_FOLLOWLOCATION, true);
				}
			}

			return curl_exec($this->connectionID);
		}

		/**
		 * Fixes curl inability to automatically follow location when safe_mode/open_basedir restriction in effect
		 *
		 * @return string
		 * @access protected
		 */
		protected function _followLocationManually()
		{
			curl_setopt($this->connectionID, CURLOPT_HEADER, true);
			$data = curl_exec($this->connectionID);

			$http_code = $this->getInfo(CURLINFO_HTTP_CODE);

			if ( $http_code == 301 || $http_code == 302 ) {
				// safe more or open_basedir restriction - do redirects manually
				list ($header) = explode("\r\n\r\n", $data, 2);
				preg_match('/(Location:|URI:)(.*?)\n/', $header, $regs);
				$url = trim(array_pop($regs));
				$url_parsed = parse_url($url);

				if ( isset($url_parsed) ) {
					curl_setopt($this->connectionID, CURLOPT_URL, $url);
					$this->lastRedirectCount++;

					return $this->_followLocationManually();
				}
			}

			list(, $body) = explode("\r\n\r\n", $data, 2);

			return $body;
		}

		/**
		 * Returns various info about request made
		 *
		 * @param int $info_type
		 * @return mixed
		 *
		 * @see http://www.php.net/manual/ru/function.curl-getinfo.php
		 * @access public
		 */
		public function getInfo($info_type)
		{
			if ( $info_type == CURLINFO_REDIRECT_COUNT && $this->followLocationLimited() ) {
				return $this->lastRedirectCount;
			}

			return curl_getinfo($this->connectionID, $info_type);
		}

		/**
		 * Detects, that follow location can't be done automatically by curl due safe_mode/open_basedir restrictions
		 *
		 * @return bool
		 * @access protected
		 */
		protected function followLocationLimited()
		{
			return (defined('SAFE_MODE') && SAFE_MODE) || ini_get('open_basedir');
		}

		/**
		 * Finalizes curl request and saves some data from curl before closing connection
		 *
		 * @param bool $close_connection
		 * @return void
		 * @access public
		 */
		public function Finalize($close_connection = true)
		{
			$this->lastErrorCode = curl_errno($this->connectionID);
			$this->lastErrorMsg = curl_error($this->connectionID);
			$this->lastHTTPCode = $this->getInfo(CURLINFO_HTTP_CODE);

			if ( $close_connection ) {
				$this->CloseConnection();
			}

			$this->_resetSettings();
		}

		/**
		 * Closes connection to server
		 *
		 * @access public
		 */
		public function CloseConnection()
		{
			curl_close($this->connectionID);

			if ( $this->debugMode ) {
				$fields_hash = Array (
					'ResponseData' => $this->lastResponse,
					'ResponseDate' => adodb_mktime(),
					'ResponseHttpCode' => $this->lastHTTPCode,
  					'CurlError' => $this->lastErrorCode != 0 ? '#' . $this->lastErrorCode . ' (' . $this->lastErrorMsg . ')' : '',
				);

				$this->Conn->doUpdate($fields_hash, TABLE_PREFIX . 'CurlLog', 'LogId = ' . $this->logId);
			}

			// restore debug mode setting
			$this->debugMode = kUtil::constOn('DBG_CURL');
		}

		/**
		 * Checks, that last curl request was successful
		 *
		 * @return bool
		 * @access public
		 */
		public function isGoodResponseCode()
		{
			if ( $this->lastErrorCode != 0 ) {
				return false;
			}

			return ($this->lastHTTPCode == 200) || ($this->lastHTTPCode >= 300 && $this->lastHTTPCode < 310);
		}
	}