Skip to content

Commit

Permalink
Merge pull request #2424 from gmponos/issue#2423
Browse files Browse the repository at this point in the history
Add IDN support for redirects also
  • Loading branch information
sagikazarmark committed Dec 20, 2019
2 parents 6ff11ab + a5a84c3 commit 283a724
Show file tree
Hide file tree
Showing 5 changed files with 101 additions and 32 deletions.
32 changes: 2 additions & 30 deletions src/Client.php
Expand Up @@ -215,37 +215,9 @@ private function buildUri($uri, array $config)
$uri = Psr7\UriResolver::resolve(Psr7\uri_for($config['base_uri']), $uri);
}

if ($uri->getHost() && isset($config['idn_conversion']) && ($config['idn_conversion'] !== false)) {
if (isset($config['idn_conversion']) && ($config['idn_conversion'] !== false)) {
$idnOptions = ($config['idn_conversion'] === true) ? IDNA_DEFAULT : $config['idn_conversion'];

$idnaVariant = defined('INTL_IDNA_VARIANT_UTS46') ? INTL_IDNA_VARIANT_UTS46 : 0;
$asciiHost = idn_to_ascii($uri->getHost(), $idnOptions, $idnaVariant, $info);
if ($asciiHost === false) {
$errorBitSet = isset($info['errors']) ? $info['errors'] : 0;

$errorConstants = array_filter(array_keys(get_defined_constants()), function ($name) {
return substr($name, 0, 11) === 'IDNA_ERROR_';
});

$errors = [];
foreach ($errorConstants as $errorConstant) {
if ($errorBitSet & constant($errorConstant)) {
$errors[] = $errorConstant;
}
}

$errorMessage = 'IDN conversion failed';
if ($errors) {
$errorMessage .= ' (errors: ' . implode(', ', $errors) . ')';
}

throw new InvalidArgumentException($errorMessage);
} else {
if ($uri->getHost() !== $asciiHost) {
// Replace URI only if the ASCII version is different
$uri = $uri->withHost($asciiHost);
}
}
$uri = _idn_uri_convert($uri, $idnOptions);
}

return $uri->getScheme() === '' && $uri->getHost() !== '' ? $uri->withScheme('http') : $uri;
Expand Down
10 changes: 8 additions & 2 deletions src/RedirectMiddleware.php
Expand Up @@ -13,7 +13,7 @@
* Request redirect middleware.
*
* Apply this middleware like other middleware using
* {@see GuzzleHttp\Middleware::redirect()}.
* {@see \GuzzleHttp\Middleware::redirect()}.
*/
class RedirectMiddleware
{
Expand Down Expand Up @@ -190,7 +190,13 @@ public function modifyRequest(
$modify['body'] = '';
}

$modify['uri'] = $this->redirectUri($request, $response, $protocols);
$uri = $this->redirectUri($request, $response, $protocols);
if (isset($options['idn_conversion']) && ($options['idn_conversion'] !== false)) {
$idnOptions = ($options['idn_conversion'] === true) ? IDNA_DEFAULT : $options['idn_conversion'];
$uri = _idn_uri_convert($uri, $idnOptions);
}

$modify['uri'] = $uri;
Psr7\rewind_body($request);

// Add the Referer header if it is told to do so and only
Expand Down
46 changes: 46 additions & 0 deletions src/functions.php
@@ -1,10 +1,12 @@
<?php
namespace GuzzleHttp;

use GuzzleHttp\Exception\InvalidArgumentException;
use GuzzleHttp\Handler\CurlHandler;
use GuzzleHttp\Handler\CurlMultiHandler;
use GuzzleHttp\Handler\Proxy;
use GuzzleHttp\Handler\StreamHandler;
use Psr\Http\Message\UriInterface;

/**
* Expands a URI template
Expand Down Expand Up @@ -344,3 +346,47 @@ function _current_time()
{
return function_exists('hrtime') ? hrtime(true) / 1e9 : microtime(true);
}


/**
* @param int $options
*
* @return UriInterface
*
* @internal
*/
function _idn_uri_convert(UriInterface $uri, $options = 0)
{
if ($uri->getHost()) {
$idnaVariant = defined('INTL_IDNA_VARIANT_UTS46') ? INTL_IDNA_VARIANT_UTS46 : 0;
$asciiHost = idn_to_ascii($uri->getHost(), $options, $idnaVariant, $info);
if ($asciiHost === false) {
$errorBitSet = isset($info['errors']) ? $info['errors'] : 0;

$errorConstants = array_filter(array_keys(get_defined_constants()), function ($name) {
return substr($name, 0, 11) === 'IDNA_ERROR_';
});

$errors = [];
foreach ($errorConstants as $errorConstant) {
if ($errorBitSet & constant($errorConstant)) {
$errors[] = $errorConstant;
}
}

$errorMessage = 'IDN conversion failed';
if ($errors) {
$errorMessage .= ' (errors: ' . implode(', ', $errors) . ')';
}

throw new InvalidArgumentException($errorMessage);
} else {
if ($uri->getHost() !== $asciiHost) {
// Replace URI only if the ASCII version is different
$uri = $uri->withHost($asciiHost);
}
}
}

return $uri;
}
34 changes: 34 additions & 0 deletions tests/ClientTest.php
Expand Up @@ -5,11 +5,13 @@
use GuzzleHttp\Cookie\CookieJar;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\Psr7;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\Psr7\Uri;
use GuzzleHttp\RequestOptions;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\ResponseInterface;

Expand Down Expand Up @@ -791,4 +793,36 @@ public function testIdnBaseUri()
self::assertSame('http://xn--d1acpjx3f.xn--p1ai/baz', (string) $mock->getLastRequest()->getUri());
self::assertSame('xn--d1acpjx3f.xn--p1ai', (string) $mock->getLastRequest()->getHeaderLine('Host'));
}

public function testIdnWithRedirect()
{
if (!extension_loaded('intl')) {
self::markTestSkipped('intl PHP extension is not loaded');
}
$mockHandler = new MockHandler([
new Response(302, ['Location' => 'http://www.tést.com/whatever']),
new Response()
]);
$handler = HandlerStack::create($mockHandler);
$requests = [];
$handler->push(Middleware::history($requests));
$client = new Client(['handler' => $handler]);

$client->request('GET', 'https://яндекс.рф/images', [
RequestOptions::ALLOW_REDIRECTS => [
'referer' => true,
'track_redirects' => true
],
'idn_conversion' => true
]);

$request = $mockHandler->getLastRequest();

self::assertSame('http://www.xn--tst-bma.com/whatever', (string) $request->getUri());
self::assertSame('www.xn--tst-bma.com', (string) $request->getHeaderLine('Host'));

$request = $requests[0]['request'];
self::assertSame('https://xn--d1acpjx3f.xn--p1ai/images', (string) $request->getUri());
self::assertSame('xn--d1acpjx3f.xn--p1ai', (string) $request->getHeaderLine('Host'));
}
}
11 changes: 11 additions & 0 deletions tests/functionsTest.php
Expand Up @@ -133,6 +133,17 @@ public function testCurrentTime()
{
self::assertGreaterThan(0, GuzzleHttp\_current_time());
}

public function testIdnConvert()
{
if (!extension_loaded('intl')) {
self::markTestSkipped('intl PHP extension is not loaded');
}

$uri = GuzzleHttp\Psr7\uri_for('https://яндекс.рф/images');
$uri = GuzzleHttp\_idn_uri_convert($uri);
self::assertSame('xn--d1acpjx3f.xn--p1ai', $uri->getHost());
}
}

final class StrClass
Expand Down

0 comments on commit 283a724

Please sign in to comment.