diff --git a/.travis.yml b/.travis.yml index cf26742..26a4ed1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,6 @@ php: - 7.0 - 5.6 - 5.5 - - 5.4 - hhvm install: diff --git a/Goutte/Client.php b/Goutte/Client.php index a4ebfc2..7d576bb 100644 --- a/Goutte/Client.php +++ b/Goutte/Client.php @@ -13,10 +13,9 @@ use GuzzleHttp\Client as GuzzleClient; use GuzzleHttp\ClientInterface as GuzzleClientInterface; +use GuzzleHttp\Cookie\CookieJar; use GuzzleHttp\Exception\RequestException; -use GuzzleHttp\Message\RequestInterface; -use GuzzleHttp\Message\Response as GuzzleResponse; -use GuzzleHttp\Post\PostFile; +use Psr\Http\Message\ResponseInterface; use Symfony\Component\BrowserKit\Client as BaseClient; use Symfony\Component\BrowserKit\Response; @@ -25,6 +24,7 @@ * * @author Fabien Potencier * @author Michael Dowling + * @author Charles Sarrazin */ class Client extends BaseClient { @@ -90,44 +90,44 @@ protected function doRequest($request) } } - $body = null; + $cookies = CookieJar::fromArray( + $this->getCookieJar()->allRawValues($request->getUri()), + $request->getServer()['HTTP_HOST'] + ); + + $requestOptions = array( + 'cookies' => $cookies, + 'allow_redirects' => false, + 'auth' => $this->auth, + ); + if (!in_array($request->getMethod(), array('GET', 'HEAD'))) { if (null !== $request->getContent()) { - $body = $request->getContent(); + $requestOptions['body'] = $request->getContent(); } else { - $body = $request->getParameters(); + $requestOptions['form_params'] = $request->getParameters(); + + if ($files = $request->getFiles()) { + $requestOptions['multipart'] = []; + $this->addPostFiles($files, $requestOptions['multipart']); + } } } - $this->getClient()->setDefaultOption('auth', $this->auth); - - $requestOptions = array( - 'body' => $body, - 'cookies' => $this->getCookieJar()->allRawValues($request->getUri()), - 'allow_redirects' => false, - ); - if (!empty($headers)) { $requestOptions['headers'] = $headers; } - $guzzleRequest = $this->getClient()->createRequest( - $request->getMethod(), - $request->getUri(), - $requestOptions - ); + $method = $request->getMethod(); + $uri = $request->getUri(); foreach ($this->headers as $name => $value) { - $guzzleRequest->setHeader($name, $value); - } - - if ('POST' == $request->getMethod() || 'PUT' == $request->getMethod() && $request->getFiles()) { - $this->addPostFiles($guzzleRequest, $request->getFiles()); + $requestOptions['headers'][$name] = $value; } // Let BrowserKit handle redirects try { - $response = $this->getClient()->send($guzzleRequest); + $response = $this->getClient()->request($method, $uri, $requestOptions); } catch (RequestException $e) { $response = $e->getResponse(); if (null === $response) { @@ -138,33 +138,45 @@ protected function doRequest($request) return $this->createResponse($response); } - protected function addPostFiles(RequestInterface $request, array $files, $arrayName = '') + protected function addPostFiles(array $files, array &$multipart, $arrayName = '') { + if (empty($files)) { + return; + } + foreach ($files as $name => $info) { if (!empty($arrayName)) { $name = $arrayName.'['.$name.']'; } + $file = [ + 'name' => $name, + ]; + if (is_array($info)) { if (isset($info['tmp_name'])) { if ('' !== $info['tmp_name']) { - $request->getBody()->addFile(new PostFile($name, fopen($info['tmp_name'], 'r'), isset($info['name']) ? $info['name'] : null)); + $file['contents'] = fopen($info['tmp_name'], 'r'); + if (isset($info['name'])) { + $file['filename'] = $info['name']; + } } else { continue; } } else { - $this->addPostFiles($request, $info, $name); + $this->addPostFiles($info, $multipart, $name); + continue; } } else { - $request->getBody()->addFile(new PostFile($name, fopen($info, 'r'))); + $file['contents'] = fopen($info, 'r'); } + + $multipart[] = $file; } } - protected function createResponse(GuzzleResponse $response) + protected function createResponse(ResponseInterface $response) { - $headers = $response->getHeaders(); - - return new Response($response->getBody(true), $response->getStatusCode(), $headers); + return new Response((string) $response->getBody(), $response->getStatusCode(), $response->getHeaders()); } } diff --git a/Goutte/Tests/ClientTest.php b/Goutte/Tests/ClientTest.php index da2e47c..d8ef647 100644 --- a/Goutte/Tests/ClientTest.php +++ b/Goutte/Tests/ClientTest.php @@ -14,31 +14,34 @@ use Goutte\Client; use GuzzleHttp\Client as GuzzleClient; use GuzzleHttp\Exception\RequestException; -use GuzzleHttp\Message\Response as GuzzleResponse; -use GuzzleHttp\Stream\Stream; -use GuzzleHttp\Subscriber\History; -use GuzzleHttp\Subscriber\Mock; -use GuzzleHttp\Post\PostFile; +use GuzzleHttp\Handler\MockHandler; +use GuzzleHttp\HandlerStack; +use GuzzleHttp\Psr7\Response as GuzzleResponse; +use GuzzleHttp\Middleware; use Symfony\Component\BrowserKit\Cookie; /** - * Goutte Client Test + * Goutte Client Test. * * @author Michael Dowling + * @author Charles Sarrazin */ class ClientTest extends \PHPUnit_Framework_TestCase { protected $history; + /** @var MockHandler */ protected $mock; - protected function getGuzzle() + protected function getGuzzle(array $responses = []) { - $this->history = new History(); - $this->mock = new Mock(); - $this->mock->addResponse(new GuzzleResponse(200, array(), Stream::factory('

Hi

'))); - $guzzle = new GuzzleClient(array('redirect.disable' => true, 'base_url' => '')); - $guzzle->getEmitter()->attach($this->mock); - $guzzle->getEmitter()->attach($this->history); + if (empty($responses)) { + $responses = [new GuzzleResponse(200, [], '

Hi

')]; + } + $this->mock = new MockHandler($responses); + $handlerStack = HandlerStack::create($this->mock); + $this->history = []; + $handlerStack->push(Middleware::history($this->history)); + $guzzle = new GuzzleClient(array('redirect.disable' => true, 'base_uri' => '', 'handler' => $handlerStack)); return $guzzle; } @@ -63,8 +66,8 @@ public function testUsesCustomHeaders() $client = new Client(); $client->setClient($guzzle); $client->setHeader('X-Test', 'test'); - $crawler = $client->request('GET', 'http://www.example.com/'); - $this->assertEquals('test', $this->history->getLastRequest()->getHeader('X-Test')); + $client->request('GET', 'http://www.example.com/'); + $this->assertEquals('test', end($this->history)['request']->getHeaderLine('X-Test')); } public function testCustomUserAgent() @@ -73,8 +76,8 @@ public function testCustomUserAgent() $client = new Client(); $client->setClient($guzzle); $client->setHeader('User-Agent', 'foo'); - $crawler = $client->request('GET', 'http://www.example.com/'); - $this->assertEquals('foo', $this->history->getLastRequest()->getHeader('User-Agent')); + $client->request('GET', 'http://www.example.com/'); + $this->assertEquals('Symfony2 BrowserKit, foo', end($this->history)['request']->getHeaderLine('User-Agent')); } public function testUsesAuth() @@ -83,11 +86,9 @@ public function testUsesAuth() $client = new Client(); $client->setClient($guzzle); $client->setAuth('me', '**'); - $crawler = $client->request('GET', 'http://www.example.com/'); - $request = $this->history->getLastRequest(); - $this->assertEquals('me', $request->getConfig()->get('auth')[0]); - $this->assertEquals('**', $request->getConfig()->get('auth')[1]); - $this->assertEquals('basic', $request->getConfig()->get('auth')[2]); + $client->request('GET', 'http://www.example.com/'); + $request = end($this->history)['request']; + $this->assertEquals('Basic bWU6Kio=', $request->getHeaderLine('Authorization')); } public function testResetsAuth() @@ -97,10 +98,9 @@ public function testResetsAuth() $client->setClient($guzzle); $client->setAuth('me', '**'); $client->resetAuth(); - $crawler = $client->request('GET', 'http://www.example.com/'); - $request = $this->history->getLastRequest(); - $this->assertNull($request->getConfig()->get('auth')[0]); - $this->assertNull($request->getConfig()->get('auth')[1]); + $client->request('GET', 'http://www.example.com/'); + $request = end($this->history)['request']; + $this->assertEquals('', $request->getHeaderLine('authorization')); } public function testUsesCookies() @@ -109,9 +109,9 @@ public function testUsesCookies() $client = new Client(); $client->setClient($guzzle); $client->getCookieJar()->set(new Cookie('test', '123')); - $crawler = $client->request('GET', 'http://www.example.com/'); - $request = $this->history->getLastRequest(); - $this->assertEquals('test=123', $request->getHeader('Cookie')); + $client->request('GET', 'http://www.example.com/'); + $request = end($this->history)['request']; + $this->assertEquals('test=123', $request->getHeaderLine('Cookie')); } public function testUsesPostFiles() @@ -122,18 +122,20 @@ public function testUsesPostFiles() $files = array( 'test' => array( 'name' => 'test.txt', - 'tmp_name' => __FILE__, + 'tmp_name' => __DIR__.'/fixtures.txt', ), ); - $crawler = $client->request('POST', 'http://www.example.com/', array(), $files); - $request = $this->history->getLastRequest(); + $client->request('POST', 'http://www.example.com/', array(), $files); + $request = end($this->history)['request']; - $files = $request->getBody()->getFiles(); - $this->assertFile(reset($files), 'test', 'test.txt', array( - 'Content-Type' => 'text/plain', - 'Content-Disposition' => 'form-data; filename="test.txt"; name="test"', - )); + $stream = $request->getBody(); + $boundary = $stream->getBoundary(); + $this->assertEquals( + "--$boundary\r\nContent-Disposition: form-data; name=\"test\"; filename=\"test.txt\"\r\nContent-Length: 4\r\n" + ."Content-Type: text/plain\r\n\r\nfoo\n\r\n--$boundary--\r\n", + $stream->getContents() + ); } public function testUsesPostNamedFiles() @@ -142,16 +144,19 @@ public function testUsesPostNamedFiles() $client = new Client(); $client->setClient($guzzle); $files = array( - 'test' => __FILE__, + 'test' => __DIR__.'/fixtures.txt', ); - $crawler = $client->request('POST', 'http://www.example.com/', array(), $files); - $request = $this->history->getLastRequest(); - $files = $request->getBody()->getFiles(); - $this->assertFile(reset($files), 'test', __FILE__, array( - 'Content-Type' => 'text/x-php', - 'Content-Disposition' => 'form-data; filename="ClientTest.php"; name="test"', - )); + $client->request('POST', 'http://www.example.com/', array(), $files); + $request = end($this->history)['request']; + + $stream = $request->getBody(); + $boundary = $stream->getBoundary(); + $this->assertEquals( + "--$boundary\r\nContent-Disposition: form-data; name=\"test\"; filename=\"fixtures.txt\"\r\nContent-Length: 4\r\n" + ."Content-Type: text/plain\r\n\r\nfoo\n\r\n--$boundary--\r\n", + $stream->getContents() + ); } public function testUsesPostFilesNestedFields() @@ -163,18 +168,21 @@ public function testUsesPostFilesNestedFields() 'form' => array( 'test' => array( 'name' => 'test.txt', - 'tmp_name' => __FILE__, + 'tmp_name' => __DIR__.'/fixtures.txt', ), ), ); - $crawler = $client->request('POST', 'http://www.example.com/', array(), $files); - $request = $this->history->getLastRequest(); - $files = $request->getBody()->getFiles(); - $this->assertFile(reset($files), 'form[test]', 'test.txt', array( - 'Content-Type' => 'text/plain', - 'Content-Disposition' => 'form-data; filename="test.txt"; name="form[test]"', - )); + $client->request('POST', 'http://www.example.com/', array(), $files); + $request = end($this->history)['request']; + + $stream = $request->getBody(); + $boundary = $stream->getBoundary(); + $this->assertEquals( + "--$boundary\r\nContent-Disposition: form-data; name=\"form[test]\"; filename=\"test.txt\"\r\nContent-Length: 4\r\n" + ."Content-Type: text/plain\r\n\r\nfoo\n\r\n--$boundary--\r\n", + $stream->getContents() + ); } public function testUsesPostFilesOnClientSide() @@ -183,16 +191,19 @@ public function testUsesPostFilesOnClientSide() $client = new Client(); $client->setClient($guzzle); $files = array( - 'test' => __FILE__, + 'test' => __DIR__.'/fixtures.txt', ); - $crawler = $client->request('POST', 'http://www.example.com/', array(), $files); - $request = $this->history->getLastRequest(); - $files = $request->getBody()->getFiles(); - $this->assertFile(reset($files), 'test', __FILE__, array( - 'Content-Type' => 'text/x-php', - 'Content-Disposition' => 'form-data; filename="ClientTest.php"; name="test"', - )); + $client->request('POST', 'http://www.example.com/', array(), $files); + $request = end($this->history)['request']; + + $stream = $request->getBody(); + $boundary = $stream->getBoundary(); + $this->assertEquals( + "--$boundary\r\nContent-Disposition: form-data; name=\"test\"; filename=\"fixtures.txt\"\r\nContent-Length: 4\r\n" + ."Content-Type: text/plain\r\n\r\nfoo\n\r\n--$boundary--\r\n", + $stream->getContents() + ); } public function testUsesPostFilesUploadError() @@ -210,10 +221,12 @@ public function testUsesPostFilesUploadError() ), ); - $crawler = $client->request('POST', 'http://www.example.com/', array(), $files); - $request = $this->history->getLastRequest(); + $client->request('POST', 'http://www.example.com/', array(), $files); + $request = end($this->history)['request']; + $stream = $request->getBody(); + $boundary = $stream->getBoundary(); - $this->assertEquals(array(), $request->getBody()->getFiles()); + $this->assertEquals("--$boundary--\r\n", $stream->getContents()); } public function testCreatesResponse() @@ -227,13 +240,12 @@ public function testCreatesResponse() public function testHandlesRedirectsCorrectly() { - $guzzle = $this->getGuzzle(); - - $this->mock->clearQueue(); - $this->mock->addResponse(new GuzzleResponse(301, array( - 'Location' => 'http://www.example.com/', - ))); - $this->mock->addResponse(new GuzzleResponse(200, [], Stream::factory('

Test

'))); + $guzzle = $this->getGuzzle([ + new GuzzleResponse(301, array( + 'Location' => 'http://www.example.com/', + )), + new GuzzleResponse(200, [], '

Test

'), + ]); $client = new Client(); $client->setClient($guzzle); @@ -247,12 +259,11 @@ public function testHandlesRedirectsCorrectly() public function testConvertsGuzzleHeadersToArrays() { - $guzzle = $this->getGuzzle(); - - $this->mock->clearQueue(); - $this->mock->addResponse(new GuzzleResponse(200, array( - 'Date' => 'Tue, 04 Jun 2013 13:22:41 GMT', - ))); + $guzzle = $this->getGuzzle([ + new GuzzleResponse(200, array( + 'Date' => 'Tue, 04 Jun 2013 13:22:41 GMT', + )), + ]); $client = new Client(); $client->setClient($guzzle); @@ -260,48 +271,30 @@ public function testConvertsGuzzleHeadersToArrays() $response = $client->getResponse(); $headers = $response->getHeaders(); - $this->assertInternalType("array", array_shift($headers), "Header not converted from Guzzle\Http\Message\Header to array"); + $this->assertInternalType('array', array_shift($headers), 'Header not converted from Guzzle\Http\Message\Header to array'); } public function testNullResponseException() { $this->setExpectedException('GuzzleHttp\Exception\RequestException'); - $guzzle = $this->getGuzzle(); - $this->mock->clearQueue(); - $exception = new RequestException('', $this->getMock('GuzzleHttp\Message\RequestInterface')); - $this->mock->addException($exception); + $guzzle = $this->getGuzzle([ + new RequestException('', $this->getMock('Psr\Http\Message\RequestInterface')), + ]); $client = new Client(); $client->setClient($guzzle); $client->request('GET', 'http://www.example.com/'); - $response = $client->getResponse(); - } - - protected function assertFile(PostFile $postFile, $fieldName, $fileName, $headers) - { - $this->assertEquals($postFile->getName(), $fieldName); - $this->assertEquals($postFile->getFilename(), $fileName); - - $postFileHeaders = $postFile->getHeaders(); - - // Note: Sort 'Content-Disposition' values before comparing, because the order changed in Guzzle 4.2.2 - $postFileHeaders['Content-Disposition'] = explode('; ', $postFileHeaders['Content-Disposition']); - sort($postFileHeaders['Content-Disposition']); - $headers['Content-Disposition'] = explode('; ', $headers['Content-Disposition']); - sort($headers['Content-Disposition']); - - $this->assertEquals($postFileHeaders, $headers); + $client->getResponse(); } public function testHttps() { - $guzzle = $this->getGuzzle(); + $guzzle = $this->getGuzzle([ + new GuzzleResponse(200, [], '

Test

'), + ]); - $this->mock->clearQueue(); - $this->mock->addResponse(new GuzzleResponse(200, [], Stream::factory('

Test

'))); $client = new Client(); $client->setClient($guzzle); $crawler = $client->request('GET', 'https://www.example.com/'); - $this->assertEquals('https', $this->history->getLastRequest()->getScheme()); $this->assertEquals('Test', $crawler->filter('p')->text()); } @@ -313,7 +306,7 @@ public function testCustomUserAgentConstructor() 'HTTP_USER_AGENT' => 'SomeHost', ]); $client->setClient($guzzle); - $crawler = $client->request('GET', 'http://www.example.com/'); - $this->assertEquals('SomeHost', $this->history->getLastRequest()->getHeader('User-Agent')); + $client->request('GET', 'http://www.example.com/'); + $this->assertEquals('SomeHost', end($this->history)['request']->getHeaderLine('User-Agent')); } } diff --git a/Goutte/Tests/fixtures.txt b/Goutte/Tests/fixtures.txt new file mode 100644 index 0000000..257cc56 --- /dev/null +++ b/Goutte/Tests/fixtures.txt @@ -0,0 +1 @@ +foo diff --git a/README.rst b/README.rst index a6aab89..c1ade39 100644 --- a/README.rst +++ b/README.rst @@ -9,10 +9,11 @@ responses. Requirements ------------ -Goutte depends on PHP 5.4+ and Guzzle 4+. +Goutte depends on PHP 5.5+ and Guzzle 6+. .. tip:: + If you need support for PHP 5.4 or Guzzle 4-5, use Goutte 2.x. If you need support for PHP 5.3 or Guzzle 3, use Goutte 1.x. Installation diff --git a/composer.json b/composer.json index 63d87e7..ca5be44 100644 --- a/composer.json +++ b/composer.json @@ -12,18 +12,18 @@ } ], "require": { - "php": ">=5.4.0", + "php": ">=5.5.0", "symfony/browser-kit": "~2.1", "symfony/css-selector": "~2.1", "symfony/dom-crawler": "~2.1", - "guzzlehttp/guzzle": ">=4,<6" + "guzzlehttp/guzzle": "^6.0" }, "autoload": { "psr-4": { "Goutte\\": "Goutte" } }, "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "3.0-dev" } } }