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