base = Config::apiBase(); } /** OAuth2 client_credentials Access-Token, gecacht bis kurz vor Ablauf. */ public function accessToken(): string { $cacheFile = self::tokenCacheFile(); if (\is_file($cacheFile)) { $c = \json_decode((string) @\file_get_contents($cacheFile), true); if ( \is_array($c) && ($c['mode'] ?? '') === Config::mode() && isset($c['token'], $c['expires']) && (int) $c['expires'] > \time() + 60 ) { return (string) $c['token']; } } $creds = Config::creds(); [$status, $body] = $this->raw( 'POST', '/v1/oauth2/token', 'grant_type=client_credentials', ['Content-Type: application/x-www-form-urlencoded'], $creds['client_id'] . ':' . $creds['secret'] ); $data = \json_decode($body, true); if ($status !== 200 || !\is_array($data) || empty($data['access_token'])) { throw new \RuntimeException('PayPal v2: OAuth2 fehlgeschlagen (HTTP ' . $status . ').'); } @\file_put_contents($cacheFile, \json_encode([ 'token' => (string) $data['access_token'], 'expires' => \time() + (int) ($data['expires_in'] ?? 300), 'mode' => Config::mode(), ]), LOCK_EX); @\chmod($cacheFile, 0600); return (string) $data['access_token']; } /** * Authentifizierter JSON-Request. * * @param array|null $payload * @param list $extraHeaders * @return array{0:int,1:array} [HTTP-Status, dekodierter Body] */ public function request(string $method, string $path, ?array $payload = null, array $extraHeaders = []): array { $headers = \array_merge( [ 'Content-Type: application/json', 'Authorization: Bearer ' . $this->accessToken(), ], $extraHeaders ); $bodyStr = $payload === null ? null : (string) \json_encode($payload, JSON_UNESCAPED_UNICODE); [$status, $body] = $this->raw($method, $path, $bodyStr, $headers); $decoded = $body === '' ? [] : \json_decode($body, true); return [$status, \is_array($decoded) ? $decoded : ['raw' => $body]]; } /** * Roher cURL-Aufruf. * * @param list $headers * @return array{0:int,1:string} */ private function raw(string $method, string $path, ?string $body, array $headers, ?string $basicAuth = null): array { $ch = \curl_init($this->base . $path); \curl_setopt_array($ch, [ CURLOPT_CUSTOMREQUEST => $method, CURLOPT_RETURNTRANSFER => true, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_TIMEOUT => 30, CURLOPT_CONNECTTIMEOUT => 10, CURLOPT_HTTPHEADER => $headers, ]); if ($basicAuth !== null) { \curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); \curl_setopt($ch, CURLOPT_USERPWD, $basicAuth); } if ($body !== null) { \curl_setopt($ch, CURLOPT_POSTFIELDS, $body); } $resp = \curl_exec($ch); if ($resp === false) { $err = \curl_error($ch); \curl_close($ch); throw new \RuntimeException('PayPal v2: HTTP-Transportfehler: ' . $err); } $status = (int) \curl_getinfo($ch, CURLINFO_HTTP_CODE); \curl_close($ch); return [$status, (string) $resp]; } private static function tokenCacheFile(): string { return Config::dir() . '/.token_cache.json'; } }