Skip to content

Commit ba1fed8

Browse files
authored
Refactored query string building to account for array handling (#269)
* Refactored query string building to account for array handling * Updated changelog
1 parent 5063bd6 commit ba1fed8

3 files changed

Lines changed: 96 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ and adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
77

88
## Unreleased
99

10+
- [#269](https://github.com/Shopify/shopify-api-php/pull/269) [bugfix] Refactored query string building to account for Shopify-specific array format
1011
- [#236](https://github.com/Shopify/shopify-api-php/pull/236) [bugfix] Correct requirements in `composer.json`
1112
- [#262](https://github.com/Shopify/shopify-api-php/pull/262) ⚠️ [Breaking] Added support for PHP 8.2, and removed support for PHP 7.4
1213
- [#264](https://github.com/Shopify/shopify-api-php/pull/264) [Patch] Remove support for currently-non-existent versions of PHP (8.3+)

src/Utils.php

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,42 @@ public static function sanitizeShopDomain(string $shop, ?string $myshopifyDomain
6060
}
6161
}
6262

63+
/**
64+
* Builds query strings that are compatible with Shopify's format for array handling
65+
* Example: IDs = [1,2,3]
66+
* PHP would generate: ids[]=1&IDs[]=2&IDs[]=3
67+
* Shopify expects: ids=["1","2","3"] (URL encoded)
68+
*
69+
* @param array $params Array of query parameters
70+
*
71+
* @return string The URL encoded query string ("foo=bar&bar=foo")
72+
*/
73+
public static function buildQueryString(array $params): string
74+
{
75+
// Exclude HMAC from query string
76+
$params = array_filter($params, function ($key) {
77+
return $key !== 'hmac';
78+
}, ARRAY_FILTER_USE_KEY);
79+
80+
// Concatenate arrays to conform with Shopify
81+
array_walk($params, function (&$value, $key) {
82+
if (!is_array($value)) {
83+
return;
84+
}
85+
86+
$escapedValues = array_map(function ($value) {
87+
return sprintf('"%s"', $value);
88+
}, $value);
89+
$concatenatedValues = implode(',', $escapedValues);
90+
$encapsulatedValues = sprintf('[%s]', $concatenatedValues);
91+
92+
$value = $encapsulatedValues;
93+
});
94+
95+
// Building the actual query using PHP's native function
96+
return http_build_query($params);
97+
}
98+
6399
/**
64100
* Determines if request is valid by processing secret key through an HMAC-SHA256 hash function
65101
*
@@ -70,12 +106,14 @@ public static function sanitizeShopDomain(string $shop, ?string $myshopifyDomain
70106
*/
71107
public static function validateHmac(array $params, string $secret): bool
72108
{
73-
$hmac = $params['hmac'] ?? '';
74-
unset($params['hmac']);
75-
76-
$computedHmac = hash_hmac('sha256', http_build_query($params), $secret);
109+
if (empty($params['hmac']) || empty($secret)) {
110+
return false;
111+
}
77112

78-
return hash_equals($hmac, $computedHmac);
113+
return hash_equals(
114+
$params['hmac'],
115+
hash_hmac('sha256', self::buildQueryString($params), $secret)
116+
);
79117
}
80118

81119
/**

tests/UtilsTest.php

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,58 @@ public function sanitizeShopWithCustomDomainsProvider()
8787
];
8888
}
8989

90+
/**
91+
* @dataProvider buildQueryStringProvider
92+
*/
93+
public function testBuildQueryString($params, $queryString)
94+
{
95+
$this->assertEquals($queryString, Utils::buildQueryString($params));
96+
}
97+
98+
public function buildQueryStringProvider()
99+
{
100+
return [
101+
[
102+
[],
103+
''
104+
],
105+
[
106+
[
107+
'hmac' => 'ThisShouldNotAppearInTheString',
108+
'foo' => 'ThisShould',
109+
'bar' => 'AsShouldThis',
110+
],
111+
'foo=ThisShould&bar=AsShouldThis'
112+
],
113+
[
114+
[
115+
'hmac' => 'ThisShouldNotAppearInTheString',
116+
'someArray' => [
117+
'foo',
118+
'bar',
119+
1,
120+
],
121+
],
122+
'someArray=%5B%22foo%22%2C%22bar%22%2C%221%22%5D' // URLEncoded for: ["foo","bar","1"]
123+
],
124+
];
125+
}
126+
127+
public function testEmptyHmac()
128+
{
129+
$this->assertEquals(false, Utils::validateHmac(
130+
[],
131+
'I AM SECRET'
132+
));
133+
134+
$this->assertEquals(false, Utils::validateHmac(
135+
[
136+
'hmac' => 'SomeHash',
137+
],
138+
''
139+
));
140+
}
141+
90142
public function testValidHmac()
91143
{
92144
// phpcs:ignore

0 commit comments

Comments
 (0)