Skip to content

Commit

Permalink
Improve backwards compatibility of multi-param handling
Browse files Browse the repository at this point in the history
 - restore parse_str behavior when using a URI containing a query string
 - improve tests
  • Loading branch information
gauthierm committed Oct 22, 2022
1 parent 11f71e3 commit 09a3f74
Show file tree
Hide file tree
Showing 5 changed files with 230 additions and 244 deletions.
4 changes: 4 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,8 @@ parameters:
-
message: '#invalid type OpenSSLAsymmetricKey#'
path: src/Credentials/RsaClientCredentials.php
-
# This can be removed when upgrading to at least PHPStan 1.1.1
message: '#Function array_is_list not found.#'
path: src/Signature/EncodesUrl.php
reportUnmatchedIgnoredErrors: false
63 changes: 39 additions & 24 deletions src/Signature/EncodesUrl.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,17 @@ protected function baseString(UriInterface $url, $method = 'POST', array $parame
$baseString .= rawurlencode($schemeHostPath) . '&';

parse_str($url->getQuery(), $query);
$data = array_merge($query, $parameters);
$query = $this->normalizeArray($query);
$queryParams = $this->paramsFromData($query, '', false, true);

// normalize data key/values
$data = $this->normalizeArray($data);
$parameters = $this->normalizeArray($parameters);
$otherParams = $this->paramsFromData($parameters);

$baseString .= $this->queryStringFromData($data);
$params = array_merge($queryParams, $otherParams);
// Sort the final key=value strings. This ensures values are also sorted.
sort($params);

$baseString .= implode('%26', $params); // join with ampersand

return $baseString;
}
Expand Down Expand Up @@ -82,18 +87,16 @@ protected function normalizeArray(array $array = [])
* Creates an array of rawurlencoded strings out of each array key/value pair
* Handles multi-dimensional arrays recursively.
*
* @param array $data Array of parameters to convert.
* @param array|null $queryParams Array to extend. False by default.
* @param string $prevKey Optional Array key to append
* @param string $isSequential Optional. Whether or not the data is a sequential array.
* @param array $data Array of parameters to convert.
* @param string $prevKey Optional Array key to append
* @param bool $isSequential Optional. Whether or not the data is a sequential array.
* @param bool $useParseStr Optional. Whether or not multi-dimentional data is structured like PHP's parse_str.
*
* @return string rawurlencoded string version of data
* @return array a list of urlencoded key-value param strings.
*/
protected function queryStringFromData($data, $queryParams = null, $prevKey = '', $isSequential = false)
protected function paramsFromData($data, $prevKey = '', $isSequential = false, $useParseStr = false): array
{
if ($initial = (null === $queryParams)) {
$queryParams = [];
}
$params = [];

foreach ($data as $key => $value) {
if ($prevKey) {
Expand All @@ -104,29 +107,41 @@ protected function queryStringFromData($data, $queryParams = null, $prevKey = ''
}
}
if (is_array($value)) {
$queryParams = $this->queryStringFromData($value, $queryParams, $key, $this->isSequentialArray($value));
$params = array_merge(
$params,
$this->paramsFromData($value, $key, ! $useParseStr && $this->isSequentialArray($value))
);
} else {
$queryParams[] = rawurlencode($key . '=' . $value); // join with equals sign
$params[] = rawurlencode($key . '=' . $value); // join with equals sign
}
}

if ($initial) {
// sort here by encoded values to ensure all values are properly
// sorted when parameter names are repeated.
sort($queryParams, SORT_STRING);
return implode('%26', $queryParams); // join with ampersand
}
return $params;
}

return $queryParams;
/**
* Creates an array of rawurlencoded strings out of each array key/value pair
* Handles multi-dimensional arrays recursively.
*
* @param array $data Array of parameters to convert.
* @param array|null $queryParams Array to extend. False by default.
* @param string $prevKey Optional Array key to append
* @param bool $isSequential Optional. Whether or not the data is a sequential array.
*
* @return string rawurlencoded string version of data
*/
protected function queryStringFromData($data, $queryParams = null, $prevKey = '', $isSequential = false)
{
return implode('%26', $this->paramsFromData($data)); // join with ampersand
}

/**
* Gets whether or not the passed array is sequential
* Gets whether or not the passed array is sequential.
*
* @param array $array The array to check.
*
* @return bool true if the array is sequential, false if it contains
* one or more associative or non-sequential keys.
* one or more associative or non-sequential keys.
*/
protected function isSequentialArray(array $array): bool
{
Expand Down
187 changes: 187 additions & 0 deletions tests/EncodesUrlTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
<?php

namespace League\OAuth1\Client\Tests;

use League\OAuth1\Client\Signature\EncodesUrl;
use Mockery as m;
use PHPUnit\Framework\TestCase;

class EncodesUrlClass
{
use EncodesUrl;
}

class EncodesUrlTest extends TestCase
{
protected function tearDown(): void
{
m::close();

parent::tearDown();
}

public function testParamsFromArray()
{
$array = ['a' => 'b'];
$res = $this->invokeParamsFromData($array);

$this->assertSame(
['a%3Db'],
$res
);
}

public function testParamsFromIndexedArray()
{
$array = ['a', 'b'];
$res = $this->invokeParamsFromData($array);

$this->assertSame(
['0%3Da', '1%3Db'],
$res
);
}

public function testParamsFromMultiValueArray()
{
$array = ['test' => ['789', '1234']];
$res = $this->invokeParamsFromData($array);

// Ensure no indices are added to param names.
$this->assertSame(
['test%3D789', 'test%3D1234'],
$res
);
}

public function testBaseStringFromMultiValueParamsArray()
{
$uri = $this->getMockUri();

$params = ['test' => ['789', '1234']];
$res = $this->invokeBaseString($uri, 'GET', $params);

// Ensure duplicate params are sorted by string value and no indices
// are added to param names.
$this->assertSame(
'GET&http%3A%2F%2Fwww.example.com&test%3D1234%26test%3D789',
$res
);
}

public function testBaseStringFromMultiValueQueryString()
{
$uri = $this->getMockUri('&test[0]=789&test[1]=1234');

$res = $this->invokeBaseString($uri, 'GET', []);

$this->assertSame(
'GET&http%3A%2F%2Fwww.example.com&test%5B0%5D%3D789%26test%5B1%5D%3D1234',
$res
);
}

public function testParamsFromMultiDimensionalArray()
{
$array = [
'a' => [
'b' => [
'c' => 'd',
],
'e' => [
'f' => 'g',
],
],
'h' => 'i',
'empty' => '',
'null' => null,
'false' => false,
];

// Convert to query string.
$res = $this->invokeParamsFromData($array);

$this->assertSame(
[
'a%5Bb%5D%5Bc%5D%3Dd',
'a%5Be%5D%5Bf%5D%3Dg',
'h%3Di',
'empty%3D',
'null%3D',
'false%3D',
],
$res
);

// Reverse engineer the string.
$res = array_map('urldecode', $res);

$this->assertSame(
[
'a[b][c]=d',
'a[e][f]=g',
'h=i',
'empty=',
'null=',
'false=',
],
$res
);

// Finally, parse the string back to an array.
parse_str(implode('&', $res), $original_array);

// And ensure it matches the orignal array (approximately).
$this->assertSame(
[
'a' => [
'b' => [
'c' => 'd',
],
'e' => [
'f' => 'g',
],
],
'h' => 'i',
'empty' => '',
'null' => '', // null value gets lost in string translation
'false' => '', // false value gets lost in string translation
],
$original_array
);
}

protected function invokeParamsFromData(array $args)
{
$signature = new EncodesUrlClass(m::mock('League\OAuth1\Client\Credentials\ClientCredentialsInterface'));
$refl = new \ReflectionObject($signature);
$method = $refl->getMethod('paramsFromData');
$method->setAccessible(true);

return $method->invokeArgs($signature, [$args]);
}

protected function invokeBaseString(...$args)
{
$signature = new EncodesUrlClass(m::mock('League\OAuth1\Client\Credentials\ClientCredentialsInterface'));
$refl = new \ReflectionObject($signature);
$method = $refl->getMethod('baseString');
$method->setAccessible(true);

return $method->invokeArgs($signature, $args);
}

protected function getMockUri(string $queryString = '')
{
$uri = m::mock('Psr\Http\Message\UriInterface');
$uri->shouldReceive([
'getScheme' => 'http',
'getHost' => 'www.example.com',
'getPort' => null,
'getPath' => '',
'getQuery' => $queryString,
]);

return $uri;
}
}
Loading

0 comments on commit 09a3f74

Please sign in to comment.