<?php 
/**
 * This class allows remote AJAX requests that are blocked by in-browser CSRF protections to 
 * be submitted via the IceBear server. It is a filthy hack to get around an issue that should
 * be fixed at the remote service with an Access-Control-Allow-Origin header.
 * 
 * We use the create and getAll methods since the API handler maps GET and POST to /api/corsproxy
 * onto those methods. Both expect an "url" property within the request.
 */
class corsproxy {


	/**
	 *
	 * @param array $request
	 * @return string|bool
	 * @throws BadRequestException
	 * @throws ForbiddenException
	 * @throws ServerException
	 */
    public static function create(array $request=[]): string|bool {
		if(!isset($request['url'])){ throw new BadRequestException('IceBear corsproxy: request has no url parameter'); }
        $url=$request['url'].'';
        unset($request['url']);
        if('put'==strtolower($_SERVER['REQUEST_METHOD'])){
            return static::doPut($url, $request, true);
        } else {
            return static::doPost($url, $request, true);
        }
    }

	/**
	 * @param array $request
	 * @return string|bool
	 * @throws BadRequestException
	 * @throws ForbiddenException
	 * @throws ServerException
	 */
    public static function getAll(array $request=[]): string|bool {
		if(!isset($request['url'])){ throw new BadRequestException('IceBear corsproxy: request has no url parameter'); }
        $url=$request['url'].'';
        unset($request['url']);
        return static::doGet($url, $request, true);
    }

    /**
     * @param $url
     * @param $request
     * @param bool $echoAndExit
     * @return bool|string
     * @throws BadRequestException
     * @throws ForbiddenException
     * @throws ServerException
     */
    public static function doGet($url, $request, bool $echoAndExit=false): bool|string {
        return static::doRequest($url, $request, 'get', $echoAndExit);
    }

    /**
     * @param $url
     * @param $request
     * @param bool $echoAndExit
     * @return bool|string
     * @throws BadRequestException
     * @throws ForbiddenException
     * @throws ServerException
     */
    public static function doPost($url, $request, bool $echoAndExit=false): bool|string {
        return static::doRequest($url, $request, 'post', $echoAndExit);
    }

    /**
     * @param $url
     * @param $request
     * @param bool $echoAndExit
     * @return bool|string
     * @throws BadRequestException
     * @throws ForbiddenException
     * @throws ServerException
     */
    public static function doPut($url, $request, bool $echoAndExit=false): bool|string {
        return static::doRequest($url, $request, 'put', $echoAndExit);
    }

	/**
	 * @param string $url
	 * @param array $request
	 * @param string $method
	 * @param bool $echoAndExit If true, echoes the response and exits. Otherwise, returns it.
	 * @return bool|string
	 * @throws BadRequestException
	 * @throws ForbiddenException
	 * @throws ServerException
	 */
    private static function doRequest(string $url, array $request, string $method, bool $echoAndExit): bool|string {
        if(!function_exists('curl_init')){
            throw new ServerException('Cannot communicate directly with remote server because their configuration does not allow it. Cannot use IceBear to relay messages. Your IceBear administrator needs to fix this: PHP cURL extension not installed.');
        }

        $method=strtolower($method);
        if(!session::getUserId()){
            throw new ForbiddenException('Cannot communicate directly with remote server because their configuration does not allow it. Cannot use IceBear to relay messages because you are not logged in.');
        } else if(!in_array($method,array('get','post','put'))){
            throw new BadRequestException('IceBear CORS proxy can only handle GET, POST or PUT');
        }
        
        //what we got from the client - cache on first sight, cURL may redirect to itself and eat them.
        //NB function_exists('apache_request_headers') is FALSE if running unit tests
        if(!isset(static::$apacheRequestHeaders) && function_exists('apache_request_headers')){
            static::$apacheRequestHeaders=apache_request_headers();
        } else {
            static::$apacheRequestHeaders=array();
        }

        //what we will actually send
        $requestHeaders=array();
        if(isset($request['httpHeaders'])){
            $requestHeaders=$request['httpHeaders'];
        }

        //wipe any previous response headers, e.g., from redirected response
        static::$responseHeaders=array();

        //We don't pass these forward - security reasons, etc.
        //We also strip out any X- headers that the client decided to send
        $dontForward=array('Cookie','Host','Origin','Referer','Connection','Content-Length','X-Prototype-Version','x-prototype-version');


        foreach(static::$apacheRequestHeaders as $header=>$value){
            if($header==="X-IceBear-Remote-Cookie") {
                $requestHeaders[] = 'Cookie: ' . $value;
            } else if($header==="X-IceBear-Remote-Origin") {
                $requestHeaders[]='Origin: '.$value;
            } else if(!in_array($header, $dontForward)){
                $requestHeaders[]=$header.': '.$value;
            }
        }

        ini_set('max_execution_time',0);

        $ch = curl_init($url);
        $options = array(
			CURLOPT_ENCODING => '',
			CURLOPT_COOKIEFILE => '',
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_RETURNTRANSFER  => true,
            CURLOPT_HEADER          => false,
            CURLOPT_HEADERFUNCTION  => array("corsproxy","curlResponseHeaderCallback"),
            CURLOPT_HTTPHEADER      =>$requestHeaders,
            CURLOPT_CONNECTTIMEOUT  => 20,
            CURLOPT_TIMEOUT         => 20,
            CURLOPT_SSL_VERIFYHOST  => 0,
            CURLOPT_SSL_VERIFYPEER  => false,
            CURLOPT_UNRESTRICTED_AUTH => true
        );
        if('post'==$method){
            $options[CURLOPT_POST]=1;
            if(isset($request['rawPostBody'])){
                $options[CURLOPT_POSTFIELDS]=$request['rawPostBody'];
                if(json_decode($request['rawPostBody'])){
                    $requestHeaders[]='Content-Type: application/json';
                    $options[CURLOPT_HTTPHEADER]=$requestHeaders;
                }
            } else {
                $options[CURLOPT_POSTFIELDS]=$request;
            }
        } else if('put'==$method){
            curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PUT");
            curl_setopt($ch, CURLOPT_POSTFIELDS, $request['rawPostBody']);
        }

        curl_setopt_array($ch,$options);
        $data = curl_exec($ch);

        $redirectUrl=curl_getinfo($ch, CURLINFO_REDIRECT_URL);
        curl_close($ch);
        if(empty($redirectUrl)){
            if($echoAndExit){
                foreach(static::$responseHeaders as $h){
                    header($h);
                    $data=str_replace($h, '', $data);
                }
                if(!empty(static::$responseCookies)){
                    foreach (static::$responseCookies as $h){
                        header($h);
                    }
                }
                echo $data;
                exit;
            }
            return $data;
        }
        //If we got here, it's a redirect
        if(!empty(static::$responseCookies)){
            foreach (static::$responseCookies as $h){
                header($h);
            }
        }

        if('put'==$method){
            return static::doPut($redirectUrl, $request, $echoAndExit);
        } else {
            return static::doPost($redirectUrl, $request, $echoAndExit);
        }
    }

	/**
	 * @param mixed $curlHandle
	 * @param string $headerLine
	 * @return int
	 * @noinspection PhpUnusedParameterInspection
	 */
	private static function curlResponseHeaderCallback(mixed $curlHandle, string $headerLine): int {
        if(!isset(static::$responseCookies)){
            static::$responseCookies=array();
        }
        if (preg_match('/^Set-Cookie:\s*([^;]*)/mi', $headerLine) == 1){
            static::$responseCookies[]='X-IceBear-Remote-'.$headerLine;
        }
        return strlen($headerLine); // Needed by curl
    }
    private static array $responseCookies;

    private static array $responseHeaders;

    private static array $apacheRequestHeaders;

 }