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