-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathGuzzleTranscoder.php
154 lines (131 loc) · 5.74 KB
/
GuzzleTranscoder.php
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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
<?php
declare(strict_types=1);
namespace Fossar\GuzzleTranscoder;
use Ddeboer\Transcoder\Transcoder;
use Ddeboer\Transcoder\TranscoderInterface;
use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\Psr7\Utils as Psr7Utils;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
class GuzzleTranscoder {
private ?TranscoderInterface $transcoder;
private string $targetEncoding;
private bool $replaceHeaders;
private bool $replaceContent;
/**
* Constructs a class for transcoding Responses.
*
* @param array{targetEncoding?: string, replaceHeaders?: bool, replaceContent?: bool} $options array supporting the following options
* - string targetEncoding: Encoding the response should be transcoded to (default: 'utf-8')
* - bool replaceHeaders: Whether charset field in Content-Type header should be updated (default: true)
* - bool replaceContent: Whether charset declarations in the body (meta tags, XML declaration) should be updated (default: false)
*/
public function __construct(array $options = []) {
$this->transcoder = null;
$this->targetEncoding = $options['targetEncoding'] ?? 'utf-8';
$this->replaceHeaders = $options['replaceHeaders'] ?? true;
$this->replaceContent = $options['replaceContent'] ?? false;
}
/**
* Returns a transcoder instance.
*/
private function createTranscoder(): TranscoderInterface {
if ($this->transcoder === null) {
$this->transcoder = Transcoder::create();
}
return $this->transcoder;
}
/**
* Converts a PSR response.
*/
public function convert(ResponseInterface $response): ResponseInterface {
$stream = $response->getBody();
/** @var array<string, list<string>> */
$headers = $response->getHeaders();
$result = $this->convertResponse($headers, (string) $stream);
if ($result !== null) {
$body = Psr7Utils::streamFor($result['content']);
$response = $response->withBody($body);
foreach ($result['headers'] as $name => $value) {
$response = $response->withHeader($name, $value);
}
}
return $response;
}
/**
* Called when the middleware is handled by the client.
*
* @template ReasonType
*
* @param callable(RequestInterface, array<string, mixed>): PromiseInterface<ResponseInterface, ReasonType> $handler
*
* @return callable(RequestInterface, array<string, mixed>): PromiseInterface<ResponseInterface, ReasonType>
*/
public function __invoke(callable $handler): callable {
return function(RequestInterface $request, array $options) use ($handler): PromiseInterface {
/** @var array<string, mixed> $options */
$promise = $handler($request, $options);
return $promise->then(fn(ResponseInterface $response): ResponseInterface => $this->convert($response));
};
}
/**
* Converts the given $content to the $targetEncoding.
*
* The original encoding is defined by (in order):
* - the 'charset' parameter of the 'content-type' header
* - the meta information in the body of an HTML (content-type: text/html)or XML (content-type: text/xml or application/xml) document
*
* If the original encoding could not be determined, null is returned.
*
* Otherwise an array containing the new headers and content is returned.
*
* @param array<string, list<string>> $headers
*
* @return ?array{headers: array<string, list<string>>, content: string}
*/
public function convertResponse(array $headers, string $content): ?array {
$headerDeclaredEncoding = null;
$bodyDeclaredEncoding = null;
$headerReplacements = [];
$contentReplacements = [];
// check the header
$type = ContentTypeExtractor::getContentTypeFromHeader($headers, $this->targetEncoding);
if ($type !== null) {
[$contentType, $headerDeclaredEncoding, $params] = $type;
$headerReplacements['content-type'] = [$contentType . (\count($params) > 0 ? '; ' . Utils::joinHttpHeaderWords($params) : '')];
} else {
return null;
}
// else, check the body
if (preg_match('#^text/html#i', $contentType)) {
[$bodyDeclaredEncoding, $contentReplacements] = ContentTypeExtractor::getContentTypeFromHtml($content, $this->targetEncoding);
} elseif (preg_match('#^(text|application)/(.+\+)?xml#i', $contentType)) { // see http://stackoverflow.com/a/3272572/413531
[$bodyDeclaredEncoding, $contentReplacements] = ContentTypeExtractor::getContentTypeFromXml($content, $this->targetEncoding);
}
$finalEncoding = null;
if ($bodyDeclaredEncoding !== null) {
$finalEncoding = $bodyDeclaredEncoding;
} elseif ($headerDeclaredEncoding !== null) {
$finalEncoding = $headerDeclaredEncoding;
} else {
return null;
}
$headers_new = $headers;
if ($this->replaceHeaders) {
foreach ($headerReplacements as $headerKey => $value) {
$headers_new = Utils::setByCaseInsensitiveKey($headers_new, $headerKey, $value);
}
}
$converted = $this->createTranscoder()->transcode($content, $finalEncoding, $this->targetEncoding);
$converted_new = $converted;
if ($this->replaceContent) {
foreach ($contentReplacements as $oldContent => $newContent) {
$converted_new = str_replace($oldContent, $newContent, $converted_new);
}
}
return [
'headers' => $headers_new,
'content' => $converted_new,
];
}
}