<?php
 declare(strict_types=1); namespace Shopify; use Shopify\Exception\UninitializedContextException; use Shopify\Exception\CookieNotFoundException; use Shopify\Exception\MissingArgumentException; use Psr\Http\Client\ClientExceptionInterface; 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; final class Utils { 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/", (string) $name)) { return $name; } else { return null; } } public static function buildQueryString(array $params): string { $params = array_filter($params, function ($key) { return $key !== 'hmac'; }, ARRAY_FILTER_USE_KEY); array_walk($params, function (&$value, $key) { if (!is_array($value)) { return; } $escapedValues = array_map(function ($value) { return sprintf('"%s"', $value); }, $value); $concatenatedValues = implode(',', $escapedValues); $encapsulatedValues = sprintf('[%s]', $concatenatedValues); $value = $encapsulatedValues; }); return http_build_query($params); } public static function validateHmac(array $params, string $secret): bool { if (empty($params['hmac']) || empty($secret)) { return false; } return hash_equals( $params['hmac'], hash_hmac('sha256', self::buildQueryString($params), $secret) ); } public static function getQueryParams(string $url): array { $queryString = parse_url($url, PHP_URL_QUERY); if (empty($queryString)) { return []; } parse_str($queryString, $params); return $params; } 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; } 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; } public static function loadCurrentSession(array $rawHeaders, array $cookies, bool $isOnline): ?Session { $sessionId = OAuth::getCurrentSessionId($rawHeaders, $cookies, $isOnline); return Context::$SESSION_STORAGE->loadSession($sessionId); } public static function decodeSessionToken(string $jwt): array { JWT::$leeway = 10; $payload = JWT::decode($jwt, new Key(Context::$API_SECRET_KEY, 'HS256')); return (array) $payload; } 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); } 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"; } public static function getVersion(): string { return require(__DIR__ . DIRECTORY_SEPARATOR . 'version.php'); } } 