* @param $method string HTTP method. Usually GET/POST
* @param $url string Full URL to act on
* @param $options options to pass to HttpRequest object
+ * Possible keys for the array:
+ * timeout Timeout length in seconds
+ * postData An array of key-value pairs or a url-encoded form data
+ * proxy The proxy to use. Will use $wgHTTPProxy (if set) otherwise.
+ * noProxy Override $wgHTTPProxy (if set) and don't use any proxy at all.
+ * sslVerifyHost (curl only) Verify the SSL certificate
+ * caInfo (curl only) Provide CA information
+ * maxRedirects Maximum number of redirects to follow (defaults to 5)
+ * followRedirects Whether to follow redirects (defaults to true)
* @returns mixed (bool)false on failure or a string on success
*/
public static function request( $method, $url, $options = array() ) {
protected $url;
protected $parsedUrl;
protected $callback;
+ protected $maxRedirects = 5;
+ protected $followRedirects = true;
+
+ protected $cookieJar;
+
+ protected $headerList = array();
+ protected $respVersion = "0.9";
+ protected $respStatus = "0.1";
+ protected $respHeaders = array();
+
public $status;
/**
* @param $url string url to use
- * @param $options array (optional) extra params to pass
- * Possible keys for the array:
- * method
- * timeout
- * targetFilePath
- * requestKey
- * postData
- * proxy
- * noProxy
- * sslVerifyHost
- * caInfo
+ * @param $options array (optional) extra params to pass (see Http::request())
*/
function __construct( $url, $options = array() ) {
global $wgHTTPTimeout;
$this->timeout = $wgHTTPTimeout;
}
- $members = array( "targetFilePath", "requestKey", "postData",
- "proxy", "noProxy", "sslVerifyHost", "caInfo", "method" );
+ $members = array( "postData", "proxy", "noProxy", "sslVerifyHost", "caInfo",
+ "method", "followRedirects", "maxRedirects" );
foreach ( $members as $o ) {
if ( isset($options[$o]) ) {
$this->$o = $options[$o];
if ( !Http::$httpEngine ) {
Http::$httpEngine = function_exists( 'curl_init' ) ? 'curl' : 'php';
} elseif ( Http::$httpEngine == 'curl' && !function_exists( 'curl_init' ) ) {
- throw new MWException( __METHOD__.': curl (http://php.net/curl) is not installed, but Http::$httpEngine is set to "curl"' );
+ throw new MWException( __METHOD__.': curl (http://php.net/curl) is not installed, but'.
+ ' Http::$httpEngine is set to "curl"' );
}
switch( Http::$httpEngine ) {
return new CurlHttpRequest( $url, $options );
case 'php':
if ( !wfIniGetBool( 'allow_url_fopen' ) ) {
- throw new MWException( __METHOD__.': allow_url_fopen needs to be enabled for pure PHP http requests to work. '.
- 'If possible, curl should be used instead. See http://php.net/curl.' );
+ throw new MWException( __METHOD__.': allow_url_fopen needs to be enabled for pure PHP'.
+ ' http requests to work. If possible, curl should be used instead. See http://php.net/curl.' );
}
return new PhpHttpRequest( $url, $options );
default:
public function proxySetup() {
global $wgHTTPProxy;
-
if ( $this->proxy ) {
return;
}
public function getHeaderList() {
$list = array();
+ if( $this->cookieJar ) {
+ $this->reqHeaders['Cookie'] = $this->cookieJar->serializeToHttpRequest();
+ }
foreach($this->reqHeaders as $name => $value) {
$list[] = "$name: $value";
}
}
/**
- * A generic callback to read in the response from a remote server
+ * A generic callback to read the body of the response from a remote
+ * server.
* @param $fh handle
* @param $content string
*/
$this->setUserAgent(Http::userAgent());
}
}
+
+ protected function parseHeader() {
+ $lastname = "";
+ foreach( $this->headerList as $header ) {
+ if( preg_match( "#^HTTP/([0-9.]+) (.*)#", $header, $match ) ) {
+ $this->respVersion = $match[1];
+ $this->respStatus = $match[2];
+ } elseif( preg_match( "#^[ \t]#", $header ) ) {
+ $last = count($this->respHeaders[$lastname]) - 1;
+ $this->respHeaders[$lastname][$last] .= "\r\n$header";
+ } elseif( preg_match( "#^([^:]*):[\t ]*(.*)#", $header, $match ) ) {
+ $this->respHeaders[strtolower( $match[1] )][] = $match[2];
+ $lastname = strtolower( $match[1] );
+ }
+ }
+
+ $this->parseCookies();
+ }
+
+ /**
+ * Returns an associative array of response headers after the
+ * request has been executed. Because some headers
+ * (e.g. Set-Cookie) can appear more than once the, each value of
+ * the associative array is an array of the values given.
+ * @return array
+ */
+ public function getResponseHeaders() {
+ if( !$this->respHeaders ) {
+ $this->parseHeader();
+ }
+ return $this->respHeaders;
+ }
+
+ /**
+ * Tells the HttpRequest object to use this pre-loaded CookieJar.
+ * @param $jar CookieJar
+ */
+ public function setCookieJar( $jar ) {
+ $this->cookieJar = $jar;
+ }
+
+ /**
+ * Returns the cookie jar in use.
+ * @returns CookieJar
+ */
+ public function getCookieJar() {
+ if( !$this->respHeaders ) {
+ $this->parseHeader();
+ }
+ return $this->cookieJar;
+ }
+
+ /**
+ * Sets a cookie. Used before a request to set up any individual
+ * cookies. Used internally after a request to parse the
+ * Set-Cookie headers.
+ * @see Cookie::set
+ */
+ public function setCookie( $name, $value = null, $attr = null) {
+ if( !$this->cookieJar ) {
+ $this->cookieJar = new CookieJar;
+ }
+ $this->cookieJar->setCookie($name, $value, $attr);
+ }
+
+ /**
+ * Parse the cookies in the response headers and store them in the cookie jar.
+ */
+ protected function parseCookies() {
+ if( isset( $this->respHeaders['set-cookie'] ) ) {
+ if( !$this->cookieJar ) {
+ $this->cookieJar = new CookieJar;
+ }
+ $url = parse_url( $this->getFinalUrl() );
+ foreach( $this->respHeaders['set-cookie'] as $cookie ) {
+ $this->cookieJar->parseCookieResponseHeader( $cookie, $url['host'] );
+ }
+ }
+ }
+
+ /**
+ * Returns the final URL after all redirections.
+ * @returns string
+ */
+ public function getFinalUrl() {
+ $finalUrl = $this->url;
+ if ( isset( $this->respHeaders['location'] ) ) {
+ $redir = $this->respHeaders['location'];
+ $finalUrl = $redir[count($redir) - 1];
+ }
+
+ return $finalUrl;
+ }
}
+
+class Cookie {
+ protected $name;
+ protected $value;
+ protected $expires;
+ protected $path;
+ protected $domain;
+ protected $isSessionKey = true;
+ // TO IMPLEMENT protected $secure
+ // TO IMPLEMENT? protected $maxAge (add onto expires)
+ // TO IMPLEMENT? protected $version
+ // TO IMPLEMENT? protected $comment
+
+ function __construct( $name, $value, $attr ) {
+ $this->name = $name;
+ $this->set( $value, $attr );
+ }
+
+ /**
+ * Sets a cookie. Used before a request to set up any individual
+ * cookies. Used internally after a request to parse the
+ * Set-Cookie headers.
+ * @param $name string the name of the cookie
+ * @param $value string the value of the cookie
+ * @param $attr array possible key/values:
+ * expires A date string
+ * path The path this cookie is used on
+ * domain Domain this cookie is used on
+ */
+ public function set( $value, $attr ) {
+ $this->value = $value;
+ if( isset( $attr['expires'] ) ) {
+ $this->isSessionKey = false;
+ $this->expires = strtotime( $attr['expires'] );
+ }
+ if( isset( $attr['path'] ) ) {
+ $this->path = $attr['path'];
+ } else {
+ $this->path = "/";
+ }
+ if( isset( $attr['domain'] ) ) {
+ $this->domain = $attr['domain'];
+ } else {
+ throw new MWException("You must specify a domain.");
+ }
+ }
+
+ /**
+ * Serialize the cookie jar into a format useful for HTTP Request headers.
+ * @param $path string the path that will be used. Required.
+ * @param $domain string the domain that will be used. Required.
+ * @return string
+ */
+ public function serializeToHttpRequest( $path, $domain ) {
+ $ret = "";
+
+ if( $this->canServeDomain( $domain )
+ && $this->canServePath( $path )
+ && $this->isUnExpired() ) {
+ $ret = $this->name ."=". $this->value;
+ }
+
+ return $ret;
+ }
+
+ protected function canServeDomain( $domain ) {
+ if( $this->domain && substr_compare( $domain, $this->domain, -strlen( $this->domain ),
+ strlen( $this->domain ), TRUE ) == 0 ) {
+ return true;
+ }
+ return false;
+ }
+
+ protected function canServePath( $path ) {
+ if( $this->path && substr_compare( $this->path, $path, 0, strlen( $this->path ) ) == 0 ) {
+ return true;
+ }
+ return false;
+ }
+
+ protected function isUnExpired() {
+ if( $this->isSessionKey || $this->expires > time() ) {
+ return true;
+ }
+ return false;
+ }
+
+}
+
+class CookieJar {
+ private $cookie;
+
+ /**
+ * Set a cookie in the cookie jar. Make sure only one cookie per-name exists.
+ * @see Cookie::set()
+ */
+ public function setCookie ($name, $value, $attr) {
+ /* cookies: case insensitive, so this should work.
+ * We'll still send the cookies back in the same case we got them, though.
+ */
+ $index = strtoupper($name);
+ if( isset( $this->cookie[$index] ) ) {
+ $this->cookie[$index]->set( $value, $attr );
+ } else {
+ $this->cookie[$index] = new Cookie( $name, $value, $attr );
+ }
+ }
+
+ /**
+ * @see Cookie::serializeToHttpRequest
+ */
+ public function serializeToHttpRequest( $path, $domain ) {
+ $cookies = array();
+
+ foreach( $this->cookie as $c ) {
+ $serialized = $c->serializeToHttpRequest( $path, $domain );
+ if ( $serialized ) $cookies[] = $serialized;
+ }
+
+ return implode("; ", $cookies);
+ }
+
+ /**
+ * Parse the content of an Set-Cookie HTTP Response header.
+ * @param $cookie string
+ */
+ public function parseCookieResponseHeader ( $cookie, $domain = null ) {
+ $len = strlen( "Set-Cookie:" );
+ if ( substr_compare( "Set-Cookie:", $cookie, 0, $len, TRUE ) === 0 ) {
+ $cookie = substr( $cookie, $len );
+ }
+
+ $bit = array_map( 'trim', explode( ";", $cookie ) );
+ list($name, $value) = explode( "=", array_shift( $bit ), 2 );
+ $attr = array();
+ foreach( $bit as $piece ) {
+ $parts = explode( "=", $piece );
+ if( count( $parts ) > 1 ) {
+ $attr[strtolower( $parts[0] )] = $parts[1];
+ } else {
+ $attr[strtolower( $parts[0] )] = true;
+ }
+ }
+ $this->setCookie( $name, $value, $attr );
+ }
+}
+
+
/**
* HttpRequest implemented using internal curl compiled into PHP
*/
class CurlHttpRequest extends HttpRequest {
+ static $curlMessageMap = array(
+ 6 => 'http-host-unreachable',
+ 28 => 'http-timed-out'
+ );
+
protected $curlOptions = array();
+ protected $headerText = "";
+
+ protected function readHeader( $fh, $content ) {
+ $this->headerText .= $content;
+ return strlen( $content );
+ }
public function execute() {
parent::execute();
$this->curlOptions[CURLOPT_TIMEOUT] = $this->timeout;
$this->curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_0;
$this->curlOptions[CURLOPT_WRITEFUNCTION] = $this->callback;
+ $this->curlOptions[CURLOPT_HEADERFUNCTION] = array($this, "readHeader");
+ $this->curlOptions[CURLOPT_FOLLOWLOCATION] = $this->followRedirects;
+ $this->curlOptions[CURLOPT_MAXREDIRS] = $this->maxRedirects;
/* not sure these two are actually necessary */
if(isset($this->reqHeaders['Referer'])) {
curl_setopt_array( $curlHandle, $this->curlOptions );
if ( false === curl_exec( $curlHandle ) ) {
- // re-using already translated error messages
- $this->status->fatal( 'upload-curl-error'.curl_errno( $curlHandle ).'-text' );
+ $code = curl_error( $curlHandle );
+
+ if ( isset( self::$curlMessageMap[$code] ) ) {
+ $this->status->fatal( self::$curlMessageMap[$code] );
+ } else {
+ $this->status->fatal( 'http-curl-error', curl_error( $curlHandle ) );
+ }
+ } else {
+ $this->headerList = explode("\r\n", $this->headerText);
}
curl_close( $curlHandle );
$options['request_fulluri'] = true;
}
+ if ( !$this->followRedirects ) {
+ $options['max_redirects'] = 0;
+ } else {
+ $options['max_redirects'] = $this->maxRedirects;
+ }
+
$options['method'] = $this->method;
$options['timeout'] = $this->timeout;
$options['header'] = implode("\r\n", $this->getHeaderList());
$this->status->fatal( 'http-timed-out', $this->url );
return $this->status;
}
-
- $this->headers = $result['wrapper_data'];
+ $this->headerList = $result['wrapper_data'];
while ( !feof( $fh ) ) {
$buf = fread( $fh, 8192 );
<?php
-if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
- require_once( 'bootstrap.php' );
+class MockCookie extends Cookie {
+ public function canServeDomain($arg) { return parent::canServeDomain($arg); }
+ public function canServePath($arg) { return parent::canServePath($arg); }
+ public function isUnExpired() { return parent::isUnExpired(); }
}
class HttpTest extends PhpUnit_Framework_TestCase {
if($proxy) {
$opt['proxy'] = $proxy;
+ } elseif( $proxy === false ) {
+ $opt['noProxy'] = true;
}
/* no postData here because the only request I could find in code so far didn't have any */
if($proxy) {
$opt['proxy'] = $proxy;
+ } elseif( $proxy === false ) {
+ $opt['noProxy'] = true;
}
foreach ( $this->test_geturl as $u ) {
if($proxy) {
$opt['proxy'] = $proxy;
+ } elseif( $proxy === false ) {
+ $opt['noProxy'] = true;
}
foreach ( $this->test_posturl as $u => $postData ) {
self::runHTTPGets(self::$proxy);
self::runHTTPPosts(self::$proxy);
self::runHTTPRequests(self::$proxy);
+
+ // Set false here to do noProxy
+ self::runHTTPGets(false);
+ self::runHTTPPosts(false);
+ self::runHTTPRequests(false);
}
function testProxyDefault() {
function testIsValidUrl() {
}
+ function testSetCookie() {
+ $c = new MockCookie( "name", "value",
+ array(
+ "domain" => ".example.com",
+ "path" => "/path/",
+ ) );
+
+ $this->assertFalse($c->canServeDomain("example.com"));
+ $this->assertFalse($c->canServeDomain("www.example.net"));
+ $this->assertTrue($c->canServeDomain("www.example.com"));
+
+ $this->assertFalse($c->canServePath("/"));
+ $this->assertFalse($c->canServePath("/bogus/path/"));
+ $this->assertFalse($c->canServePath("/path"));
+ $this->assertTrue($c->canServePath("/path/"));
+
+ $this->assertTrue($c->isUnExpired());
+
+ $this->assertEquals("", $c->serializeToHttpRequest("/path/", "www.example.net"));
+ $this->assertEquals("", $c->serializeToHttpRequest("/", "www.example.com"));
+ $this->assertEquals("name=value", $c->serializeToHttpRequest("/path/", "www.example.com"));
+
+ $c = new MockCookie( "name", "value",
+ array(
+ "domain" => ".example.com",
+ "path" => "/path/",
+ "expires" => "January 1, 1990",
+ ) );
+ $this->assertFalse($c->isUnExpired());
+ $this->assertEquals("", $c->serializeToHttpRequest("/path/", "www.example.com"));
+
+ $c = new MockCookie( "name", "value",
+ array(
+ "domain" => ".example.com",
+ "path" => "/path/",
+ "expires" => "January 1, 2999",
+ ) );
+ $this->assertTrue($c->isUnExpired());
+ $this->assertEquals("name=value", $c->serializeToHttpRequest("/path/", "www.example.com"));
+
+
+ }
+
+ function testCookieJarSetCookie() {
+ $cj = new CookieJar;
+ $cj->setCookie( "name", "value",
+ array(
+ "domain" => ".example.com",
+ "path" => "/path/",
+ ) );
+ $cj->setCookie( "name2", "value",
+ array(
+ "domain" => ".example.com",
+ "path" => "/path/sub",
+ ) );
+ $cj->setCookie( "name3", "value",
+ array(
+ "domain" => ".example.com",
+ "path" => "/",
+ ) );
+ $cj->setCookie( "name4", "value",
+ array(
+ "domain" => ".example.net",
+ "path" => "/path/",
+ ) );
+ $cj->setCookie( "name5", "value",
+ array(
+ "domain" => ".example.net",
+ "path" => "/path/",
+ "expires" => "January 1, 1999",
+ ) );
+
+ $this->assertEquals("name4=value", $cj->serializeToHttpRequest("/path/", "www.example.net"));
+ $this->assertEquals("name3=value", $cj->serializeToHttpRequest("/", "www.example.com"));
+ $this->assertEquals("name=value; name3=value", $cj->serializeToHttpRequest("/path/", "www.example.com"));
+
+ $cj->setCookie( "name5", "value",
+ array(
+ "domain" => ".example.net",
+ "path" => "/path/",
+ "expires" => "January 1, 2999",
+ ) );
+ $this->assertEquals("name4=value; name5=value", $cj->serializeToHttpRequest("/path/", "www.example.net"));
+
+ $cj->setCookie( "name4", "value",
+ array(
+ "domain" => ".example.net",
+ "path" => "/path/",
+ "expires" => "January 1, 1999",
+ ) );
+ $this->assertEquals("name5=value", $cj->serializeToHttpRequest("/path/", "www.example.net"));
+ }
+
+ function testParseResponseHeader() {
+ $cj = new CookieJar;
+
+ $h[] = "Set-Cookie: name4=value; domain=.example.com; path=/; expires=Mon, 09-Dec-2999 13:46:00 GMT";
+ $cj->parseCookieResponseHeader( $h[0] );
+ $this->assertEquals("name4=value", $cj->serializeToHttpRequest("/", "www.example.com"));
+
+ $h[] = "name4=value2; domain=.example.com; path=/path/; expires=Mon, 09-Dec-2999 13:46:00 GMT";
+ $cj->parseCookieResponseHeader( $h[1] );
+ $this->assertEquals("", $cj->serializeToHttpRequest("/", "www.example.com"));
+ $this->assertEquals("name4=value2", $cj->serializeToHttpRequest("/path/", "www.example.com"));
+
+ $h[] = "name5=value3; domain=.example.com; path=/path/; expires=Mon, 09-Dec-2999 13:46:00 GMT";
+ $cj->parseCookieResponseHeader( $h[2] );
+ $this->assertEquals("name4=value2; name5=value3", $cj->serializeToHttpRequest("/path/", "www.example.com"));
+
+ $h[] = "name6=value3; domain=.example.net; path=/path/; expires=Mon, 09-Dec-1999 13:46:00 GMT";
+ $cj->parseCookieResponseHeader( $h[3] );
+ $this->assertEquals("", $cj->serializeToHttpRequest("/path/", "www.example.net"));
+
+ $h[] = "name6=value4; domain=.example.net; path=/path/; expires=Mon, 09-Dec-2999 13:46:00 GMT";
+ $cj->parseCookieResponseHeader( $h[4] );
+ $this->assertEquals("name6=value4", $cj->serializeToHttpRequest("/path/", "www.example.net"));
+ }
+
+ function runCookieRequests() {
+ $r = HttpRequest::factory( "http://www.php.net/manual" );
+ $r->execute();
+
+ $jar = $r->getCookieJar();
+
+ $this->assertThat( $jar, $this->isInstanceOf( 'CookieJar' ) );
+ $this->assertRegExp( '/^COUNTRY=.*; LAST_LANG=.*$/', $jar->serializeToHttpRequest( "/search?q=test", "www.php.net" ) );
+ $this->assertEquals( '', $jar->serializeToHttpRequest( "/search?q=test", "www.php.com" ) );
+ }
+
+ function testCookieRequestDefault() {
+ Http::$httpEngine = false;
+ self::runCookieRequests();
+ }
+ function testCookieRequestPhp() {
+ Http::$httpEngine = 'php';
+ self::runCookieRequests();
+ }
+ function testCookieRequestCurl() {
+ if (!self::$has_curl ) {
+ $this->markTestIncomplete("This test requires curl.");
+ }
+
+ Http::$httpEngine = 'curl';
+ self::runCookieRequests();
+ }
+
}
\ No newline at end of file