-
Notifications
You must be signed in to change notification settings - Fork 189
Expand file tree
/
Copy pathUtils.php
More file actions
251 lines (217 loc) · 8.66 KB
/
Utils.php
File metadata and controls
251 lines (217 loc) · 8.66 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
<?php
declare(strict_types=1);
namespace Shopify;
use Shopify\Context;
use Shopify\Auth\OAuth;
use Shopify\Auth\Session;
use Shopify\Clients\Graphql;
use Shopify\Clients\HttpResponse;
use Shopify\Exception\InvalidArgumentException;
use Shopify\Exception\SessionNotFoundException;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
/**
* Class to store all util functions
*/
final class Utils
{
/**
* Returns a sanitized Shopify shop domain
*
* If the provided shop domain or hostname is invalid or could not be sanitized, returns null.
*
* @param string $shop A Shopify shop domain or hostname
* @param string|null $myshopifyDomain A custom Shopify domain
*
* @return string|null $name a sanitized Shopify shop domain, null if the provided domain is invalid
*/
public static function sanitizeShopDomain(string $shop, ?string $myshopifyDomain = null): ?string
{
$name = trim(strtolower($shop));
if ($myshopifyDomain) {
$allowedDomains = [preg_replace("/^\*?\.?(.*)/", "$1", $myshopifyDomain)];
} else {
$allowedDomains = ["myshopify.com", "myshopify.io"];
}
if (Context::$CUSTOM_SHOP_DOMAINS) {
$allowedDomains = array_merge(
$allowedDomains,
preg_replace("/^\*?\.?(.*)/", "$1", Context::$CUSTOM_SHOP_DOMAINS)
);
}
$allowedDomainsRegexp = "(" . implode("|", $allowedDomains) . ")";
if (!preg_match($allowedDomainsRegexp, $name) && (strpos($name, ".") === false)) {
$name .= '.' . ($myshopifyDomain ?? 'myshopify.com');
}
$name = preg_replace("/\A(https?\:\/\/)/", '', $name);
if (preg_match("/\A[a-zA-Z0-9][a-zA-Z0-9\-]*\.{$allowedDomainsRegexp}\z/", $name)) {
return $name;
} else {
return null;
}
}
/**
* Determines if request is valid by processing secret key through an HMAC-SHA256 hash function
*
* @param array $params array of parameters parsed from a URL
* @param string $secret the secret key associated with the app in the Partners Dashboard
*
* @return bool true if the generated hexdigest is equal to the hmac parameter, false otherwise
*/
public static function validateHmac(array $params, string $secret): bool
{
$hmac = $params['hmac'] ?? '';
unset($params['hmac']);
$computedHmac = hash_hmac('sha256', http_build_query($params), $secret);
return hash_equals($hmac, $computedHmac);
}
/**
* Determines if request is valid by processing secret key through an HMAC-SHA256 hash function
*
* @param array $params array of parameters parsed from a URL
* @param string $secret the secret key associated with the app in the Partners Dashboard
*
* @return bool true if the generated hexdigest is equal to the signature parameter, false otherwise
*/
public static function validateSignature(array $params, string $secret): bool
{
$signature = $params['signature'] ?? '';
unset($params['signature']);
$params = array_map(
fn($k, $v) => "{$k}=" . (is_array($v) ? implode(',', $v) : $v),
array_keys($params),
array_values($params)
);
asort($params);
$computedSignature = hash_hmac('sha256', implode($params), $secret);;
return hash_equals($signature, $computedSignature);
}
/**
* Retrieves the query string arguments from a URL, if any
*
* @param string $url the url string with query parameters to be extracted
*
* @return array $params Array of key/value pairs representing the query parameters or empty array
*/
public static function getQueryParams(string $url): array
{
$queryString = parse_url($url, PHP_URL_QUERY);
if (empty($queryString)) {
return [];
}
parse_str($queryString, $params);
return $params;
}
/**
* Checks if the current version of the app (from Context::$API_VERSION) is compatible, i.e. more recent, than the
* given reference version.
*
* @param string $referenceVersion The version to check
*
* @return bool
* @throws \Shopify\Exception\InvalidArgumentException
*/
public static function isApiVersionCompatible(string $referenceVersion): bool
{
if (Context::$API_VERSION === 'unstable' || Context::$API_VERSION === 'unversioned') {
return true;
}
if (!ctype_digit(str_replace('-', '', $referenceVersion))) {
throw new InvalidArgumentException("Reference version '$referenceVersion' is invalid");
}
$currentNumeric = (int)str_replace('-', '', Context::$API_VERSION);
$referenceNumeric = (int)str_replace('-', '', $referenceVersion);
return $currentNumeric >= $referenceNumeric;
}
/**
* Loads an offline session
* No validation is done on the shop param; ensure it comes from a safe source
*
* @param string $shop The shop url to find the offline session for
* @param bool $includeExpired Optionally include expired sessions, defaults to false
*
* @return Session|null If exists, the most recent session
* @throws \Shopify\Exception\UninitializedContextException
*/
public static function loadOfflineSession(string $shop, bool $includeExpired = false): ?Session
{
Context::throwIfUninitialized();
$sessionId = OAuth::getOfflineSessionId($shop);
$session = Context::$SESSION_STORAGE->loadSession($sessionId);
if ($session && !$includeExpired && !$session->isValid()) {
return null;
}
return $session;
}
/**
* Loads the current user's session based on the given headers and cookies.
*
* @param array $rawHeaders The headers from the HTTP request
* @param array $cookies The cookies from the HTTP request
* @param bool $isOnline Whether to load online or offline sessions
*
* @return Session|null The session or null if the session can't be found
* @throws \Shopify\Exception\CookieNotFoundException
* @throws \Shopify\Exception\MissingArgumentException
*/
public static function loadCurrentSession(array $rawHeaders, array $cookies, bool $isOnline): ?Session
{
$sessionId = OAuth::getCurrentSessionId($rawHeaders, $cookies, $isOnline);
return Context::$SESSION_STORAGE->loadSession($sessionId);
}
/**
* Decodes the given session token and extracts the session information from it
*
* @param string $jwt A compact JSON web token in the form of xxxx.yyyy.zzzz
*
* @return array The decoded payload which contains claims about the entity
*/
public static function decodeSessionToken(string $jwt): array
{
JWT::$leeway = 10;
$payload = JWT::decode($jwt, new Key(Context::$API_SECRET_KEY, 'HS256'));
return (array) $payload;
}
/**
* Forwards the GraphQL query in the HTTP request to Shopify, returning the response.
*
* @param array $rawHeaders The headers from the HTTP request
* @param array $cookies The cookies from the HTTP request
* @param string $rawBody The raw HTTP request payload
*
* @return HttpResponse
* @throws \Psr\Http\Client\ClientExceptionInterface
* @throws \Shopify\Exception\CookieNotFoundException
* @throws \Shopify\Exception\MissingArgumentException
* @throws \Shopify\Exception\SessionNotFoundException
* @throws \Shopify\Exception\UninitializedContextException
*/
public static function graphqlProxy(array $rawHeaders, array $cookies, string $rawBody): HttpResponse
{
$session = self::loadCurrentSession($rawHeaders, $cookies, true);
if (!$session) {
throw new SessionNotFoundException("Could not find session for GraphQL proxy");
}
$client = new Graphql($session->getShop(), $session->getAccessToken());
return $client->proxy($rawBody);
}
/**
* Returns the appropriate URL for the host that should load the embedded app.
*
* @param string $host The host value received from Shopify
*
* @return string
*/
public static function getEmbeddedAppUrl(string $host): string
{
if (empty($host)) {
throw new InvalidArgumentException("Host value cannot be empty");
}
$decodedHost = base64_decode($host, true);
if (!$decodedHost) {
throw new InvalidArgumentException("Host was not a valid base64 string");
}
$apiKey = Context::$API_KEY;
return "https://$decodedHost/apps/$apiKey";
}
}