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