Skip to content

Commit

Permalink
Merge pull request #64 from rmccue/cookies
Browse files Browse the repository at this point in the history
Add cookie support
  • Loading branch information
rmccue committed Sep 24, 2013
2 parents d4a493c + 485927a commit c09e97f
Show file tree
Hide file tree
Showing 8 changed files with 535 additions and 6 deletions.
15 changes: 15 additions & 0 deletions library/Requests.php
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,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 @@ -450,6 +452,7 @@ protected static function get_default_options($multirequest = false) {
'filename' => false,
'auth' => false,
'proxy' => false,
'cookies' => false,
'idn' => true,
'hooks' => null,
'transport' => null,
Expand Down Expand Up @@ -495,6 +498,16 @@ protected static function set_defaults(&$url, &$headers, &$data, &$type, &$optio
$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);
Expand Down Expand Up @@ -570,6 +583,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

0 comments on commit c09e97f

Please sign in to comment.