1: <?php
2: /*
3: * SimpleID
4: *
5: * Copyright (C) Kelvin Mo 2014-2025
6: *
7: * This program is free software; you can redistribute it and/or
8: * modify it under the terms of the GNU General Public
9: * License as published by the Free Software Foundation; either
10: * version 2 of the License, or (at your option) any later version.
11: *
12: * This program is distributed in the hope that it will be useful,
13: * but WITHOUT ANY WARRANTY; without even the implied warranty of
14: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15: * General Public License for more details.
16: *
17: * You should have received a copy of the GNU General Public
18: * License along with this program; if not, write to the Free
19: * Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
20: *
21: */
22:
23: namespace SimpleID\Protocols;
24:
25: /**
26: * Class representing a response from a HTTP request made using the
27: * FatFree framework.
28: */
29: class HTTPResponse {
30: /** @var bool */
31: private $isNetworkError = false;
32: /** @var bool */
33: private $isHTTPError = false;
34:
35: /** @var string */
36: private $version;
37:
38: /** @var string|null */
39: private $responseCode = null;
40:
41: /** @var string */
42: private $body;
43:
44: /** @var array<string, string> */
45: private $headers = [];
46:
47: /**
48: * Constructs a HTTPResponse object from a response made using the
49: * FatFree framework.
50: *
51: * @param array<string, mixed>|false $response the response from the HTTP request
52: * @see https://fatfreeframework.com/3.8/web#request
53: */
54: public function __construct($response) {
55: if ($response === false) {
56: $this->isNetworkError = true;
57: $this->isHTTPError = true;
58: return;
59: }
60:
61: $this->body = $response['body'];
62: $this->readHeaders($response['headers']);
63: }
64:
65: /**
66: * @param array<string> $headers
67: * @return void
68: */
69: private function readHeaders($headers) {
70: // Get the status line
71: $status = array_shift($headers);
72:
73: // Parse the status line
74: list($protocol, $code) = explode(' ', trim($status), 3);
75: $this->version = substr($protocol, strpos($protocol, '/') + 1);
76: $this->responseCode = $code;
77:
78: $valid_codes = [
79: 100, 101,
80: 200, 201, 202, 203, 204, 205, 206,
81: 300, 301, 302, 303, 304, 305, 307,
82: 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417,
83: 500, 501, 502, 503, 504, 505
84: ];
85:
86: // RFC 2616 states that all unknown HTTP codes must be treated the same as the
87: // base code in their class.
88: if (!in_array($code, $valid_codes)) {
89: $this->responseCode = strval(floor(intval($code) / 100) * 100);
90: }
91:
92: $this->isHTTPError = !in_array($this->responseCode, [200, 304]);
93:
94: while ($headers) {
95: // Encountering another HTTP status line means there is a follow-up response
96: // after a redirect. In this case drop all previous headers and start anew.
97: if (preg_match('@^HTTP/\d+(\.\d+)? \d{3}@', $headers[0])) {
98: $this->headers = [];
99: $this->readHeaders($headers);
100: return;
101: }
102: $field = array_shift($headers);
103: list($header, $value) = explode(':', trim($field), 2);
104:
105: // Headers are case insensitive
106: $header = self::httpCase($header);
107:
108: if (isset($this->headers[$header])) {
109: // RFC 2616, section 4.2: Multiple headers with the same field
110: // name is the same as a concatenating all the headers in a single
111: // header, separated by commas.
112: $this->headers[$header] .= ','. trim($value);
113: } else {
114: $this->headers[$header] = trim($value);
115: }
116: }
117: }
118:
119: /**
120: * Returns whether there is a network error
121: *
122: * @return bool true if there is a network error
123: */
124: public function isNetworkError() {
125: return $this->isNetworkError;
126: }
127:
128: /**
129: * Returns whether there is a HTTP or network error
130: *
131: * @return bool true if there is a HTTP or network error
132: */
133: public function isHTTPError() {
134: return $this->isHTTPError;
135: }
136:
137: /**
138: * Returns the body of the HTTP response
139: *
140: * @return string the body of the HTTP response
141: */
142: public function getBody() {
143: return $this->body;
144: }
145:
146: /**
147: * Returns the HTTP response code
148: *
149: * @return string the HTTP response code
150: */
151: public function getResponseCode() {
152: return $this->responseCode;
153: }
154:
155: /**
156: * Returns the HTTP version
157: *
158: * @return string the HTTP version
159: */
160: public function getVersion() {
161: return $this->version;
162: }
163:
164: /**
165: * Returns the value of a specified header
166: *
167: * @param string $header the header to return
168: * @return string the value of the header
169: */
170: public function getHeader($header) {
171: return $this->headers[self::httpCase($header)];
172: }
173:
174: /**
175: * Returns whether a specified header exists
176: *
177: * @param string $header the header
178: * @return bool true if the header exists
179: */
180: public function hasHeader($header) {
181: return array_key_exists(self::httpCase($header), $this->headers);
182: }
183:
184:
185: /**
186: * Returns a string formatted in HTTP case. In HTTP case, the first letter
187: * after each hyphen is capitalised
188: *
189: * @param string $str the string to convert
190: * @return string the converted string
191: */
192: static public function httpCase($str) {
193: return implode('-', array_map('ucfirst', explode('-', strtolower($str))));
194: }
195: }
196:
197: ?>