<?php

// requires php-curl
// to do: use latest CA certificate file

class CurlObject {
	
	const POSTTYPE_FORM = 1;
	const POSTTYPE_JSON = 2;
	const POSTTYPE_FILE = 3;
	
	private $url;
	private $redirect = 0;
	private $postdatatype = 0;
	private $postdata = FALSE;		//to do: implement POST, also formdata, filedata, or json
	private $body;
	private $request_headers = [];	// outgoing request headers
	private $headers;				// incoming response headers
	private $useragent = FALSE;
	private $cookies = [];
    private $errormessage;
	private $method = 'GET';
	private $connecttimeout = 0;
	
	public function URL() {
		if (func_num_args()) {
			$this->url = func_get_arg(0);
			return $this;
		}
		return $this->url;
	}

	// time is in ms  CURLOPT_CONNECTTIMEOUT_MS
	public function ConnectTimeout() {
		if (func_num_args()) {
			$this->connecttimeout = func_get_arg(0);
			return $this;
		}
		return $this->connecttimeout;
	}
	
	public function Clear() {
		$this->postdata = FALSE;
		$this->postdatatype = 0;
		$this->request_headers = [];
		return $this;
	}

	public function postdata() {
		if (func_num_args()) {
			$this->postdatatype = static::POSTTYPE_FORM;
			$this->postdata = func_get_arg(0);
			return $this;
		}
		return $this->postdata;
	}
	public function postdataJson() {
		if (func_num_args()) {
			$this->postdatatype = static::POSTTYPE_JSON;
			$this->postdata = json_encode(func_get_arg(0), JSON_UNESCAPED_SLASHES);
			return $this;
		}
		return $this->postdata;
	}
	
	public function postfile($fieldname, $filename) {
		$curlfile = new CURLFile(realpath($filename));
		$this->postdatatype = static::POSTTYPE_FILE;
		$this->postdata = [ $fieldname => $curlfile ];
		return $this;
	}
	
	// Follow Location: redirect?
	public function redirect() {
		if (func_num_args()) {
			$this->redirect = func_get_arg(0);
			return $this;
		}
		return $this->redirect;
	}
	
	public function UserAgent() {
		if (func_num_args()) {
			$this->useragent = func_get_arg(0);
			return $this;
		}
		return $this->useragent;
	}

	public function Method() {
		if (func_num_args()) {
			$this->method = strtoupper(func_get_arg(0));
			return $this;
		}
		return $this->method;
	}
	
	public function body() {
		return $this->body;
	}

	public function error() {
		return $this->errormessage;
	}
	
	// get/set outgoing request header
	public function RequestHeader() {
		if (!func_num_args()) {
			$output = [];
            if (isset($this->request_headers['_'])) {
                foreach ($this->request_headers['_'] as $key => $value) {
                    $output[] = "$key: $value";
                }
            }
			return $output;
		}
		$key = func_get_arg(0);
		$lkey = strtolower($key);
		if (isset($this->request_headers['normalized'][$lkey])) {
			$key = $this->request_headers['normalized'][$lkey];
		}
		if (func_num_args() == 1) {
			if (isset($this->request_headers['_'][$key])) {
				return $this->request_headers['_'][$key];
			}
			return FALSE;
		}
		$this->request_headers['normalized'][$lkey] = $key;
		$this->request_headers['_'][$key] = func_get_arg(1);
		return $this;
	}
	
	// header('location') => fetch incoming
	// can't set incoming headers
	public function header() {
		if (!func_num_args()) {
			return $this->headers;
		} 
		$key = func_get_arg(0);
		$lkey = strtolower($key);
		if (func_num_args() == 1) {
			return isset($this->headers[$lkey]) ? $this->headers[$lkey] : FALSE;
		}
		return $this;
	}
	
	public function Status() {
		return isset($this->headers['STATUS']) ? $this->headers['STATUS'] : FALSE;
	}
	
	public function Execute($options = NULL) {
	// prepare options
		
		$ch = curl_init();
		$options = [
            CURLOPT_URL            => $this->url,
            CURLOPT_HEADER         => 1,
            CURLOPT_SSL_VERIFYPEER => 0,
            CURLOPT_SSL_VERIFYHOST => 2,
            CURLOPT_RETURNTRANSFER => 1,
            CURLOPT_FOLLOWLOCATION => !empty($this->redirect),
        ] + (array)$options;
		if ($this->useragent) {
			$options[CURLOPT_USERAGENT] = $this->useragent;
		}
		if ($this->method != 'POST' && $this->method != 'GET') {
			curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $this->method);
		};
		list($host, $text) = $this->GetCookieText($this->url);
		if ($text != '') {
			$options[CURLOPT_HTTPHEADER][] = 'Cookie:' . $text;
		}
		$headers = $this->RequestHeader();
		if ($this->postdatatype == static::POSTTYPE_FORM) {
			$output = '';
            if (is_array($this->postdata)) {
                foreach($this->postdata as $key => $value) {
                    if ($output != '') $output .= '&';
                    $output .= rawurlencode($key) . '=' . rawurlencode($value);
                }
            } else {
                $output = $this->postdata;
            }
			$options[CURLOPT_POSTFIELDS] = $output;
//			$options[CURLOPT_POSTFIELDSIZE] = strlen($output);
			$headers[] = 'Content-Type: application/x-www-form-urlencoded';
			$headers[] = 'Content-Length: ' . strlen($output);
		} elseif ($this->postdatatype == static::POSTTYPE_JSON) {
			$options[CURLOPT_POSTFIELDS] = $this->postdata;
			$headers[] = 'Content-Type: application/json';
			$headers[] = 'Content-Length: ' . strlen($this->postdata);
		} elseif ($this->postdatatype == static::POSTTYPE_FILE) {
			$options[CURLOPT_POST] = 1;
			$options[CURLOPT_POSTFIELDS] = $this->postdata;
		}
		$headers[] = 'Expect:';

		foreach($headers as $item) {
			$options[CURLOPT_HTTPHEADER][] = $item;
		}
		
		if ($this->connecttimeout > 0) {
			$options[CURLOPT_CONNECTTIMEOUT_MS] = $this->connecttimeout;
		}

		curl_setopt_array($ch, $options);
//		error_log(json_encode($options));
//		curl_setopt($ch, CURLOPT_TIMEOUT, 60);  
// 		$options['headers'][] = 'Upgrade-Insecure-Requests: 1';
//		cookies
/*		
		if($data) {
			curl_setopt($ch, CURLOPT_POST, 1);  
			curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
		}  
*/
		$this->errormessage = '';
		$response = curl_exec ($ch);
		if($errno = curl_errno($ch)) {
			$this->errormessage = curl_strerror($errno);
			//echo "cURL error ({$errno}):\n {$error_message}";
		}
		$info = curl_getinfo($ch);
		curl_close($ch);
		$header_size = $info['header_size'];
		$rawheader = substr($response, 0, $header_size);
		$this->processResponses($rawheader, $host);
		$this->body = substr($response, $header_size);
		return $this;
	}
	
	protected function processResponses($header, $host) {
		$responses = explode("\n", $header);
		$headers = [];

		foreach($responses as $response) {
			$response = trim($response);
			$p = strpos($response, ':');
			$isHTTP = substr($response, 0, 4) == 'HTTP' ? 1 : 0;
			$item = strtolower(substr($response, 0, $p));
			if ($p !== FALSE) {
				$headers[$item] = trim(substr($response, $p + 1));
				if ($item == 'set-cookie') {
					$this->SetCookies($host, $headers[$item]);
				}
			} elseif ($isHTTP) {
				$list = explode(' ', $response);
				$headers['STATUS'] = $list[1];
			}
		}
		$this->headers = $headers;
	}
	
	public function ClearCookies($hostname = '') {
		if ($hostname == '*') {
			$this->cookies = [];
		} else {
			unset($this->cookies[$hostname]);
		}
	}
	
	public function GetCookies() {
		return $this->cookies;
	}
	
	public function GetCookieText($path = '') {
		if ($path != '') {
			$elements = parse_url($path);
			$path = isset($elements['path']) ? $elements['path'] : '/';
			$host = '.' . $elements['host'];
		} else {
			$path = '/';
			$host = '.';
		}
		$cookietext = '';
		foreach($this->cookies as $domainname => $cookielist) {
//			if (substr($domainname, - strlen($host)) == $host) {		// cookie is suitable for this domain/subdomain
			if (substr($host, - strlen($domainname)) == $domainname) {		// cookie is suitable for this domain/subdomain
				foreach($cookielist as $cookiename => $cookiedata) {
					list($cookievalue, $cookiepath) = $cookiedata;
					if (substr($path, 0, strlen($cookiepath)) == $cookiepath) {
						if ($cookietext != '') $cookietext .= ';';
						$cookietext .= ' ' . rawurlencode($cookiename) . '=' . rawurlencode($cookievalue);
					}
				}
			}
		}
		return array($host, $cookietext);
	}
	
	public function SetCookies($host, $cookietext) {
		$elements = $this->_parse_cookietext($cookietext);
		if (!isset($elements['domain'])) $elements['domain'] = $host;
	// remove a cookie ? 
		if ($elements['__expires'] > 0 &&  $elements['__expires'] < time()) {
			unset($this->cookies[$elements['domain']][$elements['name']]);
			return;
		}
	// set cookie
		if (!isset($elements['path'])) {
			$elements['path'] = '/';		// this is incorrect - it should be the path of the current webpage
		}
		$this->cookies[$elements['domain']][$elements['name']] = array($elements['value'], $elements['path']);
	}
	
	function _parse_cookietext($cookietext) {
		$result = array('__expires' => 0);
		$text = explode('; ', $cookietext);
		$count = 0;
		foreach($text as $textitem) {
			$p = strpos($textitem, '=');
			$key = substr($textitem, 0, $p);
			$value = substr($textitem, $p + 1);
			$p = strpos($value, ';');
			if ($p !== FALSE) $value = substr($value, 0, $p);	// drop ';Path=/;HttpOnly;Domain=www.microsoft.com"'?
			if (!$count) {
				$result['name'] = $key;
				$result['value'] = rawurldecode($value);
			} else {
				$key = strtolower($key);
				$result[$key] = $value;
				if ($key == 'expires') $result['__expires'] = strtotime($value);
			}
			$count++;
		}
		return $result;
	}
	
	public function assembleQueryString($param, $raw = TRUE) {
		$output = '';
		foreach($param as $key => $value) {
			if ($output != '') $output .= '&';
			if ($raw) {
				$output .= rawurlencode($key) . '=' . rawurlencode($value);
			} else {
				$output .= urlencode($key) . '=' . urlencode($value);
			}
		}
		return $output;
	}
}