1<?php
2
3namespace FPM;
4
5class Response
6{
7    const HEADER_SEPARATOR = "\r\n\r\n";
8
9    /**
10     * @var array
11     */
12    private $data;
13
14    /**
15     * @var string
16     */
17    private $rawData;
18
19    /**
20     * @var string
21     */
22    private $rawHeaders;
23
24    /**
25     * @var string
26     */
27    private $rawBody;
28
29    /**
30     * @var array
31     */
32    private $headers;
33
34    /**
35     * @var bool
36     */
37    private $valid;
38
39    /**
40     * @var bool
41     */
42    private $expectInvalid;
43
44    /**
45     * @param string|array|null $data
46     * @param bool $expectInvalid
47     */
48    public function __construct($data = null, $expectInvalid = false)
49    {
50        if (!is_array($data)) {
51            $data = [
52                'response' => $data,
53                'err_response' => null,
54                'out_response' => $data,
55            ];
56        }
57
58        $this->data = $data;
59        $this->expectInvalid = $expectInvalid;
60    }
61
62    /**
63     * @param mixed $body
64     * @param string $contentType
65     * @return Response
66     */
67    public function expectBody($body, $contentType = 'text/html')
68    {
69        if ($multiLine = is_array($body)) {
70            $body = implode("\n", $body);
71        }
72
73        if (
74            $this->checkIfValid() &&
75            $this->checkDefaultHeaders($contentType) &&
76            $body !== $this->rawBody
77        ) {
78            if ($multiLine) {
79                $this->error(
80                    "==> The expected body:\n$body\n" .
81                    "==> does not match the actual body:\n$this->rawBody"
82                );
83            } else {
84                $this->error(
85                    "The expected body '$body' does not match actual body '$this->rawBody'"
86                );
87            }
88        }
89
90        return $this;
91    }
92
93    /**
94     * @return Response
95     */
96    public function expectEmptyBody()
97    {
98        return $this->expectBody('');
99    }
100
101    /**
102     * @param string $name
103     * @param string $value
104     * @return Response
105     */
106    public function expectHeader($name, $value)
107    {
108        $this->checkHeader($name, $value);
109
110        return $this;
111    }
112
113    /**
114     * @param string $errorMessage
115     * @return Response
116     */
117    public function expectError($errorMessage)
118    {
119        $errorData = $this->getErrorData();
120        if ($errorData !== $errorMessage) {
121            $this->error(
122                "The expected error message '$errorMessage' is not equal to returned error '$errorData'"
123            );
124        }
125
126        return $this;
127    }
128
129    /**
130     * @param string $contentType
131     * @return string|null
132     */
133    public function getBody($contentType = 'text/html')
134    {
135        if ($this->checkIfValid() && $this->checkDefaultHeaders($contentType)) {
136            return $this->rawBody;
137        }
138
139        return null;
140    }
141
142    /**
143     * Print raw body
144     */
145    public function dumpBody()
146    {
147        var_dump($this->getBody());
148    }
149
150    /**
151     * Print raw body
152     */
153    public function printBody()
154    {
155        echo $this->getBody() . "\n";
156    }
157
158    /**
159     * Debug response output
160     */
161    public function debugOutput()
162    {
163        echo "-------------- RESPONSE: --------------\n";
164        echo "OUT:\n";
165        echo $this->data['out_response'];
166        echo "ERR:\n";
167        echo $this->data['err_response'];
168        echo "---------------------------------------\n\n";
169    }
170
171    /**
172     * @return string|null
173     */
174    public function getErrorData()
175    {
176        return $this->data['err_response'];
177    }
178
179    /**
180     * Check if the response is valid and if not emit error message
181     *
182     * @return bool
183     */
184    private function checkIfValid()
185    {
186        if ($this->isValid()) {
187            return true;
188        }
189
190        if (!$this->expectInvalid) {
191            $this->error("The response is invalid: $this->rawData");
192        }
193
194        return false;
195    }
196
197    /**
198     * @param string $contentType
199     * @return bool
200     */
201    private function checkDefaultHeaders($contentType)
202    {
203        // check default headers
204        return (
205            $this->checkHeader('X-Powered-By', '|^PHP/8|', true) &&
206            $this->checkHeader('Content-type', '|^' . $contentType . '(;\s?charset=\w+)?|', true)
207        );
208    }
209
210    /**
211     * @param string $name
212     * @param string $value
213     * @param bool $useRegex
214     * @return bool
215     */
216    private function checkHeader(string $name, string $value, $useRegex = false)
217    {
218        $lcName = strtolower($name);
219        $headers = $this->getHeaders();
220        if (!isset($headers[$lcName])) {
221            return $this->error("The header $name is not present");
222        }
223        $header = $headers[$lcName];
224
225        if (!$useRegex) {
226            if ($header === $value) {
227                return true;
228            }
229            return $this->error("The header $name value '$header' is not the same as '$value'");
230        }
231
232        if (!preg_match($value, $header)) {
233            return $this->error("The header $name value '$header' does not match RegExp '$value'");
234        }
235
236        return true;
237    }
238
239    /**
240     * @return array|null
241     */
242    private function getHeaders()
243    {
244        if (!$this->isValid()) {
245            return null;
246        }
247
248        if (is_array($this->headers)) {
249            return $this->headers;
250        }
251
252        $headerRows = explode("\r\n", $this->rawHeaders);
253        $headers = [];
254        foreach ($headerRows as $headerRow) {
255            $colonPosition = strpos($headerRow, ':');
256            if ($colonPosition === false) {
257                $this->error("Invalid header row (no colon): $headerRow");
258            }
259            $headers[strtolower(substr($headerRow, 0, $colonPosition))] = trim(
260                substr($headerRow, $colonPosition + 1)
261            );
262        }
263
264        return ($this->headers = $headers);
265    }
266
267    /**
268     * @return bool
269     */
270    private function isValid()
271    {
272        if ($this->valid === null) {
273            $this->processData();
274        }
275
276        return $this->valid;
277    }
278
279    /**
280     * Process data and set validity and raw data
281     */
282    private function processData()
283    {
284        $this->rawData = $this->data['out_response'];
285        $this->valid = (
286            !is_null($this->rawData) &&
287            strpos($this->rawData, self::HEADER_SEPARATOR)
288        );
289        if ($this->valid) {
290            list ($this->rawHeaders, $this->rawBody) = array_map(
291                'trim',
292                explode(self::HEADER_SEPARATOR, $this->rawData)
293            );
294        }
295    }
296
297    /**
298     * Emit error message
299     *
300     * @param string $message
301     * @return bool
302     */
303    private function error($message)
304    {
305        echo "ERROR: $message\n";
306
307        return false;
308    }
309}
310