<?php

//require __DIR__ . '/../vendor/autoload.php';

//use Yahooauc\Browser as Browser;
require_once(_PS_MODULE_DIR_.'yahooauctions/yahoomaster/src/simple_html_dom.php');
require_once(_PS_MODULE_DIR_.'yahooauctions/yahoomaster/src/Exceptions/BrowserLoginException.php');
require_once(_PS_MODULE_DIR_.'yahooauctions/yahoomaster/src/Exceptions/BrowserException.php');
//namespace Yahooauc;

//use Yahooauc\Parser;
use Yahooauc\Exceptions\BrowserException;
use Yahooauc\Exceptions\BrowserLoginException;
//use Requests_Session;
//use Requests;

class Browser
{
    private $userName            = null;
    private $userPass            = null;
    private $appId               = null;

    private $session             = null;
    private $auctionInfo         = null;

    private static $AUCTION_URL  = 'https://auctions.yahoo.co.jp/';
    private static $LOGIN_URL    = 'https://login.yahoo.co.jp/config/login';
    private static $CLOSED_USER  = 'https://auctions.yahoo.co.jp/closeduser/jp/show/mystatus';
    private static $OPEN_USER    = 'https://auctions.yahoo.co.jp/openuser/jp/show/mystatus';
    private static $BID_PREVIEW  = 'https://auctions.yahoo.co.jp/jp/show/bid_preview';
    private static $PLACE_BID    = 'https://auctions.yahoo.co.jp/jp/config/placebid';
    private static $API_URL      = 'https://auctions.yahooapis.jp/AuctionWebService/V2/auctionItem';

    private static $BROWSER_HEADERS = [
        'User-Agent' => 'Mozilla/6.0 (Windows; U; Windows NT 6.0; ja; rv:1.9.1.1) Gecko/20090715 Firefox/3.5.1 (.NET CLR 3.5.30729)',
    ];

    /**
     * Create Browser object with stored cookie session or create new session
     *
     * @param  string $userName  Your Yahoo account user name
     * @param  string $userPass  Your Yahoo account password
     * @param  string $appId     Your Yahoo application ID
     * @param  string $cookieJar Stored cookieJar object
     * @return object            Return setted Browser object
     *
     */
    public function __construct($userName, $userPass, $appId, $cookieJar = null, $requestOptons = [])
    {
        $this->userName      = $userName;
        $this->userPass      = $userPass;
        $this->appId         = $appId;
        $this->requestOptons = $requestOptons;
        
        if ($cookieJar)
        {
           
            $this->session = new Requests_Session(static::$AUCTION_URL, static::$BROWSER_HEADERS, [], ['cookies' => $cookieJar]);
        }
        else
        {
           
            $this->login();
        }
    }

    /**
     * Get Browser cookiesJar to store for the next request without login
     *
     */
    public function getCookie()
    {
        return $this->session->options['cookies'];
    }

    /**
     * Get XML odject by auction id
     *
     * @param  string $auc_id   Auction ID
     * @return SimpleXMLElement Return XML Object
     * @throws BrowserException Throw exception if auction id is invalid or not found
     *
     */
    public function getAuctionInfoAsXml($auc_id)
    {
        $query = [
            'appid' => $this->appId,
            'auctionID' => $auc_id,
        ];

        $body = $this->getBody(static::$API_URL, $query);

        $info = simplexml_load_string($body);

        if ($info->Code)
        {
            if ( (int) $info->Code == 102)
            {
                throw new BrowserException('Auction not found', 102);
            }
            else if ( (int) $info->Code == 302 )
            {
                throw new BrowserException('Auction ID is invalid', 302);
            }
        }

        $this->auctionInfo = $info;
        
        return $info;
    }

    /**
     * Return images link of lot
     *
     * @param  string $auc_id Auction ID
     * @return array          Return array with images url from stored auctionInfo
     *
     */
    public function getAuctionImgsUrl($auc_id = null)
    {
        if ($auc_id)
        {
            $this->getAuctionInfoAsXml($auc_id);
        }

        if ($this->auctionInfo == null)
            return [];
        
        $imges = [];

        foreach ($this->auctionInfo->Result->Img->children() as $img)
        {
            $imges[] = (string) $img;
        }

        return $imges;
    }

    /**
     * Get XML odject by auction id
     *
     * @return SimpleXMLElement|null Return storred XML Object or null
     *
     */
    public function getStoredAuctionInfoAsXml()
    {
        return $this->auctionInfo;
    }

    /**
     * Return only won auction IDs per page
     *
     * @param  int $page Number of page with won lots
     * @return array     Return array with only won auction IDs
     *
     */
    public function getWonIds($page = 1)
    {
        $query = [
            'select'  => 'won',
            'picsnum' => '50',
            'apg'     => $page
        ];

        $body = $this->getBody(static::$CLOSED_USER, $query);

        return Parser::getWonIds($body);
    }

    /**
     * Get lots information on bid by setted page
     *
     * @param  int $page Number of bidding page
     * @return array     Return array with lot information and bidding pages if they exist
     *
     */
    public function getBiddingLots($page = 1)
    {
        $query = [
            'select'  => 'bidding',
            'picsnum' => '50',
            'apg'     => $page
        ];

        $body = $this->getBody(static::$OPEN_USER, $query);

        return Parser::getBiddingLots($body);        
    }

    /**
     * Bid on yahoo lot
     *
     * @param  string $auc_id auction ID
     * @param  int $price     Price to bid
     * @return bool           Reurn true if bid was successful
     *
     */
    public function bid($auc_id, $price = 0)
    {
        $info = $this->getAuctionInfoAsXml($auc_id);

        if ( (string) $info->Result->Status != 'open' )
        {
            throw new BrowserException('Auction has ended');
        }

        $auc_url = (string) $info->Result->AuctionItemUrl;

        $body = $this->getBody($auc_url);

        $inputs = Parser::getHiddenInputs($body);
        $options = $this->createBidRequstOptions($inputs, $price);

        $body = $this->getBody(static::$BID_PREVIEW, $options, Requests::POST);

        $inputs = Parser::getHiddenInputs($body);
        $options = $this->createBidRequstOptions($inputs, $price);

        $body = $this->getBody(static::$PLACE_BID, $options, Requests::POST);

        return Parser::getResult($body);
    }

    /**
     * Try to login into yahoo auction with setted credentials
     *
     * @return void
     * @throws BrowserLoginException Throw exception if something wrong
     *
     */
    private function login()
    {
      
        $this->session  = new Requests_Session(static::$AUCTION_URL, static::$BROWSER_HEADERS);

        $query = [
            '.lg' => 'jp',
            '.intl' => 'jp',
            '.src' => 'auc',
            '.done' => static::$AUCTION_URL,
        ];

        $this->getBody(static::$AUCTION_URL);

        $body = $this->getBody(static::$LOGIN_URL, $query);

        preg_match_all(
            '/document\.getElementsByName\("\.albatross"\)\[0\]\.value = "(.*?)";/',
            $body,
            $albatross,
            PREG_SET_ORDER
        );

        if (!$albatross[0][1])
        {
            throw new BrowserLoginException('Albatross key not found');
        }

        $inputs = Parser::getHiddenInputs($body);

        $options = [];
      
        foreach ($inputs as $v)
        {
            if ($v['name'] == '.nojs')
            {
                continue;
            }

            if ($v['name'] == '.albatross')
            {
                $v['value'] = $albatross[0][1];
            }

            $options[$v['name']] = $v['value'];
        }

        $options['login']       = $this->userName;
        $options['passwd']      = $this->userPass;
        $options['.persistent'] = 'y';
        
         $agent = "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36";

        $reffer = "https://login.yahoo.co.jp/config/login?.lg=jp&.intl=jp&logout=1&.src=auc&.done=https%3A%2F%2Fauctions.yahoo.co.jp%2F";
        //var_dump($options);
        // log out.
        $LOGINURL = "https://login.yahoo.co.jp/id_check?login=".$options['login']."&bcrumb=".$options['bcrumb_id_check']."&verify=0&.display=&time=".strtotime(date('d-m-Y'))."&fido=0";
        
        $ch = curl_init();
        //curl_setopt($ch, CURLOPT_HTTPHEADER, $curlHeaders);
        //curl_setopt($ch, CURLOPT_HEADER, TRUE);
        curl_setopt($ch, CURLOPT_URL,$LOGINURL);
        curl_setopt($ch, CURLOPT_USERAGENT, $agent);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
        //curl_setopt($ch, CURLOPT_REFERER, $reffer);
        //curl_setopt($ch, CURLOPT_PROXY, '206.221.182.74:2083');
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
        curl_setopt($ch,CURLOPT_COOKIE, "TLS=v=1.2&r=1;B=3gi8i9le0mij9&b=3&s=d4");
        $result = curl_exec ($ch);
        //var_dump($result);
        //echo curl_error($ch);
        curl_close ($ch);
        
       
        /* Pause before submit (important because yahoo decline login if it's too fast) */
        sleep(5);

        $body = $this->getBody(static::$LOGIN_URL, $options, Requests::POST);
        //echo $body;
       
        /* Check for correct login */
        if (Parser::checkLogin($body, $this->userName) === false )
        {
            throw new BrowserLoginException('Login failed');
        }
    }

    /**
     * Send request to Yahoo and return body of HTML
     *
     * @param  string $url    Target url
     * @param  array $options Query parameters if method is GET or data values if method is POST
     * @param  string $method Request method
     * @return string         Return body of HTML
     *
     */
    private function getBody($url, $options = null, $method = Requests::GET)
    {
        $response = $this->session->request($url, [], $options, $method, $this->requestOptons);

        return $response->body;
    }

    /**
     * Create new options for request by values recived from response
     *
     * @param  array   $values  Array with pair name and value
     * @param  integer $price   Bid price
     * @return array            Return options for request
     * @throws BrowserException Throw exception if given price lower than current
     *
     */
    private function createBidRequstOptions($values, $price)
    {
        $options = [];
        $price_setted = false;

        foreach ($values as $value)
        {
            if(!$value['name'])
                continue;

            if ($value['name'] == 'setPrice')
            {
                if ($price < $value['value'])
                {
                    throw new BrowserException('Price must be upper or equal '.$value['value']);
                }
            }

            /* Do not subscribe (if store) */
            if ($value['name'] == 'mnewsoptin')
            {
                $value['value'] = 0;
            }

            if ($value['name'] == 'Bid')
            {
                $value['value'] = $price;
                $price_setted = true;
            }

            $options[$value['name']] = $value['value'];
        }

        if (!$price_setted)
        {
            $options['Bid'] = $price;
        }

        $options['Quantity'] = 1;

        return $options;
    }
}


class Requests_Session {
    /**
     * Base URL for requests
     *
     * URLs will be made absolute using this as the base
     * @var string|null
     */
    public $url = null;
 
    /**
     * Base headers for requests
     * @var array
     */
    public $headers = array();
 
    /**
     * Base data for requests
     *
     * If both the base data and the per-request data are arrays, the data will
     * be merged before sending the request.
     *
     * @var array
     */
    public $data = array();
 
    /**
     * Base options for requests
     *
     * The base options are merged with the per-request data for each request.
     * The only default option is a shared cookie jar between requests.
     *
     * Values here can also be set directly via properties on the Session
     * object, e.g. `$session->useragent = 'X';`
     *
     * @var array
     */
    public $options = array();
 
    /**
     * Create a new session
     *
     * @param string|null $url Base URL for requests
     * @param array $headers Default headers for requests
     * @param array $data Default data for requests
     * @param array $options Default options for requests
     */
    public function __construct($url = null, $headers = array(), $data = array(), $options = array()) {
        $this->url = $url;
        $this->headers = $headers;
        $this->data = $data;
        $this->options = $options;
 
        if (empty($this->options['cookies'])) {
            $this->options['cookies'] = new Requests_Cookie_Jar();
        }
    }
 
    /**
     * Get a property's value
     *
     * @param string $key Property key
     * @return mixed|null Property value, null if none found
     */
    public function __get($key) {
        if (isset($this->options[$key])) {
            return $this->options[$key];
        }
 
        return null;
    }
 
    /**
     * Set a property's value
     *
     * @param string $key Property key
     * @param mixed $value Property value
     */
    public function __set($key, $value) {
        $this->options[$key] = $value;
    }
 
    /**
     * Remove a property's value
     *
     * @param string $key Property key
     */
    public function __isset($key) {
        return isset($this->options[$key]);
    }
 
    /**
     * Remove a property's value
     *
     * @param string $key Property key
     */
    public function __unset($key) {
        if (isset($this->options[$key])) {
            unset($this->options[$key]);
        }
    }
 
    /**#@+
     * @see request()
     * @param string $url
     * @param array $headers
     * @param array $options
     * @return Requests_Response
     */
    /**
     * Send a GET request
     */
    public function get($url, $headers = array(), $options = array()) {
        return $this->request($url, $headers, null, Requests::GET, $options);
    }
 
    /**
     * Send a HEAD request
     */
    public function head($url, $headers = array(), $options = array()) {
        return $this->request($url, $headers, null, Requests::HEAD, $options);
    }
 
    /**
     * Send a DELETE request
     */
    public function delete($url, $headers = array(), $options = array()) {
        return $this->request($url, $headers, null, Requests::DELETE, $options);
    }
    /**#@-*/
 
    /**#@+
     * @see request()
     * @param string $url
     * @param array $headers
     * @param array $data
     * @param array $options
     * @return Requests_Response
     */
    /**
     * Send a POST request
     */
    public function post($url, $headers = array(), $data = array(), $options = array()) {
        return $this->request($url, $headers, $data, Requests::POST, $options);
    }
 
    /**
     * Send a PUT request
     */
    public function put($url, $headers = array(), $data = array(), $options = array()) {
        return $this->request($url, $headers, $data, Requests::PUT, $options);
    }
 
    /**
     * Send a PATCH request
     *
     * Note: Unlike {@see post} and {@see put}, `$headers` is required, as the
     * specification recommends that should send an ETag
     *
     * @link https://tools.ietf.org/html/rfc5789
     */
    public function patch($url, $headers, $data = array(), $options = array()) {
        return $this->request($url, $headers, $data, Requests::PATCH, $options);
    }
    /**#@-*/
 
    /**
     * Main interface for HTTP requests
     *
     * This method initiates a request and sends it via a transport before
     * parsing.
     *
     * @see Requests::request()
     *
     * @throws Requests_Exception On invalid URLs (`nonhttp`)
     *
     * @param string $url URL to request
     * @param array $headers Extra headers to send with the request
     * @param array|null $data Data to send either as a query string for GET/HEAD requests, or in the body for POST requests
     * @param string $type HTTP request type (use Requests constants)
     * @param array $options Options for the request (see {@see Requests::request})
     * @return Requests_Response
     */
    public function request($url, $headers = array(), $data = array(), $type = Requests::GET, $options = array()) {
        $request = $this->merge_request(compact('url', 'headers', 'data', 'options'));
 
        return Requests::request($request['url'], $request['headers'], $request['data'], $type, $request['options']);
    }
 
    /**
     * Send multiple HTTP requests simultaneously
     *
     * @see Requests::request_multiple()
     *
     * @param array $requests Requests data (see {@see Requests::request_multiple})
     * @param array $options Global and default options (see {@see Requests::request})
     * @return array Responses (either Requests_Response or a Requests_Exception object)
     */
    public function request_multiple($requests, $options = array()) {
        foreach ($requests as $key => $request) {
            $requests[$key] = $this->merge_request($request, false);
        }
 
        $options = array_merge($this->options, $options);
 
        // Disallow forcing the type, as that's a per request setting
        unset($options['type']);
 
        return Requests::request_multiple($requests, $options);
    }
 
    /**
     * Merge a request's data with the default data
     *
     * @param array $request Request data (same form as {@see request_multiple})
     * @param boolean $merge_options Should we merge options as well?
     * @return array Request data
     */
    protected function merge_request($request, $merge_options = true) {
        if ($this->url !== null) {
            $request['url'] = Requests_IRI::absolutize($this->url, $request['url']);
            $request['url'] = $request['url']->uri;
        }
 
        if (empty($request['headers'])) {
            $request['headers'] = array();
        }
        $request['headers'] = array_merge($this->headers, $request['headers']);
 
        if (empty($request['data'])) {
            if (is_array($this->data)) {
                $request['data'] = $this->data;
            }
        }
        elseif (is_array($request['data']) && is_array($this->data)) {
            $request['data'] = array_merge($this->data, $request['data']);
        }
 
        if ($merge_options !== false) {
            $request['options'] = array_merge($this->options, $request['options']);
 
            // Disallow forcing the type, as that's a per request setting
            unset($request['options']['type']);
        }
 
        return $request;
    }
}


class Requests_Cookie_Jar{
    /**
     * Actual item data
     *
     * @var array
     */
    protected $cookies = array();
 
    /**
     * Create a new jar
     *
     * @param array $cookies Existing cookie values
     */
    public function __construct($cookies = array()) {
        $this->cookies = $cookies;
    }
 
    /**
     * Normalise cookie data into a Requests_Cookie
     *
     * @param string|Requests_Cookie $cookie
     * @return Requests_Cookie
     */
    public function normalize_cookie($cookie, $key = null) {
        if ($cookie instanceof Requests_Cookie) {
            return $cookie;
        }
 
        return Requests_Cookie::parse($cookie, $key);
    }
 
    /**
     * Normalise cookie data into a Requests_Cookie
     *
     * @codeCoverageIgnore
     * @deprecated Use {@see Requests_Cookie_Jar::normalize_cookie}
     * @return Requests_Cookie
     */
    public function normalizeCookie($cookie, $key = null) {
        return $this->normalize_cookie($cookie, $key);
    }
 
    /**
     * Check if the given item exists
     *
     * @param string $key Item key
     * @return boolean Does the item exist?
     */
    public function offsetExists($key) {
        return isset($this->cookies[$key]);
    }
 
    /**
     * Get the value for the item
     *
     * @param string $key Item key
     * @return string Item value
     */
    public function offsetGet($key) {
        if (!isset($this->cookies[$key])) {
            return null;
        }
 
        return $this->cookies[$key];
    }
 
    /**
     * Set the given item
     *
     * @throws Requests_Exception On attempting to use dictionary as list (`invalidset`)
     *
     * @param string $key Item name
     * @param string $value Item value
     */
    public function offsetSet($key, $value) {
        if ($key === null) {
            throw new Requests_Exception('Object is a dictionary, not a list', 'invalidset');
        }
 
        $this->cookies[$key] = $value;
    }
 
    /**
     * Unset the given header
     *
     * @param string $key
     */
    public function offsetUnset($key) {
        unset($this->cookies[$key]);
    }
 
    /**
     * Get an iterator for the data
     *
     * @return ArrayIterator
     */
    public function getIterator() {
        return new ArrayIterator($this->cookies);
    }
 
    /**
     * Register the cookie handler with the request's hooking system
     *
     * @param Requests_Hooker $hooks Hooking system
     */
    public function register(Requests_Hooker $hooks) {
        $hooks->register('requests.before_request', array($this, 'before_request'));
        $hooks->register('requests.before_redirect_check', array($this, 'before_redirect_check'));
    }
 
    /**
     * Add Cookie header to a request if we have any
     *
     * As per RFC 6265, cookies are separated by '; '
     *
     * @param string $url
     * @param array $headers
     * @param array $data
     * @param string $type
     * @param array $options
     */
    public function before_request($url, &$headers, &$data, &$type, &$options) {
        if (!$url instanceof Requests_IRI) {
            $url = new Requests_IRI($url);
        }
 
        if (!empty($this->cookies)) {
            $cookies = array();
            foreach ($this->cookies as $key => $cookie) {
                $cookie = $this->normalize_cookie($cookie, $key);
 
                // Skip expired cookies
                if ($cookie->is_expired()) {
                    continue;
                }
 
                if ($cookie->domain_matches($url->host)) {
                    $cookies[] = $cookie->format_for_header();
                }
            }
 
            $headers['Cookie'] = implode('; ', $cookies);
        }
    }
 
    /**
     * Parse all cookies from a response and attach them to the response
     *
     * @var Requests_Response $response
     */
    public function before_redirect_check(Requests_Response &$return) {
        $url = $return->url;
        if (!$url instanceof Requests_IRI) {
            $url = new Requests_IRI($url);
        }
 
        $cookies = Requests_Cookie::parse_from_headers($return->headers, $url);
        $this->cookies = array_merge($this->cookies, $cookies);
        $return->cookies = $this;
    }
}



class Requests {
    /**
     * POST method
     *
     * @var string
     */
    const POST = 'POST';
 
    /**
     * PUT method
     *
     * @var string
     */
    const PUT = 'PUT';
 
    /**
     * GET method
     *
     * @var string
     */
    const GET = 'GET';
 
    /**
     * HEAD method
     *
     * @var string
     */
    const HEAD = 'HEAD';
 
    /**
     * DELETE method
     *
     * @var string
     */
    const DELETE = 'DELETE';
 
    /**
     * OPTIONS method
     *
     * @var string
     */
    const OPTIONS = 'OPTIONS';
 
    /**
     * TRACE method
     *
     * @var string
     */
    const TRACE = 'TRACE';
 
    /**
     * PATCH method
     *
     * @link https://tools.ietf.org/html/rfc5789
     * @var string
     */
    const PATCH = 'PATCH';
 
    /**
     * Default size of buffer size to read streams
     *
     * @var integer
     */
    const BUFFER_SIZE = 1160;
 
    /**
     * Current version of Requests
     *
     * @var string
     */
    const VERSION = '1.7';
 
    /**
     * Registered transport classes
     *
     * @var array
     */
    protected static $transports = array();
 
    /**
     * Selected transport name
     *
     * Use {@see get_transport()} instead
     *
     * @var array
     */
    public static $transport = array();
 
    /**
     * Default certificate path.
     *
     * @see Requests::get_certificate_path()
     * @see Requests::set_certificate_path()
     *
     * @var string
     */
    protected static $certificate_path;
 
    /**
     * This is a static class, do not instantiate it
     *
     * @codeCoverageIgnore
     */
    private function __construct() {}
 
    /**
     * Autoloader for Requests
     *
     * Register this with {@see register_autoloader()} if you'd like to avoid
     * having to create your own.
     *
     * (You can also use `spl_autoload_register` directly if you'd prefer.)
     *
     * @codeCoverageIgnore
     *
     * @param string $class Class name to load
     */
    public static function autoloader($class) {
        // Check that the class starts with "Requests"
        if (strpos($class, 'Requests') !== 0) {
            return;
        }
 
        $file = str_replace('_', '/', $class);
        if (file_exists(dirname(__FILE__) . '/' . $file . '.php')) {
            require_once(dirname(__FILE__) . '/' . $file . '.php');
        }
    }
 
    /**
     * Register the built-in autoloader
     *
     * @codeCoverageIgnore
     */
    public static function register_autoloader() {
        spl_autoload_register(array('Requests', 'autoloader'));
    }
 
    /**
     * Register a transport
     *
     * @param string $transport Transport class to add, must support the Requests_Transport interface
     */
    public static function add_transport($transport) {
        if (empty(self::$transports)) {
            self::$transports = array(
                'Requests_Transport_cURL',
                'Requests_Transport_fsockopen',
            );
        }
 
        self::$transports = array_merge(self::$transports, array($transport));
    }
 
    /**
     * Get a working transport
     *
     * @throws Requests_Exception If no valid transport is found (`notransport`)
     * @return Requests_Transport
     */
    protected static function get_transport($capabilities = array()) {
        // Caching code, don't bother testing coverage
        // @codeCoverageIgnoreStart
        // array of capabilities as a string to be used as an array key
        ksort($capabilities);
        $cap_string = serialize($capabilities);
 
        // Don't search for a transport if it's already been done for these $capabilities
        if (isset(self::$transport[$cap_string]) && self::$transport[$cap_string] !== null) {
            return new self::$transport[$cap_string]();
        }
        // @codeCoverageIgnoreEnd
 
        if (empty(self::$transports)) {
            self::$transports = array(
                'Requests_Transport_cURL',
                'Requests_Transport_fsockopen',
            );
        }
 
        // Find us a working transport
        foreach (self::$transports as $class) {
            if (!class_exists($class)) {
                continue;
            }
 
            $result = call_user_func(array($class, 'test'), $capabilities);
            if ($result) {
                self::$transport[$cap_string] = $class;
                break;
            }
        }
        if (self::$transport[$cap_string] === null) {
            throw new Requests_Exception('No working transports found', 'notransport', self::$transports);
        }
 
        return new self::$transport[$cap_string]();
    }
 
    /**#@+
     * @see request()
     * @param string $url
     * @param array $headers
     * @param array $options
     * @return Requests_Response
     */
    /**
     * Send a GET request
     */
    public static function get($url, $headers = array(), $options = array()) {
        return self::request($url, $headers, null, self::GET, $options);
    }
 
    /**
     * Send a HEAD request
     */
    public static function head($url, $headers = array(), $options = array()) {
        return self::request($url, $headers, null, self::HEAD, $options);
    }
 
    /**
     * Send a DELETE request
     */
    public static function delete($url, $headers = array(), $options = array()) {
        return self::request($url, $headers, null, self::DELETE, $options);
    }
 
    /**
     * Send a TRACE request
     */
    public static function trace($url, $headers = array(), $options = array()) {
        return self::request($url, $headers, null, self::TRACE, $options);
    }
    /**#@-*/
 
    /**#@+
     * @see request()
     * @param string $url
     * @param array $headers
     * @param array $data
     * @param array $options
     * @return Requests_Response
     */
    /**
     * Send a POST request
     */
    public static function post($url, $headers = array(), $data = array(), $options = array()) {
        return self::request($url, $headers, $data, self::POST, $options);
    }
    /**
     * Send a PUT request
     */
    public static function put($url, $headers = array(), $data = array(), $options = array()) {
        return self::request($url, $headers, $data, self::PUT, $options);
    }
 
    /**
     * Send an OPTIONS request
     */
    public static function options($url, $headers = array(), $data = array(), $options = array()) {
        return self::request($url, $headers, $data, self::OPTIONS, $options);
    }
 
    /**
     * Send a PATCH request
     *
     * Note: Unlike {@see post} and {@see put}, `$headers` is required, as the
     * specification recommends that should send an ETag
     *
     * @link https://tools.ietf.org/html/rfc5789
     */
    public static function patch($url, $headers, $data = array(), $options = array()) {
        return self::request($url, $headers, $data, self::PATCH, $options);
    }
    /**#@-*/
 
    /**
     * Main interface for HTTP requests
     *
     * This method initiates a request and sends it via a transport before
     * parsing.
     *
     * The `$options` parameter takes an associative array with the following
     * options:
     *
     * - `timeout`: How long should we wait for a response?
     *    Note: for cURL, a minimum of 1 second applies, as DNS resolution
     *    operates at second-resolution only.
     *    (float, seconds with a millisecond precision, default: 10, example: 0.01)
     * - `connect_timeout`: How long should we wait while trying to connect?
     *    (float, seconds with a millisecond precision, default: 10, example: 0.01)
     * - `useragent`: Useragent to send to the server
     *    (string, default: php-requests/$version)
     * - `follow_redirects`: Should we follow 3xx redirects?
     *    (boolean, default: true)
     * - `redirects`: How many times should we redirect before erroring?
     *    (integer, default: 10)
     * - `blocking`: Should we block processing on this request?
     *    (boolean, default: true)
     * - `filename`: File to stream the body to instead.
     *    (string|boolean, default: false)
     * - `auth`: Authentication handler or array of user/password details to use
     *    for Basic authentication
     *    (Requests_Auth|array|boolean, default: false)
     * - `proxy`: Proxy details to use for proxy by-passing and authentication
     *    (Requests_Proxy|array|string|boolean, default: false)
     * - `max_bytes`: Limit for the response body size.
     *    (integer|boolean, default: false)
     * - `idn`: Enable IDN parsing
     *    (boolean, default: true)
     * - `transport`: Custom transport. Either a class name, or a
     *    transport object. Defaults to the first working transport from
     *    {@see getTransport()}
     *    (string|Requests_Transport, default: {@see getTransport()})
     * - `hooks`: Hooks handler.
     *    (Requests_Hooker, default: new Requests_Hooks())
     * - `verify`: Should we verify SSL certificates? Allows passing in a custom
     *    certificate file as a string. (Using true uses the system-wide root
     *    certificate store instead, but this may have different behaviour
     *    across transports.)
     *    (string|boolean, default: library/Requests/Transport/cacert.pem)
     * - `verifyname`: Should we verify the common name in the SSL certificate?
     *    (boolean: default, true)
     * - `data_format`: How should we send the `$data` parameter?
     *    (string, one of 'query' or 'body', default: 'query' for
     *    HEAD/GET/DELETE, 'body' for POST/PUT/OPTIONS/PATCH)
     *
     * @throws Requests_Exception On invalid URLs (`nonhttp`)
     *
     * @param string $url URL to request
     * @param array $headers Extra headers to send with the request
     * @param array|null $data Data to send either as a query string for GET/HEAD requests, or in the body for POST requests
     * @param string $type HTTP request type (use Requests constants)
     * @param array $options Options for the request (see description for more information)
     * @return Requests_Response
     */
    public static function request($url, $headers = array(), $data = array(), $type = self::GET, $options = array()) {
        if (empty($options['type'])) {
            $options['type'] = $type;
        }
        $options = array_merge(self::get_default_options(), $options);
 
        self::set_defaults($url, $headers, $data, $type, $options);
 
        $options['hooks']->dispatch('requests.before_request', array(&$url, &$headers, &$data, &$type, &$options));
 
        if (!empty($options['transport'])) {
            $transport = $options['transport'];
 
            if (is_string($options['transport'])) {
                $transport = new $transport();
            }
        }
        else {
            $need_ssl = (0 === stripos($url, 'https://'));
            $capabilities = array('ssl' => $need_ssl);
            $transport = self::get_transport($capabilities);
        }
        $response = $transport->request($url, $headers, $data, $options);
 
        $options['hooks']->dispatch('requests.before_parse', array(&$response, $url, $headers, $data, $type, $options));
 
        return self::parse_response($response, $url, $headers, $data, $options);
    }
 
    /**
     * Send multiple HTTP requests simultaneously
     *
     * The `$requests` parameter takes an associative or indexed array of
     * request fields. The key of each request can be used to match up the
     * request with the returned data, or with the request passed into your
     * `multiple.request.complete` callback.
     *
     * The request fields value is an associative array with the following keys:
     *
     * - `url`: Request URL Same as the `$url` parameter to
     *    {@see Requests::request}
     *    (string, required)
     * - `headers`: Associative array of header fields. Same as the `$headers`
     *    parameter to {@see Requests::request}
     *    (array, default: `array()`)
     * - `data`: Associative array of data fields or a string. Same as the
     *    `$data` parameter to {@see Requests::request}
     *    (array|string, default: `array()`)
     * - `type`: HTTP request type (use Requests constants). Same as the `$type`
     *    parameter to {@see Requests::request}
     *    (string, default: `Requests::GET`)
     * - `cookies`: Associative array of cookie name to value, or cookie jar.
     *    (array|Requests_Cookie_Jar)
     *
     * If the `$options` parameter is specified, individual requests will
     * inherit options from it. This can be used to use a single hooking system,
     * or set all the types to `Requests::POST`, for example.
     *
     * In addition, the `$options` parameter takes the following global options:
     *
     * - `complete`: A callback for when a request is complete. Takes two
     *    parameters, a Requests_Response/Requests_Exception reference, and the
     *    ID from the request array (Note: this can also be overridden on a
     *    per-request basis, although that's a little silly)
     *    (callback)
     *
     * @param array $requests Requests data (see description for more information)
     * @param array $options Global and default options (see {@see Requests::request})
     * @return array Responses (either Requests_Response or a Requests_Exception object)
     */
    public static function request_multiple($requests, $options = array()) {
        $options = array_merge(self::get_default_options(true), $options);
 
        if (!empty($options['hooks'])) {
            $options['hooks']->register('transport.internal.parse_response', array('Requests', 'parse_multiple'));
            if (!empty($options['complete'])) {
                $options['hooks']->register('multiple.request.complete', $options['complete']);
            }
        }
 
        foreach ($requests as $id => &$request) {
            if (!isset($request['headers'])) {
                $request['headers'] = array();
            }
            if (!isset($request['data'])) {
                $request['data'] = array();
            }
            if (!isset($request['type'])) {
                $request['type'] = self::GET;
            }
            if (!isset($request['options'])) {
                $request['options'] = $options;
                $request['options']['type'] = $request['type'];
            }
            else {
                if (empty($request['options']['type'])) {
                    $request['options']['type'] = $request['type'];
                }
                $request['options'] = array_merge($options, $request['options']);
            }
 
            self::set_defaults($request['url'], $request['headers'], $request['data'], $request['type'], $request['options']);
 
            // Ensure we only hook in once
            if ($request['options']['hooks'] !== $options['hooks']) {
                $request['options']['hooks']->register('transport.internal.parse_response', array('Requests', 'parse_multiple'));
                if (!empty($request['options']['complete'])) {
                    $request['options']['hooks']->register('multiple.request.complete', $request['options']['complete']);
                }
            }
        }
        unset($request);
 
        if (!empty($options['transport'])) {
            $transport = $options['transport'];
 
            if (is_string($options['transport'])) {
                $transport = new $transport();
            }
        }
        else {
            $transport = self::get_transport();
        }
        $responses = $transport->request_multiple($requests, $options);
 
        foreach ($responses as $id => &$response) {
            // If our hook got messed with somehow, ensure we end up with the
            // correct response
            if (is_string($response)) {
                $request = $requests[$id];
                self::parse_multiple($response, $request);
                $request['options']['hooks']->dispatch('multiple.request.complete', array(&$response, $id));
            }
        }
 
        return $responses;
    }
 
    /**
     * Get the default options
     *
     * @see Requests::request() for values returned by this method
     * @param boolean $multirequest Is this a multirequest?
     * @return array Default option values
     */
    protected static function get_default_options($multirequest = false) {
        $defaults = array(
            'timeout' => 10,
            'connect_timeout' => 10,
            'useragent' => 'php-requests/' . self::VERSION,
            'protocol_version' => 1.1,
            'redirected' => 0,
            'redirects' => 10,
            'follow_redirects' => true,
            'blocking' => true,
            'type' => self::GET,
            'filename' => false,
            'auth' => false,
            'proxy' => false,
            'cookies' => false,
            'max_bytes' => false,
            'idn' => true,
            'hooks' => null,
            'transport' => null,
            'verify' => Requests::get_certificate_path(),
            'verifyname' => true,
        );
        if ($multirequest !== false) {
            $defaults['complete'] = null;
        }
        return $defaults;
    }
 
    /**
     * Get default certificate path.
     *
     * @return string Default certificate path.
     */
    public static function get_certificate_path() {
        if ( ! empty( Requests::$certificate_path ) ) {
            return Requests::$certificate_path;
        }
 
        return dirname(__FILE__) . '/Requests/Transport/cacert.pem';
    }
 
    /**
     * Set default certificate path.
     *
     * @param string $path Certificate path, pointing to a PEM file.
     */
    public static function set_certificate_path( $path ) {
        Requests::$certificate_path = $path;
    }
 
    /**
     * Set the default values
     *
     * @param string $url URL to request
     * @param array $headers Extra headers to send with the request
     * @param array|null $data Data to send either as a query string for GET/HEAD requests, or in the body for POST requests
     * @param string $type HTTP request type
     * @param array $options Options for the request
     * @return array $options
     */
    protected static function set_defaults(&$url, &$headers, &$data, &$type, &$options) {
        if (!preg_match('/^http(s)?:\/\//i', $url, $matches)) {
            throw new Requests_Exception('Only HTTP(S) requests are handled.', 'nonhttp', $url);
        }
 
        if (empty($options['hooks'])) {
            $options['hooks'] = new Requests_Hooks();
        }
 
        if (is_array($options['auth'])) {
            $options['auth'] = new Requests_Auth_Basic($options['auth']);
        }
        if ($options['auth'] !== false) {
            $options['auth']->register($options['hooks']);
        }
 
        if (is_string($options['proxy']) || is_array($options['proxy'])) {
            $options['proxy'] = new Requests_Proxy_HTTP($options['proxy']);
        }
        if ($options['proxy'] !== false) {
            $options['proxy']->register($options['hooks']);
        }
 
        if (is_array($options['cookies'])) {
            $options['cookies'] = new Requests_Cookie_Jar($options['cookies']);
        }
        elseif (empty($options['cookies'])) {
            $options['cookies'] = new Requests_Cookie_Jar();
        }
        if ($options['cookies'] !== false) {
            $options['cookies']->register($options['hooks']);
        }
 
        if ($options['idn'] !== false) {
            $iri = new Requests_IRI($url);
            $iri->host = Requests_IDNAEncoder::encode($iri->ihost);
            $url = $iri->uri;
        }
 
        // Massage the type to ensure we support it.
        $type = strtoupper($type);
 
        if (!isset($options['data_format'])) {
            if (in_array($type, array(self::HEAD, self::GET, self::DELETE))) {
                $options['data_format'] = 'query';
            }
            else {
                $options['data_format'] = 'body';
            }
        }
    }
 
    /**
     * HTTP response parser
     *
     * @throws Requests_Exception On missing head/body separator (`requests.no_crlf_separator`)
     * @throws Requests_Exception On missing head/body separator (`noversion`)
     * @throws Requests_Exception On missing head/body separator (`toomanyredirects`)
     *
     * @param string $headers Full response text including headers and body
     * @param string $url Original request URL
     * @param array $req_headers Original $headers array passed to {@link request()}, in case we need to follow redirects
     * @param array $req_data Original $data array passed to {@link request()}, in case we need to follow redirects
     * @param array $options Original $options array passed to {@link request()}, in case we need to follow redirects
     * @return Requests_Response
     */
    protected static function parse_response($headers, $url, $req_headers, $req_data, $options) {
        $return = new Requests_Response();
        if (!$options['blocking']) {
            return $return;
        }
 
        $return->raw = $headers;
        $return->url = $url;
 
        if (!$options['filename']) {
            if (($pos = strpos($headers, "\r\n\r\n")) === false) {
                // Crap!
                throw new Requests_Exception('Missing header/body separator', 'requests.no_crlf_separator');
            }
 
            $headers = substr($return->raw, 0, $pos);
            $return->body = substr($return->raw, $pos + strlen("\n\r\n\r"));
        }
        else {
            $return->body = '';
        }
        // Pretend CRLF = LF for compatibility (RFC 2616, section 19.3)
        $headers = str_replace("\r\n", "\n", $headers);
        // Unfold headers (replace [CRLF] 1*( SP | HT ) with SP) as per RFC 2616 (section 2.2)
        $headers = preg_replace('/\n[ \t]/', ' ', $headers);
        $headers = explode("\n", $headers);
        preg_match('#^HTTP/(1\.\d)[ \t]+(\d+)#i', array_shift($headers), $matches);
        if (empty($matches)) {
            throw new Requests_Exception('Response could not be parsed', 'noversion', $headers);
        }
        $return->protocol_version = (float) $matches[1];
        $return->status_code = (int) $matches[2];
        if ($return->status_code >= 200 && $return->status_code < 300) {
            $return->success = true;
        }
 
        foreach ($headers as $header) {
            list($key, $value) = explode(':', $header, 2);
            $value = trim($value);
            preg_replace('#(\s+)#i', ' ', $value);
            $return->headers[$key] = $value;
        }
        if (isset($return->headers['transfer-encoding'])) {
            $return->body = self::decode_chunked($return->body);
            unset($return->headers['transfer-encoding']);
        }
        if (isset($return->headers['content-encoding'])) {
            $return->body = self::decompress($return->body);
        }
 
        //fsockopen and cURL compatibility
        if (isset($return->headers['connection'])) {
            unset($return->headers['connection']);
        }
 
        $options['hooks']->dispatch('requests.before_redirect_check', array(&$return, $req_headers, $req_data, $options));
 
        if ($return->is_redirect() && $options['follow_redirects'] === true) {
            if (isset($return->headers['location']) && $options['redirected'] < $options['redirects']) {
                if ($return->status_code === 303) {
                    $options['type'] = self::GET;
                }
                $options['redirected']++;
                $location = $return->headers['location'];
                if (strpos($location, 'http://') !== 0 && strpos($location, 'https://') !== 0) {
                    // relative redirect, for compatibility make it absolute
                    $location = Requests_IRI::absolutize($url, $location);
                    $location = $location->uri;
                }
 
                $hook_args = array(
                    &$location,
                    &$req_headers,
                    &$req_data,
                    &$options,
                    $return
                );
                $options['hooks']->dispatch('requests.before_redirect', $hook_args);
                $redirected = self::request($location, $req_headers, $req_data, $options['type'], $options);
                $redirected->history[] = $return;
                return $redirected;
            }
            elseif ($options['redirected'] >= $options['redirects']) {
                throw new Requests_Exception('Too many redirects', 'toomanyredirects', $return);
            }
        }
 
        $return->redirects = $options['redirected'];
 
        $options['hooks']->dispatch('requests.after_request', array(&$return, $req_headers, $req_data, $options));
        return $return;
    }
 
    /**
     * Callback for `transport.internal.parse_response`
     *
     * Internal use only. Converts a raw HTTP response to a Requests_Response
     * while still executing a multiple request.
     *
     * @param string $response Full response text including headers and body (will be overwritten with Response instance)
     * @param array $request Request data as passed into {@see Requests::request_multiple()}
     * @return null `$response` is either set to a Requests_Response instance, or a Requests_Exception object
     */
    public static function parse_multiple(&$response, $request) {
        try {
            $url = $request['url'];
            $headers = $request['headers'];
            $data = $request['data'];
            $options = $request['options'];
            $response = self::parse_response($response, $url, $headers, $data, $options);
        }
        catch (Requests_Exception $e) {
            $response = $e;
        }
    }
 
    /**
     * Decoded a chunked body as per RFC 2616
     *
     * @see https://tools.ietf.org/html/rfc2616#section-3.6.1
     * @param string $data Chunked body
     * @return string Decoded body
     */
    protected static function decode_chunked($data) {
        if (!preg_match('/^([0-9a-f]+)(?:;(?:[\w-]*)(?:=(?:(?:[\w-]*)*|"(?:[^\r\n])*"))?)*\r\n/i', trim($data))) {
            return $data;
        }
 
 
 
        $decoded = '';
        $encoded = $data;
 
        while (true) {
            $is_chunked = (bool) preg_match('/^([0-9a-f]+)(?:;(?:[\w-]*)(?:=(?:(?:[\w-]*)*|"(?:[^\r\n])*"))?)*\r\n/i', $encoded, $matches);
            if (!$is_chunked) {
                // Looks like it's not chunked after all
                return $data;
            }
 
            $length = hexdec(trim($matches[1]));
            if ($length === 0) {
                // Ignore trailer headers
                return $decoded;
            }
 
            $chunk_length = strlen($matches[0]);
            $decoded .= substr($encoded, $chunk_length, $length);
            $encoded = substr($encoded, $chunk_length + $length + 2);
 
            if (trim($encoded) === '0' || empty($encoded)) {
                return $decoded;
            }
        }
 
        // We'll never actually get down here
        // @codeCoverageIgnoreStart
    }
    // @codeCoverageIgnoreEnd
 
    /**
     * Convert a key => value array to a 'key: value' array for headers
     *
     * @param array $array Dictionary of header values
     * @return array List of headers
     */
    public static function flatten($array) {
        $return = array();
        foreach ($array as $key => $value) {
            $return[] = sprintf('%s: %s', $key, $value);
        }
        return $return;
    }
 
    /**
     * Convert a key => value array to a 'key: value' array for headers
     *
     * @codeCoverageIgnore
     * @deprecated Misspelling of {@see Requests::flatten}
     * @param array $array Dictionary of header values
     * @return array List of headers
     */
    public static function flattern($array) {
        return self::flatten($array);
    }
 
    /**
     * Decompress an encoded body
     *
     * Implements gzip, compress and deflate. Guesses which it is by attempting
     * to decode.
     *
     * @param string $data Compressed data in one of the above formats
     * @return string Decompressed string
     */
    public static function decompress($data) {
        if (substr($data, 0, 2) !== "\x1f\x8b" && substr($data, 0, 2) !== "\x78\x9c") {
            // Not actually compressed. Probably cURL ruining this for us.
            return $data;
        }
 
        if (function_exists('gzdecode') && ($decoded = @gzdecode($data)) !== false) {
            return $decoded;
        }
        elseif (function_exists('gzinflate') && ($decoded = @gzinflate($data)) !== false) {
            return $decoded;
        }
        elseif (($decoded = self::compatible_gzinflate($data)) !== false) {
            return $decoded;
        }
        elseif (function_exists('gzuncompress') && ($decoded = @gzuncompress($data)) !== false) {
            return $decoded;
        }
 
        return $data;
    }
 
    /**
     * Decompression of deflated string while staying compatible with the majority of servers.
     *
     * Certain Servers will return deflated data with headers which PHP's gzinflate()
     * function cannot handle out of the box. The following function has been created from
     * various snippets on the gzinflate() PHP documentation.
     *
     * Warning: Magic numbers within. Due to the potential different formats that the compressed
     * data may be returned in, some "magic offsets" are needed to ensure proper decompression
     * takes place. For a simple progmatic way to determine the magic offset in use, see:
     * https://core.trac.wordpress.org/ticket/18273
     *
     * @since 2.8.1
     * @link https://core.trac.wordpress.org/ticket/18273
     * @link https://secure.php.net/manual/en/function.gzinflate.php#70875
     * @link https://secure.php.net/manual/en/function.gzinflate.php#77336
     *
     * @param string $gzData String to decompress.
     * @return string|bool False on failure.
     */
    public static function compatible_gzinflate($gzData) {
        // Compressed data might contain a full zlib header, if so strip it for
        // gzinflate()
        if (substr($gzData, 0, 3) == "\x1f\x8b\x08") {
            $i = 10;
            $flg = ord(substr($gzData, 3, 1));
            if ($flg > 0) {
                if ($flg & 4) {
                    list($xlen) = unpack('v', substr($gzData, $i, 2));
                    $i = $i + 2 + $xlen;
                }
                if ($flg & 8) {
                    $i = strpos($gzData, "\0", $i) + 1;
                }
                if ($flg & 16) {
                    $i = strpos($gzData, "\0", $i) + 1;
                }
                if ($flg & 2) {
                    $i = $i + 2;
                }
            }
            $decompressed = self::compatible_gzinflate(substr($gzData, $i));
            if (false !== $decompressed) {
                return $decompressed;
            }
        }
 
        // If the data is Huffman Encoded, we must first strip the leading 2
        // byte Huffman marker for gzinflate()
        // The response is Huffman coded by many compressors such as
        // java.util.zip.Deflater, Ruby’s Zlib::Deflate, and .NET's
        // System.IO.Compression.DeflateStream.
        //
        // See https://decompres.blogspot.com/ for a quick explanation of this
        // data type
        $huffman_encoded = false;
 
        // low nibble of first byte should be 0x08
        list(, $first_nibble)    = unpack('h', $gzData);
 
        // First 2 bytes should be divisible by 0x1F
        list(, $first_two_bytes) = unpack('n', $gzData);
 
        if (0x08 == $first_nibble && 0 == ($first_two_bytes % 0x1F)) {
            $huffman_encoded = true;
        }
 
        if ($huffman_encoded) {
            if (false !== ($decompressed = @gzinflate(substr($gzData, 2)))) {
                return $decompressed;
            }
        }
 
        if ("\x50\x4b\x03\x04" == substr($gzData, 0, 4)) {
            // ZIP file format header
            // Offset 6: 2 bytes, General-purpose field
            // Offset 26: 2 bytes, filename length
            // Offset 28: 2 bytes, optional field length
            // Offset 30: Filename field, followed by optional field, followed
            // immediately by data
            list(, $general_purpose_flag) = unpack('v', substr($gzData, 6, 2));
 
            // If the file has been compressed on the fly, 0x08 bit is set of
            // the general purpose field. We can use this to differentiate
            // between a compressed document, and a ZIP file
            $zip_compressed_on_the_fly = (0x08 == (0x08 & $general_purpose_flag));
 
            if (!$zip_compressed_on_the_fly) {
                // Don't attempt to decode a compressed zip file
                return $gzData;
            }
 
            // Determine the first byte of data, based on the above ZIP header
            // offsets:
            $first_file_start = array_sum(unpack('v2', substr($gzData, 26, 4)));
            if (false !== ($decompressed = @gzinflate(substr($gzData, 30 + $first_file_start)))) {
                return $decompressed;
            }
            return false;
        }
 
        // Finally fall back to straight gzinflate
        if (false !== ($decompressed = @gzinflate($gzData))) {
            return $decompressed;
        }
 
        // Fallback for all above failing, not expected, but included for
        // debugging and preventing regressions and to track stats
        if (false !== ($decompressed = @gzinflate(substr($gzData, 2)))) {
            return $decompressed;
        }
 
        return false;
    }
 
    public static function match_domain($host, $reference) {
        // Check for a direct match
        if ($host === $reference) {
            return true;
        }
 
        // Calculate the valid wildcard match if the host is not an IP address
        // Also validates that the host has 3 parts or more, as per Firefox's
        // ruleset.
        $parts = explode('.', $host);
        if (ip2long($host) === false && count($parts) >= 3) {
            $parts[0] = '*';
            $wildcard = implode('.', $parts);
            if ($wildcard === $reference) {
                return true;
            }
        }
 
        return false;
    }
}



class Requests_IRI {
    /**
     * Scheme
     *
     * @var string
     */
    protected $scheme = null;
 
    /**
     * User Information
     *
     * @var string
     */
    protected $iuserinfo = null;
 
    /**
     * ihost
     *
     * @var string
     */
    protected $ihost = null;
 
    /**
     * Port
     *
     * @var string
     */
    protected $port = null;
 
    /**
     * ipath
     *
     * @var string
     */
    protected $ipath = '';
 
    /**
     * iquery
     *
     * @var string
     */
    protected $iquery = null;
 
    /**
     * ifragment
     *
     * @var string
     */
    protected $ifragment = null;
 
    /**
     * Normalization database
     *
     * Each key is the scheme, each value is an array with each key as the IRI
     * part and value as the default value for that part.
     */
    protected $normalization = array(
        'acap' => array(
            'port' => 674
        ),
        'dict' => array(
            'port' => 2628
        ),
        'file' => array(
            'ihost' => 'localhost'
        ),
        'http' => array(
            'port' => 80,
        ),
        'https' => array(
            'port' => 443,
        ),
    );
 
    /**
     * Return the entire IRI when you try and read the object as a string
     *
     * @return string
     */
    public function __toString() {
        return $this->get_iri();
    }
 
    /**
     * Overload __set() to provide access via properties
     *
     * @param string $name Property name
     * @param mixed $value Property value
     */
    public function __set($name, $value) {
        if (method_exists($this, 'set_' . $name)) {
            call_user_func(array($this, 'set_' . $name), $value);
        }
        elseif (
               $name === 'iauthority'
            || $name === 'iuserinfo'
            || $name === 'ihost'
            || $name === 'ipath'
            || $name === 'iquery'
            || $name === 'ifragment'
        ) {
            call_user_func(array($this, 'set_' . substr($name, 1)), $value);
        }
    }
 
    /**
     * Overload __get() to provide access via properties
     *
     * @param string $name Property name
     * @return mixed
     */
    public function __get($name) {
        // isset() returns false for null, we don't want to do that
        // Also why we use array_key_exists below instead of isset()
        $props = get_object_vars($this);
 
        if (
            $name === 'iri' ||
            $name === 'uri' ||
            $name === 'iauthority' ||
            $name === 'authority'
        ) {
            $method = 'get_' . $name;
            $return = $this->$method();
        }
        elseif (array_key_exists($name, $props)) {
            $return = $this->$name;
        }
        // host -> ihost
        elseif (($prop = 'i' . $name) && array_key_exists($prop, $props)) {
            $name = $prop;
            $return = $this->$prop;
        }
        // ischeme -> scheme
        elseif (($prop = substr($name, 1)) && array_key_exists($prop, $props)) {
            $name = $prop;
            $return = $this->$prop;
        }
        else {
            trigger_error('Undefined property: ' . get_class($this) . '::' . $name, E_USER_NOTICE);
            $return = null;
        }
 
        if ($return === null && isset($this->normalization[$this->scheme][$name])) {
            return $this->normalization[$this->scheme][$name];
        }
        else {
            return $return;
        }
    }
 
    /**
     * Overload __isset() to provide access via properties
     *
     * @param string $name Property name
     * @return bool
     */
    public function __isset($name) {
        return (method_exists($this, 'get_' . $name) || isset($this->$name));
    }
 
    /**
     * Overload __unset() to provide access via properties
     *
     * @param string $name Property name
     */
    public function __unset($name) {
        if (method_exists($this, 'set_' . $name)) {
            call_user_func(array($this, 'set_' . $name), '');
        }
    }
 
    /**
     * Create a new IRI object, from a specified string
     *
     * @param string|null $iri
     */
    public function __construct($iri = null) {
        $this->set_iri($iri);
    }
 
    /**
     * Create a new IRI object by resolving a relative IRI
     *
     * Returns false if $base is not absolute, otherwise an IRI.
     *
     * @param IRI|string $base (Absolute) Base IRI
     * @param IRI|string $relative Relative IRI
     * @return IRI|false
     */
    public static function absolutize($base, $relative) {
        if (!($relative instanceof Requests_IRI)) {
            $relative = new Requests_IRI($relative);
        }
        if (!$relative->is_valid()) {
            return false;
        }
        elseif ($relative->scheme !== null) {
            return clone $relative;
        }
 
        if (!($base instanceof Requests_IRI)) {
            $base = new Requests_IRI($base);
        }
        if ($base->scheme === null || !$base->is_valid()) {
            return false;
        }
 
        if ($relative->get_iri() !== '') {
            if ($relative->iuserinfo !== null || $relative->ihost !== null || $relative->port !== null) {
                $target = clone $relative;
                $target->scheme = $base->scheme;
            }
            else {
                $target = new Requests_IRI;
                $target->scheme = $base->scheme;
                $target->iuserinfo = $base->iuserinfo;
                $target->ihost = $base->ihost;
                $target->port = $base->port;
                if ($relative->ipath !== '') {
                    if ($relative->ipath[0] === '/') {
                        $target->ipath = $relative->ipath;
                    }
                    elseif (($base->iuserinfo !== null || $base->ihost !== null || $base->port !== null) && $base->ipath === '') {
                        $target->ipath = '/' . $relative->ipath;
                    }
                    elseif (($last_segment = strrpos($base->ipath, '/')) !== false) {
                        $target->ipath = substr($base->ipath, 0, $last_segment + 1) . $relative->ipath;
                    }
                    else {
                        $target->ipath = $relative->ipath;
                    }
                    $target->ipath = $target->remove_dot_segments($target->ipath);
                    $target->iquery = $relative->iquery;
                }
                else {
                    $target->ipath = $base->ipath;
                    if ($relative->iquery !== null) {
                        $target->iquery = $relative->iquery;
                    }
                    elseif ($base->iquery !== null) {
                        $target->iquery = $base->iquery;
                    }
                }
                $target->ifragment = $relative->ifragment;
            }
        }
        else {
            $target = clone $base;
            $target->ifragment = null;
        }
        $target->scheme_normalization();
        return $target;
    }
 
    /**
     * Parse an IRI into scheme/authority/path/query/fragment segments
     *
     * @param string $iri
     * @return array
     */
    protected function parse_iri($iri) {
        $iri = trim($iri, "\x20\x09\x0A\x0C\x0D");
        $has_match = preg_match('/^((?P<scheme>[^:\/?#]+):)?(\/\/(?P<authority>[^\/?#]*))?(?P<path>[^?#]*)(\?(?P<query>[^#]*))?(#(?P<fragment>.*))?$/', $iri, $match);
        if (!$has_match) {
            throw new Requests_Exception('Cannot parse supplied IRI', 'iri.cannot_parse', $iri);
        }
 
        if ($match[1] === '') {
            $match['scheme'] = null;
        }
        if (!isset($match[3]) || $match[3] === '') {
            $match['authority'] = null;
        }
        if (!isset($match[5])) {
            $match['path'] = '';
        }
        if (!isset($match[6]) || $match[6] === '') {
            $match['query'] = null;
        }
        if (!isset($match[8]) || $match[8] === '') {
            $match['fragment'] = null;
        }
        return $match;
    }
 
    /**
     * Remove dot segments from a path
     *
     * @param string $input
     * @return string
     */
    protected function remove_dot_segments($input) {
        $output = '';
        while (strpos($input, './') !== false || strpos($input, '/.') !== false || $input === '.' || $input === '..') {
            // A: If the input buffer begins with a prefix of "../" or "./",
            // then remove that prefix from the input buffer; otherwise,
            if (strpos($input, '../') === 0) {
                $input = substr($input, 3);
            }
            elseif (strpos($input, './') === 0) {
                $input = substr($input, 2);
            }
            // B: if the input buffer begins with a prefix of "/./" or "/.",
            // where "." is a complete path segment, then replace that prefix
            // with "/" in the input buffer; otherwise,
            elseif (strpos($input, '/./') === 0) {
                $input = substr($input, 2);
            }
            elseif ($input === '/.') {
                $input = '/';
            }
            // C: if the input buffer begins with a prefix of "/../" or "/..",
            // where ".." is a complete path segment, then replace that prefix
            // with "/" in the input buffer and remove the last segment and its
            // preceding "/" (if any) from the output buffer; otherwise,
            elseif (strpos($input, '/../') === 0) {
                $input = substr($input, 3);
                $output = substr_replace($output, '', strrpos($output, '/'));
            }
            elseif ($input === '/..') {
                $input = '/';
                $output = substr_replace($output, '', strrpos($output, '/'));
            }
            // D: if the input buffer consists only of "." or "..", then remove
            // that from the input buffer; otherwise,
            elseif ($input === '.' || $input === '..') {
                $input = '';
            }
            // E: move the first path segment in the input buffer to the end of
            // the output buffer, including the initial "/" character (if any)
            // and any subsequent characters up to, but not including, the next
            // "/" character or the end of the input buffer
            elseif (($pos = strpos($input, '/', 1)) !== false) {
                $output .= substr($input, 0, $pos);
                $input = substr_replace($input, '', 0, $pos);
            }
            else {
                $output .= $input;
                $input = '';
            }
        }
        return $output . $input;
    }
 
    /**
     * Replace invalid character with percent encoding
     *
     * @param string $string Input string
     * @param string $extra_chars Valid characters not in iunreserved or
     *                            iprivate (this is ASCII-only)
     * @param bool $iprivate Allow iprivate
     * @return string
     */
    protected function replace_invalid_with_pct_encoding($string, $extra_chars, $iprivate = false) {
        // Normalize as many pct-encoded sections as possible
        $string = preg_replace_callback('/(?:%[A-Fa-f0-9]{2})+/', array(&$this, 'remove_iunreserved_percent_encoded'), $string);
 
        // Replace invalid percent characters
        $string = preg_replace('/%(?![A-Fa-f0-9]{2})/', '%25', $string);
 
        // Add unreserved and % to $extra_chars (the latter is safe because all
        // pct-encoded sections are now valid).
        $extra_chars .= 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~%';
 
        // Now replace any bytes that aren't allowed with their pct-encoded versions
        $position = 0;
        $strlen = strlen($string);
        while (($position += strspn($string, $extra_chars, $position)) < $strlen) {
            $value = ord($string[$position]);
 
            // Start position
            $start = $position;
 
            // By default we are valid
            $valid = true;
 
            // No one byte sequences are valid due to the while.
            // Two byte sequence:
            if (($value & 0xE0) === 0xC0) {
                $character = ($value & 0x1F) << 6;
                $length = 2;
                $remaining = 1;
            }
            // Three byte sequence:
            elseif (($value & 0xF0) === 0xE0) {
                $character = ($value & 0x0F) << 12;
                $length = 3;
                $remaining = 2;
            }
            // Four byte sequence:
            elseif (($value & 0xF8) === 0xF0) {
                $character = ($value & 0x07) << 18;
                $length = 4;
                $remaining = 3;
            }
            // Invalid byte:
            else {
                $valid = false;
                $length = 1;
                $remaining = 0;
            }
 
            if ($remaining) {
                if ($position + $length <= $strlen) {
                    for ($position++; $remaining; $position++) {
                        $value = ord($string[$position]);
 
                        // Check that the byte is valid, then add it to the character:
                        if (($value & 0xC0) === 0x80) {
                            $character |= ($value & 0x3F) << (--$remaining * 6);
                        }
                        // If it is invalid, count the sequence as invalid and reprocess the current byte:
                        else {
                            $valid = false;
                            $position--;
                            break;
                        }
                    }
                }
                else {
                    $position = $strlen - 1;
                    $valid = false;
                }
            }
 
            // Percent encode anything invalid or not in ucschar
            if (
                // Invalid sequences
                !$valid
                // Non-shortest form sequences are invalid
                || $length > 1 && $character <= 0x7F
                || $length > 2 && $character <= 0x7FF
                || $length > 3 && $character <= 0xFFFF
                // Outside of range of ucschar codepoints
                // Noncharacters
                || ($character & 0xFFFE) === 0xFFFE
                || $character >= 0xFDD0 && $character <= 0xFDEF
                || (
                    // Everything else not in ucschar
                       $character > 0xD7FF && $character < 0xF900
                    || $character < 0xA0
                    || $character > 0xEFFFD
                )
                && (
                    // Everything not in iprivate, if it applies
                       !$iprivate
                    || $character < 0xE000
                    || $character > 0x10FFFD
                )
            ) {
                // If we were a character, pretend we weren't, but rather an error.
                if ($valid) {
                    $position--;
                }
 
                for ($j = $start; $j <= $position; $j++) {
                    $string = substr_replace($string, sprintf('%%%02X', ord($string[$j])), $j, 1);
                    $j += 2;
                    $position += 2;
                    $strlen += 2;
                }
            }
        }
 
        return $string;
    }
 
    /**
     * Callback function for preg_replace_callback.
     *
     * Removes sequences of percent encoded bytes that represent UTF-8
     * encoded characters in iunreserved
     *
     * @param array $match PCRE match
     * @return string Replacement
     */
    protected function remove_iunreserved_percent_encoded($match) {
        // As we just have valid percent encoded sequences we can just explode
        // and ignore the first member of the returned array (an empty string).
        $bytes = explode('%', $match[0]);
 
        // Initialize the new string (this is what will be returned) and that
        // there are no bytes remaining in the current sequence (unsurprising
        // at the first byte!).
        $string = '';
        $remaining = 0;
 
        // Loop over each and every byte, and set $value to its value
        for ($i = 1, $len = count($bytes); $i < $len; $i++) {
            $value = hexdec($bytes[$i]);
 
            // If we're the first byte of sequence:
            if (!$remaining) {
                // Start position
                $start = $i;
 
                // By default we are valid
                $valid = true;
 
                // One byte sequence:
                if ($value <= 0x7F) {
                    $character = $value;
                    $length = 1;
                }
                // Two byte sequence:
                elseif (($value & 0xE0) === 0xC0) {
                    $character = ($value & 0x1F) << 6;
                    $length = 2;
                    $remaining = 1;
                }
                // Three byte sequence:
                elseif (($value & 0xF0) === 0xE0) {
                    $character = ($value & 0x0F) << 12;
                    $length = 3;
                    $remaining = 2;
                }
                // Four byte sequence:
                elseif (($value & 0xF8) === 0xF0) {
                    $character = ($value & 0x07) << 18;
                    $length = 4;
                    $remaining = 3;
                }
                // Invalid byte:
                else {
                    $valid = false;
                    $remaining = 0;
                }
            }
            // Continuation byte:
            else {
                // Check that the byte is valid, then add it to the character:
                if (($value & 0xC0) === 0x80) {
                    $remaining--;
                    $character |= ($value & 0x3F) << ($remaining * 6);
                }
                // If it is invalid, count the sequence as invalid and reprocess the current byte as the start of a sequence:
                else {
                    $valid = false;
                    $remaining = 0;
                    $i--;
                }
            }
 
            // If we've reached the end of the current byte sequence, append it to Unicode::$data
            if (!$remaining) {
                // Percent encode anything invalid or not in iunreserved
                if (
                    // Invalid sequences
                    !$valid
                    // Non-shortest form sequences are invalid
                    || $length > 1 && $character <= 0x7F
                    || $length > 2 && $character <= 0x7FF
                    || $length > 3 && $character <= 0xFFFF
                    // Outside of range of iunreserved codepoints
                    || $character < 0x2D
                    || $character > 0xEFFFD
                    // Noncharacters
                    || ($character & 0xFFFE) === 0xFFFE
                    || $character >= 0xFDD0 && $character <= 0xFDEF
                    // Everything else not in iunreserved (this is all BMP)
                    || $character === 0x2F
                    || $character > 0x39 && $character < 0x41
                    || $character > 0x5A && $character < 0x61
                    || $character > 0x7A && $character < 0x7E
                    || $character > 0x7E && $character < 0xA0
                    || $character > 0xD7FF && $character < 0xF900
                ) {
                    for ($j = $start; $j <= $i; $j++) {
                        $string .= '%' . strtoupper($bytes[$j]);
                    }
                }
                else {
                    for ($j = $start; $j <= $i; $j++) {
                        $string .= chr(hexdec($bytes[$j]));
                    }
                }
            }
        }
 
        // If we have any bytes left over they are invalid (i.e., we are
        // mid-way through a multi-byte sequence)
        if ($remaining) {
            for ($j = $start; $j < $len; $j++) {
                $string .= '%' . strtoupper($bytes[$j]);
            }
        }
 
        return $string;
    }
 
    protected function scheme_normalization() {
        if (isset($this->normalization[$this->scheme]['iuserinfo']) && $this->iuserinfo === $this->normalization[$this->scheme]['iuserinfo']) {
            $this->iuserinfo = null;
        }
        if (isset($this->normalization[$this->scheme]['ihost']) && $this->ihost === $this->normalization[$this->scheme]['ihost']) {
            $this->ihost = null;
        }
        if (isset($this->normalization[$this->scheme]['port']) && $this->port === $this->normalization[$this->scheme]['port']) {
            $this->port = null;
        }
        if (isset($this->normalization[$this->scheme]['ipath']) && $this->ipath === $this->normalization[$this->scheme]['ipath']) {
            $this->ipath = '';
        }
        if (isset($this->ihost) && empty($this->ipath)) {
            $this->ipath = '/';
        }
        if (isset($this->normalization[$this->scheme]['iquery']) && $this->iquery === $this->normalization[$this->scheme]['iquery']) {
            $this->iquery = null;
        }
        if (isset($this->normalization[$this->scheme]['ifragment']) && $this->ifragment === $this->normalization[$this->scheme]['ifragment']) {
            $this->ifragment = null;
        }
    }
 
    /**
     * Check if the object represents a valid IRI. This needs to be done on each
     * call as some things change depending on another part of the IRI.
     *
     * @return bool
     */
    public function is_valid() {
        $isauthority = $this->iuserinfo !== null || $this->ihost !== null || $this->port !== null;
        if ($this->ipath !== '' &&
            (
                $isauthority && $this->ipath[0] !== '/' ||
                (
                    $this->scheme === null &&
                    !$isauthority &&
                    strpos($this->ipath, ':') !== false &&
                    (strpos($this->ipath, '/') === false ? true : strpos($this->ipath, ':') < strpos($this->ipath, '/'))
                )
            )
        ) {
            return false;
        }
 
        return true;
    }
 
    /**
     * Set the entire IRI. Returns true on success, false on failure (if there
     * are any invalid characters).
     *
     * @param string $iri
     * @return bool
     */
    protected function set_iri($iri) {
        static $cache;
        if (!$cache) {
            $cache = array();
        }
 
        if ($iri === null) {
            return true;
        }
        if (isset($cache[$iri])) {
            list($this->scheme,
                 $this->iuserinfo,
                 $this->ihost,
                 $this->port,
                 $this->ipath,
                 $this->iquery,
                 $this->ifragment,
                 $return) = $cache[$iri];
            return $return;
        }
 
        $parsed = $this->parse_iri((string) $iri);
 
        $return = $this->set_scheme($parsed['scheme'])
            && $this->set_authority($parsed['authority'])
            && $this->set_path($parsed['path'])
            && $this->set_query($parsed['query'])
            && $this->set_fragment($parsed['fragment']);
 
        $cache[$iri] = array($this->scheme,
                             $this->iuserinfo,
                             $this->ihost,
                             $this->port,
                             $this->ipath,
                             $this->iquery,
                             $this->ifragment,
                             $return);
        return $return;
    }
 
    /**
     * Set the scheme. Returns true on success, false on failure (if there are
     * any invalid characters).
     *
     * @param string $scheme
     * @return bool
     */
    protected function set_scheme($scheme) {
        if ($scheme === null) {
            $this->scheme = null;
        }
        elseif (!preg_match('/^[A-Za-z][0-9A-Za-z+\-.]*$/', $scheme)) {
            $this->scheme = null;
            return false;
        }
        else {
            $this->scheme = strtolower($scheme);
        }
        return true;
    }
 
    /**
     * Set the authority. Returns true on success, false on failure (if there are
     * any invalid characters).
     *
     * @param string $authority
     * @return bool
     */
    protected function set_authority($authority) {
        static $cache;
        if (!$cache) {
            $cache = array();
        }
 
        if ($authority === null) {
            $this->iuserinfo = null;
            $this->ihost = null;
            $this->port = null;
            return true;
        }
        if (isset($cache[$authority])) {
            list($this->iuserinfo,
                 $this->ihost,
                 $this->port,
                 $return) = $cache[$authority];
 
            return $return;
        }
 
        $remaining = $authority;
        if (($iuserinfo_end = strrpos($remaining, '@')) !== false) {
            $iuserinfo = substr($remaining, 0, $iuserinfo_end);
            $remaining = substr($remaining, $iuserinfo_end + 1);
        }
        else {
            $iuserinfo = null;
        }
        if (($port_start = strpos($remaining, ':', strpos($remaining, ']'))) !== false) {
            $port = substr($remaining, $port_start + 1);
            if ($port === false || $port === '') {
                $port = null;
            }
            $remaining = substr($remaining, 0, $port_start);
        }
        else {
            $port = null;
        }
 
        $return = $this->set_userinfo($iuserinfo) &&
                  $this->set_host($remaining) &&
                  $this->set_port($port);
 
        $cache[$authority] = array($this->iuserinfo,
                                   $this->ihost,
                                   $this->port,
                                   $return);
 
        return $return;
    }
 
    /**
     * Set the iuserinfo.
     *
     * @param string $iuserinfo
     * @return bool
     */
    protected function set_userinfo($iuserinfo) {
        if ($iuserinfo === null) {
            $this->iuserinfo = null;
        }
        else {
            $this->iuserinfo = $this->replace_invalid_with_pct_encoding($iuserinfo, '!$&\'()*+,;=:');
            $this->scheme_normalization();
        }
 
        return true;
    }
 
    /**
     * Set the ihost. Returns true on success, false on failure (if there are
     * any invalid characters).
     *
     * @param string $ihost
     * @return bool
     */
    protected function set_host($ihost) {
        if ($ihost === null) {
            $this->ihost = null;
            return true;
        }
        if (substr($ihost, 0, 1) === '[' && substr($ihost, -1) === ']') {
            if (Requests_IPv6::check_ipv6(substr($ihost, 1, -1))) {
                $this->ihost = '[' . Requests_IPv6::compress(substr($ihost, 1, -1)) . ']';
            }
            else {
                $this->ihost = null;
                return false;
            }
        }
        else {
            $ihost = $this->replace_invalid_with_pct_encoding($ihost, '!$&\'()*+,;=');
 
            // Lowercase, but ignore pct-encoded sections (as they should
            // remain uppercase). This must be done after the previous step
            // as that can add unescaped characters.
            $position = 0;
            $strlen = strlen($ihost);
            while (($position += strcspn($ihost, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ%', $position)) < $strlen) {
                if ($ihost[$position] === '%') {
                    $position += 3;
                }
                else {
                    $ihost[$position] = strtolower($ihost[$position]);
                    $position++;
                }
            }
 
            $this->ihost = $ihost;
        }
 
        $this->scheme_normalization();
 
        return true;
    }
 
    /**
     * Set the port. Returns true on success, false on failure (if there are
     * any invalid characters).
     *
     * @param string $port
     * @return bool
     */
    protected function set_port($port) {
        if ($port === null) {
            $this->port = null;
            return true;
        }
 
        if (strspn($port, '0123456789') === strlen($port)) {
            $this->port = (int) $port;
            $this->scheme_normalization();
            return true;
        }
 
        $this->port = null;
        return false;
    }
 
    /**
     * Set the ipath.
     *
     * @param string $ipath
     * @return bool
     */
    protected function set_path($ipath) {
        static $cache;
        if (!$cache) {
            $cache = array();
        }
 
        $ipath = (string) $ipath;
 
        if (isset($cache[$ipath])) {
            $this->ipath = $cache[$ipath][(int) ($this->scheme !== null)];
        }
        else {
            $valid = $this->replace_invalid_with_pct_encoding($ipath, '!$&\'()*+,;=@:/');
            $removed = $this->remove_dot_segments($valid);
 
            $cache[$ipath] = array($valid, $removed);
            $this->ipath = ($this->scheme !== null) ? $removed : $valid;
        }
        $this->scheme_normalization();
        return true;
    }
 
    /**
     * Set the iquery.
     *
     * @param string $iquery
     * @return bool
     */
    protected function set_query($iquery) {
        if ($iquery === null) {
            $this->iquery = null;
        }
        else {
            $this->iquery = $this->replace_invalid_with_pct_encoding($iquery, '!$&\'()*+,;=:@/?', true);
            $this->scheme_normalization();
        }
        return true;
    }
 
    /**
     * Set the ifragment.
     *
     * @param string $ifragment
     * @return bool
     */
    protected function set_fragment($ifragment) {
        if ($ifragment === null) {
            $this->ifragment = null;
        }
        else {
            $this->ifragment = $this->replace_invalid_with_pct_encoding($ifragment, '!$&\'()*+,;=:@/?');
            $this->scheme_normalization();
        }
        return true;
    }
 
    /**
     * Convert an IRI to a URI (or parts thereof)
     *
     * @param string|bool IRI to convert (or false from {@see get_iri})
     * @return string|false URI if IRI is valid, false otherwise.
     */
    protected function to_uri($string) {
        if (!is_string($string)) {
            return false;
        }
 
        static $non_ascii;
        if (!$non_ascii) {
            $non_ascii = implode('', range("\x80", "\xFF"));
        }
 
        $position = 0;
        $strlen = strlen($string);
        while (($position += strcspn($string, $non_ascii, $position)) < $strlen) {
            $string = substr_replace($string, sprintf('%%%02X', ord($string[$position])), $position, 1);
            $position += 3;
            $strlen += 2;
        }
 
        return $string;
    }
 
    /**
     * Get the complete IRI
     *
     * @return string
     */
    protected function get_iri() {
        if (!$this->is_valid()) {
            return false;
        }
 
        $iri = '';
        if ($this->scheme !== null) {
            $iri .= $this->scheme . ':';
        }
        if (($iauthority = $this->get_iauthority()) !== null) {
            $iri .= '//' . $iauthority;
        }
        $iri .= $this->ipath;
        if ($this->iquery !== null) {
            $iri .= '?' . $this->iquery;
        }
        if ($this->ifragment !== null) {
            $iri .= '#' . $this->ifragment;
        }
 
        return $iri;
    }
 
    /**
     * Get the complete URI
     *
     * @return string
     */
    protected function get_uri() {
        return $this->to_uri($this->get_iri());
    }
 
    /**
     * Get the complete iauthority
     *
     * @return string
     */
    protected function get_iauthority() {
        if ($this->iuserinfo === null && $this->ihost === null && $this->port === null) {
            return null;
        }
 
        $iauthority = '';
        if ($this->iuserinfo !== null) {
            $iauthority .= $this->iuserinfo . '@';
        }
        if ($this->ihost !== null) {
            $iauthority .= $this->ihost;
        }
        if ($this->port !== null) {
            $iauthority .= ':' . $this->port;
        }
        return $iauthority;
    }
 
    /**
     * Get the complete authority
     *
     * @return string
     */
    protected function get_authority() {
        $iauthority = $this->get_iauthority();
        if (is_string($iauthority)) {
            return $this->to_uri($iauthority);
        }
        else {
            return $iauthority;
        }
    }
}



class Requests_Hooks implements Requests_Hooker {
    /**
     * Registered callbacks for each hook
     *
     * @var array
     */
    protected $hooks = array();
 
    /**
     * Constructor
     */
    public function __construct() {
        // pass
    }
 
    /**
     * Register a callback for a hook
     *
     * @param string $hook Hook name
     * @param callback $callback Function/method to call on event
     * @param int $priority Priority number. <0 is executed earlier, >0 is executed later
     */
    public function register($hook, $callback, $priority = 0) {
        if (!isset($this->hooks[$hook])) {
            $this->hooks[$hook] = array();
        }
        if (!isset($this->hooks[$hook][$priority])) {
            $this->hooks[$hook][$priority] = array();
        }
 
        $this->hooks[$hook][$priority][] = $callback;
    }
 
    /**
     * Dispatch a message
     *
     * @param string $hook Hook name
     * @param array $parameters Parameters to pass to callbacks
     * @return boolean Successfulness
     */
    public function dispatch($hook, $parameters = array()) {
        if (empty($this->hooks[$hook])) {
            return false;
        }
 
        foreach ($this->hooks[$hook] as $priority => $hooked) {
            foreach ($hooked as $callback) {
                call_user_func_array($callback, $parameters);
            }
        }
 
        return true;
    }
}



interface Requests_Hooker {

    public function register($hook, $callback, $priority = 0);

    public function dispatch($hook, $parameters = array());
 }



class Requests_IDNAEncoder {
    /**
     * ACE prefix used for IDNA
     *
     * @see https://tools.ietf.org/html/rfc3490#section-5
     * @var string
     */
    const ACE_PREFIX = 'xn--';
 
    /**#@+
     * Bootstrap constant for Punycode
     *
     * @see https://tools.ietf.org/html/rfc3492#section-5
     * @var int
     */
    const BOOTSTRAP_BASE         = 36;
    const BOOTSTRAP_TMIN         = 1;
    const BOOTSTRAP_TMAX         = 26;
    const BOOTSTRAP_SKEW         = 38;
    const BOOTSTRAP_DAMP         = 700;
    const BOOTSTRAP_INITIAL_BIAS = 72;
    const BOOTSTRAP_INITIAL_N    = 128;
    /**#@-*/
 
    /**
     * Encode a hostname using Punycode
     *
     * @param string $string Hostname
     * @return string Punycode-encoded hostname
     */
    public static function encode($string) {
        $parts = explode('.', $string);
        foreach ($parts as &$part) {
            $part = self::to_ascii($part);
        }
        return implode('.', $parts);
    }
 
    /**
     * Convert a UTF-8 string to an ASCII string using Punycode
     *
     * @throws Requests_Exception Provided string longer than 64 ASCII characters (`idna.provided_too_long`)
     * @throws Requests_Exception Prepared string longer than 64 ASCII characters (`idna.prepared_too_long`)
     * @throws Requests_Exception Provided string already begins with xn-- (`idna.provided_is_prefixed`)
     * @throws Requests_Exception Encoded string longer than 64 ASCII characters (`idna.encoded_too_long`)
     *
     * @param string $string ASCII or UTF-8 string (max length 64 characters)
     * @return string ASCII string
     */
    public static function to_ascii($string) {
        // Step 1: Check if the string is already ASCII
        if (self::is_ascii($string)) {
            // Skip to step 7
            if (strlen($string) < 64) {
                return $string;
            }
 
            throw new Requests_Exception('Provided string is too long', 'idna.provided_too_long', $string);
        }
 
        // Step 2: nameprep
        $string = self::nameprep($string);
 
        // Step 3: UseSTD3ASCIIRules is false, continue
        // Step 4: Check if it's ASCII now
        if (self::is_ascii($string)) {
            // Skip to step 7
            if (strlen($string) < 64) {
                return $string;
            }
 
            throw new Requests_Exception('Prepared string is too long', 'idna.prepared_too_long', $string);
        }
 
        // Step 5: Check ACE prefix
        if (strpos($string, self::ACE_PREFIX) === 0) {
            throw new Requests_Exception('Provided string begins with ACE prefix', 'idna.provided_is_prefixed', $string);
        }
 
        // Step 6: Encode with Punycode
        $string = self::punycode_encode($string);
 
        // Step 7: Prepend ACE prefix
        $string = self::ACE_PREFIX . $string;
 
        // Step 8: Check size
        if (strlen($string) < 64) {
            return $string;
        }
 
        throw new Requests_Exception('Encoded string is too long', 'idna.encoded_too_long', $string);
    }
 
    /**
     * Check whether a given string contains only ASCII characters
     *
     * @internal (Testing found regex was the fastest implementation)
     *
     * @param string $string
     * @return bool Is the string ASCII-only?
     */
    protected static function is_ascii($string) {
        return (preg_match('/(?:[^\x00-\x7F])/', $string) !== 1);
    }
 
    /**
     * Prepare a string for use as an IDNA name
     *
     * @todo Implement this based on RFC 3491 and the newer 5891
     * @param string $string
     * @return string Prepared string
     */
    protected static function nameprep($string) {
        return $string;
    }
 
    /**
     * Convert a UTF-8 string to a UCS-4 codepoint array
     *
     * Based on Requests_IRI::replace_invalid_with_pct_encoding()
     *
     * @throws Requests_Exception Invalid UTF-8 codepoint (`idna.invalidcodepoint`)
     * @param string $input
     * @return array Unicode code points
     */
    protected static function utf8_to_codepoints($input) {
        $codepoints = array();
 
        // Get number of bytes
        $strlen = strlen($input);
 
        for ($position = 0; $position < $strlen; $position++) {
            $value = ord($input[$position]);
 
            // One byte sequence:
            if ((~$value & 0x80) === 0x80) {
                $character = $value;
                $length = 1;
                $remaining = 0;
            }
            // Two byte sequence:
            elseif (($value & 0xE0) === 0xC0) {
                $character = ($value & 0x1F) << 6;
                $length = 2;
                $remaining = 1;
            }
            // Three byte sequence:
            elseif (($value & 0xF0) === 0xE0) {
                $character = ($value & 0x0F) << 12;
                $length = 3;
                $remaining = 2;
            }
            // Four byte sequence:
            elseif (($value & 0xF8) === 0xF0) {
                $character = ($value & 0x07) << 18;
                $length = 4;
                $remaining = 3;
            }
            // Invalid byte:
            else {
                throw new Requests_Exception('Invalid Unicode codepoint', 'idna.invalidcodepoint', $value);
            }
 
            if ($remaining > 0) {
                if ($position + $length > $strlen) {
                    throw new Requests_Exception('Invalid Unicode codepoint', 'idna.invalidcodepoint', $character);
                }
                for ($position++; $remaining > 0; $position++) {
                    $value = ord($input[$position]);
 
                    // If it is invalid, count the sequence as invalid and reprocess the current byte:
                    if (($value & 0xC0) !== 0x80) {
                        throw new Requests_Exception('Invalid Unicode codepoint', 'idna.invalidcodepoint', $character);
                    }
 
                    $character |= ($value & 0x3F) << (--$remaining * 6);
                }
                $position--;
            }
 
            if (
                // Non-shortest form sequences are invalid
                   $length > 1 && $character <= 0x7F
                || $length > 2 && $character <= 0x7FF
                || $length > 3 && $character <= 0xFFFF
                // Outside of range of ucschar codepoints
                // Noncharacters
                || ($character & 0xFFFE) === 0xFFFE
                || $character >= 0xFDD0 && $character <= 0xFDEF
                || (
                    // Everything else not in ucschar
                       $character > 0xD7FF && $character < 0xF900
                    || $character < 0x20
                    || $character > 0x7E && $character < 0xA0
                    || $character > 0xEFFFD
                )
            ) {
                throw new Requests_Exception('Invalid Unicode codepoint', 'idna.invalidcodepoint', $character);
            }
 
            $codepoints[] = $character;
        }
 
        return $codepoints;
    }
 
    /**
     * RFC3492-compliant encoder
     *
     * @internal Pseudo-code from Section 6.3 is commented with "#" next to relevant code
     * @throws Requests_Exception On character outside of the domain (never happens with Punycode) (`idna.character_outside_domain`)
     *
     * @param string $input UTF-8 encoded string to encode
     * @return string Punycode-encoded string
     */
    public static function punycode_encode($input) {
        $output = '';
#       let n = initial_n
        $n = self::BOOTSTRAP_INITIAL_N;
#       let delta = 0
        $delta = 0;
#       let bias = initial_bias
        $bias = self::BOOTSTRAP_INITIAL_BIAS;
#       let h = b = the number of basic code points in the input
        $h = $b = 0; // see loop
#       copy them to the output in order
        $codepoints = self::utf8_to_codepoints($input);
        $extended = array();
 
        foreach ($codepoints as $char) {
            if ($char < 128) {
                // Character is valid ASCII
                // TODO: this should also check if it's valid for a URL
                $output .= chr($char);
                $h++;
            }
            // Check if the character is non-ASCII, but below initial n
            // This never occurs for Punycode, so ignore in coverage
            // @codeCoverageIgnoreStart
            elseif ($char < $n) {
                throw new Requests_Exception('Invalid character', 'idna.character_outside_domain', $char);
            }
            // @codeCoverageIgnoreEnd
            else {
                $extended[$char] = true;
            }
        }
        $extended = array_keys($extended);
        sort($extended);
        $b = $h;
#       [copy them] followed by a delimiter if b > 0
        if (strlen($output) > 0) {
            $output .= '-';
        }
#       {if the input contains a non-basic code point < n then fail}
#       while h < length(input) do begin
        while ($h < count($codepoints)) {
#           let m = the minimum code point >= n in the input
            $m = array_shift($extended);
            //printf('next code point to insert is %s' . PHP_EOL, dechex($m));
#           let delta = delta + (m - n) * (h + 1), fail on overflow
            $delta += ($m - $n) * ($h + 1);
#           let n = m
            $n = $m;
#           for each code point c in the input (in order) do begin
            for ($num = 0; $num < count($codepoints); $num++) {
                $c = $codepoints[$num];
#               if c < n then increment delta, fail on overflow
                if ($c < $n) {
                    $delta++;
                }
#               if c == n then begin
                elseif ($c === $n) {
#                   let q = delta
                    $q = $delta;
#                   for k = base to infinity in steps of base do begin
                    for ($k = self::BOOTSTRAP_BASE; ; $k += self::BOOTSTRAP_BASE) {
#                       let t = tmin if k <= bias {+ tmin}, or
#                               tmax if k >= bias + tmax, or k - bias otherwise
                        if ($k <= ($bias + self::BOOTSTRAP_TMIN)) {
                            $t = self::BOOTSTRAP_TMIN;
                        }
                        elseif ($k >= ($bias + self::BOOTSTRAP_TMAX)) {
                            $t = self::BOOTSTRAP_TMAX;
                        }
                        else {
                            $t = $k - $bias;
                        }
#                       if q < t then break
                        if ($q < $t) {
                            break;
                        }
#                       output the code point for digit t + ((q - t) mod (base - t))
                        $digit = $t + (($q - $t) % (self::BOOTSTRAP_BASE - $t));
                        $output .= self::digit_to_char($digit);
#                       let q = (q - t) div (base - t)
                        $q = floor(($q - $t) / (self::BOOTSTRAP_BASE - $t));
#                   end
                    }
#                   output the code point for digit q
                    $output .= self::digit_to_char($q);
#                   let bias = adapt(delta, h + 1, test h equals b?)
                    $bias = self::adapt($delta, $h + 1, $h === $b);
#                   let delta = 0
                    $delta = 0;
#                   increment h
                    $h++;
#               end
                }
#           end
            }
#           increment delta and n
            $delta++;
            $n++;
#       end
        }
 
        return $output;
    }
 
    /**
     * Convert a digit to its respective character
     *
     * @see https://tools.ietf.org/html/rfc3492#section-5
     * @throws Requests_Exception On invalid digit (`idna.invalid_digit`)
     *
     * @param int $digit Digit in the range 0-35
     * @return string Single character corresponding to digit
     */
    protected static function digit_to_char($digit) {
        // @codeCoverageIgnoreStart
        // As far as I know, this never happens, but still good to be sure.
        if ($digit < 0 || $digit > 35) {
            throw new Requests_Exception(sprintf('Invalid digit %d', $digit), 'idna.invalid_digit', $digit);
        }
        // @codeCoverageIgnoreEnd
        $digits = 'abcdefghijklmnopqrstuvwxyz0123456789';
        return substr($digits, $digit, 1);
    }
 
    /**
     * Adapt the bias
     *
     * @see https://tools.ietf.org/html/rfc3492#section-6.1
     * @param int $delta
     * @param int $numpoints
     * @param bool $firsttime
     * @return int New bias
     */
    protected static function adapt($delta, $numpoints, $firsttime) {
#   function adapt(delta,numpoints,firsttime):
#       if firsttime then let delta = delta div damp
        if ($firsttime) {
            $delta = floor($delta / self::BOOTSTRAP_DAMP);
        }
#       else let delta = delta div 2
        else {
            $delta = floor($delta / 2);
        }
#       let delta = delta + (delta div numpoints)
        $delta += floor($delta / $numpoints);
#       let k = 0
        $k = 0;
#       while delta > ((base - tmin) * tmax) div 2 do begin
        $max = floor(((self::BOOTSTRAP_BASE - self::BOOTSTRAP_TMIN) * self::BOOTSTRAP_TMAX) / 2);
        while ($delta > $max) {
#           let delta = delta div (base - tmin)
            $delta = floor($delta / (self::BOOTSTRAP_BASE - self::BOOTSTRAP_TMIN));
#           let k = k + base
            $k += self::BOOTSTRAP_BASE;
#       end
        }
#       return k + (((base - tmin + 1) * delta) div (delta + skew))
        return $k + floor(((self::BOOTSTRAP_BASE - self::BOOTSTRAP_TMIN + 1) * $delta) / ($delta + self::BOOTSTRAP_SKEW));
    }
}



class Requests_Exception extends Exception {
    /**
     * Type of exception
     *
     * @var string
     */
    protected $type;
 
    /**
     * Data associated with the exception
     *
     * @var mixed
     */
    protected $data;
 
    /**
     * Create a new exception
     *
     * @param string $message Exception message
     * @param string $type Exception type
     * @param mixed $data Associated data
     * @param integer $code Exception numerical code, if applicable
     */
    public function __construct($message, $type, $data = null, $code = 0) {
        parent::__construct($message, $code);
 
        $this->type = $type;
        $this->data = $data;
    }
 
    /**
     * Like {@see getCode()}, but a string code.
     *
     * @codeCoverageIgnore
     * @return string
     */
    public function getType() {
        return $this->type;
    }
 
    /**
     * Gives any relevant data
     *
     * @codeCoverageIgnore
     * @return mixed
     */
    public function getData() {
        return $this->data;
    }
}


interface Requests_Transport {

     public function request($url, $headers = array(), $data = array(), $options = array());


     public function request_multiple($requests, $options);
 

     public static function test();
 }


class Requests_Transport_cURL implements Requests_Transport {
    const CURL_7_10_5 = 0x070A05;
    const CURL_7_16_2 = 0x071002;
 
    /**
     * Raw HTTP data
     *
     * @var string
     */
    public $headers = '';
 
    /**
     * Raw body data
     *
     * @var string
     */
    public $response_data = '';
 
    /**
     * Information on the current request
     *
     * @var array cURL information array, see {@see https://secure.php.net/curl_getinfo}
     */
    public $info;
 
    /**
     * Version string
     *
     * @var long
     */
    public $version;
 
    /**
     * cURL handle
     *
     * @var resource
     */
    protected $handle;
 
    /**
     * Hook dispatcher instance
     *
     * @var Requests_Hooks
     */
    protected $hooks;
 
    /**
     * Have we finished the headers yet?
     *
     * @var boolean
     */
    protected $done_headers = false;
 
    /**
     * If streaming to a file, keep the file pointer
     *
     * @var resource
     */
    protected $stream_handle;
 
    /**
     * How many bytes are in the response body?
     *
     * @var int
     */
    protected $response_bytes;
 
    /**
     * What's the maximum number of bytes we should keep?
     *
     * @var int|bool Byte count, or false if no limit.
     */
    protected $response_byte_limit;
 
    /**
     * Constructor
     */
    public function __construct() {
        $curl = curl_version();
        $this->version = $curl['version_number'];
        $this->handle = curl_init();
 
        curl_setopt($this->handle, CURLOPT_HEADER, false);
        curl_setopt($this->handle, CURLOPT_RETURNTRANSFER, 1);
        if ($this->version >= self::CURL_7_10_5) {
            curl_setopt($this->handle, CURLOPT_ENCODING, '');
        }
        if (defined('CURLOPT_PROTOCOLS')) {
            curl_setopt($this->handle, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
        }
        if (defined('CURLOPT_REDIR_PROTOCOLS')) {
            curl_setopt($this->handle, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
        }
    }
 
    /**
     * Destructor
     */
    public function __destruct() {
        if (is_resource($this->handle)) {
            curl_close($this->handle);
        }
    }
 
    /**
     * Perform a request
     *
     * @throws Requests_Exception On a cURL error (`curlerror`)
     *
     * @param string $url URL to request
     * @param array $headers Associative array of request headers
     * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD
     * @param array $options Request options, see {@see Requests::response()} for documentation
     * @return string Raw HTTP result
     */
    public function request($url, $headers = array(), $data = array(), $options = array()) {
        $this->hooks = $options['hooks'];
 
        $this->setup_handle($url, $headers, $data, $options);
 
        $options['hooks']->dispatch('curl.before_send', array(&$this->handle));
 
        if ($options['filename'] !== false) {
            $this->stream_handle = fopen($options['filename'], 'wb');
        }
 
        $this->response_data = '';
        $this->response_bytes = 0;
        $this->response_byte_limit = false;
        if ($options['max_bytes'] !== false) {
            $this->response_byte_limit = $options['max_bytes'];
        }
 
        if (isset($options['verify'])) {
            if ($options['verify'] === false) {
                curl_setopt($this->handle, CURLOPT_SSL_VERIFYHOST, 0);
                curl_setopt($this->handle, CURLOPT_SSL_VERIFYPEER, 0);
            }
            elseif (is_string($options['verify'])) {
                curl_setopt($this->handle, CURLOPT_CAINFO, $options['verify']);
            }
        }
 
        if (isset($options['verifyname']) && $options['verifyname'] === false) {
            curl_setopt($this->handle, CURLOPT_SSL_VERIFYHOST, 0);
        }
 
        curl_exec($this->handle);
        $response = $this->response_data;
 
        $options['hooks']->dispatch('curl.after_send', array());
 
        if (curl_errno($this->handle) === 23 || curl_errno($this->handle) === 61) {
            // Reset encoding and try again
            curl_setopt($this->handle, CURLOPT_ENCODING, 'none');
 
            $this->response_data = '';
            $this->response_bytes = 0;
            curl_exec($this->handle);
            $response = $this->response_data;
        }
 
        $this->process_response($response, $options);
 
        // Need to remove the $this reference from the curl handle.
        // Otherwise Requests_Transport_cURL wont be garbage collected and the curl_close() will never be called.
        curl_setopt($this->handle, CURLOPT_HEADERFUNCTION, null);
        curl_setopt($this->handle, CURLOPT_WRITEFUNCTION, null);
 
        return $this->headers;
    }
 
    /**
     * Send multiple requests simultaneously
     *
     * @param array $requests Request data
     * @param array $options Global options
     * @return array Array of Requests_Response objects (may contain Requests_Exception or string responses as well)
     */
    public function request_multiple($requests, $options) {
        // If you're not requesting, we can't get any responses ¯\_(ツ)_/¯
        if (empty($requests)) {
            return array();
        }
 
        $multihandle = curl_multi_init();
        $subrequests = array();
        $subhandles = array();
 
        $class = get_class($this);
        foreach ($requests as $id => $request) {
            $subrequests[$id] = new $class();
            $subhandles[$id] = $subrequests[$id]->get_subrequest_handle($request['url'], $request['headers'], $request['data'], $request['options']);
            $request['options']['hooks']->dispatch('curl.before_multi_add', array(&$subhandles[$id]));
            curl_multi_add_handle($multihandle, $subhandles[$id]);
        }
 
        $completed = 0;
        $responses = array();
 
        $request['options']['hooks']->dispatch('curl.before_multi_exec', array(&$multihandle));
 
        do {
            $active = false;
 
            do {
                $status = curl_multi_exec($multihandle, $active);
            }
            while ($status === CURLM_CALL_MULTI_PERFORM);
 
            $to_process = array();
 
            // Read the information as needed
            while ($done = curl_multi_info_read($multihandle)) {
                $key = array_search($done['handle'], $subhandles, true);
                if (!isset($to_process[$key])) {
                    $to_process[$key] = $done;
                }
            }
 
            // Parse the finished requests before we start getting the new ones
            foreach ($to_process as $key => $done) {
                $options = $requests[$key]['options'];
                if (CURLE_OK !== $done['result']) {
                    //get error string for handle.
                    $reason = curl_error($done['handle']);
                    $exception = new Requests_Exception_Transport_cURL(
                                    $reason,
                                    Requests_Exception_Transport_cURL::EASY,
                                    $done['handle'],
                                    $done['result']
                                );
                    $responses[$key] = $exception;
                    $options['hooks']->dispatch('transport.internal.parse_error', array(&$responses[$key], $requests[$key]));
                }
                else {
                    $responses[$key] = $subrequests[$key]->process_response($subrequests[$key]->response_data, $options);
 
                    $options['hooks']->dispatch('transport.internal.parse_response', array(&$responses[$key], $requests[$key]));
                }
 
                curl_multi_remove_handle($multihandle, $done['handle']);
                curl_close($done['handle']);
 
                if (!is_string($responses[$key])) {
                    $options['hooks']->dispatch('multiple.request.complete', array(&$responses[$key], $key));
                }
                $completed++;
            }
        }
        while ($active || $completed < count($subrequests));
 
        $request['options']['hooks']->dispatch('curl.after_multi_exec', array(&$multihandle));
 
        curl_multi_close($multihandle);
 
        return $responses;
    }
 
    /**
     * Get the cURL handle for use in a multi-request
     *
     * @param string $url URL to request
     * @param array $headers Associative array of request headers
     * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD
     * @param array $options Request options, see {@see Requests::response()} for documentation
     * @return resource Subrequest's cURL handle
     */
    public function &get_subrequest_handle($url, $headers, $data, $options) {
        $this->setup_handle($url, $headers, $data, $options);
 
        if ($options['filename'] !== false) {
            $this->stream_handle = fopen($options['filename'], 'wb');
        }
 
        $this->response_data = '';
        $this->response_bytes = 0;
        $this->response_byte_limit = false;
        if ($options['max_bytes'] !== false) {
            $this->response_byte_limit = $options['max_bytes'];
        }
        $this->hooks = $options['hooks'];
 
        return $this->handle;
    }
 
    /**
     * Setup the cURL handle for the given data
     *
     * @param string $url URL to request
     * @param array $headers Associative array of request headers
     * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD
     * @param array $options Request options, see {@see Requests::response()} for documentation
     */
    protected function setup_handle($url, $headers, $data, $options) {
        $options['hooks']->dispatch('curl.before_request', array(&$this->handle));
 
        // Force closing the connection for old versions of cURL (<7.22).
        if ( ! isset( $headers['Connection'] ) ) {
            $headers['Connection'] = 'close';
        }
 
        $headers = Requests::flatten($headers);
 
        if (!empty($data)) {
            $data_format = $options['data_format'];
 
            if ($data_format === 'query') {
                $url = self::format_get($url, $data);
                $data = '';
            }
            elseif (!is_string($data)) {
                $data = http_build_query($data, null, '&');
            }
        }
 
        switch ($options['type']) {
            case Requests::POST:
                curl_setopt($this->handle, CURLOPT_POST, true);
                curl_setopt($this->handle, CURLOPT_POSTFIELDS, $data);
                break;
            case Requests::HEAD:
                curl_setopt($this->handle, CURLOPT_CUSTOMREQUEST, $options['type']);
                curl_setopt($this->handle, CURLOPT_NOBODY, true);
                break;
            case Requests::TRACE:
                curl_setopt($this->handle, CURLOPT_CUSTOMREQUEST, $options['type']);
                break;
            case Requests::PATCH:
            case Requests::PUT:
            case Requests::DELETE:
            case Requests::OPTIONS:
            default:
                curl_setopt($this->handle, CURLOPT_CUSTOMREQUEST, $options['type']);
                if (!empty($data)) {
                    curl_setopt($this->handle, CURLOPT_POSTFIELDS, $data);
                }
        }
 
        // cURL requires a minimum timeout of 1 second when using the system
        // DNS resolver, as it uses `alarm()`, which is second resolution only.
        // There's no way to detect which DNS resolver is being used from our
        // end, so we need to round up regardless of the supplied timeout.
        //
        // https://github.com/curl/curl/blob/4f45240bc84a9aa648c8f7243be7b79e9f9323a5/lib/hostip.c#L606-L609
        $timeout = max($options['timeout'], 1);
 
        if (is_int($timeout) || $this->version < self::CURL_7_16_2) {
            curl_setopt($this->handle, CURLOPT_TIMEOUT, ceil($timeout));
        }
        else {
            curl_setopt($this->handle, CURLOPT_TIMEOUT_MS, round($timeout * 1000));
        }
 
        if (is_int($options['connect_timeout']) || $this->version < self::CURL_7_16_2) {
            curl_setopt($this->handle, CURLOPT_CONNECTTIMEOUT, ceil($options['connect_timeout']));
        }
        else {
            curl_setopt($this->handle, CURLOPT_CONNECTTIMEOUT_MS, round($options['connect_timeout'] * 1000));
        }
        curl_setopt($this->handle, CURLOPT_URL, $url);
        curl_setopt($this->handle, CURLOPT_REFERER, $url);
        curl_setopt($this->handle, CURLOPT_USERAGENT, $options['useragent']);
        if (!empty($headers)) {
            curl_setopt($this->handle, CURLOPT_HTTPHEADER, $headers);
        }
        if ($options['protocol_version'] === 1.1) {
            curl_setopt($this->handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
        }
        else {
            curl_setopt($this->handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0);
        }
 
        if (true === $options['blocking']) {
            curl_setopt($this->handle, CURLOPT_HEADERFUNCTION, array(&$this, 'stream_headers'));
            curl_setopt($this->handle, CURLOPT_WRITEFUNCTION, array(&$this, 'stream_body'));
            curl_setopt($this->handle, CURLOPT_BUFFERSIZE, Requests::BUFFER_SIZE);
        }
    }
 
    /**
     * Process a response
     *
     * @param string $response Response data from the body
     * @param array $options Request options
     * @return string HTTP response data including headers
     */
    public function process_response($response, $options) {
        if ($options['blocking'] === false) {
            $fake_headers = '';
            $options['hooks']->dispatch('curl.after_request', array(&$fake_headers));
            return false;
        }
        if ($options['filename'] !== false) {
            fclose($this->stream_handle);
            $this->headers = trim($this->headers);
        }
        else {
            $this->headers .= $response;
        }
 
        if (curl_errno($this->handle)) {
            $error = sprintf(
                'cURL error %s: %s',
                curl_errno($this->handle),
                curl_error($this->handle)
            );
            throw new Requests_Exception($error, 'curlerror', $this->handle);
        }
        $this->info = curl_getinfo($this->handle);
 
        $options['hooks']->dispatch('curl.after_request', array(&$this->headers, &$this->info));
        return $this->headers;
    }
 
    /**
     * Collect the headers as they are received
     *
     * @param resource $handle cURL resource
     * @param string $headers Header string
     * @return integer Length of provided header
     */
    public function stream_headers($handle, $headers) {
        // Why do we do this? cURL will send both the final response and any
        // interim responses, such as a 100 Continue. We don't need that.
        // (We may want to keep this somewhere just in case)
        if ($this->done_headers) {
            $this->headers = '';
            $this->done_headers = false;
        }
        $this->headers .= $headers;
 
        if ($headers === "\r\n") {
            $this->done_headers = true;
        }
        return strlen($headers);
    }
 
    /**
     * Collect data as it's received
     *
     * @since 1.6.1
     *
     * @param resource $handle cURL resource
     * @param string $data Body data
     * @return integer Length of provided data
     */
    public function stream_body($handle, $data) {
        $this->hooks->dispatch('request.progress', array($data, $this->response_bytes, $this->response_byte_limit));
        $data_length = strlen($data);
 
        // Are we limiting the response size?
        if ($this->response_byte_limit) {
            if ($this->response_bytes === $this->response_byte_limit) {
                // Already at maximum, move on
                return $data_length;
            }
 
            if (($this->response_bytes + $data_length) > $this->response_byte_limit) {
                // Limit the length
                $limited_length = ($this->response_byte_limit - $this->response_bytes);
                $data = substr($data, 0, $limited_length);
            }
        }
 
        if ($this->stream_handle) {
            fwrite($this->stream_handle, $data);
        }
        else {
            $this->response_data .= $data;
        }
 
        $this->response_bytes += strlen($data);
        return $data_length;
    }
 
    /**
     * Format a URL given GET data
     *
     * @param string $url
     * @param array|object $data Data to build query using, see {@see https://secure.php.net/http_build_query}
     * @return string URL with data
     */
    protected static function format_get($url, $data) {
        if (!empty($data)) {
            $url_parts = parse_url($url);
            if (empty($url_parts['query'])) {
                $query = $url_parts['query'] = '';
            }
            else {
                $query = $url_parts['query'];
            }
 
            $query .= '&' . http_build_query($data, null, '&');
            $query = trim($query, '&');
 
            if (empty($url_parts['query'])) {
                $url .= '?' . $query;
            }
            else {
                $url = str_replace($url_parts['query'], $query, $url);
            }
        }
        return $url;
    }
 
    /**
     * Whether this transport is valid
     *
     * @codeCoverageIgnore
     * @return boolean True if the transport is valid, false otherwise.
     */
    public static function test($capabilities = array()) {
        if (!function_exists('curl_init') || !function_exists('curl_exec')) {
            return false;
        }
 
        // If needed, check that our installed curl version supports SSL
        if (isset($capabilities['ssl']) && $capabilities['ssl']) {
            $curl_version = curl_version();
            if (!(CURL_VERSION_SSL & $curl_version['features'])) {
                return false;
            }
        }
 
        return true;
    }
}


class Requests_Response {
    /**
     * Constructor
     */
    public function __construct() {
        $this->headers = new Requests_Response_Headers();
        $this->cookies = new Requests_Cookie_Jar();
    }
 
    /**
     * Response body
     *
     * @var string
     */
    public $body = '';
 
    /**
     * Raw HTTP data from the transport
     *
     * @var string
     */
    public $raw = '';
 
    /**
     * Headers, as an associative array
     *
     * @var Requests_Response_Headers Array-like object representing headers
     */
    public $headers = array();
 
    /**
     * Status code, false if non-blocking
     *
     * @var integer|boolean
     */
    public $status_code = false;
 
    /**
     * Protocol version, false if non-blocking
     * @var float|boolean
     */
    public $protocol_version = false;
 
    /**
     * Whether the request succeeded or not
     *
     * @var boolean
     */
    public $success = false;
 
    /**
     * Number of redirects the request used
     *
     * @var integer
     */
    public $redirects = 0;
 
    /**
     * URL requested
     *
     * @var string
     */
    public $url = '';
 
    /**
     * Previous requests (from redirects)
     *
     * @var array Array of Requests_Response objects
     */
    public $history = array();
 
    /**
     * Cookies from the request
     *
     * @var Requests_Cookie_Jar Array-like object representing a cookie jar
     */
    public $cookies = array();
 
    /**
     * Is the response a redirect?
     *
     * @return boolean True if redirect (3xx status), false if not.
     */
    public function is_redirect() {
        $code = $this->status_code;
        return in_array($code, array(300, 301, 302, 303, 307)) || $code > 307 && $code < 400;
    }
 
    /**
     * Throws an exception if the request was not successful
     *
     * @throws Requests_Exception If `$allow_redirects` is false, and code is 3xx (`response.no_redirects`)
     * @throws Requests_Exception_HTTP On non-successful status code. Exception class corresponds to code (e.g. {@see Requests_Exception_HTTP_404})
     * @param boolean $allow_redirects Set to false to throw on a 3xx as well
     */
    public function throw_for_status($allow_redirects = true) {
        if ($this->is_redirect()) {
            if (!$allow_redirects) {
                throw new Requests_Exception('Redirection not allowed', 'response.no_redirects', $this);
            }
        }
        elseif (!$this->success) {
            $exception = Requests_Exception_HTTP::get_class($this->status_code);
            throw new $exception(null, $this);
        }
    }
}

class Requests_Utility_CaseInsensitiveDictionary implements ArrayAccess, IteratorAggregate {
    /**
     * Actual item data
     *
     * @var array
     */
    protected $data = array();
 
    /**
     * Creates a case insensitive dictionary.
     *
     * @param array $data Dictionary/map to convert to case-insensitive
     */
    public function __construct(array $data = array()) {
        foreach ($data as $key => $value) {
            $this->offsetSet($key, $value);
        }
    }
 
    /**
     * Check if the given item exists
     *
     * @param string $key Item key
     * @return boolean Does the item exist?
     */
    public function offsetExists($key) {
        $key = strtolower($key);
        return isset($this->data[$key]);
    }
 
    /**
     * Get the value for the item
     *
     * @param string $key Item key
     * @return string Item value
     */
    public function offsetGet($key) {
        $key = strtolower($key);
        if (!isset($this->data[$key])) {
            return null;
        }
 
        return $this->data[$key];
    }
 
    /**
     * Set the given item
     *
     * @throws Requests_Exception On attempting to use dictionary as list (`invalidset`)
     *
     * @param string $key Item name
     * @param string $value Item value
     */
    public function offsetSet($key, $value) {
        if ($key === null) {
            throw new Requests_Exception('Object is a dictionary, not a list', 'invalidset');
        }
 
        $key = strtolower($key);
        $this->data[$key] = $value;
    }
 
    /**
     * Unset the given header
     *
     * @param string $key
     */
    public function offsetUnset($key) {
        unset($this->data[strtolower($key)]);
    }
 
    /**
     * Get an iterator for the data
     *
     * @return ArrayIterator
     */
    public function getIterator() {
        return new ArrayIterator($this->data);
    }
 
    /**
     * Get the headers as an array
     *
     * @return array Header data
     */
    public function getAll() {
        return $this->data;
    }
}

class Requests_Response_Headers extends Requests_Utility_CaseInsensitiveDictionary {
    /**
     * Get the given header
     *
     * Unlike {@see self::getValues()}, this returns a string. If there are
     * multiple values, it concatenates them with a comma as per RFC2616.
     *
     * Avoid using this where commas may be used unquoted in values, such as
     * Set-Cookie headers.
     *
     * @param string $key
     * @return string Header value
     */
    public function offsetGet($key) {
        $key = strtolower($key);
        if (!isset($this->data[$key])) {
            return null;
        }
 
        return $this->flatten($this->data[$key]);
    }
 
    /**
     * Set the given item
     *
     * @throws Requests_Exception On attempting to use dictionary as list (`invalidset`)
     *
     * @param string $key Item name
     * @param string $value Item value
     */
    public function offsetSet($key, $value) {
        if ($key === null) {
            throw new Requests_Exception('Object is a dictionary, not a list', 'invalidset');
        }
 
        $key = strtolower($key);
 
        if (!isset($this->data[$key])) {
            $this->data[$key] = array();
        }
 
        $this->data[$key][] = $value;
    }
 
    /**
     * Get all values for a given header
     *
     * @param string $key
     * @return array Header values
     */
    public function getValues($key) {
        $key = strtolower($key);
        if (!isset($this->data[$key])) {
            return null;
        }
 
        return $this->data[$key];
    }
 
    /**
     * Flattens a value into a string
     *
     * Converts an array into a string by imploding values with a comma, as per
     * RFC2616's rules for folding headers.
     *
     * @param string|array $value Value to flatten
     * @return string Flattened value
     */
    public function flatten($value) {
        if (is_array($value)) {
            $value = implode(',', $value);
        }
 
        return $value;
    }
 
    /**
     * Get an iterator for the data
     *
     * Converts the internal
     * @return ArrayIterator
     */
    public function getIterator() {
        return new Requests_Utility_FilteredIterator($this->data, array($this, 'flatten'));
    }
}


class Requests_Cookie {
    /**
     * Cookie name.
     *
     * @var string
     */
    public $name;
 
    /**
     * Cookie value.
     *
     * @var string
     */
    public $value;
 
    /**
     * Cookie attributes
     *
     * Valid keys are (currently) path, domain, expires, max-age, secure and
     * httponly.
     *
     * @var Requests_Utility_CaseInsensitiveDictionary|array Array-like object
     */
    public $attributes = array();
 
    /**
     * Cookie flags
     *
     * Valid keys are (currently) creation, last-access, persistent and
     * host-only.
     *
     * @var array
     */
    public $flags = array();
 
    /**
     * Reference time for relative calculations
     *
     * This is used in place of `time()` when calculating Max-Age expiration and
     * checking time validity.
     *
     * @var int
     */
    public $reference_time = 0;
 
    /**
     * Create a new cookie object
     *
     * @param string $name
     * @param string $value
     * @param array|Requests_Utility_CaseInsensitiveDictionary $attributes Associative array of attribute data
     */
    public function __construct($name, $value, $attributes = array(), $flags = array(), $reference_time = null) {
        $this->name = $name;
        $this->value = $value;
        $this->attributes = $attributes;
        $default_flags = array(
            'creation' => time(),
            'last-access' => time(),
            'persistent' => false,
            'host-only' => true,
        );
        $this->flags = array_merge($default_flags, $flags);
 
        $this->reference_time = time();
        if ($reference_time !== null) {
            $this->reference_time = $reference_time;
        }
 
        $this->normalize();
    }
 
    /**
     * Check if a cookie is expired.
     *
     * Checks the age against $this->reference_time to determine if the cookie
     * is expired.
     *
     * @return boolean True if expired, false if time is valid.
     */
    public function is_expired() {
        // RFC6265, s. 4.1.2.2:
        // If a cookie has both the Max-Age and the Expires attribute, the Max-
        // Age attribute has precedence and controls the expiration date of the
        // cookie.
        if (isset($this->attributes['max-age'])) {
            $max_age = $this->attributes['max-age'];
            return $max_age < $this->reference_time;
        }
 
        if (isset($this->attributes['expires'])) {
            $expires = $this->attributes['expires'];
            return $expires < $this->reference_time;
        }
 
        return false;
    }
 
    /**
     * Check if a cookie is valid for a given URI
     *
     * @param Requests_IRI $uri URI to check
     * @return boolean Whether the cookie is valid for the given URI
     */
    public function uri_matches(Requests_IRI $uri) {
        if (!$this->domain_matches($uri->host)) {
            return false;
        }
 
        if (!$this->path_matches($uri->path)) {
            return false;
        }
 
        return empty($this->attributes['secure']) || $uri->scheme === 'https';
    }
 
    /**
     * Check if a cookie is valid for a given domain
     *
     * @param string $string Domain to check
     * @return boolean Whether the cookie is valid for the given domain
     */
    public function domain_matches($string) {
        if (!isset($this->attributes['domain'])) {
            // Cookies created manually; cookies created by Requests will set
            // the domain to the requested domain
            return true;
        }
 
        $domain_string = $this->attributes['domain'];
        if ($domain_string === $string) {
            // The domain string and the string are identical.
            return true;
        }
 
        // If the cookie is marked as host-only and we don't have an exact
        // match, reject the cookie
        if ($this->flags['host-only'] === true) {
            return false;
        }
 
        if (strlen($string) <= strlen($domain_string)) {
            // For obvious reasons, the string cannot be a suffix if the domain
            // is shorter than the domain string
            return false;
        }
 
        if (substr($string, -1 * strlen($domain_string)) !== $domain_string) {
            // The domain string should be a suffix of the string.
            return false;
        }
 
        $prefix = substr($string, 0, strlen($string) - strlen($domain_string));
        if (substr($prefix, -1) !== '.') {
            // The last character of the string that is not included in the
            // domain string should be a %x2E (".") character.
            return false;
        }
 
        // The string should be a host name (i.e., not an IP address).
        return !preg_match('#^(.+\.)\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$#', $string);
    }
 
    /**
     * Check if a cookie is valid for a given path
     *
     * From the path-match check in RFC 6265 section 5.1.4
     *
     * @param string $request_path Path to check
     * @return boolean Whether the cookie is valid for the given path
     */
    public function path_matches($request_path) {
        if (empty($request_path)) {
            // Normalize empty path to root
            $request_path = '/';
        }
 
        if (!isset($this->attributes['path'])) {
            // Cookies created manually; cookies created by Requests will set
            // the path to the requested path
            return true;
        }
 
        $cookie_path = $this->attributes['path'];
 
        if ($cookie_path === $request_path) {
            // The cookie-path and the request-path are identical.
            return true;
        }
 
        if (strlen($request_path) > strlen($cookie_path) && substr($request_path, 0, strlen($cookie_path)) === $cookie_path) {
            if (substr($cookie_path, -1) === '/') {
                // The cookie-path is a prefix of the request-path, and the last
                // character of the cookie-path is %x2F ("/").
                return true;
            }
 
            if (substr($request_path, strlen($cookie_path), 1) === '/') {
                // The cookie-path is a prefix of the request-path, and the
                // first character of the request-path that is not included in
                // the cookie-path is a %x2F ("/") character.
                return true;
            }
        }
 
        return false;
    }
 
    /**
     * Normalize cookie and attributes
     *
     * @return boolean Whether the cookie was successfully normalized
     */
    public function normalize() {
        foreach ($this->attributes as $key => $value) {
            $orig_value = $value;
            $value = $this->normalize_attribute($key, $value);
            if ($value === null) {
                unset($this->attributes[$key]);
                continue;
            }
 
            if ($value !== $orig_value) {
                $this->attributes[$key] = $value;
            }
        }
 
        return true;
    }
 
    /**
     * Parse an individual cookie attribute
     *
     * Handles parsing individual attributes from the cookie values.
     *
     * @param string $name Attribute name
     * @param string|boolean $value Attribute value (string value, or true if empty/flag)
     * @return mixed Value if available, or null if the attribute value is invalid (and should be skipped)
     */
    protected function normalize_attribute($name, $value) {
        switch (strtolower($name)) {
            case 'expires':
                // Expiration parsing, as per RFC 6265 section 5.2.1
                if (is_int($value)) {
                    return $value;
                }
 
                $expiry_time = strtotime($value);
                if ($expiry_time === false) {
                    return null;
                }
 
                return $expiry_time;
 
            case 'max-age':
                // Expiration parsing, as per RFC 6265 section 5.2.2
                if (is_int($value)) {
                    return $value;
                }
 
                // Check that we have a valid age
                if (!preg_match('/^-?\d+$/', $value)) {
                    return null;
                }
 
                $delta_seconds = (int) $value;
                if ($delta_seconds <= 0) {
                    $expiry_time = 0;
                }
                else {
                    $expiry_time = $this->reference_time + $delta_seconds;
                }
 
                return $expiry_time;
 
            case 'domain':
                // Domain normalization, as per RFC 6265 section 5.2.3
                if ($value[0] === '.') {
                    $value = substr($value, 1);
                }
 
                return $value;
 
            default:
                return $value;
        }
    }
 
    /**
     * Format a cookie for a Cookie header
     *
     * This is used when sending cookies to a server.
     *
     * @return string Cookie formatted for Cookie header
     */
    public function format_for_header() {
        return sprintf('%s=%s', $this->name, $this->value);
    }
 
    /**
     * Format a cookie for a Cookie header
     *
     * @codeCoverageIgnore
     * @deprecated Use {@see Requests_Cookie::format_for_header}
     * @return string
     */
    public function formatForHeader() {
        return $this->format_for_header();
    }
 
    /**
     * Format a cookie for a Set-Cookie header
     *
     * This is used when sending cookies to clients. This isn't really
     * applicable to client-side usage, but might be handy for debugging.
     *
     * @return string Cookie formatted for Set-Cookie header
     */
    public function format_for_set_cookie() {
        $header_value = $this->format_for_header();
        if (!empty($this->attributes)) {
            $parts = array();
            foreach ($this->attributes as $key => $value) {
                // Ignore non-associative attributes
                if (is_numeric($key)) {
                    $parts[] = $value;
                }
                else {
                    $parts[] = sprintf('%s=%s', $key, $value);
                }
            }
 
            $header_value .= '; ' . implode('; ', $parts);
        }
        return $header_value;
    }
 
    /**
     * Format a cookie for a Set-Cookie header
     *
     * @codeCoverageIgnore
     * @deprecated Use {@see Requests_Cookie::format_for_set_cookie}
     * @return string
     */
    public function formatForSetCookie() {
        return $this->format_for_set_cookie();
    }
 
    /**
     * Get the cookie value
     *
     * Attributes and other data can be accessed via methods.
     */
    public function __toString() {
        return $this->value;
    }
 
    /**
     * Parse a cookie string into a cookie object
     *
     * Based on Mozilla's parsing code in Firefox and related projects, which
     * is an intentional deviation from RFC 2109 and RFC 2616. RFC 6265
     * specifies some of this handling, but not in a thorough manner.
     *
     * @param string Cookie header value (from a Set-Cookie header)
     * @return Requests_Cookie Parsed cookie object
     */
    public static function parse($string, $name = '', $reference_time = null) {
        $parts = explode(';', $string);
        $kvparts = array_shift($parts);
 
        if (!empty($name)) {
            $value = $string;
        }
        elseif (strpos($kvparts, '=') === false) {
            // Some sites might only have a value without the equals separator.
            // Deviate from RFC 6265 and pretend it was actually a blank name
            // (`=foo`)
            //
            // https://bugzilla.mozilla.org/show_bug.cgi?id=169091
            $name = '';
            $value = $kvparts;
        }
        else {
            list($name, $value) = explode('=', $kvparts, 2);
        }
        $name = trim($name);
        $value = trim($value);
 
        // Attribute key are handled case-insensitively
        $attributes = new Requests_Utility_CaseInsensitiveDictionary();
 
        if (!empty($parts)) {
            foreach ($parts as $part) {
                if (strpos($part, '=') === false) {
                    $part_key = $part;
                    $part_value = true;
                }
                else {
                    list($part_key, $part_value) = explode('=', $part, 2);
                    $part_value = trim($part_value);
                }
 
                $part_key = trim($part_key);
                $attributes[$part_key] = $part_value;
            }
        }
 
        return new Requests_Cookie($name, $value, $attributes, array(), $reference_time);
    }
 
    /**
     * Parse all Set-Cookie headers from request headers
     *
     * @param Requests_Response_Headers $headers Headers to parse from
     * @param Requests_IRI|null $origin URI for comparing cookie origins
     * @param int|null $time Reference time for expiration calculation
     * @return array
     */
    public static function parse_from_headers(Requests_Response_Headers $headers, Requests_IRI $origin = null, $time = null) {
        $cookie_headers = $headers->getValues('Set-Cookie');
        if (empty($cookie_headers)) {
            return array();
        }
 
        $cookies = array();
        foreach ($cookie_headers as $header) {
            $parsed = self::parse($header, '', $time);
 
            // Default domain/path attributes
            if (empty($parsed->attributes['domain']) && !empty($origin)) {
                $parsed->attributes['domain'] = $origin->host;
                $parsed->flags['host-only'] = true;
            }
            else {
                $parsed->flags['host-only'] = false;
            }
 
            $path_is_valid = (!empty($parsed->attributes['path']) && $parsed->attributes['path'][0] === '/');
            if (!$path_is_valid && !empty($origin)) {
                $path = $origin->path;
 
                // Default path normalization as per RFC 6265 section 5.1.4
                if (substr($path, 0, 1) !== '/') {
                    // If the uri-path is empty or if the first character of
                    // the uri-path is not a %x2F ("/") character, output
                    // %x2F ("/") and skip the remaining steps.
                    $path = '/';
                }
                elseif (substr_count($path, '/') === 1) {
                    // If the uri-path contains no more than one %x2F ("/")
                    // character, output %x2F ("/") and skip the remaining
                    // step.
                    $path = '/';
                }
                else {
                    // Output the characters of the uri-path from the first
                    // character up to, but not including, the right-most
                    // %x2F ("/").
                    $path = substr($path, 0, strrpos($path, '/'));
                }
                $parsed->attributes['path'] = $path;
            }
 
            // Reject invalid cookie domains
            if (!empty($origin) && !$parsed->domain_matches($origin->host)) {
                continue;
            }
 
            $cookies[$parsed->name] = $parsed;
        }
 
        return $cookies;
    }
 
    /**
     * Parse all Set-Cookie headers from request headers
     *
     * @codeCoverageIgnore
     * @deprecated Use {@see Requests_Cookie::parse_from_headers}
     * @return string
     */
    public static function parseFromHeaders(Requests_Response_Headers $headers) {
        return self::parse_from_headers($headers);
    }
}

class HtmlDomParser {
	
	/**
	 * @return \simplehtmldom_1_5\simple_html_dom
	 */
	static public function file_get_html() {
		return call_user_func_array ( '\simplehtmldom_1_5\file_get_html' , func_get_args() );
	}
	/**
	 * get html dom from string
	 * @return \simplehtmldom_1_5\simple_html_dom
	 */
	static public function str_get_html() {
		return call_user_func_array ( '\simplehtmldom_1_5\str_get_html' , func_get_args() );
	}
}

class Parser
{
    private static $JP          = array(",", "円", "分", "時間", "日");
    private static $EN          = array("", "", "min", "hour", "day");
    private static $BID_SUCCESS = '入札を受け付けました。あなたが現在の最高額入札者です。';
    private static $PRICE_UP    = '/再入札/';
    private static $AUCTION_WON = 'おめでとうございます!!　あなたが落札しました。';
    private static $TABLE_WON   = 8;
    private static $TABLE_BID   = 8;

    /**
     * Check correct Login
     *
     * @param  string $body     HTML of login page
     * @param  string $userName Login user name
     * @return bool             Return true if login is correct or false is fail
     *
     */
    public static function checkLogin(&$body, &$userName)
    {
        $html = static::getHtmlDom($body);

        if ($p_result = $html->find('div[class=yjmthloginarea]', 0))
        {
            if ($userName == trim($p_result->find('strong', 0)->innertext))
            {
                return true;
            }
        }

        return false;
    }

    /**
     * Get only Auction IDs from Auction won page
     *
     * @param  string $body HTML page with won auctions
     * @return array        Array of auction ids
     *
     */
    public static function getWonIds(&$body)
    {
        $html = static::getHtmlDom($body);

        $ids = [];

        $won_table = self::findTable($html, self::$TABLE_WON);

        if (!$won_table)
        {
            return null;
        }
        
        $first_tr = true;

        foreach ($won_table->children as $key => $tr)
        {
            $a_tr = [];
            if (!$tr->children())
                continue;

            if ($first_tr){
                $first_tr = false;
                continue;
            }
            $a_td = $tr->children();
            $ids[] = strip_tags($a_td[1]->innertext);
        }

        return $ids;
    }

    /**
     * Get all values (like ID, Title, Price etc.) from bidding page
     *
     * @param  string $body HTML page with bidding auctions
     * @return array  Return array with auctions in bid
     *
     */
    public static function getBiddingLots(&$body)
    {
        $html = static::getHtmlDom($body);

        $lots = [];

        $bidding_table = self::findTable($html, self::$TABLE_BID);

        if (!$bidding_table)
        {
            return $lots;
        }

        $is_header = true;

        foreach ($bidding_table->children as $i => $tr)
        {
            $lot = [];

            if ( !$tr->children())
                continue;

            if ($is_header)
            {
                $is_header = false;
                continue;
            }

            foreach ($tr->children() as $j => $td)
            {
                if ($j > 6)
                    break;

                switch ($j)
                {
                    case 0:
                        continue;
                        break;
                    case 1:
                        $tmp = explode('/', $td->find('a', 0)->href);
                        $lot['id'] = end($tmp);
                        $lot['title'] = trim(strip_tags($td->innertext));
                        break;
                    case 2:
                        $lot['price'] = trim(str_replace(static::$JP, static::$EN, strip_tags($td->innertext)));
                        break;
                    case 3:
                        $lot['bids'] = trim(strip_tags($td->innertext));
                        break;
                    case 4:
                        $lot['vendor'] = trim(strip_tags($td->innertext));
                        break;
                    case 5:
                        $lot['bidder'] = trim(strip_tags($td->innertext));
                        break;
                    case 6:
                        $lot['end'] = trim(str_replace(static::$JP, static::$EN, strip_tags($td->innertext)));
                        break;
                    default:
                        break;
                }
            }
            
            $lots[] = $lot;
        }
        
        return $lots;
    }

    /**
     * Parse HTML page for hidden fields in form
     *
     * @param  string $body    Html page with form
     * @return array           Return array with pair name and value
     * @throws ParserException Throw exception if POST form not found
     *
     */
    public static function getHiddenInputs(&$body)
    {
      
        $html = static::getHtmlDom($body);
        //echo $html;
        $arr = [];

        if ($form = $html->find('form[method=post]', 0))
        {
            $inputs = $form->find('input[type=hidden]');

            foreach ($inputs as $input)
            {
                $arr[] = ['name' => $input->name, 'value' => $input->value];
            }
        }
        else
        {
            throw new ParserException('Page POST form not found');
        }

        return $arr;
    }

    /**
     * Check result of bid.
     *
     * @param  string $body    Html page with bid result
     * @return bool            Return true if bid was success or false if unknown result (like won lots limit etc.)
     *
     * @throws ParserException Throw exception if bid was not success
     *
     */
    public static function getResult(&$body)
    {
        $html = static::getHtmlDom($body);

        if ($p_result = $html->find('div[id=modAlertBox]', 0))
        {
            if (static::$BID_SUCCESS == trim($p_result->find('strong', 0)->innertext))
            {
                return true;
            }
            else if (static::$AUCTION_WON == trim($p_result->find('strong', 0)->innertext))
            {
                return true;
            }
            else
            {
                throw new ParserException('Page says: '.$p_result->innertext);
            }
        }
        else if ($p_result = $html->find('div[class=decSmryTable]', 0))
        {
            if (preg_match(static::$PRICE_UP, $p_result))
            {
                throw new ParserException('Price goes up', 10);
            }
            else
            {
                throw new ParserException('Parser could not find result, maybe price goes up');
            }
        }

        return false;
    }

    /**
     * Create DOM tree from HTML
     *
     * @throws ParserException Throw exception if HTML is empty
     *
     */
    private static function getHtmlDom(&$body)
    {
        if (!$body)
        {
            throw new ParserException('Body of HTML Document is empty');
        }
       
        return HtmlDomParser::str_get_html($body);
    }
    
    /**
     * Find table with auctions
     *
     * @param  string $html Html with table of auctions
     * @param  string $col  Find table with $col count
     * @return object|null  Return HtmlDomParser element with table of auctions or null if not found
     *
     */
    private static function findTable(&$html = null, $col = null)
    {
        if ($tables = $html->find('table'))
        {
            foreach ($tables as $table)
            {
                if ( $t_children = $table->children() )
                {
                    foreach ($t_children as $t_child)
                    {
                        if ( $t_child->tag = 'tbody')
                        {
                            if ( $t_body_children = $t_child->children() )
                            {
                                if ( count($t_body_children) == $col )
                                {
                                    return $table;
                                }
                            }
                        }
                    }
                }
            }
        }

        return null;
    }
}

class ParserException extends \Exception {}


