Repository navigation
Expand file tree
/
Copy pathWebhookPayload.php
More file actions
114 lines (101 loc) · 4.17 KB
/
Copy pathWebhookPayload.php
File metadata and controls
114 lines (101 loc) · 4.17 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
<?php
/**
* @file WebhookPayload.php
*
* @brief Pure payload-formatting helpers used by SendWebhookJob.
*/
namespace APP\plugins\generic\webhook;
class WebhookPayload
{
/**
* Normalize an OJS date string to a plain Y-m-d string, or null if $date is
* empty or cannot be parsed.
*
* Deliberately free of any OJS/PKP dependency so it can be unit-tested
* without bootstrapping OJS (see WebhookUrlValidator).
*/
public static function formatDate(?string $date): ?string
{
if (!$date) {
return null;
}
$timestamp = strtotime($date);
return $timestamp !== false ? date('Y-m-d', $timestamp) : null;
}
/**
* Reduce the controlled-vocabulary keyword entries (`[{"name": ..., "source": ...,
* "identifier": ...}]`) to a plain list of strings, as agreed with the
* Zeitschriftenserver side. Entries without a `name` are dropped.
*/
public static function reduceKeywords(array $keywords): array
{
return array_values(array_filter(array_map(
fn ($keyword) => $keyword['name'] ?? null,
$keywords
)));
}
/**
* The HMAC-SHA256 signature value for $body keyed with $secret, or null if no
* secret is configured. The secret itself never crosses the wire - only this
* signature does, as the X-Webhook-Signature header's value (prefixed with
* "sha256=" by the caller).
*/
public static function signatureValue(string $body, ?string $secret): ?string
{
if (!$secret) {
return null;
}
return hash_hmac('sha256', $body, $secret);
}
/**
* Whether a non-2xx HTTP status from the webhook endpoint is worth retrying with backoff:
*
* - 5xx: the Zeitschriftenserver could not say for sure whether the article already exists
* yet - safe and necessary to retry, since its endpoint is idempotent.
* - 0: no status at all, e.g. a connection dropped mid-response without curl reporting its
* own errno.
* - 408 Request Timeout, 425 Too Early, 429 Too Many Requests: client errors by number, but
* transient by definition - e.g. a reverse proxy rate-limiting a bulk issue publish.
*
* Everything else (other 4xx, and 3xx - redirects are deliberately not followed, since a POST
* may turn into a GET on the way) means the request itself or the configured URL is wrong
* and needs fixing, not repeating.
*/
public static function isRetryableStatus(int $statusCode): bool
{
return $statusCode === 0
|| $statusCode >= 500
|| in_array($statusCode, [408, 425, 429], true);
}
/**
* Reduce a decoded 2xx response body to the fields SendWebhookJob uses, each of a known type:
* `doi`/`id`/`url` as non-empty strings or null, `warnings` as a list of `{code, detail}` with
* a string `code`.
*
* By the time the body is parsed, the webhook has already been delivered - a body that is not
* a JSON object (e.g. `"ok"`, `true`, or invalid JSON), or a field of an unexpected type, must
* therefore never throw: that would make the queue retry, and re-send, a request the receiving
* end already processed, and finally record it as failed.
*/
public static function normalizeResponse(mixed $decoded): array
{
if (!is_array($decoded)) {
$decoded = [];
}
$string = fn ($value) => (is_string($value) || is_int($value) || is_float($value)) && (string) $value !== ''
? (string) $value
: null;
$warnings = is_array($decoded['warnings'] ?? null) ? $decoded['warnings'] : [];
return [
'doi' => $string($decoded['doi'] ?? null),
'id' => $string($decoded['id'] ?? null),
'url' => $string($decoded['url'] ?? null),
'warnings' => array_values(array_filter(array_map(
fn ($warning) => is_array($warning) && $string($warning['code'] ?? null) !== null
? ['code' => $string($warning['code']), 'detail' => $string($warning['detail'] ?? null)]
: null,
$warnings
))),
];
}
}