From: Gergő Tisza Date: Wed, 26 Oct 2016 04:08:14 +0000 (-0700) Subject: Clean up http classes a bit X-Git-Tag: 1.31.0-rc.0~4707^2 X-Git-Url: http://git.cyclocoop.org///%22%40url%40//%22?a=commitdiff_plain;h=1531659d252cd85c6f625a79c7e1f6e5f847fe3f;p=lhc%2Fweb%2Fwiklou.git Clean up http classes a bit * added integration tests. We probably don't want automated tests to make external requests but these make manual testing more convenient. Documented some oddities discovered by testing. * made ::$status, ::proxySetup() and ::getHeaderList() protected; they were not referenced in any gerrit-hosted extension and they provide no useful functionality to external callers. Similarly, marked ::read() and ::errorHandler() as internal (these are used as callbacks so can't be protected) * removed inheritance abuse in ::execute() * documented ::execute() as returning a StatusValue (but keep returning a Status for now) * changed setCookie argument defaults to ones that make sense * replaced MWException * moved unit tests to the correct location * fixed some code style issues Change-Id: I5852fc75badc5d475ae30ec2c9376bde7024bd95 --- diff --git a/RELEASE-NOTES-1.29 b/RELEASE-NOTES-1.29 index ab52544fc6..21a94c5d4a 100644 --- a/RELEASE-NOTES-1.29 +++ b/RELEASE-NOTES-1.29 @@ -56,6 +56,8 @@ changes to languages because of Phabricator reports. SearchEngineFactory::getSearchEngineClass() instead. * $wgSessionsInMemcached (deprecated in 1.20) was removed. No replacement is required as all sessions are stored in Object Cache now. +* MWHttpRequest::execute() should be considered to return a StatusValue; the + Status return type is deprecated. == Compatibility == diff --git a/includes/http/CurlHttpRequest.php b/includes/http/CurlHttpRequest.php index f58c3a9a5b..7fd3e835c4 100644 --- a/includes/http/CurlHttpRequest.php +++ b/includes/http/CurlHttpRequest.php @@ -38,11 +38,10 @@ class CurlHttpRequest extends MWHttpRequest { } public function execute() { - - parent::execute(); + $this->prepare(); if ( !$this->status->isOK() ) { - return $this->status; + return Status::wrap( $this->status ); // TODO B/C; move this to callers } $this->curlOptions[CURLOPT_PROXY] = $this->proxy; @@ -102,7 +101,7 @@ class CurlHttpRequest extends MWHttpRequest { $curlHandle = curl_init( $this->url ); if ( !curl_setopt_array( $curlHandle, $this->curlOptions ) ) { - throw new MWException( "Error setting curl options." ); + throw new InvalidArgumentException( "Error setting curl options." ); } if ( $this->followRedirects && $this->canFollowRedirects() ) { @@ -140,7 +139,7 @@ class CurlHttpRequest extends MWHttpRequest { $this->parseHeader(); $this->setStatus(); - return $this->status; + return Status::wrap( $this->status ); // TODO B/C; move this to callers } /** diff --git a/includes/http/Http.php b/includes/http/Http.php index 43ae2d0e8f..a68a63b7f0 100644 --- a/includes/http/Http.php +++ b/includes/http/Http.php @@ -74,7 +74,7 @@ class Http { } else { $errors = $status->getErrorsByType( 'error' ); $logger = LoggerFactory::getInstance( 'http' ); - $logger->warning( $status->getWikiText( false, false, 'en' ), + $logger->warning( Status::wrap( $status )->getWikiText( false, false, 'en' ), [ 'error' => $errors, 'caller' => $caller, 'content' => $req->getContent() ] ); return false; } diff --git a/includes/http/MWHttpRequest.php b/includes/http/MWHttpRequest.php index 08883ae44f..a42b6d0991 100644 --- a/includes/http/MWHttpRequest.php +++ b/includes/http/MWHttpRequest.php @@ -46,9 +46,11 @@ class MWHttpRequest implements LoggerAwareInterface { protected $reqHeaders = []; protected $url; protected $parsedUrl; + /** @var callable */ protected $callback; protected $maxRedirects = 5; protected $followRedirects = false; + protected $connectTimeout; /** * @var CookieJar @@ -60,7 +62,8 @@ class MWHttpRequest implements LoggerAwareInterface { protected $respStatus = "200 Ok"; protected $respHeaders = []; - public $status; + /** @var StatusValue */ + protected $status; /** * @var Profiler @@ -98,9 +101,9 @@ class MWHttpRequest implements LoggerAwareInterface { } if ( !$this->parsedUrl || !Http::isValidURI( $this->url ) ) { - $this->status = Status::newFatal( 'http-invalid-url', $url ); + $this->status = StatusValue::newFatal( 'http-invalid-url', $url ); } else { - $this->status = Status::newGood( 100 ); // continue + $this->status = StatusValue::newGood( 100 ); // continue } if ( isset( $options['timeout'] ) && $options['timeout'] != 'default' ) { @@ -161,7 +164,7 @@ class MWHttpRequest implements LoggerAwareInterface { * @param string $url Url to use * @param array $options (optional) extra params to pass (see Http::request()) * @param string $caller The method making this request, for profiling - * @throws MWException + * @throws DomainException * @return CurlHttpRequest|PhpHttpRequest * @see MWHttpRequest::__construct */ @@ -169,7 +172,7 @@ class MWHttpRequest implements LoggerAwareInterface { 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' . + throw new DomainException( __METHOD__ . ': curl (http://php.net/curl) is not installed, but' . ' Http::$httpEngine is set to "curl"' ); } @@ -186,7 +189,7 @@ class MWHttpRequest implements LoggerAwareInterface { return new CurlHttpRequest( $url, $options, $caller, Profiler::instance() ); case 'php': if ( !wfIniGetBool( 'allow_url_fopen' ) ) { - throw new MWException( __METHOD__ . ': allow_url_fopen ' . + throw new DomainException( __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.' @@ -194,7 +197,7 @@ class MWHttpRequest implements LoggerAwareInterface { } return new PhpHttpRequest( $url, $options, $caller, Profiler::instance() ); default: - throw new MWException( __METHOD__ . ': The setting of Http::$httpEngine is not valid.' ); + throw new DomainException( __METHOD__ . ': The setting of Http::$httpEngine is not valid.' ); } } @@ -222,7 +225,7 @@ class MWHttpRequest implements LoggerAwareInterface { * * @return void */ - public function proxySetup() { + protected function proxySetup() { // If there is an explicit proxy set and proxies are not disabled, then use it if ( $this->proxy && !$this->noProxy ) { return; @@ -300,7 +303,7 @@ class MWHttpRequest implements LoggerAwareInterface { * Get an array of the headers * @return array */ - public function getHeaderList() { + protected function getHeaderList() { $list = []; if ( $this->cookieJar ) { @@ -333,12 +336,14 @@ class MWHttpRequest implements LoggerAwareInterface { * bytes are reported handled than were passed to you, the HTTP fetch * will be aborted. * - * @param callable $callback - * @throws MWException + * @param callable|null $callback + * @throws InvalidArgumentException */ public function setCallback( $callback ) { - if ( !is_callable( $callback ) ) { - throw new MWException( 'Invalid MwHttpRequest callback' ); + if ( is_null( $callback ) ) { + $callback = [ $this, 'read' ]; + } elseif ( !is_callable( $callback ) ) { + throw new InvalidArgumentException( __METHOD__ . ': invalid callback' ); } $this->callback = $callback; } @@ -350,6 +355,7 @@ class MWHttpRequest implements LoggerAwareInterface { * @param resource $fh * @param string $content * @return int + * @internal */ public function read( $fh, $content ) { $this->content .= $content; @@ -359,9 +365,14 @@ class MWHttpRequest implements LoggerAwareInterface { /** * Take care of whatever is necessary to perform the URI request. * - * @return Status + * @return StatusValue + * @note currently returns Status for B/C */ public function execute() { + throw new LogicException( 'children must override this' ); + } + + protected function prepare() { $this->content = ""; if ( strtoupper( $this->method ) == "HEAD" ) { @@ -371,7 +382,7 @@ class MWHttpRequest implements LoggerAwareInterface { $this->proxySetup(); // set up any proxy as needed if ( !$this->callback ) { - $this->setCallback( [ $this, 'read' ] ); + $this->setCallback( null ); } if ( !isset( $this->reqHeaders['User-Agent'] ) ) { @@ -494,6 +505,8 @@ class MWHttpRequest implements LoggerAwareInterface { /** * Tells the MWHttpRequest object to use this pre-loaded CookieJar. * + * To read response cookies from the jar, getCookieJar must be called first. + * * @param CookieJar $jar */ public function setCookieJar( $jar ) { @@ -519,14 +532,18 @@ class MWHttpRequest implements LoggerAwareInterface { * Set-Cookie headers. * @see Cookie::set * @param string $name - * @param mixed $value + * @param string $value * @param array $attr */ - public function setCookie( $name, $value = null, $attr = null ) { + public function setCookie( $name, $value, $attr = [] ) { if ( !$this->cookieJar ) { $this->cookieJar = new CookieJar; } + if ( $this->parsedUrl && !isset( $attr['domain'] ) ) { + $attr['domain'] = $this->parsedUrl['host']; + } + $this->cookieJar->setCookie( $name, $value, $attr ); } diff --git a/includes/http/PhpHttpRequest.php b/includes/http/PhpHttpRequest.php index 2af000fac0..d8a9949d2f 100644 --- a/includes/http/PhpHttpRequest.php +++ b/includes/http/PhpHttpRequest.php @@ -87,6 +87,7 @@ class PhpHttpRequest extends MWHttpRequest { * is completely useless (something like "fopen: failed to open stream") * so normal methods of handling errors programmatically * like get_last_error() don't work. + * @internal */ public function errorHandler( $errno, $errstr ) { $n = count( $this->fopenErrors ) + 1; @@ -94,8 +95,7 @@ class PhpHttpRequest extends MWHttpRequest { } public function execute() { - - parent::execute(); + $this->prepare(); if ( is_array( $this->postData ) ) { $this->postData = wfArrayToCgi( $this->postData ); @@ -227,12 +227,12 @@ class PhpHttpRequest extends MWHttpRequest { . ': error opening connection: {errstr1}', $this->fopenErrors ); } $this->status->fatal( 'http-request-error' ); - return $this->status; + return Status::wrap( $this->status ); // TODO B/C; move this to callers } if ( $result['timed_out'] ) { $this->status->fatal( 'http-timed-out', $this->url ); - return $this->status; + return Status::wrap( $this->status ); // TODO B/C; move this to callers } // If everything went OK, or we received some error code @@ -253,6 +253,6 @@ class PhpHttpRequest extends MWHttpRequest { } fclose( $fh ); - return $this->status; + return Status::wrap( $this->status ); // TODO B/C; move this to callers } } diff --git a/includes/libs/CookieJar.php b/includes/libs/CookieJar.php index 910a7ca82d..8f5700abb9 100644 --- a/includes/libs/CookieJar.php +++ b/includes/libs/CookieJar.php @@ -19,7 +19,11 @@ * @ingroup HTTP */ +/** + * Cookie jar to use with MWHttpRequest. Does not handle cookie unsetting. + */ class CookieJar { + /** @var Cookie[] */ private $cookie = []; /** diff --git a/tests/common/TestsAutoLoader.php b/tests/common/TestsAutoLoader.php index 66df315e10..b67c9abfee 100644 --- a/tests/common/TestsAutoLoader.php +++ b/tests/common/TestsAutoLoader.php @@ -29,11 +29,13 @@ $wgAutoloadClasses += [ # tests/common 'TestSetup' => "$testDir/common/TestSetup.php", + # tests/integration + 'MWHttpRequestTestCase' => "$testDir/integration/includes/http/MWHttpRequestTestCase.php", + # tests/parser 'DbTestPreviewer' => "$testDir/parser/DbTestPreviewer.php", 'DbTestRecorder' => "$testDir/parser/DbTestRecorder.php", 'DjVuSupport' => "$testDir/parser/DjVuSupport.php", - 'TestRecorder' => "$testDir/parser/TestRecorder.php", 'MultiTestRecorder' => "$testDir/parser/MultiTestRecorder.php", 'ParserTestMockParser' => "$testDir/parser/ParserTestMockParser.php", 'ParserTestRunner' => "$testDir/parser/ParserTestRunner.php", diff --git a/tests/integration/includes/http/CurlHttpRequestTest.php b/tests/integration/includes/http/CurlHttpRequestTest.php new file mode 100644 index 0000000000..04f80f434f --- /dev/null +++ b/tests/integration/includes/http/CurlHttpRequestTest.php @@ -0,0 +1,5 @@ +oldHttpEngine = Http::$httpEngine; + Http::$httpEngine = static::$httpEngine; + + try { + $request = MWHttpRequest::factory( 'null:' ); + } catch ( DomainException $e ) { + $this->markTestSkipped( static::$httpEngine . ' engine not supported' ); + } + + if ( static::$httpEngine === 'php' ) { + $this->assertInstanceOf( PhpHttpRequest::class, $request ); + } else { + $this->assertInstanceOf( CurlHttpRequest::class, $request ); + } + } + + public function tearDown() { + parent::tearDown(); + Http::$httpEngine = $this->oldHttpEngine; + } + + // -------------------- + + public function testIsRedirect() { + $request = MWHttpRequest::factory( 'http://httpbin.org/get' ); + $status = $request->execute(); + $this->assertTrue( $status->isGood() ); + $this->assertFalse( $request->isRedirect() ); + + $request = MWHttpRequest::factory( 'http://httpbin.org/redirect/1' ); + $status = $request->execute(); + $this->assertTrue( $status->isGood() ); + $this->assertTrue( $request->isRedirect() ); + } + + public function testgetFinalUrl() { + $request = MWHttpRequest::factory( 'http://httpbin.org/redirect/3' ); + if ( !$request->canFollowRedirects() ) { + $this->markTestSkipped( 'cannot follow redirects' ); + } + $status = $request->execute(); + $this->assertTrue( $status->isGood() ); + $this->assertNotSame( 'http://httpbin.org/get', $request->getFinalUrl() ); + + $request = MWHttpRequest::factory( 'http://httpbin.org/redirect/3', [ 'followRedirects' + => true ] ); + $status = $request->execute(); + $this->assertTrue( $status->isGood() ); + $this->assertSame( 'http://httpbin.org/get', $request->getFinalUrl() ); + $this->assertResponseFieldValue( 'url', 'http://httpbin.org/get', $request ); + + $request = MWHttpRequest::factory( 'http://httpbin.org/redirect/3', [ 'followRedirects' + => true ] ); + $status = $request->execute(); + $this->assertTrue( $status->isGood() ); + $this->assertSame( 'http://httpbin.org/get', $request->getFinalUrl() ); + $this->assertResponseFieldValue( 'url', 'http://httpbin.org/get', $request ); + + if ( static::$httpEngine === 'curl' ) { + $this->markTestIncomplete( 'maxRedirects seems to be ignored by CurlHttpRequest' ); + return; + } + + $request = MWHttpRequest::factory( 'http://httpbin.org/redirect/3', [ 'followRedirects' + => true, 'maxRedirects' => 1 ] ); + $status = $request->execute(); + $this->assertTrue( $status->isGood() ); + $this->assertNotSame( 'http://httpbin.org/get', $request->getFinalUrl() ); + } + + public function testSetCookie() { + $request = MWHttpRequest::factory( 'http://httpbin.org/cookies' ); + $request->setCookie( 'foo', 'bar' ); + $request->setCookie( 'foo2', 'bar2', [ 'domain' => 'example.com' ] ); + $status = $request->execute(); + $this->assertTrue( $status->isGood() ); + $this->assertResponseFieldValue( 'cookies', [ 'foo' => 'bar' ], $request ); + } + + public function testSetCookieJar() { + $request = MWHttpRequest::factory( 'http://httpbin.org/cookies' ); + $cookieJar = new CookieJar(); + $cookieJar->setCookie( 'foo', 'bar', [ 'domain' => 'httpbin.org' ] ); + $cookieJar->setCookie( 'foo2', 'bar2', [ 'domain' => 'example.com' ] ); + $request->setCookieJar( $cookieJar ); + $status = $request->execute(); + $this->assertTrue( $status->isGood() ); + $this->assertResponseFieldValue( 'cookies', [ 'foo' => 'bar' ], $request ); + + $request = MWHttpRequest::factory( 'http://httpbin.org/cookies/set?foo=bar' ); + $cookieJar = new CookieJar(); + $request->setCookieJar( $cookieJar ); + $status = $request->execute(); + $this->assertTrue( $status->isGood() ); + $this->assertHasCookie( 'foo', 'bar', $request->getCookieJar() ); + + $this->markTestIncomplete( 'CookieJar does not handle deletion' ); + return; + + $request = MWHttpRequest::factory( 'http://httpbin.org/cookies/delete?foo' ); + $cookieJar = new CookieJar(); + $cookieJar->setCookie( 'foo', 'bar', [ 'domain' => 'httpbin.org' ] ); + $cookieJar->setCookie( 'foo2', 'bar2', [ 'domain' => 'httpbin.org' ] ); + $request->setCookieJar( $cookieJar ); + $status = $request->execute(); + $this->assertTrue( $status->isGood() ); + $this->assertNotHasCookie( 'foo', $request->getCookieJar() ); + $this->assertHasCookie( 'foo2', 'bar2', $request->getCookieJar() ); + } + + public function testGetResponseHeaders() { + $request = MWHttpRequest::factory( 'http://httpbin.org/response-headers?Foo=bar' ); + $status = $request->execute(); + $this->assertTrue( $status->isGood() ); + $headers = array_change_key_case( $request->getResponseHeaders(), CASE_LOWER ); + $this->assertArrayHasKey( 'foo', $headers ); + $this->assertSame( $request->getResponseHeader( 'Foo' ), 'bar' ); + } + + public function testSetHeader() { + $request = MWHttpRequest::factory( 'http://httpbin.org/headers' ); + $request->setHeader( 'Foo', 'bar' ); + $status = $request->execute(); + $this->assertTrue( $status->isGood() ); + $this->assertResponseFieldValue( [ 'headers', 'Foo' ], 'bar', $request ); + } + + public function testGetStatus() { + $request = MWHttpRequest::factory( 'http://httpbin.org/status/418' ); + $status = $request->execute(); + $this->assertFalse( $status->isOK() ); + $this->assertSame( $request->getStatus(), 418 ); + } + + public function testSetUserAgent() { + $request = MWHttpRequest::factory( 'http://httpbin.org/user-agent' ); + $request->setUserAgent( 'foo' ); + $status = $request->execute(); + $this->assertTrue( $status->isGood() ); + $this->assertResponseFieldValue( 'user-agent', 'foo', $request ); + } + + public function testSetData() { + $request = MWHttpRequest::factory( 'http://httpbin.org/post', [ 'method' => 'POST' ] ); + $request->setData( [ 'foo' => 'bar', 'foo2' => 'bar2' ] ); + $status = $request->execute(); + $this->assertTrue( $status->isGood() ); + $this->assertResponseFieldValue( 'form', [ 'foo' => 'bar', 'foo2' => 'bar2' ], $request ); + } + + public function testSetCallback() { + if ( static::$httpEngine === 'php' ) { + $this->markTestIncomplete( 'PhpHttpRequest does not use setCallback()' ); + return; + } + + $request = MWHttpRequest::factory( 'http://httpbin.org/ip' ); + $data = ''; + $request->setCallback( function ( $fh, $content ) use ( &$data ) { + $data .= $content; + return strlen( $content ); + } ); + $status = $request->execute(); + $this->assertTrue( $status->isGood() ); + $data = json_decode( $data, true ); + $this->assertInternalType( 'array', $data ); + $this->assertArrayHasKey( 'origin', $data ); + } + + // -------------------- + + protected function assertResponseFieldValue( $key, $expectedValue, MWHttpRequest $response ) { + $this->assertSame( 200, $response->getStatus(), 'response status is not 200' ); + $data = json_decode( $response->getContent(), true ); + $this->assertInternalType( 'array', $data, 'response is not JSON' ); + $keyPath = ''; + foreach ( (array)$key as $keySegment ) { + $keyPath .= ( $keyPath ? '.' : '' ) . $keySegment; + $this->assertArrayHasKey( $keySegment, $data, $keyPath . ' not found' ); + $data = $data[$keySegment]; + } + $this->assertSame( $expectedValue, $data ); + } + + protected function assertHasCookie( $expectedName, $expectedValue, CookieJar $cookieJar ) { + $cookieJar = TestingAccessWrapper::newFromObject( $cookieJar ); + $cookies = array_change_key_case( $cookieJar->cookie, CASE_LOWER ); + $this->assertArrayHasKey( strtolower( $expectedName ), $cookies ); + $cookie = TestingAccessWrapper::newFromObject( + $cookies[strtolower( $expectedName )] ); + $this->assertSame( $expectedValue, $cookie->value ); + } + + protected function assertNotHasCookie( $name, CookieJar $cookieJar ) { + $cookieJar = TestingAccessWrapper::newFromObject( $cookieJar ); + $this->assertArrayNotHasKey( strtolower( $name ), + array_change_key_case( $cookieJar->cookie, CASE_LOWER ) ); + } +} + diff --git a/tests/integration/includes/http/PhpHttpRequestTest.php b/tests/integration/includes/http/PhpHttpRequestTest.php new file mode 100644 index 0000000000..d0222a5e76 --- /dev/null +++ b/tests/integration/includes/http/PhpHttpRequestTest.php @@ -0,0 +1,5 @@ +assertEquals( $expected, $ok, $msg ); - } - - public static function cookieDomains() { - return [ - [ false, "org" ], - [ false, ".org" ], - [ true, "wikipedia.org" ], - [ true, ".wikipedia.org" ], - [ false, "co.uk" ], - [ false, ".co.uk" ], - [ false, "gov.uk" ], - [ false, ".gov.uk" ], - [ true, "supermarket.uk" ], - [ false, "uk" ], - [ false, ".uk" ], - [ false, "127.0.0." ], - [ false, "127." ], - [ false, "127.0.0.1." ], - [ true, "127.0.0.1" ], - [ false, "333.0.0.1" ], - [ true, "example.com" ], - [ false, "example.com." ], - [ true, ".example.com" ], - - [ true, ".example.com", "www.example.com" ], - [ false, "example.com", "www.example.com" ], - [ true, "127.0.0.1", "127.0.0.1" ], - [ false, "127.0.0.1", "localhost" ], - ]; - } - - /** - * Test Http::isValidURI() - * @bug 27854 : Http::isValidURI is too lax - * @dataProvider provideURI - * @covers Http::isValidURI - */ - public function testIsValidUri( $expect, $URI, $message = '' ) { - $this->assertEquals( - $expect, - (bool)Http::isValidURI( $URI ), - $message - ); - } - - /** - * @covers Http::getProxy - */ - public function testGetProxy() { - $this->setMwGlobals( 'wgHTTPProxy', 'proxy.domain.tld' ); - $this->assertEquals( - 'proxy.domain.tld', - Http::getProxy() - ); - } - - /** - * Feeds URI to test a long regular expression in Http::isValidURI - */ - public static function provideURI() { - /** Format: 'boolean expectation', 'URI to test', 'Optional message' */ - return [ - [ false, '¿non sens before!! http://a', 'Allow anything before URI' ], - - # (http|https) - only two schemes allowed - [ true, 'http://www.example.org/' ], - [ true, 'https://www.example.org/' ], - [ true, 'http://www.example.org', 'URI without directory' ], - [ true, 'http://a', 'Short name' ], - [ true, 'http://étoile', 'Allow UTF-8 in hostname' ], # 'étoile' is french for 'star' - [ false, '\\host\directory', 'CIFS share' ], - [ false, 'gopher://host/dir', 'Reject gopher scheme' ], - [ false, 'telnet://host', 'Reject telnet scheme' ], - - # :\/\/ - double slashes - [ false, 'http//example.org', 'Reject missing colon in protocol' ], - [ false, 'http:/example.org', 'Reject missing slash in protocol' ], - [ false, 'http:example.org', 'Must have two slashes' ], - # Following fail since hostname can be made of anything - [ false, 'http:///example.org', 'Must have exactly two slashes, not three' ], - - # (\w+:{0,1}\w*@)? - optional user:pass - [ true, 'http://user@host', 'Username provided' ], - [ true, 'http://user:@host', 'Username provided, no password' ], - [ true, 'http://user:pass@host', 'Username and password provided' ], - - # (\S+) - host part is made of anything not whitespaces - // commented these out in order to remove @group Broken - // @todo are these valid tests? if so, fix Http::isValidURI so it can handle them - // [ false, 'http://!"èèè¿¿¿~~\'', 'hostname is made of any non whitespace' ], - // [ false, 'http://exam:ple.org/', 'hostname can not use colons!' ], - - # (:[0-9]+)? - port number - [ true, 'http://example.org:80/' ], - [ true, 'https://example.org:80/' ], - [ true, 'http://example.org:443/' ], - [ true, 'https://example.org:443/' ], - - # Part after the hostname is / or / with something else - [ true, 'http://example/#' ], - [ true, 'http://example/!' ], - [ true, 'http://example/:' ], - [ true, 'http://example/.' ], - [ true, 'http://example/?' ], - [ true, 'http://example/+' ], - [ true, 'http://example/=' ], - [ true, 'http://example/&' ], - [ true, 'http://example/%' ], - [ true, 'http://example/@' ], - [ true, 'http://example/-' ], - [ true, 'http://example//' ], - [ true, 'http://example/&' ], - - # Fragment - [ true, 'http://exam#ple.org', ], # This one is valid, really! - [ true, 'http://example.org:80#anchor' ], - [ true, 'http://example.org/?id#anchor' ], - [ true, 'http://example.org/?#anchor' ], - - [ false, 'http://a ¿non !!sens after', 'Allow anything after URI' ], - ]; - } - - /** - * Warning: - * - * These tests are for code that makes use of an artifact of how CURL - * handles header reporting on redirect pages, and will need to be - * rewritten when bug 29232 is taken care of (high-level handling of - * HTTP redirects). - */ - public function testRelativeRedirections() { - $h = MWHttpRequestTester::factory( 'http://oldsite/file.ext', [], __METHOD__ ); - - # Forge a Location header - $h->setRespHeaders( 'location', [ - 'http://newsite/file.ext', - '/newfile.ext', - ] - ); - # Verify we correctly fix the Location - $this->assertEquals( - 'http://newsite/newfile.ext', - $h->getFinalUrl(), - "Relative file path Location: interpreted as full URL" - ); - - $h->setRespHeaders( 'location', [ - 'https://oldsite/file.ext' - ] - ); - $this->assertEquals( - 'https://oldsite/file.ext', - $h->getFinalUrl(), - "Location to the HTTPS version of the site" - ); - - $h->setRespHeaders( 'location', [ - '/anotherfile.ext', - 'http://anotherfile/hoster.ext', - 'https://anotherfile/hoster.ext' - ] - ); - $this->assertEquals( - 'https://anotherfile/hoster.ext', - $h->getFinalUrl( "Relative file path Location: should keep the latest host and scheme!" ) - ); - } - - /** - * Constant values are from PHP 5.3.28 using cURL 7.24.0 - * @see https://secure.php.net/manual/en/curl.constants.php - * - * All constant values are present so that developers don’t need to remember - * to add them if added at a later date. The commented out constants were - * not found anywhere in the MediaWiki core code. - * - * Commented out constants that were not available in: - * HipHop VM 3.3.0 (rel) - * Compiler: heads/master-0-g08810d920dfff59e0774cf2d651f92f13a637175 - * Repo schema: 3214fc2c684a4520485f715ee45f33f2182324b1 - * Extension API: 20140829 - * - * Commented out constants that were removed in PHP 5.6.0 - * - * @covers CurlHttpRequest::execute - */ - public function provideCurlConstants() { - return [ - [ 'CURLAUTH_ANY' ], - [ 'CURLAUTH_ANYSAFE' ], - [ 'CURLAUTH_BASIC' ], - [ 'CURLAUTH_DIGEST' ], - [ 'CURLAUTH_GSSNEGOTIATE' ], - [ 'CURLAUTH_NTLM' ], - // [ 'CURLCLOSEPOLICY_CALLBACK' ], // removed in PHP 5.6.0 - // [ 'CURLCLOSEPOLICY_LEAST_RECENTLY_USED' ], // removed in PHP 5.6.0 - // [ 'CURLCLOSEPOLICY_LEAST_TRAFFIC' ], // removed in PHP 5.6.0 - // [ 'CURLCLOSEPOLICY_OLDEST' ], // removed in PHP 5.6.0 - // [ 'CURLCLOSEPOLICY_SLOWEST' ], // removed in PHP 5.6.0 - [ 'CURLE_ABORTED_BY_CALLBACK' ], - [ 'CURLE_BAD_CALLING_ORDER' ], - [ 'CURLE_BAD_CONTENT_ENCODING' ], - [ 'CURLE_BAD_FUNCTION_ARGUMENT' ], - [ 'CURLE_BAD_PASSWORD_ENTERED' ], - [ 'CURLE_COULDNT_CONNECT' ], - [ 'CURLE_COULDNT_RESOLVE_HOST' ], - [ 'CURLE_COULDNT_RESOLVE_PROXY' ], - [ 'CURLE_FAILED_INIT' ], - [ 'CURLE_FILESIZE_EXCEEDED' ], - [ 'CURLE_FILE_COULDNT_READ_FILE' ], - [ 'CURLE_FTP_ACCESS_DENIED' ], - [ 'CURLE_FTP_BAD_DOWNLOAD_RESUME' ], - [ 'CURLE_FTP_CANT_GET_HOST' ], - [ 'CURLE_FTP_CANT_RECONNECT' ], - [ 'CURLE_FTP_COULDNT_GET_SIZE' ], - [ 'CURLE_FTP_COULDNT_RETR_FILE' ], - [ 'CURLE_FTP_COULDNT_SET_ASCII' ], - [ 'CURLE_FTP_COULDNT_SET_BINARY' ], - [ 'CURLE_FTP_COULDNT_STOR_FILE' ], - [ 'CURLE_FTP_COULDNT_USE_REST' ], - [ 'CURLE_FTP_PORT_FAILED' ], - [ 'CURLE_FTP_QUOTE_ERROR' ], - [ 'CURLE_FTP_SSL_FAILED' ], - [ 'CURLE_FTP_USER_PASSWORD_INCORRECT' ], - [ 'CURLE_FTP_WEIRD_227_FORMAT' ], - [ 'CURLE_FTP_WEIRD_PASS_REPLY' ], - [ 'CURLE_FTP_WEIRD_PASV_REPLY' ], - [ 'CURLE_FTP_WEIRD_SERVER_REPLY' ], - [ 'CURLE_FTP_WEIRD_USER_REPLY' ], - [ 'CURLE_FTP_WRITE_ERROR' ], - [ 'CURLE_FUNCTION_NOT_FOUND' ], - [ 'CURLE_GOT_NOTHING' ], - [ 'CURLE_HTTP_NOT_FOUND' ], - [ 'CURLE_HTTP_PORT_FAILED' ], - [ 'CURLE_HTTP_POST_ERROR' ], - [ 'CURLE_HTTP_RANGE_ERROR' ], - [ 'CURLE_LDAP_CANNOT_BIND' ], - [ 'CURLE_LDAP_INVALID_URL' ], - [ 'CURLE_LDAP_SEARCH_FAILED' ], - [ 'CURLE_LIBRARY_NOT_FOUND' ], - [ 'CURLE_MALFORMAT_USER' ], - [ 'CURLE_OBSOLETE' ], - [ 'CURLE_OK' ], - [ 'CURLE_OPERATION_TIMEOUTED' ], - [ 'CURLE_OUT_OF_MEMORY' ], - [ 'CURLE_PARTIAL_FILE' ], - [ 'CURLE_READ_ERROR' ], - [ 'CURLE_RECV_ERROR' ], - [ 'CURLE_SEND_ERROR' ], - [ 'CURLE_SHARE_IN_USE' ], - // [ 'CURLE_SSH' ], // not present in HHVM 3.3.0-dev - [ 'CURLE_SSL_CACERT' ], - [ 'CURLE_SSL_CERTPROBLEM' ], - [ 'CURLE_SSL_CIPHER' ], - [ 'CURLE_SSL_CONNECT_ERROR' ], - [ 'CURLE_SSL_ENGINE_NOTFOUND' ], - [ 'CURLE_SSL_ENGINE_SETFAILED' ], - [ 'CURLE_SSL_PEER_CERTIFICATE' ], - [ 'CURLE_TELNET_OPTION_SYNTAX' ], - [ 'CURLE_TOO_MANY_REDIRECTS' ], - [ 'CURLE_UNKNOWN_TELNET_OPTION' ], - [ 'CURLE_UNSUPPORTED_PROTOCOL' ], - [ 'CURLE_URL_MALFORMAT' ], - [ 'CURLE_URL_MALFORMAT_USER' ], - [ 'CURLE_WRITE_ERROR' ], - [ 'CURLFTPAUTH_DEFAULT' ], - [ 'CURLFTPAUTH_SSL' ], - [ 'CURLFTPAUTH_TLS' ], - // [ 'CURLFTPMETHOD_MULTICWD' ], // not present in HHVM 3.3.0-dev - // [ 'CURLFTPMETHOD_NOCWD' ], // not present in HHVM 3.3.0-dev - // [ 'CURLFTPMETHOD_SINGLECWD' ], // not present in HHVM 3.3.0-dev - [ 'CURLFTPSSL_ALL' ], - [ 'CURLFTPSSL_CONTROL' ], - [ 'CURLFTPSSL_NONE' ], - [ 'CURLFTPSSL_TRY' ], - // [ 'CURLINFO_CERTINFO' ], // not present in HHVM 3.3.0-dev - [ 'CURLINFO_CONNECT_TIME' ], - [ 'CURLINFO_CONTENT_LENGTH_DOWNLOAD' ], - [ 'CURLINFO_CONTENT_LENGTH_UPLOAD' ], - [ 'CURLINFO_CONTENT_TYPE' ], - [ 'CURLINFO_EFFECTIVE_URL' ], - [ 'CURLINFO_FILETIME' ], - [ 'CURLINFO_HEADER_OUT' ], - [ 'CURLINFO_HEADER_SIZE' ], - [ 'CURLINFO_HTTP_CODE' ], - [ 'CURLINFO_NAMELOOKUP_TIME' ], - [ 'CURLINFO_PRETRANSFER_TIME' ], - [ 'CURLINFO_PRIVATE' ], - [ 'CURLINFO_REDIRECT_COUNT' ], - [ 'CURLINFO_REDIRECT_TIME' ], - // [ 'CURLINFO_REDIRECT_URL' ], // not present in HHVM 3.3.0-dev - [ 'CURLINFO_REQUEST_SIZE' ], - [ 'CURLINFO_SIZE_DOWNLOAD' ], - [ 'CURLINFO_SIZE_UPLOAD' ], - [ 'CURLINFO_SPEED_DOWNLOAD' ], - [ 'CURLINFO_SPEED_UPLOAD' ], - [ 'CURLINFO_SSL_VERIFYRESULT' ], - [ 'CURLINFO_STARTTRANSFER_TIME' ], - [ 'CURLINFO_TOTAL_TIME' ], - [ 'CURLMSG_DONE' ], - [ 'CURLM_BAD_EASY_HANDLE' ], - [ 'CURLM_BAD_HANDLE' ], - [ 'CURLM_CALL_MULTI_PERFORM' ], - [ 'CURLM_INTERNAL_ERROR' ], - [ 'CURLM_OK' ], - [ 'CURLM_OUT_OF_MEMORY' ], - [ 'CURLOPT_AUTOREFERER' ], - [ 'CURLOPT_BINARYTRANSFER' ], - [ 'CURLOPT_BUFFERSIZE' ], - [ 'CURLOPT_CAINFO' ], - [ 'CURLOPT_CAPATH' ], - // [ 'CURLOPT_CERTINFO' ], // not present in HHVM 3.3.0-dev - // [ 'CURLOPT_CLOSEPOLICY' ], // removed in PHP 5.6.0 - [ 'CURLOPT_CONNECTTIMEOUT' ], - [ 'CURLOPT_CONNECTTIMEOUT_MS' ], - [ 'CURLOPT_COOKIE' ], - [ 'CURLOPT_COOKIEFILE' ], - [ 'CURLOPT_COOKIEJAR' ], - [ 'CURLOPT_COOKIESESSION' ], - [ 'CURLOPT_CRLF' ], - [ 'CURLOPT_CUSTOMREQUEST' ], - [ 'CURLOPT_DNS_CACHE_TIMEOUT' ], - [ 'CURLOPT_DNS_USE_GLOBAL_CACHE' ], - [ 'CURLOPT_EGDSOCKET' ], - [ 'CURLOPT_ENCODING' ], - [ 'CURLOPT_FAILONERROR' ], - [ 'CURLOPT_FILE' ], - [ 'CURLOPT_FILETIME' ], - [ 'CURLOPT_FOLLOWLOCATION' ], - [ 'CURLOPT_FORBID_REUSE' ], - [ 'CURLOPT_FRESH_CONNECT' ], - [ 'CURLOPT_FTPAPPEND' ], - [ 'CURLOPT_FTPLISTONLY' ], - [ 'CURLOPT_FTPPORT' ], - [ 'CURLOPT_FTPSSLAUTH' ], - [ 'CURLOPT_FTP_CREATE_MISSING_DIRS' ], - // [ 'CURLOPT_FTP_FILEMETHOD' ], // not present in HHVM 3.3.0-dev - // [ 'CURLOPT_FTP_SKIP_PASV_IP' ], // not present in HHVM 3.3.0-dev - [ 'CURLOPT_FTP_SSL' ], - [ 'CURLOPT_FTP_USE_EPRT' ], - [ 'CURLOPT_FTP_USE_EPSV' ], - [ 'CURLOPT_HEADER' ], - [ 'CURLOPT_HEADERFUNCTION' ], - [ 'CURLOPT_HTTP200ALIASES' ], - [ 'CURLOPT_HTTPAUTH' ], - [ 'CURLOPT_HTTPGET' ], - [ 'CURLOPT_HTTPHEADER' ], - [ 'CURLOPT_HTTPPROXYTUNNEL' ], - [ 'CURLOPT_HTTP_VERSION' ], - [ 'CURLOPT_INFILE' ], - [ 'CURLOPT_INFILESIZE' ], - [ 'CURLOPT_INTERFACE' ], - [ 'CURLOPT_IPRESOLVE' ], - // [ 'CURLOPT_KEYPASSWD' ], // not present in HHVM 3.3.0-dev - [ 'CURLOPT_KRB4LEVEL' ], - [ 'CURLOPT_LOW_SPEED_LIMIT' ], - [ 'CURLOPT_LOW_SPEED_TIME' ], - [ 'CURLOPT_MAXCONNECTS' ], - [ 'CURLOPT_MAXREDIRS' ], - // [ 'CURLOPT_MAX_RECV_SPEED_LARGE' ], // not present in HHVM 3.3.0-dev - // [ 'CURLOPT_MAX_SEND_SPEED_LARGE' ], // not present in HHVM 3.3.0-dev - [ 'CURLOPT_NETRC' ], - [ 'CURLOPT_NOBODY' ], - [ 'CURLOPT_NOPROGRESS' ], - [ 'CURLOPT_NOSIGNAL' ], - [ 'CURLOPT_PORT' ], - [ 'CURLOPT_POST' ], - [ 'CURLOPT_POSTFIELDS' ], - [ 'CURLOPT_POSTQUOTE' ], - [ 'CURLOPT_POSTREDIR' ], - [ 'CURLOPT_PRIVATE' ], - [ 'CURLOPT_PROGRESSFUNCTION' ], - // [ 'CURLOPT_PROTOCOLS' ], // not present in HHVM 3.3.0-dev - [ 'CURLOPT_PROXY' ], - [ 'CURLOPT_PROXYAUTH' ], - [ 'CURLOPT_PROXYPORT' ], - [ 'CURLOPT_PROXYTYPE' ], - [ 'CURLOPT_PROXYUSERPWD' ], - [ 'CURLOPT_PUT' ], - [ 'CURLOPT_QUOTE' ], - [ 'CURLOPT_RANDOM_FILE' ], - [ 'CURLOPT_RANGE' ], - [ 'CURLOPT_READDATA' ], - [ 'CURLOPT_READFUNCTION' ], - // [ 'CURLOPT_REDIR_PROTOCOLS' ], // not present in HHVM 3.3.0-dev - [ 'CURLOPT_REFERER' ], - [ 'CURLOPT_RESUME_FROM' ], - [ 'CURLOPT_RETURNTRANSFER' ], - // [ 'CURLOPT_SSH_AUTH_TYPES' ], // not present in HHVM 3.3.0-dev - // [ 'CURLOPT_SSH_HOST_PUBLIC_KEY_MD5' ], // not present in HHVM 3.3.0-dev - // [ 'CURLOPT_SSH_PRIVATE_KEYFILE' ], // not present in HHVM 3.3.0-dev - // [ 'CURLOPT_SSH_PUBLIC_KEYFILE' ], // not present in HHVM 3.3.0-dev - [ 'CURLOPT_SSLCERT' ], - [ 'CURLOPT_SSLCERTPASSWD' ], - [ 'CURLOPT_SSLCERTTYPE' ], - [ 'CURLOPT_SSLENGINE' ], - [ 'CURLOPT_SSLENGINE_DEFAULT' ], - [ 'CURLOPT_SSLKEY' ], - [ 'CURLOPT_SSLKEYPASSWD' ], - [ 'CURLOPT_SSLKEYTYPE' ], - [ 'CURLOPT_SSLVERSION' ], - [ 'CURLOPT_SSL_CIPHER_LIST' ], - [ 'CURLOPT_SSL_VERIFYHOST' ], - [ 'CURLOPT_SSL_VERIFYPEER' ], - [ 'CURLOPT_STDERR' ], - [ 'CURLOPT_TCP_NODELAY' ], - [ 'CURLOPT_TIMECONDITION' ], - [ 'CURLOPT_TIMEOUT' ], - [ 'CURLOPT_TIMEOUT_MS' ], - [ 'CURLOPT_TIMEVALUE' ], - [ 'CURLOPT_TRANSFERTEXT' ], - [ 'CURLOPT_UNRESTRICTED_AUTH' ], - [ 'CURLOPT_UPLOAD' ], - [ 'CURLOPT_URL' ], - [ 'CURLOPT_USERAGENT' ], - [ 'CURLOPT_USERPWD' ], - [ 'CURLOPT_VERBOSE' ], - [ 'CURLOPT_WRITEFUNCTION' ], - [ 'CURLOPT_WRITEHEADER' ], - // [ 'CURLPROTO_ALL' ], // not present in HHVM 3.3.0-dev - // [ 'CURLPROTO_DICT' ], // not present in HHVM 3.3.0-dev - // [ 'CURLPROTO_FILE' ], // not present in HHVM 3.3.0-dev - // [ 'CURLPROTO_FTP' ], // not present in HHVM 3.3.0-dev - // [ 'CURLPROTO_FTPS' ], // not present in HHVM 3.3.0-dev - // [ 'CURLPROTO_HTTP' ], // not present in HHVM 3.3.0-dev - // [ 'CURLPROTO_HTTPS' ], // not present in HHVM 3.3.0-dev - // [ 'CURLPROTO_LDAP' ], // not present in HHVM 3.3.0-dev - // [ 'CURLPROTO_LDAPS' ], // not present in HHVM 3.3.0-dev - // [ 'CURLPROTO_SCP' ], // not present in HHVM 3.3.0-dev - // [ 'CURLPROTO_SFTP' ], // not present in HHVM 3.3.0-dev - // [ 'CURLPROTO_TELNET' ], // not present in HHVM 3.3.0-dev - // [ 'CURLPROTO_TFTP' ], // not present in HHVM 3.3.0-dev - [ 'CURLPROXY_HTTP' ], - // [ 'CURLPROXY_SOCKS4' ], // not present in HHVM 3.3.0-dev - [ 'CURLPROXY_SOCKS5' ], - // [ 'CURLSSH_AUTH_DEFAULT' ], // not present in HHVM 3.3.0-dev - // [ 'CURLSSH_AUTH_HOST' ], // not present in HHVM 3.3.0-dev - // [ 'CURLSSH_AUTH_KEYBOARD' ], // not present in HHVM 3.3.0-dev - // [ 'CURLSSH_AUTH_NONE' ], // not present in HHVM 3.3.0-dev - // [ 'CURLSSH_AUTH_PASSWORD' ], // not present in HHVM 3.3.0-dev - // [ 'CURLSSH_AUTH_PUBLICKEY' ], // not present in HHVM 3.3.0-dev - [ 'CURLVERSION_NOW' ], - [ 'CURL_HTTP_VERSION_1_0' ], - [ 'CURL_HTTP_VERSION_1_1' ], - [ 'CURL_HTTP_VERSION_NONE' ], - [ 'CURL_IPRESOLVE_V4' ], - [ 'CURL_IPRESOLVE_V6' ], - [ 'CURL_IPRESOLVE_WHATEVER' ], - [ 'CURL_NETRC_IGNORED' ], - [ 'CURL_NETRC_OPTIONAL' ], - [ 'CURL_NETRC_REQUIRED' ], - [ 'CURL_TIMECOND_IFMODSINCE' ], - [ 'CURL_TIMECOND_IFUNMODSINCE' ], - [ 'CURL_TIMECOND_LASTMOD' ], - [ 'CURL_VERSION_IPV6' ], - [ 'CURL_VERSION_KERBEROS4' ], - [ 'CURL_VERSION_LIBZ' ], - [ 'CURL_VERSION_SSL' ], - ]; - } - - /** - * Added this test based on an issue experienced with HHVM 3.3.0-dev - * where it did not define a cURL constant. - * - * @bug 70570 - * @dataProvider provideCurlConstants - */ - public function testCurlConstants( $value ) { - $this->assertTrue( defined( $value ), $value . ' not defined' ); - } -} - -/** - * Class to let us overwrite MWHttpRequest respHeaders variable - */ -class MWHttpRequestTester extends MWHttpRequest { - // function derived from the MWHttpRequest factory function but - // returns appropriate tester class here - public static function factory( $url, $options = null, $caller = __METHOD__ ) { - 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"' ); - } - - switch ( Http::$httpEngine ) { - case 'curl': - return new CurlHttpRequestTester( $url, $options, $caller ); - 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.' ); - } - - return new PhpHttpRequestTester( $url, $options, $caller ); - default: - } - } -} - -class CurlHttpRequestTester extends CurlHttpRequest { - function setRespHeaders( $name, $value ) { - $this->respHeaders[$name] = $value; - } -} - -class PhpHttpRequestTester extends PhpHttpRequest { - function setRespHeaders( $name, $value ) { - $this->respHeaders[$name] = $value; - } -} diff --git a/tests/phpunit/includes/http/HttpTest.php b/tests/phpunit/includes/http/HttpTest.php new file mode 100644 index 0000000000..7e98d1c069 --- /dev/null +++ b/tests/phpunit/includes/http/HttpTest.php @@ -0,0 +1,534 @@ +assertEquals( $expected, $ok, $msg ); + } + + public static function cookieDomains() { + return [ + [ false, "org" ], + [ false, ".org" ], + [ true, "wikipedia.org" ], + [ true, ".wikipedia.org" ], + [ false, "co.uk" ], + [ false, ".co.uk" ], + [ false, "gov.uk" ], + [ false, ".gov.uk" ], + [ true, "supermarket.uk" ], + [ false, "uk" ], + [ false, ".uk" ], + [ false, "127.0.0." ], + [ false, "127." ], + [ false, "127.0.0.1." ], + [ true, "127.0.0.1" ], + [ false, "333.0.0.1" ], + [ true, "example.com" ], + [ false, "example.com." ], + [ true, ".example.com" ], + + [ true, ".example.com", "www.example.com" ], + [ false, "example.com", "www.example.com" ], + [ true, "127.0.0.1", "127.0.0.1" ], + [ false, "127.0.0.1", "localhost" ], + ]; + } + + /** + * Test Http::isValidURI() + * @bug 27854 : Http::isValidURI is too lax + * @dataProvider provideURI + * @covers Http::isValidURI + */ + public function testIsValidUri( $expect, $URI, $message = '' ) { + $this->assertEquals( + $expect, + (bool)Http::isValidURI( $URI ), + $message + ); + } + + /** + * @covers Http::getProxy + */ + public function testGetProxy() { + $this->setMwGlobals( 'wgHTTPProxy', 'proxy.domain.tld' ); + $this->assertEquals( + 'proxy.domain.tld', + Http::getProxy() + ); + } + + /** + * Feeds URI to test a long regular expression in Http::isValidURI + */ + public static function provideURI() { + /** Format: 'boolean expectation', 'URI to test', 'Optional message' */ + return [ + [ false, '¿non sens before!! http://a', 'Allow anything before URI' ], + + # (http|https) - only two schemes allowed + [ true, 'http://www.example.org/' ], + [ true, 'https://www.example.org/' ], + [ true, 'http://www.example.org', 'URI without directory' ], + [ true, 'http://a', 'Short name' ], + [ true, 'http://étoile', 'Allow UTF-8 in hostname' ], # 'étoile' is french for 'star' + [ false, '\\host\directory', 'CIFS share' ], + [ false, 'gopher://host/dir', 'Reject gopher scheme' ], + [ false, 'telnet://host', 'Reject telnet scheme' ], + + # :\/\/ - double slashes + [ false, 'http//example.org', 'Reject missing colon in protocol' ], + [ false, 'http:/example.org', 'Reject missing slash in protocol' ], + [ false, 'http:example.org', 'Must have two slashes' ], + # Following fail since hostname can be made of anything + [ false, 'http:///example.org', 'Must have exactly two slashes, not three' ], + + # (\w+:{0,1}\w*@)? - optional user:pass + [ true, 'http://user@host', 'Username provided' ], + [ true, 'http://user:@host', 'Username provided, no password' ], + [ true, 'http://user:pass@host', 'Username and password provided' ], + + # (\S+) - host part is made of anything not whitespaces + // commented these out in order to remove @group Broken + // @todo are these valid tests? if so, fix Http::isValidURI so it can handle them + // [ false, 'http://!"èèè¿¿¿~~\'', 'hostname is made of any non whitespace' ], + // [ false, 'http://exam:ple.org/', 'hostname can not use colons!' ], + + # (:[0-9]+)? - port number + [ true, 'http://example.org:80/' ], + [ true, 'https://example.org:80/' ], + [ true, 'http://example.org:443/' ], + [ true, 'https://example.org:443/' ], + + # Part after the hostname is / or / with something else + [ true, 'http://example/#' ], + [ true, 'http://example/!' ], + [ true, 'http://example/:' ], + [ true, 'http://example/.' ], + [ true, 'http://example/?' ], + [ true, 'http://example/+' ], + [ true, 'http://example/=' ], + [ true, 'http://example/&' ], + [ true, 'http://example/%' ], + [ true, 'http://example/@' ], + [ true, 'http://example/-' ], + [ true, 'http://example//' ], + [ true, 'http://example/&' ], + + # Fragment + [ true, 'http://exam#ple.org', ], # This one is valid, really! + [ true, 'http://example.org:80#anchor' ], + [ true, 'http://example.org/?id#anchor' ], + [ true, 'http://example.org/?#anchor' ], + + [ false, 'http://a ¿non !!sens after', 'Allow anything after URI' ], + ]; + } + + /** + * Warning: + * + * These tests are for code that makes use of an artifact of how CURL + * handles header reporting on redirect pages, and will need to be + * rewritten when bug 29232 is taken care of (high-level handling of + * HTTP redirects). + */ + public function testRelativeRedirections() { + $h = MWHttpRequestTester::factory( 'http://oldsite/file.ext', [], __METHOD__ ); + + # Forge a Location header + $h->setRespHeaders( 'location', [ + 'http://newsite/file.ext', + '/newfile.ext', + ] + ); + # Verify we correctly fix the Location + $this->assertEquals( + 'http://newsite/newfile.ext', + $h->getFinalUrl(), + "Relative file path Location: interpreted as full URL" + ); + + $h->setRespHeaders( 'location', [ + 'https://oldsite/file.ext' + ] + ); + $this->assertEquals( + 'https://oldsite/file.ext', + $h->getFinalUrl(), + "Location to the HTTPS version of the site" + ); + + $h->setRespHeaders( 'location', [ + '/anotherfile.ext', + 'http://anotherfile/hoster.ext', + 'https://anotherfile/hoster.ext' + ] + ); + $this->assertEquals( + 'https://anotherfile/hoster.ext', + $h->getFinalUrl( "Relative file path Location: should keep the latest host and scheme!" ) + ); + } + + /** + * Constant values are from PHP 5.3.28 using cURL 7.24.0 + * @see https://secure.php.net/manual/en/curl.constants.php + * + * All constant values are present so that developers don’t need to remember + * to add them if added at a later date. The commented out constants were + * not found anywhere in the MediaWiki core code. + * + * Commented out constants that were not available in: + * HipHop VM 3.3.0 (rel) + * Compiler: heads/master-0-g08810d920dfff59e0774cf2d651f92f13a637175 + * Repo schema: 3214fc2c684a4520485f715ee45f33f2182324b1 + * Extension API: 20140829 + * + * Commented out constants that were removed in PHP 5.6.0 + * + * @covers CurlHttpRequest::execute + */ + public function provideCurlConstants() { + return [ + [ 'CURLAUTH_ANY' ], + [ 'CURLAUTH_ANYSAFE' ], + [ 'CURLAUTH_BASIC' ], + [ 'CURLAUTH_DIGEST' ], + [ 'CURLAUTH_GSSNEGOTIATE' ], + [ 'CURLAUTH_NTLM' ], + // [ 'CURLCLOSEPOLICY_CALLBACK' ], // removed in PHP 5.6.0 + // [ 'CURLCLOSEPOLICY_LEAST_RECENTLY_USED' ], // removed in PHP 5.6.0 + // [ 'CURLCLOSEPOLICY_LEAST_TRAFFIC' ], // removed in PHP 5.6.0 + // [ 'CURLCLOSEPOLICY_OLDEST' ], // removed in PHP 5.6.0 + // [ 'CURLCLOSEPOLICY_SLOWEST' ], // removed in PHP 5.6.0 + [ 'CURLE_ABORTED_BY_CALLBACK' ], + [ 'CURLE_BAD_CALLING_ORDER' ], + [ 'CURLE_BAD_CONTENT_ENCODING' ], + [ 'CURLE_BAD_FUNCTION_ARGUMENT' ], + [ 'CURLE_BAD_PASSWORD_ENTERED' ], + [ 'CURLE_COULDNT_CONNECT' ], + [ 'CURLE_COULDNT_RESOLVE_HOST' ], + [ 'CURLE_COULDNT_RESOLVE_PROXY' ], + [ 'CURLE_FAILED_INIT' ], + [ 'CURLE_FILESIZE_EXCEEDED' ], + [ 'CURLE_FILE_COULDNT_READ_FILE' ], + [ 'CURLE_FTP_ACCESS_DENIED' ], + [ 'CURLE_FTP_BAD_DOWNLOAD_RESUME' ], + [ 'CURLE_FTP_CANT_GET_HOST' ], + [ 'CURLE_FTP_CANT_RECONNECT' ], + [ 'CURLE_FTP_COULDNT_GET_SIZE' ], + [ 'CURLE_FTP_COULDNT_RETR_FILE' ], + [ 'CURLE_FTP_COULDNT_SET_ASCII' ], + [ 'CURLE_FTP_COULDNT_SET_BINARY' ], + [ 'CURLE_FTP_COULDNT_STOR_FILE' ], + [ 'CURLE_FTP_COULDNT_USE_REST' ], + [ 'CURLE_FTP_PORT_FAILED' ], + [ 'CURLE_FTP_QUOTE_ERROR' ], + [ 'CURLE_FTP_SSL_FAILED' ], + [ 'CURLE_FTP_USER_PASSWORD_INCORRECT' ], + [ 'CURLE_FTP_WEIRD_227_FORMAT' ], + [ 'CURLE_FTP_WEIRD_PASS_REPLY' ], + [ 'CURLE_FTP_WEIRD_PASV_REPLY' ], + [ 'CURLE_FTP_WEIRD_SERVER_REPLY' ], + [ 'CURLE_FTP_WEIRD_USER_REPLY' ], + [ 'CURLE_FTP_WRITE_ERROR' ], + [ 'CURLE_FUNCTION_NOT_FOUND' ], + [ 'CURLE_GOT_NOTHING' ], + [ 'CURLE_HTTP_NOT_FOUND' ], + [ 'CURLE_HTTP_PORT_FAILED' ], + [ 'CURLE_HTTP_POST_ERROR' ], + [ 'CURLE_HTTP_RANGE_ERROR' ], + [ 'CURLE_LDAP_CANNOT_BIND' ], + [ 'CURLE_LDAP_INVALID_URL' ], + [ 'CURLE_LDAP_SEARCH_FAILED' ], + [ 'CURLE_LIBRARY_NOT_FOUND' ], + [ 'CURLE_MALFORMAT_USER' ], + [ 'CURLE_OBSOLETE' ], + [ 'CURLE_OK' ], + [ 'CURLE_OPERATION_TIMEOUTED' ], + [ 'CURLE_OUT_OF_MEMORY' ], + [ 'CURLE_PARTIAL_FILE' ], + [ 'CURLE_READ_ERROR' ], + [ 'CURLE_RECV_ERROR' ], + [ 'CURLE_SEND_ERROR' ], + [ 'CURLE_SHARE_IN_USE' ], + // [ 'CURLE_SSH' ], // not present in HHVM 3.3.0-dev + [ 'CURLE_SSL_CACERT' ], + [ 'CURLE_SSL_CERTPROBLEM' ], + [ 'CURLE_SSL_CIPHER' ], + [ 'CURLE_SSL_CONNECT_ERROR' ], + [ 'CURLE_SSL_ENGINE_NOTFOUND' ], + [ 'CURLE_SSL_ENGINE_SETFAILED' ], + [ 'CURLE_SSL_PEER_CERTIFICATE' ], + [ 'CURLE_TELNET_OPTION_SYNTAX' ], + [ 'CURLE_TOO_MANY_REDIRECTS' ], + [ 'CURLE_UNKNOWN_TELNET_OPTION' ], + [ 'CURLE_UNSUPPORTED_PROTOCOL' ], + [ 'CURLE_URL_MALFORMAT' ], + [ 'CURLE_URL_MALFORMAT_USER' ], + [ 'CURLE_WRITE_ERROR' ], + [ 'CURLFTPAUTH_DEFAULT' ], + [ 'CURLFTPAUTH_SSL' ], + [ 'CURLFTPAUTH_TLS' ], + // [ 'CURLFTPMETHOD_MULTICWD' ], // not present in HHVM 3.3.0-dev + // [ 'CURLFTPMETHOD_NOCWD' ], // not present in HHVM 3.3.0-dev + // [ 'CURLFTPMETHOD_SINGLECWD' ], // not present in HHVM 3.3.0-dev + [ 'CURLFTPSSL_ALL' ], + [ 'CURLFTPSSL_CONTROL' ], + [ 'CURLFTPSSL_NONE' ], + [ 'CURLFTPSSL_TRY' ], + // [ 'CURLINFO_CERTINFO' ], // not present in HHVM 3.3.0-dev + [ 'CURLINFO_CONNECT_TIME' ], + [ 'CURLINFO_CONTENT_LENGTH_DOWNLOAD' ], + [ 'CURLINFO_CONTENT_LENGTH_UPLOAD' ], + [ 'CURLINFO_CONTENT_TYPE' ], + [ 'CURLINFO_EFFECTIVE_URL' ], + [ 'CURLINFO_FILETIME' ], + [ 'CURLINFO_HEADER_OUT' ], + [ 'CURLINFO_HEADER_SIZE' ], + [ 'CURLINFO_HTTP_CODE' ], + [ 'CURLINFO_NAMELOOKUP_TIME' ], + [ 'CURLINFO_PRETRANSFER_TIME' ], + [ 'CURLINFO_PRIVATE' ], + [ 'CURLINFO_REDIRECT_COUNT' ], + [ 'CURLINFO_REDIRECT_TIME' ], + // [ 'CURLINFO_REDIRECT_URL' ], // not present in HHVM 3.3.0-dev + [ 'CURLINFO_REQUEST_SIZE' ], + [ 'CURLINFO_SIZE_DOWNLOAD' ], + [ 'CURLINFO_SIZE_UPLOAD' ], + [ 'CURLINFO_SPEED_DOWNLOAD' ], + [ 'CURLINFO_SPEED_UPLOAD' ], + [ 'CURLINFO_SSL_VERIFYRESULT' ], + [ 'CURLINFO_STARTTRANSFER_TIME' ], + [ 'CURLINFO_TOTAL_TIME' ], + [ 'CURLMSG_DONE' ], + [ 'CURLM_BAD_EASY_HANDLE' ], + [ 'CURLM_BAD_HANDLE' ], + [ 'CURLM_CALL_MULTI_PERFORM' ], + [ 'CURLM_INTERNAL_ERROR' ], + [ 'CURLM_OK' ], + [ 'CURLM_OUT_OF_MEMORY' ], + [ 'CURLOPT_AUTOREFERER' ], + [ 'CURLOPT_BINARYTRANSFER' ], + [ 'CURLOPT_BUFFERSIZE' ], + [ 'CURLOPT_CAINFO' ], + [ 'CURLOPT_CAPATH' ], + // [ 'CURLOPT_CERTINFO' ], // not present in HHVM 3.3.0-dev + // [ 'CURLOPT_CLOSEPOLICY' ], // removed in PHP 5.6.0 + [ 'CURLOPT_CONNECTTIMEOUT' ], + [ 'CURLOPT_CONNECTTIMEOUT_MS' ], + [ 'CURLOPT_COOKIE' ], + [ 'CURLOPT_COOKIEFILE' ], + [ 'CURLOPT_COOKIEJAR' ], + [ 'CURLOPT_COOKIESESSION' ], + [ 'CURLOPT_CRLF' ], + [ 'CURLOPT_CUSTOMREQUEST' ], + [ 'CURLOPT_DNS_CACHE_TIMEOUT' ], + [ 'CURLOPT_DNS_USE_GLOBAL_CACHE' ], + [ 'CURLOPT_EGDSOCKET' ], + [ 'CURLOPT_ENCODING' ], + [ 'CURLOPT_FAILONERROR' ], + [ 'CURLOPT_FILE' ], + [ 'CURLOPT_FILETIME' ], + [ 'CURLOPT_FOLLOWLOCATION' ], + [ 'CURLOPT_FORBID_REUSE' ], + [ 'CURLOPT_FRESH_CONNECT' ], + [ 'CURLOPT_FTPAPPEND' ], + [ 'CURLOPT_FTPLISTONLY' ], + [ 'CURLOPT_FTPPORT' ], + [ 'CURLOPT_FTPSSLAUTH' ], + [ 'CURLOPT_FTP_CREATE_MISSING_DIRS' ], + // [ 'CURLOPT_FTP_FILEMETHOD' ], // not present in HHVM 3.3.0-dev + // [ 'CURLOPT_FTP_SKIP_PASV_IP' ], // not present in HHVM 3.3.0-dev + [ 'CURLOPT_FTP_SSL' ], + [ 'CURLOPT_FTP_USE_EPRT' ], + [ 'CURLOPT_FTP_USE_EPSV' ], + [ 'CURLOPT_HEADER' ], + [ 'CURLOPT_HEADERFUNCTION' ], + [ 'CURLOPT_HTTP200ALIASES' ], + [ 'CURLOPT_HTTPAUTH' ], + [ 'CURLOPT_HTTPGET' ], + [ 'CURLOPT_HTTPHEADER' ], + [ 'CURLOPT_HTTPPROXYTUNNEL' ], + [ 'CURLOPT_HTTP_VERSION' ], + [ 'CURLOPT_INFILE' ], + [ 'CURLOPT_INFILESIZE' ], + [ 'CURLOPT_INTERFACE' ], + [ 'CURLOPT_IPRESOLVE' ], + // [ 'CURLOPT_KEYPASSWD' ], // not present in HHVM 3.3.0-dev + [ 'CURLOPT_KRB4LEVEL' ], + [ 'CURLOPT_LOW_SPEED_LIMIT' ], + [ 'CURLOPT_LOW_SPEED_TIME' ], + [ 'CURLOPT_MAXCONNECTS' ], + [ 'CURLOPT_MAXREDIRS' ], + // [ 'CURLOPT_MAX_RECV_SPEED_LARGE' ], // not present in HHVM 3.3.0-dev + // [ 'CURLOPT_MAX_SEND_SPEED_LARGE' ], // not present in HHVM 3.3.0-dev + [ 'CURLOPT_NETRC' ], + [ 'CURLOPT_NOBODY' ], + [ 'CURLOPT_NOPROGRESS' ], + [ 'CURLOPT_NOSIGNAL' ], + [ 'CURLOPT_PORT' ], + [ 'CURLOPT_POST' ], + [ 'CURLOPT_POSTFIELDS' ], + [ 'CURLOPT_POSTQUOTE' ], + [ 'CURLOPT_POSTREDIR' ], + [ 'CURLOPT_PRIVATE' ], + [ 'CURLOPT_PROGRESSFUNCTION' ], + // [ 'CURLOPT_PROTOCOLS' ], // not present in HHVM 3.3.0-dev + [ 'CURLOPT_PROXY' ], + [ 'CURLOPT_PROXYAUTH' ], + [ 'CURLOPT_PROXYPORT' ], + [ 'CURLOPT_PROXYTYPE' ], + [ 'CURLOPT_PROXYUSERPWD' ], + [ 'CURLOPT_PUT' ], + [ 'CURLOPT_QUOTE' ], + [ 'CURLOPT_RANDOM_FILE' ], + [ 'CURLOPT_RANGE' ], + [ 'CURLOPT_READDATA' ], + [ 'CURLOPT_READFUNCTION' ], + // [ 'CURLOPT_REDIR_PROTOCOLS' ], // not present in HHVM 3.3.0-dev + [ 'CURLOPT_REFERER' ], + [ 'CURLOPT_RESUME_FROM' ], + [ 'CURLOPT_RETURNTRANSFER' ], + // [ 'CURLOPT_SSH_AUTH_TYPES' ], // not present in HHVM 3.3.0-dev + // [ 'CURLOPT_SSH_HOST_PUBLIC_KEY_MD5' ], // not present in HHVM 3.3.0-dev + // [ 'CURLOPT_SSH_PRIVATE_KEYFILE' ], // not present in HHVM 3.3.0-dev + // [ 'CURLOPT_SSH_PUBLIC_KEYFILE' ], // not present in HHVM 3.3.0-dev + [ 'CURLOPT_SSLCERT' ], + [ 'CURLOPT_SSLCERTPASSWD' ], + [ 'CURLOPT_SSLCERTTYPE' ], + [ 'CURLOPT_SSLENGINE' ], + [ 'CURLOPT_SSLENGINE_DEFAULT' ], + [ 'CURLOPT_SSLKEY' ], + [ 'CURLOPT_SSLKEYPASSWD' ], + [ 'CURLOPT_SSLKEYTYPE' ], + [ 'CURLOPT_SSLVERSION' ], + [ 'CURLOPT_SSL_CIPHER_LIST' ], + [ 'CURLOPT_SSL_VERIFYHOST' ], + [ 'CURLOPT_SSL_VERIFYPEER' ], + [ 'CURLOPT_STDERR' ], + [ 'CURLOPT_TCP_NODELAY' ], + [ 'CURLOPT_TIMECONDITION' ], + [ 'CURLOPT_TIMEOUT' ], + [ 'CURLOPT_TIMEOUT_MS' ], + [ 'CURLOPT_TIMEVALUE' ], + [ 'CURLOPT_TRANSFERTEXT' ], + [ 'CURLOPT_UNRESTRICTED_AUTH' ], + [ 'CURLOPT_UPLOAD' ], + [ 'CURLOPT_URL' ], + [ 'CURLOPT_USERAGENT' ], + [ 'CURLOPT_USERPWD' ], + [ 'CURLOPT_VERBOSE' ], + [ 'CURLOPT_WRITEFUNCTION' ], + [ 'CURLOPT_WRITEHEADER' ], + // [ 'CURLPROTO_ALL' ], // not present in HHVM 3.3.0-dev + // [ 'CURLPROTO_DICT' ], // not present in HHVM 3.3.0-dev + // [ 'CURLPROTO_FILE' ], // not present in HHVM 3.3.0-dev + // [ 'CURLPROTO_FTP' ], // not present in HHVM 3.3.0-dev + // [ 'CURLPROTO_FTPS' ], // not present in HHVM 3.3.0-dev + // [ 'CURLPROTO_HTTP' ], // not present in HHVM 3.3.0-dev + // [ 'CURLPROTO_HTTPS' ], // not present in HHVM 3.3.0-dev + // [ 'CURLPROTO_LDAP' ], // not present in HHVM 3.3.0-dev + // [ 'CURLPROTO_LDAPS' ], // not present in HHVM 3.3.0-dev + // [ 'CURLPROTO_SCP' ], // not present in HHVM 3.3.0-dev + // [ 'CURLPROTO_SFTP' ], // not present in HHVM 3.3.0-dev + // [ 'CURLPROTO_TELNET' ], // not present in HHVM 3.3.0-dev + // [ 'CURLPROTO_TFTP' ], // not present in HHVM 3.3.0-dev + [ 'CURLPROXY_HTTP' ], + // [ 'CURLPROXY_SOCKS4' ], // not present in HHVM 3.3.0-dev + [ 'CURLPROXY_SOCKS5' ], + // [ 'CURLSSH_AUTH_DEFAULT' ], // not present in HHVM 3.3.0-dev + // [ 'CURLSSH_AUTH_HOST' ], // not present in HHVM 3.3.0-dev + // [ 'CURLSSH_AUTH_KEYBOARD' ], // not present in HHVM 3.3.0-dev + // [ 'CURLSSH_AUTH_NONE' ], // not present in HHVM 3.3.0-dev + // [ 'CURLSSH_AUTH_PASSWORD' ], // not present in HHVM 3.3.0-dev + // [ 'CURLSSH_AUTH_PUBLICKEY' ], // not present in HHVM 3.3.0-dev + [ 'CURLVERSION_NOW' ], + [ 'CURL_HTTP_VERSION_1_0' ], + [ 'CURL_HTTP_VERSION_1_1' ], + [ 'CURL_HTTP_VERSION_NONE' ], + [ 'CURL_IPRESOLVE_V4' ], + [ 'CURL_IPRESOLVE_V6' ], + [ 'CURL_IPRESOLVE_WHATEVER' ], + [ 'CURL_NETRC_IGNORED' ], + [ 'CURL_NETRC_OPTIONAL' ], + [ 'CURL_NETRC_REQUIRED' ], + [ 'CURL_TIMECOND_IFMODSINCE' ], + [ 'CURL_TIMECOND_IFUNMODSINCE' ], + [ 'CURL_TIMECOND_LASTMOD' ], + [ 'CURL_VERSION_IPV6' ], + [ 'CURL_VERSION_KERBEROS4' ], + [ 'CURL_VERSION_LIBZ' ], + [ 'CURL_VERSION_SSL' ], + ]; + } + + /** + * Added this test based on an issue experienced with HHVM 3.3.0-dev + * where it did not define a cURL constant. + * + * @bug 70570 + * @dataProvider provideCurlConstants + */ + public function testCurlConstants( $value ) { + $this->assertTrue( defined( $value ), $value . ' not defined' ); + } +} + +/** + * Class to let us overwrite MWHttpRequest respHeaders variable + */ +class MWHttpRequestTester extends MWHttpRequest { + // function derived from the MWHttpRequest factory function but + // returns appropriate tester class here + public static function factory( $url, $options = null, $caller = __METHOD__ ) { + if ( !Http::$httpEngine ) { + Http::$httpEngine = function_exists( 'curl_init' ) ? 'curl' : 'php'; + } elseif ( Http::$httpEngine == 'curl' && !function_exists( 'curl_init' ) ) { + throw new DomainException( __METHOD__ . ': curl (http://php.net/curl) is not installed, but' . + 'Http::$httpEngine is set to "curl"' ); + } + + switch ( Http::$httpEngine ) { + case 'curl': + return new CurlHttpRequestTester( $url, $options, $caller ); + case 'php': + if ( !wfIniGetBool( 'allow_url_fopen' ) ) { + throw new DomainException( __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 PhpHttpRequestTester( $url, $options, $caller ); + default: + } + } +} + +class CurlHttpRequestTester extends CurlHttpRequest { + function setRespHeaders( $name, $value ) { + $this->respHeaders[$name] = $value; + } +} + +class PhpHttpRequestTester extends PhpHttpRequest { + function setRespHeaders( $name, $value ) { + $this->respHeaders[$name] = $value; + } +}