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