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 $contentType 103 * @return string|null 104 */ 105 public function getBody($contentType = 'text/html') 106 { 107 if ($this->checkIfValid() && $this->checkDefaultHeaders($contentType)) { 108 return $this->rawBody; 109 } 110 111 return null; 112 } 113 114 /** 115 * Print raw body 116 */ 117 public function dumpBody() 118 { 119 var_dump($this->getBody()); 120 } 121 122 /** 123 * Print raw body 124 */ 125 public function printBody() 126 { 127 echo $this->getBody(); 128 } 129 130 /** 131 * Debug response output 132 */ 133 public function debugOutput() 134 { 135 echo "-------------- RESPONSE: --------------\n"; 136 echo "OUT:\n"; 137 echo $this->data['out_response']; 138 echo "ERR:\n"; 139 echo $this->data['err_response']; 140 echo "---------------------------------------\n\n"; 141 } 142 143 /** 144 * @return string|null 145 */ 146 public function getErrorData() 147 { 148 return $this->data['err_response']; 149 } 150 151 /** 152 * Check if the response is valid and if not emit error message 153 * 154 * @return bool 155 */ 156 private function checkIfValid() 157 { 158 if ($this->isValid()) { 159 return true; 160 } 161 162 if (!$this->expectInvalid) { 163 $this->error("The response is invalid: $this->rawData"); 164 } 165 166 return false; 167 } 168 169 /** 170 * @param string $contentType 171 * @return bool 172 */ 173 private function checkDefaultHeaders($contentType) 174 { 175 // check default headers 176 return ( 177 $this->checkHeader('X-Powered-By', '|^PHP/7|', true) && 178 $this->checkHeader('Content-type', '|^' . $contentType . '(;\s?charset=\w+)?|', true) 179 ); 180 } 181 182 /** 183 * @param string $name 184 * @param string $value 185 * @param bool $useRegex 186 * @return bool 187 */ 188 private function checkHeader(string $name, string $value, $useRegex = false) 189 { 190 $lcName = strtolower($name); 191 $headers = $this->getHeaders(); 192 if (!isset($headers[$lcName])) { 193 return $this->error("The header $name is not present"); 194 } 195 $header = $headers[$lcName]; 196 197 if (!$useRegex) { 198 if ($header === $value) { 199 return true; 200 } 201 return $this->error("The header $name value '$header' is not the same as '$value'"); 202 } 203 204 if (!preg_match($value, $header)) { 205 return $this->error("The header $name value '$header' does not match RegExp '$value'"); 206 } 207 208 return true; 209 } 210 211 /** 212 * @return array|null 213 */ 214 private function getHeaders() 215 { 216 if (!$this->isValid()) { 217 return null; 218 } 219 220 if (is_array($this->headers)) { 221 return $this->headers; 222 } 223 224 $headerRows = explode("\r\n", $this->rawHeaders); 225 $headers = []; 226 foreach ($headerRows as $headerRow) { 227 $colonPosition = strpos($headerRow, ':'); 228 if ($colonPosition === false) { 229 $this->error("Invalid header row (no colon): $headerRow"); 230 } 231 $headers[strtolower(substr($headerRow, 0, $colonPosition))] = trim( 232 substr($headerRow, $colonPosition + 1) 233 ); 234 } 235 236 return ($this->headers = $headers); 237 } 238 239 /** 240 * @return bool 241 */ 242 private function isValid() 243 { 244 if ($this->valid === null) { 245 $this->processData(); 246 } 247 248 return $this->valid; 249 } 250 251 /** 252 * Process data and set validity and raw data 253 */ 254 private function processData() 255 { 256 $this->rawData = $this->data['out_response']; 257 $this->valid = ( 258 !is_null($this->rawData) && 259 strpos($this->rawData, self::HEADER_SEPARATOR) 260 ); 261 if ($this->valid) { 262 list ($this->rawHeaders, $this->rawBody) = array_map( 263 'trim', 264 explode(self::HEADER_SEPARATOR, $this->rawData) 265 ); 266 } 267 } 268 269 /** 270 * Emit error message 271 * 272 * @param string $message 273 * @return bool 274 */ 275 private function error($message) 276 { 277 echo "ERROR: $message\n"; 278 279 return false; 280 } 281} 282