CurlBrowser.php

<?php

namespace Tlf\Tester;

class CurlBrowser {

    /**
     * The default host to connect to, such as http://localhost:3183
     */
    public string $default_host;


    /**
     * Array of responses as built by `curl_get_response()`, in the order they were received
     */
    public array $responses = [];

    public function __construct(?string $default_host = null){
        $this->default_host = $default_host;
    }

    /**
     * the body of the last response
     */
    public function __toString(){
        return end($this->responses)['body'];
    }


    /**
     * @return the last response or false if no responses
     */
    public function last(){
        return end($this->responses);
    }

    /**
     * Follow the redirect sent with the last request
     * @return true if a redirect happened, false if there was no `Location` header to goto
     */
    public function follow(){
        $response = end($this->responses);
        if (!isset($response['headers']['Location']))return false;

        $this->get($response['headers']['Location']);
         
        return true;
    }

    public function get($path, $params=[], $headers=[], $curl_opts = []){
        $ch = curl_init();

        $url = $this->make_url($path,$params);

        if (isset($headers['Cookie'])){
            $cookie = $headers['Cookie'];
            unset($headers['Cookie']);
            curl_setopt($ch, CURLOPT_COOKIE, $cookie);
        }

        $curl_headers = [];
        foreach ($headers as $key=>$value){
            if (is_int($key)){
                $curl_headers[] = $value;
                continue;
            }
            $curl_headers[] = $key.': '.$value;
        }

        curl_setopt_array($ch,
            [
            CURLOPT_URL => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST => false,
            CURLOPT_HEADER => true,
            CURLOPT_FOLLOWLOCATION => false,
            CURLOPT_HTTPHEADER => $curl_headers,
            ]
        );


        if (count($curl_opts) > 0 ){
            curl_setopt_array($ch, $curl_opts);
        }


        $response = $this->curl_get_response($ch);
        curl_close($ch);

        $this->responses[] = $response;
        return $response;
    }

    /**
     *
     * @beta
     *
     * @param $params POST params
     * @param $headers array like `['HeaderKey: header_value', 'Cookie'=> 'cookie_key=cookie_value;cookie2=value2;']` ... ONLY `Cookie` should have an array key. All other header keys should be in the array value and use a numeric index
     * @param $files ... idr
     * @param $curl_opts array of php `CURLOPT`s to set
     */
    public function post($path, $params=[], $headers=[], $files=[], $curl_opts = []){
        $ch = curl_init();

        $url = $this->make_url($path,[]);

        if (isset($headers['Cookie'])){
            $cookie = $headers['Cookie'];
            unset($headers['Cookie']);
            curl_setopt($ch, CURLOPT_COOKIE, $cookie);
        }
        $curl_headers = [];
        foreach ($headers as $key=>$value){
            if (is_int($key)){
                $curl_headers[] = $value;
                continue;
            }
            $curl_headers[] = $key.': '.$value;
        }

        foreach ($files as $key=>$f){
            if (!is_file($f)){
                throw new \Exception("File '$f' for key '$key' does not exist or is not a file.");
            }
            $cf = new \CURLFile($f, null, basename($f));
            $params[$key] = $cf;
        }
        if (is_string($params)){
            $post_fields = $params;
        }else if (count($files) > 0){
            $post_fields = $params;
        } else {
            $post_fields = http_build_query($params);
        }

        curl_setopt_array($ch,
            [
            CURLOPT_URL => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => $post_fields,
            CURLOPT_HEADER => true,
            CURLOPT_FOLLOWLOCATION => false,
            CURLOPT_HTTPHEADER => $curl_headers,
            ]
        );

        if (count($curl_opts) > 0 ){
            foreach ($curl_opts as $key=>$value){
                curl_setopt($ch, $key, $value);
            }
            // curl_setopt_array($ch, $curl_opts);
        }

        $response = $this->curl_get_response($ch);
        curl_close($ch);
        $this->responses[] = $response;
        return $response;
    }

    /**
     * @param $url the url to connect to. if default_host is set, just needs a path
     * @param $params array of paramaters to encode into the url. If your url path includes `?params=whatever`, it will not play nicely with this.
     */
    public function make_url(string $url, array $params=[]): string {
        $parsed = parse_url($url);
        if (!isset($parsed[PHP_URL_HOST]))$url = $this->default_host.$url;

        if ($params!=[]) $url .= '?' . http_build_query($params);
        return $url;
    }


    /**
     * @param $header_value, for `Set-cookie: whatever whatever`, this should be `whatever whatever`
     * @return array parsed cookie
     *
     *
     * @issue has no handling for a cookie value containing a semi-colon or an equal sign
     */
    public function parse_cookie_header($header_value): array {
        $cookie = [];
        $parts = explode(';', $header_value);
        $parts = array_map('trim',$parts);

        $main = array_shift($parts);
        $main_parts = explode('=', $main);
        $cookie['name'] = $main_parts[0];
        $cookie['value'] = $main_parts[1];


        foreach ($parts as $str){
            $kv_parts = explode('=', $str);
            if (count($kv_parts)==1)$cookie[$str] = true;
            else $cookie[$kv_parts[0]] = $kv_parts[1];
        }
        return $cookie;
    }


    /**
     * @param $ch curl handle
     * @param $files array of key=>absolute file paths
     * @param $params array to use as CURLOPT_POSTFIELDS
     */
    public function curl_add_files($ch, array $files, array &$params){
        foreach ($files as $name=>$path){
            $mimetype = mime_content_type($path);
            // $mimetype = false;
            if ($mimetype==false){
                $ext = pathinfo($path,PATHINFO_EXTENSION);
                $list = require(dirname(__DIR__).'/mime_type_map.php');
                $mimetype = $list['mimes'][$ext][0];
            }
            $params[$name] = new \CURLFile($path, $mimetype, basename($path));
        }
    }

    public function curl_get_response($ch){
        $response = curl_exec($ch);

        $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);

        $header_text = substr($response,0,$header_size);

        $parts = explode("\n", $header_text);
        $parts = array_map('trim', $parts);
        $headers = [];
        $cookies = [];
        foreach ($parts as $line){
            $pos = strpos($line,':');
            if ($pos===false)continue;
            $key = substr($line,0,$pos);
            $value = trim(substr($line,$pos+1));
            if (isset($headers[$key])){
                if (!is_array($headers[$key]))$headers[$key] = [$headers[$key]];
                $headers[$key][] = $value;
            } else {
                $headers[$key] = $value;
            }
            if ($key=='Set-Cookie'){
                $cookie = $this->parse_cookie_header($value);
                $cookies[$cookie['name']] = $cookie;
            }
        }

        // TODO: parse Set-Cookie headers 

        $body = substr($response,$header_size);
        return [
            'header_text'=>$header_text,
            'body'=>$body,
            'headers'=>$headers,
            'cookies'=>$cookies,
        ];
    }
}