1 | <?php |
||||
2 | |||||
3 | /** |
||||
4 | * @file Request.php |
||||
5 | * @brief This file contains the Request class. |
||||
6 | * @details |
||||
7 | * @author Filippo F. Fadda |
||||
8 | */ |
||||
9 | |||||
10 | |||||
11 | namespace Surfer\Message; |
||||
12 | |||||
13 | |||||
14 | use ToolBag\Helper\ArrayHelper; |
||||
15 | |||||
16 | |||||
17 | /** |
||||
18 | * @brief This class represents an HTTP request. Since CouchDB is a RESTful server, we need make requests through an |
||||
19 | * HTTP client. That's the purpose of this class: emulate an HTTP request. |
||||
20 | * @nosubgrouping |
||||
21 | */ |
||||
22 | final class Request extends Message { |
||||
23 | |||||
24 | /** @name Request Header Fields */ |
||||
25 | //!@{ |
||||
26 | |||||
27 | /** |
||||
28 | * @brief Used to tell the server what media types are okay to send. |
||||
29 | * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1 |
||||
30 | */ |
||||
31 | const ACCEPT_HF = "Accept"; |
||||
32 | |||||
33 | /** |
||||
34 | * @brief Used to tell the server what charsets are okay to send. |
||||
35 | * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.2 |
||||
36 | */ |
||||
37 | const ACCEPT_CHARSET_HF = "Accept-Charset"; |
||||
38 | |||||
39 | /** |
||||
40 | * @brief Used to tell the server what encodings are okay to send. |
||||
41 | * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.3 |
||||
42 | */ |
||||
43 | const ACCEPT_ENCODING_HF = "Accept-Encoding"; |
||||
44 | |||||
45 | /** |
||||
46 | * @brief Used the server which languages are okay to send. |
||||
47 | * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4 |
||||
48 | */ |
||||
49 | const ACCEPT_LANGUAGE_HF = "Accept-Language"; |
||||
50 | |||||
51 | /** |
||||
52 | * @brief Contains the data the client is supplying to the server to authenticate itself. |
||||
53 | * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.8 |
||||
54 | */ |
||||
55 | const AUTHORIZATION_HF = "Authorization"; |
||||
56 | |||||
57 | /** |
||||
58 | * @brief Allows a client to list server behaviors that it requires for a request. |
||||
59 | * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.20 |
||||
60 | */ |
||||
61 | const EXPECT_HF = "Expect"; |
||||
62 | |||||
63 | /** |
||||
64 | * @brief The From request-header field, if given, SHOULD contain an Internet e-mail address for the human user who |
||||
65 | * controls the requesting user agent. |
||||
66 | * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.22 |
||||
67 | */ |
||||
68 | const FROM_HF = "From"; |
||||
69 | |||||
70 | /** |
||||
71 | * @brief Hostname and port of the server to which the request is being sent. |
||||
72 | * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.23 |
||||
73 | */ |
||||
74 | const HOST_HF = "Host"; |
||||
75 | |||||
76 | /** |
||||
77 | * @brief The If-Match request-header field is used with a method to make it conditional. A client that has one or |
||||
78 | * more entities previously obtained from the resource can verify that one of those entities is current by including |
||||
79 | * a list of their associated entity tags in the If-Match header field. |
||||
80 | * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.24 |
||||
81 | */ |
||||
82 | const IF_MATCH_HF = "If-Match"; |
||||
83 | |||||
84 | /** |
||||
85 | * @brief Restricts the request unless the resource has been modified since the specified date. |
||||
86 | * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.25 |
||||
87 | */ |
||||
88 | const IF_MODIFIED_SINCE_HF = "If-Modified-Since"; |
||||
89 | |||||
90 | /** |
||||
91 | * @brief The If-None-Match request-header field is used with a method to make it conditional. A client that has one |
||||
92 | * or more entities previously obtained from the resource can verify that none of those entities is current by including |
||||
93 | * a list of their associated entity tags in the If-None-Match header field. The purpose of this feature is to allow |
||||
94 | * efficient updates of cached information with a minimum amount of transaction overhead. |
||||
95 | * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.26 |
||||
96 | */ |
||||
97 | const IF_NONE_MATCH_HF = "If-None_Match"; |
||||
98 | |||||
99 | /** |
||||
100 | * @brief If a client has a partial copy of an entity in its cache, and wishes to have an up-to-date copy of the entire |
||||
101 | * entity in its cache, it could use the Range request-header with a conditional GET. |
||||
102 | * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.27 |
||||
103 | */ |
||||
104 | const IF_RANGE_HF = "If-Range"; |
||||
105 | |||||
106 | /** |
||||
107 | * @brief Restricts the request unless the resource has not been modified since the specified date. |
||||
108 | * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.28 |
||||
109 | */ |
||||
110 | const IF_UNMODIFIED_SINCE_HF = "If-Unmodified-Since-Header"; |
||||
111 | |||||
112 | /** |
||||
113 | * @brief The maximum number of times a request should be forwarded to another proxy or gateway on its way to the |
||||
114 | * origin server. |
||||
115 | * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.31 |
||||
116 | */ |
||||
117 | const MAX_FORWARDS_HF = "Max-Forwards"; |
||||
118 | |||||
119 | /** |
||||
120 | * @brief Same as AUTHORIZATION_HF, but used when authenticating with a proxy. |
||||
121 | * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.34 |
||||
122 | */ |
||||
123 | const PROXY_AUTHORIZATION_HF = "Proxy-Authorization"; |
||||
124 | |||||
125 | /** |
||||
126 | * @brief Request only part of an entity. Bytes are numbered from 0. |
||||
127 | * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35 |
||||
128 | */ |
||||
129 | const RANGE_HF = "Range"; |
||||
130 | |||||
131 | /** |
||||
132 | * @brief The Referer request-header field allows the client to specify, for the server's benefit, the address (URI) |
||||
133 | * of the resource from which the Request-URI was obtained. |
||||
134 | * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.36 |
||||
135 | */ |
||||
136 | const REFERER_HF = "Referer"; |
||||
137 | |||||
138 | /** |
||||
139 | * @brief The TE request-header field indicates what extension transfer-codings it is willing to accept in the response |
||||
140 | * and whether or not it is willing to accept trailer fields in a chunked transfer-coding. Its value may consist of |
||||
141 | * the keyword "trailers" and/or a comma-separated list of extension transfer-coding names with optional accept |
||||
142 | * parameters. |
||||
143 | * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.39 |
||||
144 | */ |
||||
145 | const TE_HF = "TE"; |
||||
146 | |||||
147 | /** |
||||
148 | * @brief Ask the server to upgrade to another protocol.; |
||||
149 | * @see http://en.wikipedia.org/wiki/Upgrade_HF |
||||
150 | */ |
||||
151 | const UPGRADE_HF = "Upgrade"; |
||||
152 | |||||
153 | /** |
||||
154 | * @brief User agent info. |
||||
155 | * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.43 |
||||
156 | */ |
||||
157 | const USER_AGENT_HF = "User-Agent"; |
||||
158 | |||||
159 | /** |
||||
160 | * @brief Used by clients to pass a token to the server. |
||||
161 | * @see http://en.wikipedia.org/wiki/HTTP_Cookie |
||||
162 | */ |
||||
163 | const COOKIE_HF = "Cookie"; |
||||
164 | |||||
165 | /** |
||||
166 | * @brief Mainly used to identify Ajax requests. Most JavaScript frameworks send this header with value of XMLHttpRequest. |
||||
167 | */ |
||||
168 | const X_REQUESTED_WITH_HF = "X-Requested-With"; |
||||
169 | |||||
170 | /** |
||||
171 | * @brief Requests a web application to disable their tracking of a user. Note that, as of yet, this is largely ignored |
||||
172 | * by web applications. It does however open the door to future legislation requiring web applications to comply with |
||||
173 | * a user's request to not be tracked. Mozilla implements the DNT header with a similar purpose. |
||||
174 | * @see http://en.wikipedia.org/wiki/X-Do-Not-Track |
||||
175 | */ |
||||
176 | const X_DO_NOT_TRACK_HF = "X-Do-Not-Track"; |
||||
177 | |||||
178 | /** |
||||
179 | * @brief Requests a web application to disable their tracking of a user. |
||||
180 | * @details This is Mozilla's version of the X-Do-Not-Track header (since Firefox 4.0 Beta 11). Safari and IE9 also |
||||
181 | * have support for this header.[11] On March 7, 2011, a draft proposal was submitted to IETF. |
||||
182 | */ |
||||
183 | const DNT_HF = "DNT"; |
||||
184 | |||||
185 | /** |
||||
186 | * @brief A de facto standard for identifying the originating IP address of a client connecting to a web server through |
||||
187 | * an HTTP proxy or load balancer. |
||||
188 | * @see http://en.wikipedia.org/wiki/X-Forwarded-For |
||||
189 | */ |
||||
190 | const X_FORWARDED_FOR_HF = "X-Forwarded-For"; |
||||
191 | |||||
192 | const DESTINATION_HF = "Destination"; //!< This header field is not supported by HTTP 1.1. It's a special header field used by CouchDB. |
||||
193 | const X_COUCHDB_WWW_AUTHENTICATE_HF = "X-CouchDB-WWW-Authenticate"; //!< This header field is not supported by HTTP 1.1. It's a special method header field by CouchDB. |
||||
194 | const X_COUCHDB_FULL_COMMIT_HF = "X-Couch-Full-Commit"; //!< This header field is not supported by HTTP 1.1. It's a special header field used by CouchDB. |
||||
195 | |||||
196 | //!@} |
||||
197 | |||||
198 | // Stores the header fields supported by a Request. |
||||
199 | protected static $supportedHeaderFields = array( // Cannot use [] syntax otherwise Doxygen generates a warning. |
||||
200 | self::ACCEPT_HF => NULL, |
||||
201 | self::ACCEPT_CHARSET_HF => NULL, |
||||
202 | self::ACCEPT_ENCODING_HF => NULL, |
||||
203 | self::ACCEPT_LANGUAGE_HF => NULL, |
||||
204 | self::AUTHORIZATION_HF => NULL, |
||||
205 | self::EXPECT_HF => NULL, |
||||
206 | self::FROM_HF => NULL, |
||||
207 | self::HOST_HF => NULL, |
||||
208 | self::IF_MATCH_HF => NULL, |
||||
209 | self::IF_MODIFIED_SINCE_HF => NULL, |
||||
210 | self::IF_NONE_MATCH_HF => NULL, |
||||
211 | self::IF_RANGE_HF => NULL, |
||||
212 | self::IF_UNMODIFIED_SINCE_HF => NULL, |
||||
213 | self::MAX_FORWARDS_HF => NULL, |
||||
214 | self::PROXY_AUTHORIZATION_HF => NULL, |
||||
215 | self::RANGE_HF => NULL, |
||||
216 | self::REFERER_HF => NULL, |
||||
217 | self::TE_HF => NULL, |
||||
218 | self::UPGRADE_HF => NULL, |
||||
219 | self::USER_AGENT_HF => NULL, |
||||
220 | self::COOKIE_HF => NULL, |
||||
221 | self::X_REQUESTED_WITH_HF => NULL, |
||||
222 | self::X_DO_NOT_TRACK_HF => NULL, |
||||
223 | self::DNT_HF => NULL, |
||||
224 | self::X_FORWARDED_FOR_HF => NULL, |
||||
225 | self::DESTINATION_HF => NULL, |
||||
226 | self::X_COUCHDB_WWW_AUTHENTICATE_HF => NULL, |
||||
227 | self::X_COUCHDB_FULL_COMMIT_HF => NULL, |
||||
228 | ); |
||||
229 | |||||
230 | /** @name Request Methods */ |
||||
231 | //!@{ |
||||
232 | const GET_METHOD = "GET"; |
||||
233 | const HEAD_METHOD = "HEAD"; |
||||
234 | const POST_METHOD = "POST"; |
||||
235 | const PUT_METHOD = "PUT"; |
||||
236 | const DELETE_METHOD = "DELETE"; |
||||
237 | const COPY_METHOD = "COPY"; //!< This method is not supported by HTTP 1.1. It's a special method used by CouchDB. |
||||
238 | //!@} |
||||
239 | |||||
240 | // Stores the request methods supported by HTTP 1.1 protocol. |
||||
241 | private static $supportedMethods = array( // Cannot use [] syntax otherwise Doxygen generates a warning. |
||||
242 | self::GET_METHOD => NULL, |
||||
243 | self::HEAD_METHOD => NULL, |
||||
244 | self::POST_METHOD => NULL, |
||||
245 | self::PUT_METHOD => NULL, |
||||
246 | self::DELETE_METHOD => NULL, |
||||
247 | self::COPY_METHOD => NULL |
||||
248 | ); |
||||
249 | |||||
250 | // Used to know if the constructor has been already called. |
||||
251 | private static $initialized = FALSE; |
||||
252 | |||||
253 | // Stores the request method. |
||||
254 | private $method; |
||||
255 | |||||
256 | // Stores the request method. |
||||
257 | private $path; |
||||
258 | |||||
259 | // Stores the request query's parameters. |
||||
260 | private $queryParams = []; |
||||
261 | |||||
262 | |||||
263 | /** |
||||
264 | * @brief Creates an instance of Request class. |
||||
265 | * @param string $method The HTTP method for the request. |
||||
266 | * @param string $path The absolute path of the request. |
||||
267 | * @param array $queryParams (optional) Associative array of query parameters. |
||||
268 | * @param array $headerFields (optional) Associative array of header fields. |
||||
269 | */ |
||||
270 | public function __construct($method, $path, array $queryParams = NULL, array $headerFields = NULL) { |
||||
271 | parent::__construct(); |
||||
272 | |||||
273 | // We can avoid to call the following code every time a Request instance is created, testing a static property. |
||||
274 | // Because the static nature of self::$initialized, this code will be executed only one time, even multiple Request |
||||
275 | // instances are created. |
||||
276 | if (!self::$initialized) { |
||||
277 | self::$initialized = TRUE; |
||||
278 | self::$supportedHeaderFields += parent::$supportedHeaderFields; |
||||
279 | } |
||||
280 | |||||
281 | $this->setMethod($method); |
||||
282 | $this->setPath($path); |
||||
283 | |||||
284 | if (isset($queryParams)) |
||||
285 | $this->setMultipleQueryParamsAtOnce($queryParams); |
||||
286 | |||||
287 | if (isset($headerFields)) |
||||
288 | $this->setMultipleHeaderFieldsAtOnce($headerFields); |
||||
289 | } |
||||
290 | |||||
291 | |||||
292 | /** |
||||
293 | * Returns a comprehensible representation of the HTTP Request to be used for debugging purpose. |
||||
294 | * @retval string |
||||
295 | */ |
||||
296 | public function __toString() { |
||||
297 | $request = [ |
||||
298 | $this->getMethod()." ".$this->getPath().$this->getQueryString(), |
||||
299 | $this->getHeaderAsString(), |
||||
300 | $this->getBody() |
||||
301 | ]; |
||||
302 | |||||
303 | return implode(PHP_EOL.PHP_EOL, $request); |
||||
304 | } |
||||
305 | |||||
306 | |||||
307 | /** |
||||
308 | * @brief Retrieves the HTTP method used by the current request. |
||||
309 | * @retval string |
||||
310 | */ |
||||
311 | public function getMethod() { |
||||
312 | return $this->method; |
||||
313 | } |
||||
314 | |||||
315 | |||||
316 | /** |
||||
317 | * @brief Sets the HTTP method used by the current request. |
||||
318 | * @param string $method The HTTP method for the request. You should use one of the public constants, like GET_METHOD. |
||||
319 | */ |
||||
320 | public function setMethod($method) { |
||||
321 | if (array_key_exists($method, self::$supportedMethods)) |
||||
322 | $this->method = $method; |
||||
323 | else |
||||
324 | throw new \UnexpectedValueException("$method method not supported. Use addCustomMethod() to add an unsupported method."); |
||||
325 | } |
||||
326 | |||||
327 | |||||
328 | /** |
||||
329 | * @brief Adds a non standard HTTP method. |
||||
330 | * @param string $method The HTTP custom method. |
||||
331 | */ |
||||
332 | public static function addCustomMethod($method) { |
||||
333 | if (array_key_exists($method, self::$supportedMethods)) |
||||
334 | throw new \UnexpectedValueException("$method method is supported and already exists."); |
||||
335 | else |
||||
336 | self::$supportedMethods[] = $method; |
||||
337 | } |
||||
338 | |||||
339 | |||||
340 | /** |
||||
341 | * @brief Gets the absolute path for the current request. |
||||
342 | * @retval string |
||||
343 | */ |
||||
344 | public function getPath() { |
||||
345 | return $this->path; |
||||
346 | } |
||||
347 | |||||
348 | |||||
349 | /** |
||||
350 | * @brief Sets the request absolute path. |
||||
351 | * @param string $path The absolute path of the request. |
||||
352 | */ |
||||
353 | public function setPath($path) { |
||||
354 | if (is_string($path)) |
||||
0 ignored issues
–
show
introduced
by
![]() |
|||||
355 | $this->path = addslashes($path); |
||||
356 | else |
||||
357 | throw new \InvalidArgumentException("\$path must be a string."); |
||||
358 | } |
||||
359 | |||||
360 | |||||
361 | /** |
||||
362 | * @brief Used to set a query parameter. You can set many query parameters you want. |
||||
363 | * @param string $name Parameter name. |
||||
364 | * @param string $value Parameter value. |
||||
365 | */ |
||||
366 | public function setQueryParam($name, $value) { |
||||
367 | if (preg_match('/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/', $name)) |
||||
368 | $this->queryParams[$name] = $value; |
||||
369 | else |
||||
370 | throw new \InvalidArgumentException("\$name must start with a letter or underscore, followed by any number of |
||||
371 | letters, numbers, or underscores."); |
||||
372 | } |
||||
373 | |||||
374 | |||||
375 | /** |
||||
376 | * @brief Used to set many parameters at once. |
||||
377 | * @param array $params An associative array of parameters. |
||||
378 | */ |
||||
379 | public function setMultipleQueryParamsAtOnce(array $params) { |
||||
380 | if (ArrayHelper::isAssociative($params)) |
||||
381 | foreach ($params as $name => $value) |
||||
382 | $this->setQueryParam($name, $value); |
||||
383 | else |
||||
384 | throw new \InvalidArgumentException("\$params must be an associative array."); |
||||
385 | } |
||||
386 | |||||
387 | |||||
388 | /** |
||||
389 | * @brief Generates URL-encoded query string. |
||||
390 | * @retval string |
||||
391 | */ |
||||
392 | public function getQueryString() { |
||||
393 | if (empty($this->queryParams)) |
||||
394 | return ""; |
||||
395 | else |
||||
396 | // Encoding is based on RFC 3986. |
||||
397 | return "?".http_build_query($this->queryParams, NULL, "&", PHP_QUERY_RFC3986); |
||||
0 ignored issues
–
show
NULL of type null is incompatible with the type string expected by parameter $numeric_prefix of http_build_query() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
398 | } |
||||
399 | |||||
400 | |||||
401 | /** |
||||
402 | * @brief Parses the given query string and sets every single parameter contained into it. |
||||
403 | */ |
||||
404 | public function setQueryString($value) { |
||||
405 | if (!empty($value)) { |
||||
406 | $query = ltrim($value, "?"); |
||||
407 | |||||
408 | $params = explode('&', $query); |
||||
409 | |||||
410 | foreach ($params as $param) { |
||||
411 | @list($name, $value) = explode('=', $param, 2); |
||||
412 | $this->setQueryParam($name, $value); |
||||
413 | } |
||||
414 | } |
||||
415 | } |
||||
416 | |||||
417 | |||||
418 | /** |
||||
419 | * @brief This helper forces request to use the Authorization Basic mode. |
||||
420 | * @param string $userName User name. |
||||
421 | * @param string $password Password. |
||||
422 | */ |
||||
423 | public function setBasicAuth($userName, $password) { |
||||
424 | $this->setHeaderField(self::AUTHORIZATION_HF, "Basic ".base64_encode("$userName:$password")); |
||||
425 | } |
||||
426 | |||||
427 | |||||
428 | /** |
||||
429 | * @brief This helper forces request to use the Bearer authentication mode. |
||||
430 | * @param string $value The value of the bearer. |
||||
431 | */ |
||||
432 | public function setBearerAuth($value) { |
||||
433 | $this->setHeaderField(self::AUTHORIZATION_HF, "Bearer ".(string)$value); |
||||
434 | } |
||||
435 | |||||
436 | |||||
437 | /** |
||||
438 | * @brief Returns a list of all supported methods. |
||||
439 | * @retval array An associative array |
||||
440 | */ |
||||
441 | public function getSupportedMethods() { |
||||
442 | return self::$supportedMethods; |
||||
443 | } |
||||
444 | |||||
445 | } |