From f10b32ae28810a64712028f6135a5fb123c8b5f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathias=20Gla=CC=88ser?= Date: Wed, 12 Aug 2026 16:47:24 +0200 Subject: [PATCH] Implementiere read-only Shopify API Client --- .../erp/import-integration/shopify-api.php | 128 ++++++++++++++++++ .../import-integration/shopify-webhook.php | 2 +- 2 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 modules/erp/import-integration/shopify-api.php diff --git a/modules/erp/import-integration/shopify-api.php b/modules/erp/import-integration/shopify-api.php new file mode 100644 index 0000000..1b0abaf --- /dev/null +++ b/modules/erp/import-integration/shopify-api.php @@ -0,0 +1,128 @@ + $value) { + if ($name !== '' && $value !== '') { + $headerLines[] = $name . ': ' . $value; + } + } + + $context = stream_context_create([ + 'http' => [ + 'method' => $method, + 'header' => implode("\r\n", $headerLines), + 'content' => $body, + 'timeout' => $timeoutSeconds, + 'ignore_errors' => true, + ], + ]); + + $responseBody = @file_get_contents($url, false, $context); + $responseHeaders = $http_response_header ?? []; + $status = 0; + if (isset($responseHeaders[0]) && preg_match('#HTTP/\S+\s+(\d{3})#', $responseHeaders[0], $matches) === 1) { + $status = (int) $matches[1]; + } + + if ($responseBody === false || $responseBody === '') { + throw new RuntimeException('Shopify API returned an empty response'); + } + + try { + $decoded = json_decode($responseBody, true, 512, JSON_THROW_ON_ERROR); + } catch (JsonException $exception) { + throw new RuntimeException('Shopify API returned invalid JSON', 0, $exception); + } + + if (!is_array($decoded)) { + throw new RuntimeException('Shopify API returned a non-object JSON response'); + } + + return ['status' => $status, 'body' => $decoded]; +} + +function shopify_api_credentials(array $env): array +{ + $shop = trim(env_value('SHOPIFY_SHOP', $env)); + $clientId = trim(env_value('SHOPIFY_CLIENT_ID', $env)); + $clientSecret = trim(env_value('SHOPIFY_CLIENT_SECRET', $env)); + $apiVersion = trim(env_value('SHOPIFY_API_VERSION', $env)); + + if ($shop === '' || $clientId === '' || $clientSecret === '' || $apiVersion === '') { + throw new RuntimeException('Incomplete Shopify API configuration'); + } + + if (!preg_match('/^[a-z0-9][a-z0-9-]*\.myshopify\.com$/', $shop)) { + throw new RuntimeException('Invalid Shopify shop domain'); + } + + if (!preg_match('/^\d{4}-\d{2}$/', $apiVersion)) { + throw new RuntimeException('Invalid Shopify API version'); + } + + return [ + 'shop' => $shop, + 'client_id' => $clientId, + 'client_secret' => $clientSecret, + 'api_version' => $apiVersion, + ]; +} + +function shopify_api_access_token(array $env): string +{ + $credentials = shopify_api_credentials($env); + $body = http_build_query([ + 'grant_type' => 'client_credentials', + 'client_id' => $credentials['client_id'], + 'client_secret' => $credentials['client_secret'], + ], '', '&', PHP_QUERY_RFC3986); + + $response = shopify_api_request_json( + 'https://' . $credentials['shop'] . '/admin/oauth/access_token', + 'POST', + ['Content-Type' => 'application/x-www-form-urlencoded', 'Accept' => 'application/json'], + $body + ); + + if ($response['status'] < 200 || $response['status'] >= 300) { + throw new RuntimeException('Shopify access-token request failed'); + } + + $token = trim((string) ($response['body']['access_token'] ?? '')); + if ($token === '') { + throw new RuntimeException('Shopify access-token response did not contain a token'); + } + + return $token; +} + +function shopify_graphql_query(array $env, string $query, array $variables = []): array +{ + if (trim($query) === '') { + throw new InvalidArgumentException('Shopify GraphQL query must not be empty'); + } + + $credentials = shopify_api_credentials($env); + $token = shopify_api_access_token($env); + $body = json_encode(['query' => $query, 'variables' => $variables], JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES); + $response = shopify_api_request_json( + 'https://' . $credentials['shop'] . '/admin/api/' . $credentials['api_version'] . '/graphql.json', + 'POST', + ['Content-Type' => 'application/json', 'Accept' => 'application/json', 'X-Shopify-Access-Token' => $token], + $body + ); + + if ($response['status'] < 200 || $response['status'] >= 300) { + throw new RuntimeException('Shopify GraphQL request failed'); + } + + $errors = $response['body']['errors'] ?? []; + if (is_array($errors) && $errors !== []) { + throw new RuntimeException('Shopify GraphQL returned errors'); + } + + return (array) ($response['body']['data'] ?? []); +} diff --git a/modules/erp/import-integration/shopify-webhook.php b/modules/erp/import-integration/shopify-webhook.php index e66b7f4..efae453 100644 --- a/modules/erp/import-integration/shopify-webhook.php +++ b/modules/erp/import-integration/shopify-webhook.php @@ -79,7 +79,7 @@ function shopify_webhook_finish_run(PDO $pdo, int $runId, string $status, array function handle_shopify_webhook(PDO $pdo, array $env, string $rawPayload, array $server): array { $headers = shopify_webhook_headers($server); - $secret = env_value('SHOPIFY_API_SECRET', $env); + $secret = env_value('SHOPIFY_CLIENT_SECRET', $env); if (($server['REQUEST_METHOD'] ?? '') !== 'POST') { return ['http_status' => 405, 'payload' => ['status' => 'rejected', 'error' => 'method_not_allowed']];