Files

134 lines
4.1 KiB
PHP

<?php
declare(strict_types=1);
namespace PaypalV2;
/**
* Duenner HTTP-Client fuer die PayPal-REST-API (Orders v2).
*
* Sicherheit:
* - TLS-Verifikation ist ERZWUNGEN (kein CURLOPT_SSL_VERIFYPEER=false wie im Legacy-NVP).
* - Access-Token wird ausserhalb des Webroots zwischengespeichert (chmod 600).
*/
final class PayPalClient
{
private string $base;
public function __construct()
{
$this->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<string,mixed>|null $payload
* @param list<string> $extraHeaders
* @return array{0:int,1:array<mixed>} [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<string> $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';
}
}