Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add cookie support #64

Merged
merged 13 commits into from
Sep 24, 2013
15 changes: 15 additions & 0 deletions library/Requests.php
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,8 @@ public static function request($url, $headers = array(), $data = array(), $type
* - `data`: Associative array of options. Same as the `$options` parameter
* to {@see Requests::request}
* (array, default: see {@see Requests::request})
* - `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,
Expand Down Expand Up @@ -447,6 +449,7 @@ protected static function get_default_options($multirequest = false) {
'type' => self::GET,
'filename' => false,
'auth' => false,
'cookies' => false,
'idn' => true,
'hooks' => null,
'transport' => null,
Expand Down Expand Up @@ -485,6 +488,16 @@ protected static function set_defaults(&$url, &$headers, &$data, &$type, &$optio
$options['auth']->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);
Expand Down Expand Up @@ -560,6 +573,8 @@ protected static function parse_response($headers, $url, $req_headers, $req_data
unset($return->headers['connection']);
}

$options['hooks']->dispatch('requests.before_redirect_check', array(&$return, $req_headers, $req_data, $options));

if ((in_array($return->status_code, array(300, 301, 302, 303, 307)) || $return->status_code > 307 && $return->status_code < 400) && $options['follow_redirects'] === true) {
if (isset($return->headers['location']) && $options['redirected'] < $options['redirects']) {
if ($return->status_code === 303) {
Expand Down
171 changes: 171 additions & 0 deletions library/Requests/Cookie.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
<?php
/**
* Cookie storage object
*
* @package Requests
* @subpackage Cookies
*/

/**
* Cookie storage object
*
* @package Requests
* @subpackage Cookies
*/
class Requests_Cookie {
/**
*
* @var string
*/
public $name;

/**
* @var string
*/
public $value;

/**
* Cookie attributes
*
* Valid keys are (currently) path, domain, expires, max-age, secure and
* httponly.
*
* @var array
*/
public $attributes = array();

/**
* Create a new cookie object
*
* @param string $name
* @param string $value
* @param array $attributes Associative array of attribute data
*/
public function __construct($name, $value, $attributes = array()) {
$this->name = $name;
$this->value = $value;
$this->attributes = $attributes;
}

/**
* 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 formatForHeader() {
return sprintf('%s=%s', $this->name, $this->value);
}

/**
* 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 formatForSetCookie() {
$header_value = $this->formatForHeader();
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;
}

/**
* 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 = '') {
$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);
}

/**
* Parse all Set-Cookie headers from request headers
*
* @param Requests_Response_Headers $headers
* @return array
*/
public static function parseFromHeaders(Requests_Response_Headers $headers) {
$cookie_headers = $headers->getValues('Set-Cookie');
if (empty($cookie_headers)) {
return array();
}

$cookies = array();
foreach ($cookie_headers as $header) {
$parsed = self::parse($header);
$cookies[$parsed->name] = $parsed;
}

return $cookies;
}
}
146 changes: 146 additions & 0 deletions library/Requests/Cookie/Jar.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
<?php
/**
* Cookie holder object
*
* @package Requests
* @subpackage Cookies
*/

/**
* Cookie holder object
*
* @package Requests
* @subpackage Cookies
*/
class Requests_Cookie_Jar implements ArrayAccess, IteratorAggregate {
/**
* 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 normalizeCookie($cookie, $key = null) {
if ($cookie instanceof Requests_Cookie) {
return $cookie;
}

return Requests_Cookie::parse($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 (!empty($this->cookies)) {
$cookies = array();
foreach ($this->cookies as $key => $cookie) {
$cookie = $this->normalizeCookie($cookie, $key);
$cookies[] = $cookie->formatForHeader();
}

$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) {
$cookies = Requests_Cookie::parseFromHeaders($return->headers);
$this->cookies = array_merge($this->cookies, $cookies);
$return->cookies = $this;
}
}
5 changes: 5 additions & 0 deletions library/Requests/Response.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ public function __construct() {
*/
public $history = array();

/**
* Cookies from the request
*/
public $cookies = array();

/**
* Throws an exception if the request was not successful
*
Expand Down
Loading