response.inc 7.6 KB

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