Version: current

      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

[Raise a SilverStripe Framework issue/bug](https://github.com/silverstripe/silverstripe-framework/issues/new)
- [Raise a SilverStripe CMS issue/bug](https://github.com/silverstripe/silverstripe-cms/issues/new)
- Please use the Silverstripe Forums to ask development related questions. -