/**
  * Process the request
  * 
  * @throws lots of various exceptions
  */
 public static function process()
 {
     try {
         @session_start();
         // If undergoing maintenance report it as an error
         if (Config::get('maintenance')) {
             throw new RestUndergoingMaintenanceException();
         }
         // Split request path to get tokens
         $path = array();
         if (array_key_exists('PATH_INFO', $_SERVER)) {
             $path = array_filter(explode('/', $_SERVER['PATH_INFO']));
         }
         // Get method from possible headers
         $method = null;
         foreach (array('X_HTTP_METHOD_OVERRIDE', 'REQUEST_METHOD') as $k) {
             if (!array_key_exists($k, $_SERVER)) {
                 continue;
             }
             $method = strtolower($_SERVER[$k]);
         }
         // Record called method (for log), fail if unknown
         if (!in_array($method, array('get', 'post', 'put', 'delete'))) {
             throw new RestMethodNotAllowedException();
         }
         // Get endpoint (first token), fail if none
         $endpoint = array_shift($path);
         if (!$endpoint) {
             throw RestEndpointNotFound();
         }
         // Request data accessor
         self::$request = new RestRequest($method, $endpoint, $path);
         // Because php://input can only be read once for PUT requests we rely on a shared getter
         $input = Request::body();
         // Get request content type from possible headers
         $type = array_key_exists('CONTENT_TYPE', $_SERVER) ? $_SERVER['CONTENT_TYPE'] : null;
         if (!$type && array_key_exists('HTTP_CONTENT_TYPE', $_SERVER)) {
             $type = $_SERVER['HTTP_CONTENT_TYPE'];
         }
         // Parse content type
         $type_parts = array_map('trim', explode(';', $type));
         $type = array_shift($type_parts);
         self::$request->properties['type'] = $type;
         $type_properties = array();
         foreach ($type_parts as $part) {
             $part = array_map('trim', explode('=', $part));
             if (count($part) == 2) {
                 self::$request->properties[$part[0]] = $part[1];
             }
         }
         Logger::debug('Got "' . $method . '" request for endpoint "' . $endpoint . '/' . implode('/', $path) . '" with ' . strlen($input) . ' bytes payload');
         // Parse body
         switch ($type) {
             case 'text/plain':
                 self::$request->rawinput = trim(Utilities::sanitizeInput($input));
                 break;
             case 'application/octet-stream':
                 // Don't sanitize binary input !
                 self::$request->rawinput = $input;
                 break;
             case 'application/x-www-form-urlencoded':
                 $data = array();
                 parse_str($input, $data);
                 self::$request->input = (object) Utilities::sanitizeInput($data);
                 break;
             case 'application/json':
             default:
                 self::$request->input = json_decode(trim(Utilities::sanitizeInput($input)));
         }
         // Get authentication state (fills auth data in relevant classes)
         Auth::isAuthenticated();
         if (Auth::isRemoteApplication()) {
             // Remote applications must honor ACLs
             $application = AuthRemote::application();
             if (!$application->allowedTo($method, $endpoint)) {
                 throw new RestNotAllowedException();
             }
         } else {
             if (Auth::isRemoteUser()) {
                 // Nothing peculiar to do
             } else {
                 if (in_array($method, array('post', 'put', 'delete'))) {
                     // SP or Guest, lets do XSRF check
                     $token_name = 'HTTP_X_SECURITY_TOKEN';
                     $token = array_key_exists($token_name, $_SERVER) ? $_SERVER[$token_name] : '';
                     if ($method == 'post' && array_key_exists('security-token', $_POST)) {
                         $token = $_POST['security-token'];
                     }
                     if (!$token || !Utilities::checkSecurityToken($token)) {
                         throw new RestXSRFTokenInvalidException($token);
                     }
                 }
             }
         }
         // JSONP specifics
         if (array_key_exists('callback', $_GET) && $method != 'get') {
             throw new RestJSONPonlyGETException();
         }
         // Get response filters
         foreach ($_GET as $k => $v) {
             switch ($k) {
                 case 'count':
                 case 'startIndex':
                     if (preg_match('`^[0-9]+$`', $v)) {
                         self::$request->{$k} = (int) $v;
                     }
                     break;
                 case 'format':
                     break;
                 case 'filterOp':
                     if (is_array($v)) {
                         foreach ($v as $p => $f) {
                             self::$request->filterOp[$p] = array();
                             foreach (array('equals', 'startWith', 'contains', 'present') as $k) {
                                 if (array_key_exists($k, $f)) {
                                     self::$request->filterOp[$p][$k] = $f[$k];
                                 }
                             }
                         }
                     }
                     break;
                 case 'sortOrder':
                     if (in_array($v, array('ascending', 'descending'))) {
                         self::$request->sortOrder = $v;
                     }
                     break;
                 case 'updatedSince':
                     // updatedSince takes ISO date, relative N days|weeks|months|years format and epoch timestamp (UTC)
                     $updatedSince = null;
                     if (preg_match('`^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(Z|[+-][0-9]{2}:[0-9]{2})$`', $v)) {
                         // ISO date
                         $localetz = new DateTimeZone(Config::get('default_timezone'));
                         $offset = $localetz->getOffset(new DateTime($v));
                         $updatedSince = strtotime($v) + $offset;
                     } else {
                         if (preg_match('`^([0-9]+)\\s*(hour|day|week|month|year)s?$`', $v, $m)) {
                             // Relative N day|days|week|weeks|month|months|year|years format
                             $updatedSince = strtotime('-' . $m[1] . ' ' . $m[2]);
                         } else {
                             if (preg_match('`^[0-9]+$`', $v)) {
                                 $updatedSince = (int) $v;
                             }
                         }
                     }
                     // Epoch timestamp
                     if (!$updatedSince || !is_numeric($updatedSince)) {
                         throw new RestUpdatedSinceBadFormatException($updatedSince);
                     }
                     self::$request->updatedSince = $updatedSince;
                     break;
             }
         }
         $event = new Event('rest_request', self::$request);
         $data = $event->trigger(function () {
             $request = RestServer::getRequest();
             // Forward to handler, fail if unknown or method not implemented
             $class = ucfirst($request->endpoint) . 'Endpoint';
             if (!file_exists(APPLICATION_BASE . '/classes/endpoints/' . $class . '.class.php') && !file_exists(APPLICATION_BASE . '/classes/core/endpoints/' . $class . '.class.php')) {
                 throw new RestEndpointNotFoundException();
             }
             if (!method_exists($class, $request->method)) {
                 throw new RestMethodNotImplementedException();
             }
             Logger::debug('Forwarding call to ' . $class . '::' . $request->method . '() handler');
             return call_user_func_array($class . '::' . $request->method, $request->path);
         });
         Logger::debug('Got data to send back');
         // Output data
         if (array_key_exists('callback', $_GET)) {
             header('Content-Type: text/javascript');
             $callback = preg_replace('`[^a-z0-9_\\.-]`i', '', $_GET['callback']);
             echo $callback . '(' . json_encode($data) . ');';
             exit;
         }
         if (array_key_exists('iframe_callback', $_GET)) {
             header('Content-Type: text/html');
             $callback = preg_replace('`[^a-z0-9_\\.-]`i', '', $_GET['iframe_callback']);
             echo '<html><body><script type="text/javascript">window.parent.' . $callback . '(' . json_encode($data) . ');</script></body></html>';
             exit;
         }
         header('Content-Type: application/json');
         if ($method == 'post' && $data) {
             RestUtilities::sendResponseCode(201);
             if (substr($data['path'], 0, 1) != '/') {
                 $data['path'] = '/' . $data['path'];
             }
             header('Location: ' . Config::get('application_url') . 'rest.php' . $data['path']);
             $data = $data['data'];
         }
         echo json_encode($data);
     } catch (Exception $e) {
         // Return exceptions as HTTP errors
         $code = $e->getCode();
         if ($code < 400 || $code >= 600) {
             $code = 500;
         }
         RestUtilities::sendResponseCode($code);
         header('Content-Type: application/json');
         echo json_encode(array('message' => $e->getMessage(), 'uid' => method_exists($e, 'getUid') ? $e->getUid() : null, 'details' => method_exists($e, 'getDetails') ? $e->getDetails() : null));
     }
 }