1 <?php
2
3 /**
4 * Represents a HTTP-request, including a URL that is tokenised for parsing, and a request method (GET/POST/PUT/DELETE).
5 * This is used by {@link RequestHandler} objects to decide what to do.
6 *
7 * The intention is that a single SS_HTTPRequest object can be passed from one object to another, each object calling
8 * match() to get the information that they need out of the URL. This is generally handled by
9 * {@link RequestHandler::handleRequest()}.
10 *
11 * @todo Accept X_HTTP_METHOD_OVERRIDE http header and $_REQUEST['_method'] to override request types (useful for webclients
12 * not supporting PUT and DELETE)
13 *
14 * @package sapphire
15 * @subpackage control
16 */
17 class SS_HTTPRequest implements ArrayAccess {
18
19 /**
20 * @var string $url
21 */
22 protected $url;
23
24 /**
25 * The non-extension parts of the passed URL as an array, originally exploded by the "/" separator.
26 * All elements of the URL are loaded in here,
27 * and subsequently popped out of the array by {@link shift()}.
28 * Only use this structure for internal request handling purposes.
29 */
30 protected $dirParts;
31
32 /**
33 * @var string $extension The URL extension (if present)
34 */
35 protected $extension;
36
37 /**
38 * @var string $httpMethod The HTTP method in all uppercase: GET/PUT/POST/DELETE/HEAD
39 */
40 protected $httpMethod;
41
42 /**
43 * @var array $getVars Contains alls HTTP GET parameters passed into this request.
44 */
45 protected $getVars = array();
46
47 /**
48 * @var array $postVars Contains alls HTTP POST parameters passed into this request.
49 */
50 protected $postVars = array();
51
52 /**
53 * HTTP Headers like "Content-Type: text/xml"
54 *
55 * @see http://en.wikipedia.org/wiki/List_of_HTTP_headers
56 * @var array
57 */
58 protected $headers = array();
59
60 /**
61 * Raw HTTP body, used by PUT and POST requests.
62 *
63 * @var string
64 */
65 protected $body;
66
67 /**
68 * @var array $allParams Contains an assiciative array of all
69 * arguments matched in all calls to {@link RequestHandler->handleRequest()}.
70 * It's a "historical record" that's specific to the current call of
71 * {@link handleRequest()}, and is only complete once the "last call" to that method is made.
72 */
73 protected $allParams = array();
74
75 /**
76 * @var array $latestParams Contains an associative array of all
77 * arguments matched in the current call from {@link RequestHandler->handleRequest()},
78 * as denoted with a "$"-prefix in the $url_handlers definitions.
79 * Contains different states throughout its lifespan, so just useful
80 * while processed in {@link RequestHandler} and to get the last
81 * processes arguments.
82 */
83 protected $latestParams = array();
84
85 protected $unshiftedButParsedParts = 0;
86
87 /**
88 * Construct a SS_HTTPRequest from a URL relative to the site root.
89 */
90 function __construct($httpMethod, $url, $getVars = array(), $postVars = array(), $body = null) {
91 $this->httpMethod = strtoupper(self::detect_method($httpMethod, $postVars));
92 $this->url = $url;
93
94 if(Director::is_relative_url($url)) {
95 $this->url = preg_replace(array('/\/+/','/^\//', '/\/$/'),array('/','',''), $this->url);
96 }
97 if(preg_match('/^(.*)\.([A-Za-z][A-Za-z0-9]*)$/', $this->url, $matches)) {
98 $this->url = $matches[1];
99 $this->extension = $matches[2];
100 }
101 if($this->url) $this->dirParts = preg_split('|/+|', $this->url);
102 else $this->dirParts = array();
103
104 $this->getVars = (array)$getVars;
105 $this->postVars = (array)$postVars;
106 $this->body = $body;
107 }
108
109 function isGET() {
110 return $this->httpMethod == 'GET';
111 }
112
113 function isPOST() {
114 return $this->httpMethod == 'POST';
115 }
116
117 function isPUT() {
118 return $this->httpMethod == 'PUT';
119 }
120
121 function isDELETE() {
122 return $this->httpMethod == 'DELETE';
123 }
124
125 function isHEAD() {
126 return $this->httpMethod == 'HEAD';
127 }
128
129 function setBody($body) {
130 $this->body = $body;
131 }
132
133 function getBody() {
134 return $this->body;
135 }
136
137 function getVars() {
138 return $this->getVars;
139 }
140 function postVars() {
141 return $this->postVars;
142 }
143
144 /**
145 * Returns all combined HTTP GET and POST parameters
146 * passed into this request. If a parameter with the same
147 * name exists in both arrays, the POST value is returned.
148 *
149 * @return array
150 */
151 function requestVars() {
152 return array_merge($this->getVars, $this->postVars);
153 }
154
155 function getVar($name) {
156 if(isset($this->getVars[$name])) return $this->getVars[$name];
157 }
158
159 function postVar($name) {
160 if(isset($this->postVars[$name])) return $this->postVars[$name];
161 }
162
163 function requestVar($name) {
164 if(isset($this->postVars[$name])) return $this->postVars[$name];
165 if(isset($this->getVars[$name])) return $this->getVars[$name];
166 }
167
168 /**
169 * Returns a possible file extension found in parsing the URL
170 * as denoted by a "."-character near the end of the URL.
171 * Doesn't necessarily have to belong to an existing file,
172 * for example used for {@link RestfulServer} content-type-switching.
173 *
174 * @return string
175 */
176 function getExtension() {
177 return $this->extension;
178 }
179
180 /**
181 * Checks if the {@link SS_HTTPRequest->getExtension()} on this request matches one of the more common media types
182 * embedded into a webpage - e.g. css, png.
183 *
184 * This is useful for things like determining wether to display a fully rendered error page or not. Note that the
185 * media file types is not at all comprehensive.
186 *
187 * @return bool
188 */
189 public function isMedia() {
190 return in_array($this->getExtension(), array('css', 'js', 'jpg', 'jpeg', 'gif', 'png', 'bmp', 'ico'));
191 }
192
193 /**
194 * Add a HTTP header to the response, replacing any header of the same name.
195 *
196 * @param string $header Example: "Content-Type"
197 * @param string $value Example: "text/xml"
198 */
199 function addHeader($header, $value) {
200 $this->headers[$header] = $value;
201 }
202
203 /**
204 * @return array
205 */
206 function getHeaders() {
207 return $this->headers;
208 }
209
210 /**
211 * Remove an existing HTTP header
212 *
213 * @param string $header
214 */
215 function getHeader($header) {
216 return (isset($this->headers[$header])) ? $this->headers[$header] : null;
217 }
218
219 /**
220 * Remove an existing HTTP header by its name,
221 * e.g. "Content-Type".
222 *
223 * @param string $header
224 */
225 function removeHeader($header) {
226 if(isset($this->headers[$header])) unset($this->headers[$header]);
227 }
228
229 /**
230 * @return string
231 */
232 function getURL() {
233 return ($this->getExtension()) ? $this->url . '.' . $this->getExtension() : $this->url;
234 }
235
236 /**
237 * Enables the existence of a key-value pair in the request to be checked using
238 * array syntax, so isset($request['title']) will check for $_POST['title'] and $_GET['title']
239 *
240 * @param unknown_type $offset
241 * @return boolean
242 */
243 function offsetExists($offset) {
244 if(isset($this->postVars[$offset])) return true;
245 if(isset($this->getVars[$offset])) return true;
246 return false;
247 }
248
249 /**
250 * Access a request variable using array syntax. eg: $request['title'] instead of $request->postVar('title')
251 *
252 * @param unknown_type $offset
253 * @return unknown
254 */
255 function offsetGet($offset) {
256 return $this->requestVar($offset);
257 }
258
259 /**
260 * @ignore
261 */
262 function offsetSet($offset, $value) {}
263
264 /**
265 * @ignore
266 */
267 function offsetUnset($offset) {}
268
269 /**
270 * Construct an SS_HTTPResponse that will deliver a file to the client
271 */
272 static function send_file($fileData, $fileName, $mimeType = null) {
273 if(!$mimeType) $mimeType = HTTP::getMimeType($fileName);
274
275 $response = new SS_HTTPResponse($fileData);
276 $response->addHeader("Content-Type", "$mimeType; name=\"" . addslashes($fileName) . "\"");
277 $response->addHeader("Content-disposition", "attachment; filename=" . addslashes($fileName));
278 $response->addHeader("Content-Length", strlen($fileData));
279 $response->addHeader("Pragma", ""); // Necessary because IE has issues sending files over SSL
280
281 return $response;
282 }
283
284 /**
285 * Matches a URL pattern
286 * The pattern can contain a number of segments, separated by / (and an extension indicated by a .)
287 *
288 * The parts can be either literals, or, if they start with a $ they are interpreted as variables.
289 * - Literals must be provided in order to match
290 * - $Variables are optional
291 * - However, if you put ! at the end of a variable, then it becomes mandatory.
292 *
293 * For example:
294 * - admin/crm/list will match admin/crm/$Action/$ID/$OtherID, but it won't match admin/crm/$Action!/$ClassName!
295 *
296 * The pattern can optionally start with an HTTP method and a space. For example, "POST $Controller/$Action".
297 * This is used to define a rule that only matches on a specific HTTP method.
298 */
299 function match($pattern, $shiftOnSuccess = false) {
300 // Check if a specific method is required
301 if(preg_match('/^([A-Za-z]+) +(.*)$/', $pattern, $matches)) {
302 $requiredMethod = $matches[1];
303 if($requiredMethod != $this->httpMethod) return false;
304
305 // If we get this far, we can match the URL pattern as usual.
306 $pattern = $matches[2];
307 }
308
309 // Special case for the root URL controller
310 if(!$pattern) {
311 return ($this->dirParts == array()) ? array('Matched' => true) : false;
312 }
313
314 // Check for the '//' marker that represents the "shifting point"
315 $doubleSlashPoint = strpos($pattern, '//');
316 if($doubleSlashPoint !== false) {
317 $shiftCount = substr_count(substr($pattern,0,$doubleSlashPoint), '/') + 1;
318 $pattern = str_replace('//', '/', $pattern);
319 $patternParts = explode('/', $pattern);
320
321
322 } else {
323 $patternParts = explode('/', $pattern);
324 $shiftCount = sizeof($patternParts);
325 }
326
327 $matched = true;
328 $arguments = array();
329 foreach($patternParts as $i => $part) {
330 $part = trim($part);
331
332 // Match a variable
333 if(isset($part[0]) && $part[0] == '$') {
334 // A variable ending in ! is required
335 if(substr($part,-1) == '!') {
336 $varRequired = true;
337 $varName = substr($part,1,-1);
338 } else {
339 $varRequired = false;
340 $varName = substr($part,1);
341 }
342
343 // Fail if a required variable isn't populated
344 if($varRequired && !isset($this->dirParts[$i])) return false;
345
346 $arguments[$varName] = isset($this->dirParts[$i]) ? $this->dirParts[$i] : null;
347 if($part == '$Controller' && (!ClassInfo::exists($arguments['Controller']) || !ClassInfo::is_subclass_of($arguments['Controller'], 'Controller'))) {
348 return false;
349 }
350
351 // Literal parts with extension
352 } else if(isset($this->dirParts[$i]) && $this->dirParts[$i] . '.' . $this->extension == $part) {
353 continue;
354
355 // Literal parts must always be there
356 } else if(!isset($this->dirParts[$i]) || $this->dirParts[$i] != $part) {
357 return false;
358 }
359
360 }
361
362 if($shiftOnSuccess) {
363 $this->shift($shiftCount);
364 // We keep track of pattern parts that we looked at but didn't shift off.
365 // This lets us say that we have *parsed* the whole URL even when we haven't *shifted* it all
366 $this->unshiftedButParsedParts = sizeof($patternParts) - $shiftCount;
367 }
368
369 $this->latestParams = $arguments;
370
371 // Load the arguments that actually have a value into $this->allParams
372 // This ensures that previous values aren't overridden with blanks
373 foreach($arguments as $k => $v) {
374 if($v || !isset($this->allParams[$k])) $this->allParams[$k] = $v;
375 }
376
377 if($arguments === array()) $arguments['_matched'] = true;
378 return $arguments;
379 }
380
381 function allParams() {
382 return $this->allParams;
383 }
384
385 /**
386 * Shift all the parameter values down a key space, and return the shifted value.
387 *
388 * @return string
389 */
390 public function shiftAllParams() {
391 $keys = array_keys($this->allParams);
392 $values = array_values($this->allParams);
393 $value = array_shift($values);
394
395 // push additional unparsed URL parts onto the parameter stack
396 if(array_key_exists($this->unshiftedButParsedParts, $this->dirParts)) {
397 $values[] = $this->dirParts[$this->unshiftedButParsedParts];
398 }
399
400 foreach($keys as $position => $key) {
401 $this->allParams[$key] = isset($values[$position]) ? $values[$position] : null;
402 }
403
404 return $value;
405 }
406
407 function latestParams() {
408 return $this->latestParams;
409 }
410 function latestParam($name) {
411 if(isset($this->latestParams[$name]))
412 return $this->latestParams[$name];
413 else
414 return null;
415 }
416
417 /**
418 * Finds a named URL parameter (denoted by "$"-prefix in $url_handlers)
419 * from the full URL.
420 *
421 * @param string $name
422 * @return string Value of the URL parameter (if found)
423 */
424 function param($name) {
425 if(isset($this->allParams[$name])) return $this->allParams[$name];
426 else return null;
427 }
428
429 /**
430 * Returns the unparsed part of the original URL
431 * separated by commas. This is used by {@link RequestHandler->handleRequest()}
432 * to determine if further URL processing is necessary.
433 *
434 * @return string Partial URL
435 */
436 function remaining() {
437 return implode("/", $this->dirParts);
438 }
439
440 /**
441 * Returns true if this is a URL that will match without shifting off any of the URL.
442 * This is used by the request handler to prevent infinite parsing loops.
443 */
444 function isEmptyPattern($pattern) {
445 if(preg_match('/^([A-Za-z]+) +(.*)$/', $pattern, $matches)) {
446 $pattern = $matches[2];
447 }
448
449 if(trim($pattern) == "") return true;
450 }
451
452 /**
453 * Shift one or more parts off the beginning of the URL.
454 * If you specify shifting more than 1 item off, then the items will be returned as an array
455 */
456 function shift($count = 1) {
457 if($count == 1) return array_shift($this->dirParts);
458 else for($i=0;$i<$count;$i++) $return[] = array_shift($this->dirParts);
459 }
460
461 /**
462 * Returns true if the URL has been completely parsed.
463 * This will respect parsed but unshifted directory parts.
464 */
465 function allParsed() {
466 return sizeof($this->dirParts) <= $this->unshiftedButParsedParts;
467 }
468
469 /**
470 * Returns the client IP address which
471 * originated this request.
472 *
473 * @return string
474 */
475 function getIP() {
476 if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
477 //check ip from share internet
478 return $_SERVER['HTTP_CLIENT_IP'];
479 } elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
480 //to check ip is pass from proxy
481 return $_SERVER['HTTP_X_FORWARDED_FOR'];
482 } elseif(isset($_SERVER['REMOTE_ADDR'])) {
483 return $_SERVER['REMOTE_ADDR'];
484 }
485 }
486
487 /**
488 * Returns all mimetypes from the HTTP "Accept" header
489 * as an array.
490 *
491 * @param boolean $includeQuality Don't strip away optional "quality indicators", e.g. "application/xml;q=0.9" (Default: false)
492 * @return array
493 */
494 function getAcceptMimetypes($includeQuality = false) {
495 $mimetypes = array();
496 $mimetypesWithQuality = explode(',',$this->getHeader('Accept'));
497 foreach($mimetypesWithQuality as $mimetypeWithQuality) {
498 $mimetypes[] = ($includeQuality) ? $mimetypeWithQuality : preg_replace('/;.*/', '', $mimetypeWithQuality);
499 }
500 return $mimetypes;
501 }
502
503 /**
504 * @return string HTTP method (all uppercase)
505 */
506 public function httpMethod() {
507 return $this->httpMethod;
508 }
509
510 /**
511 * Gets the "real" HTTP method for a request.
512 *
513 * Used to work around browser limitations of form
514 * submissions to GET and POST, by overriding the HTTP method
515 * with a POST parameter called "_method" for PUT, DELETE, HEAD.
516 * Using GET for the "_method" override is not supported,
517 * as GET should never carry out state changes.
518 * Alternatively you can use a custom HTTP header 'X-HTTP-Method-Override'
519 * to override the original method in {@link Director::direct()}.
520 * The '_method' POST parameter overrules the custom HTTP header.
521 *
522 * @param string $origMethod Original HTTP method from the browser request
523 * @param array $postVars
524 * @return string HTTP method (all uppercase)
525 */
526 public static function detect_method($origMethod, $postVars) {
527 if(isset($postVars['_method'])) {
528 if(!in_array(strtoupper($postVars['_method']), array('GET','POST','PUT','DELETE','HEAD'))) {
529 user_error('Director::direct(): Invalid "_method" parameter', E_USER_ERROR);
530 }
531 return strtoupper($postVars['_method']);
532 } else {
533 return $origMethod;
534 }
535 }
536 }
537