1<?php
2
3
4namespace ComboStrap;
5
6
7use ComboStrap\Test\BrowserRunner;
8use ComboStrap\Xml\XmlDocument;
9use Exception;
10use TestRequest;
11
12class HttpResponse
13{
14    public const EXIT_KEY = 'exit';
15
16
17    const MESSAGE_ATTRIBUTE = "message";
18    const CANONICAL = "http:response";
19
20    /**
21     * The value must be `Content-type` and not `Content-Type`
22     *
23     * Php will change it this way.
24     * For instance with {@link header()}, the following:
25     * `header("Content-Type: text/html")`
26     * is rewritten as:
27     * `Content-type: text/html;charset=UTF-8`
28     */
29    public const HEADER_CONTENT_TYPE = "Content-type";
30    public const LOCATION_HEADER_NAME = "Location";
31
32    /**
33     * @var int
34     */
35    private $status;
36
37    private $canonical = "support";
38    /**
39     * @var \Doku_Event
40     */
41    private $event;
42    /**
43     * @var array
44     */
45    private $headers = [];
46
47    private string $body;
48    private Mime $mime;
49    private bool $hasEnded = false;
50    private \TestResponse $dokuwikiResponseObject;
51    private ExecutionContext $executionContext;
52
53
54    /**
55     * TODO: constructor should be
56     */
57    private function __construct()
58    {
59
60    }
61
62
63    /**
64     * @throws ExceptionBadArgument
65     */
66    public static function getStatusFromException(\Exception $e): int
67    {
68        if ($e instanceof ExceptionNotFound || $e instanceof ExceptionNotExists) {
69            return HttpResponseStatus::NOT_FOUND;
70        } elseif ($e instanceof ExceptionBadArgument) {
71            return HttpResponseStatus::BAD_REQUEST; // bad request
72        } elseif ($e instanceof ExceptionBadSyntax) {
73            return 415; // unsupported media type
74        } elseif ($e instanceof ExceptionBadState || $e instanceof ExceptionInternal) {
75            return HttpResponseStatus::INTERNAL_ERROR; //
76        }
77        return HttpResponseStatus::INTERNAL_ERROR;
78    }
79
80
81    public static function createFromExecutionContext(ExecutionContext $executionContext): HttpResponse
82    {
83        return (new HttpResponse())->setExecutionContext($executionContext);
84    }
85
86    public function setExecutionContext(ExecutionContext $executionContext): HttpResponse
87    {
88        $this->executionContext = $executionContext;
89        return $this;
90
91    }
92
93    public static function createFromDokuWikiResponse(\TestResponse $response): HttpResponse
94    {
95        $statusCode = $response->getStatusCode();
96        if ($statusCode === null) {
97            $statusCode = HttpResponseStatus::ALL_GOOD;
98        }
99        try {
100            $contentTypeHeader = Http::getFirstHeader(self::HEADER_CONTENT_TYPE, $response->getHeaders());
101            $contentTypeValue = Http::extractHeaderValue($contentTypeHeader);
102            $mime = Mime::create($contentTypeValue);
103        } catch (ExceptionNotFound|ExceptionNotExists $e) {
104            $mime = Mime::getBinary();
105        }
106        return (new HttpResponse())
107            ->setStatus($statusCode)
108            ->setBody($response->getContent(), $mime)
109            ->setHeaders($response->getHeaders())
110            ->setDokuWikiResponse($response);
111    }
112
113
114    public function setEvent(\Doku_Event $event): HttpResponse
115    {
116        $this->event = $event;
117        return $this;
118    }
119
120
121    public function end()
122    {
123
124        $this->hasEnded = true;
125
126        /**
127         * Execution context can be unset
128         * when it's used via a {@link  self::createFromDokuWikiResponse()}
129         */
130        if (isset($this->executionContext)) {
131            $this->executionContext->closeMainExecutingFetcher();
132        }
133
134        if (isset($this->mime)) {
135            Http::setMime($this->mime->toString());
136        } else {
137            Http::setMime(Mime::PLAIN_TEXT);
138        }
139
140        // header should before the status
141        // because for instance a `"Location` header changes the status to 302
142        foreach ($this->headers as $header) {
143            header($header);
144        }
145
146        if ($this->status !== null) {
147            Http::setStatus($this->status);
148        } else {
149            $status = Http::getStatus();
150            if ($status === null) {
151                Http::setStatus(HttpResponseStatus::INTERNAL_ERROR);
152                LogUtility::log2file("No status was set for this soft exit, the default was set instead", LogUtility::LVL_MSG_ERROR, $this->canonical);
153            }
154        }
155
156        /**
157         * Payload
158         */
159        if (isset($this->body)) {
160            echo $this->body;
161        }
162
163        /**
164         * Exit
165         * (Test run ?)
166         */
167        $isTestRun = ExecutionContext::getActualOrCreateFromEnv()->isTestRun();
168        if (!$isTestRun) {
169            if ($this->status !== HttpResponseStatus::ALL_GOOD && isset($this->body)) {
170                // if this is a 304, there is no body, no message
171                LogUtility::log2file("Bad Http Response: $this->status : {$this->getBody()}", LogUtility::LVL_MSG_ERROR, $this->canonical);
172            }
173            exit;
174        }
175
176        /**
177         * Test run
178         * We can't exit, we need
179         * to send all data back to the {@link TestRequest}
180         *
181         * Note that the {@link TestRequest} exists only in a test
182         * run (note in a normal installation)
183         */
184        $testRequest = TestRequest::getRunning();
185        if (isset($this->body)) {
186            $testRequest->addData(self::EXIT_KEY, $this->body);
187        }
188
189        /**
190         * Output buffer
191         * Stop the buffer
192         * Test request starts a buffer at {@link TestRequest::execute()},
193         * it will capture the body until this point
194         */
195        ob_end_clean();
196        /**
197         * To avoid phpunit warning `Test code or tested code did not (only) close its own output buffers`
198         * and
199         * Send the output to the void
200         */
201        ob_start(function ($value) {
202        });
203
204        /**
205         * We try to set the dokuwiki processing
206         * but it does not work every time
207         * to stop the propagation and prevent the default
208         */
209        if ($this->event !== null) {
210            $this->event->stopPropagation();
211            $this->event->preventDefault();
212        }
213
214        /**
215         * In test, we don't exit to get the data, the code execution will come here then
216         * but {@link act_dispatch() Act dispatch} calls always the template,
217         * We create a fake empty template
218         */
219        global $conf;
220        $template = "combo_test";
221        $conf['template'] = $template;
222        $main = LocalPath::createFromPathString(DOKU_INC . "lib/tpl/$template/main.php");
223        FileSystems::setContent($main, "");
224
225    }
226
227    public
228    function setCanonical($canonical): HttpResponse
229    {
230        $this->canonical = $canonical;
231        return $this;
232    }
233
234
235    public
236    function addHeader(string $header): HttpResponse
237    {
238        $this->headers[] = $header;
239        return $this;
240    }
241
242    /**
243     * @param string|array $messages
244     */
245    public
246    function setBodyAsJsonMessage($messages): HttpResponse
247    {
248        if (is_array($messages) && sizeof($messages) == 0) {
249            $messages = ["No information, no errors"];
250        }
251        $message = json_encode(["message" => $messages]);
252        $this->setBody($message, Mime::getJson());
253        return $this;
254    }
255
256
257    public
258    function setBody(string $body, Mime $mime): HttpResponse
259    {
260        $this->body = $body;
261        $this->mime = $mime;
262        return $this;
263    }
264
265    /**
266     * @return string
267     */
268    public
269    function getBody(): string
270    {
271        return $this->body;
272    }
273
274
275    /**
276     */
277    public
278    function getHeaders(string $headerName): array
279    {
280
281        return Http::getHeadersForName($headerName, $this->headers);
282
283    }
284
285    /**
286     * Return the first header value (as an header may have duplicates)
287     * @throws ExceptionNotFound
288     */
289    public
290    function getHeader(string $headerName): string
291    {
292        $headers = $this->getHeaders($headerName);
293        if (count($headers) == 0) {
294            throw new ExceptionNotFound("No header found for the name $headerName");
295        }
296        return $headers[0];
297
298    }
299
300    /**
301     * @throws ExceptionNotFound - if the header was not found
302     * @throws ExceptionNotExists - if the header value could not be identified
303     */
304    public
305    function getHeaderValue(string $headerName): string
306    {
307        $header = $this->getHeader($headerName);
308        return Http::extractHeaderValue($header);
309    }
310
311    public
312    function setHeaders(array $headers): HttpResponse
313    {
314        $this->headers = $headers;
315        return $this;
316    }
317
318    /**
319     * @throws ExceptionBadSyntax
320     */
321    public
322    function getBodyAsHtmlDom(): XmlDocument
323    {
324        return XmlDocument::createHtmlDocFromMarkup($this->getBody());
325    }
326
327    public
328    function setStatus(int $status): HttpResponse
329    {
330        $this->status = $status;
331        return $this;
332    }
333
334
335    public
336    function setStatusAndBodyFromException(\Exception $e): HttpResponse
337    {
338
339        try {
340            $this->setStatus(self::getStatusFromException($e));
341        } catch (ExceptionBadArgument $e) {
342            $this->setStatus(HttpResponseStatus::INTERNAL_ERROR);
343        }
344        $this->setBodyAsJsonMessage($e->getMessage());
345        return $this;
346    }
347
348    public
349    function getStatus(): int
350    {
351        return $this->status;
352    }
353
354    public
355    function hasEnded(): bool
356    {
357        return $this->hasEnded;
358    }
359
360    /**
361     * @throws ExceptionBadSyntax
362     */
363    public function getBodyAsJsonArray(): array
364    {
365        return Json::createFromString($this->getBody())->toArray();
366    }
367
368    private function setDokuWikiResponse(\TestResponse $response): HttpResponse
369    {
370        $this->dokuwikiResponseObject = $response;
371        return $this;
372    }
373
374    public function getDokuWikiResponse(): \TestResponse
375    {
376        return $this->dokuwikiResponseObject;
377    }
378
379    /**
380     * @param Exception $e
381     * @return $this
382     */
383    public function setException(Exception $e): HttpResponse
384    {
385        /**
386         * Don't throw an error on exception
387         * as this may be wanted
388         */
389        $message = "<p>{$e->getMessage()}</p>";
390        try {
391            $status = self::getStatusFromException($e);
392            $this->setStatus($status);
393        } catch (ExceptionBadArgument $e) {
394            $this->setStatus(HttpResponseStatus::INTERNAL_ERROR);
395            $message = "<p>{$e->getMessage()}</p>$message";
396        }
397        $this->setBody($message, Mime::getHtml());
398        return $this;
399    }
400
401    /**
402     *
403     */
404    public function getBodyContentType(): string
405    {
406        try {
407            return $this->getHeader(self::HEADER_CONTENT_TYPE);
408        } catch (ExceptionNotFound $e) {
409            return Mime::BINARY_MIME;
410        }
411    }
412
413    /**
414     * @param int $waitTimeInSecondToComplete - the wait time after the load event to complete
415     * @return $this
416     */
417    public function executeBodyAsHtmlPage(int $waitTimeInSecondToComplete = 0): HttpResponse
418    {
419        $browserRunner = BrowserRunner::create();
420        $this->body = $browserRunner
421            ->executeHtmlPage($this->getBody(), $waitTimeInSecondToComplete)
422            ->getHtml();
423        if ($browserRunner->getExitCode() !== 0) {
424            throw new ExceptionRuntime("HtmlPage Execution Error: \n{$browserRunner->getExitMessage()} ");
425        }
426        return $this;
427    }
428
429    /**
430     * @throws ExceptionBadSyntax
431     */
432    public function getBodyAsJson(): Json
433    {
434        return Json::createFromString($this->getBody());
435    }
436
437
438}
439