class RESTServer in Services 6.3
Same name and namespace in other branches
- 7.3 servers/rest_server/includes/RESTServer.inc \RESTServer
@file Class for handling REST calls.
Hierarchy
- class \RESTServer
Expanded class hierarchy of RESTServer
File
- servers/
rest_server/ includes/ RESTServer.inc, line 8 - Class for handling REST calls.
View source
class RESTServer {
private $endpoint;
/**
* Handles the call to the REST server
*
* @param string $canonical_path
* @param string $endpoint_path
* @return void
*/
public function handle($canonical_path, $endpoint_path) {
$this->endpoint_path = $endpoint_path;
services_set_server_info('resource_uri_formatter', array(
&$this,
'uri_formatter',
));
// Determine the request method
$method = $_SERVER['REQUEST_METHOD'];
if ($method == 'POST' && isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) {
$method = $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'];
}
if ($method == 'POST' && (isset($_GET['_method']) && $_GET['_method'])) {
$method = $_GET['_method'];
}
if (isset($_GET['_method'])) {
unset($_GET['_method']);
}
// Extract response format info from the canonical path.
$matches = array();
$response_format = '';
if (preg_match('/^(.+)\\.([^\\.^\\/]+)$/', $canonical_path, $matches)) {
$canonical_path = $matches[1];
$response_format = $matches[2];
}
// Prepare $path array and $resource_name.
$path = explode('/', $canonical_path);
$resource_name = array_shift($path);
// Response will vary with accept headers
// if no format was supplied as path suffix
if (empty($response_format)) {
drupal_set_header('Vary: Accept');
}
$endpoint = services_get_server_info('endpoint', '');
$endpoint_definition = services_endpoint_load($endpoint);
$this->endpoint = $endpoint_definition;
// Get the server settings from the endpoint.
$this->settings = !empty($endpoint_definition->server_settings['rest_server']) ? $endpoint_definition->server_settings['rest_server'] : array();
// Normalize the settings so that we get the expected structure
// and sensible defaults.
$this->settings = rest_server_setup_settings($this->settings);
$resources = services_get_resources($endpoint);
$controller = FALSE;
if (!empty($resource_name) && isset($resources[$resource_name])) {
$resource = $resources[$resource_name];
// Get the operation and fill with default values
$controller = $this
->resolveController($resource, $method, $path);
}
else {
//This will stop the 404 from happening when you request just the endpoint.
if ($endpoint_definition->path == $resource_name) {
$response = t('Services Endpoint "@name" has been setup successfully.', array(
'@name' => $endpoint,
));
drupal_alter('services_endpoint_response', $response);
return $response;
}
return services_error(t('Could not find resource @name.', array(
'@name' => $resource_name,
)), 404);
}
if (!$controller) {
return services_error(t('Could not find the controller.'), 404);
}
// Parse the request data
$arguments = $this
->getControllerArguments($controller, $path, $method);
// Any authentication needed for REST Server must be set in the cookies
$auth_arguments = $_COOKIE;
$formats = $this
->responseFormatters();
// Negotiate response format based on accept-headers if we
// don't have a response format
if (empty($response_format)) {
$mime_candidates = array();
$mime_map = array();
// Add all formatters that accepts raw data, or supports the format model
foreach ($formats as $format => $formatter) {
if (!isset($formatter['model']) || $this
->supportedControllerModel($controller, $formatter)) {
foreach ($formatter['mime types'] as $m) {
$mime_candidates[] = $m;
$mime_map[$m] = $format;
}
}
}
// Get the best matching format, default to json
$response_format = 'json';
if (isset($_SERVER['HTTP_ACCEPT'])) {
$mime = $this
->mimeParse();
$mime_type = $mime
->best_match($mime_candidates, $_SERVER['HTTP_ACCEPT']);
$response_format = $mime_map[$mime_type];
}
}
// Check if we support the response format and determine the mime type
if (empty($mime_type) && isset($formats[$response_format])) {
$formatter = $formats[$response_format];
if (!isset($formatter['model']) || $this
->supportedControllerModel($controller, $formatter)) {
$mime_type = $formatter['mime types'][0];
}
}
if (empty($response_format) || empty($mime_type)) {
return services_error(t('Unknown or unsupported response format.'), 406);
}
// Give the model (if any) a opportunity to alter the arguments.
// This might be needed for the model to ensure that all the required
// information is requested.
if (isset($formatter['model'])) {
$cm =& $controller['models'][$formatter['model']];
if (!isset($cm['arguments'])) {
$cm['arguments'] = array();
}
// Check if any of the model arguments have been overridden
if (isset($cm['allow_overrides'])) {
foreach ($cm['allow_overrides'] as $arg) {
if (isset($_GET[$formatter['model'] . ':' . $arg])) {
$cm['arguments'][$arg] = $_GET[$formatter['model'] . ':' . $arg];
}
}
}
if (isset($cm['class']) && class_exists($cm['class'])) {
if (method_exists($cm['class'], 'alterArguments')) {
call_user_func_array($cm['class'] . '::alterArguments', array(
&$arguments,
$cm['arguments'],
));
}
}
}
try {
$result = services_controller_execute($controller, $arguments);
} catch (ServicesException $e) {
$errors = $this
->handleException($e);
drupal_alter('rest_server_execute_errors', $errors, $controller, $arguments);
$result = $errors;
}
$formatter = $formats[$response_format];
// Set the content type and render output
drupal_set_header('Content-type: ' . $mime_type);
return $this
->renderFormatterView($controller, $formatter, $result);
}
/**
* Formats a resource uri
*
* @param array $path
* An array of strings containing the component parts of the path to the resource.
* @return string
* Returns the formatted resource uri
*/
public function uri_formatter($path) {
return url($this->endpoint_path . '/' . join($path, '/'), array(
'absolute' => TRUE,
));
}
/**
* Parses controller arguments from request
*
* @param array $controller
* The controller definition
* @param array $path
* @param string $method
* The method used to make the request
* @param array $sources
* An array of the sources used for getting the arguments for the call
* @return void
*/
private function getControllerArguments($controller, $path, $method) {
$data = $this
->parseRequest($method, $controller);
$headers = $this
->parseRequestHeaders();
$sources = array(
'path' => $path,
'param' => $_GET,
'data' => $data,
'headers' => $headers,
);
// Map source data to arguments.
$arguments = array();
if (isset($controller['args'])) {
foreach ($controller['args'] as $i => $info) {
// Fill in argument from source
if (isset($info['source'])) {
if (is_array($info['source'])) {
list($source) = array_keys($info['source']);
$key = $info['source'][$source];
if (isset($sources[$source][$key])) {
$arguments[$i] = $sources[$source][$key];
}
}
else {
if (isset($sources[$info['source']])) {
$arguments[$i] = $sources[$info['source']];
}
}
// Convert to array if argument expected to be array.
if ($info['type'] == 'array' && isset($arguments[$i])) {
$arguments[$i] = (array) $arguments[$i];
}
}
// When argument isn't set, insert default value if provided or
// throw a exception if the argument isn't optional.
if (!isset($arguments[$i])) {
if (!isset($info['optional']) || !$info['optional']) {
return services_error(t('Missing required argument @arg', array(
'@arg' => $info['name'],
)), 401);
}
// Set default value or NULL if default value is not set.
$arguments[$i] = isset($info['default value']) ? $info['default value'] : NULL;
}
}
}
return $arguments;
}
private function parseRequestHeaders() {
$headers = array();
if (!empty($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
$headers['IF_MODIFIED_SINCE'] = strtotime(preg_replace('/;.*$/', '', $_SERVER['HTTP_IF_MODIFIED_SINCE']));
}
return $headers;
}
private function parseRequest($method, $controller) {
switch ($method) {
case 'POST':
case 'PUT':
if (isset($_SERVER['CONTENT_TYPE'])) {
$type = self::parseContentHeader($_SERVER['CONTENT_TYPE']);
}
// Get the mime type for the request, default to form-urlencoded
if (isset($type['value'])) {
$mime = $type['value'];
}
else {
$mime = 'application/x-www-form-urlencoded';
}
// Get the parser for the mime type
$parser = $this
->requestParser($mime, $controller);
if (!$parser) {
return services_error(t('Unsupported request content type @mime', array(
'@mime' => $mime,
)), 406);
}
// php://input is not available with enctype="multipart/form-data".
// see http://php.net/manual/en/wrappers.php.php
if ($mime == 'multipart/form-data') {
$data = call_user_func($parser);
}
else {
// Read the raw input stream.
if (module_exists('inputstream')) {
$handle = fopen('drupal://input', 'r');
}
else {
$handle = fopen('php://input', 'r');
}
if ($handle) {
$data = call_user_func($parser, $handle);
fclose($handle);
}
}
return $data;
default:
return array();
}
}
public static function parseContentHeader($value) {
$ret_val = array();
$value_pattern = '/^([^;]+)(;\\s*(.+)\\s*)?$/';
$param_pattern = '/([a-z]+)=(([^\\"][^;]+)|(\\"(\\\\"|[^"])+\\"))/';
$vm = array();
if (preg_match($value_pattern, $value, $vm)) {
$ret_val['value'] = $vm[1];
if (count($vm) > 2) {
$pm = array();
if (preg_match_all($param_pattern, $vm[3], $pm)) {
$pcount = count($pm[0]);
for ($i = 0; $i < $pcount; $i++) {
$value = $pm[2][$i];
if (drupal_substr($value, 0, 1) == '"') {
$value = stripcslashes(drupal_substr($value, 1, mb_strlen($value) - 2));
}
$ret_val['param'][$pm[1][$i]] = $value;
}
}
}
}
return $ret_val;
}
public static function contentFromStream($handle) {
$content = '';
while (!feof($handle)) {
$content .= fread($handle, 8192);
}
return $content;
}
public static function fileRecieve($handle, $validators = array()) {
$validators['file_validate_name_length'] = array();
$type = RESTServer::parseContentHeader($_SERVER['CONTENT_TYPE']);
$disposition = RESTServer::parseContentHeader($_SERVER['HTTP_CONTENT_DISPOSITION']);
$filename = file_munge_filename(trim(basename($disposition['param']['filename'])));
// Rename potentially executable files, to help prevent exploits.
if (preg_match('/\\.(php|pl|py|cgi|asp|js)$/i', $filename) && drupal_substr($filename, -4) != '.txt') {
$type['value'] = 'text/plain';
$filename .= '.txt';
}
$filepath = file_destination(file_create_path(file_directory_temp() . '/' . $filename), FILE_EXISTS_RENAME);
$file = (object) array(
'uid' => 0,
'filename' => $filename,
'filepath' => $filepath,
'filemime' => $type['value'],
'status' => FILE_STATUS_TEMPORARY,
'timestamp' => time(),
);
RESTServer::streamToFile($handle, $filepath);
$file->filesize = filesize($filepath);
// Call the validation functions.
$errors = array();
foreach ($validators as $function => $args) {
array_unshift($args, $file);
$errors = array_merge($errors, call_user_func_array($function, $args));
}
if (!empty($errors)) {
return services_error(t('Errors while validating the file - @errors', array(
'@errors' => implode(" ", $errors),
)), 406);
}
drupal_write_record('files', $file);
return $file;
}
public static function streamToFile($source, $file) {
$fp = fopen($file, 'w');
if ($fp) {
self::streamCopy($source, $fp);
fclose($fp);
return TRUE;
}
return FALSE;
}
public static function streamCopy($source, $destination) {
while (!feof($source)) {
$content = fread($source, 8192);
fwrite($destination, $content);
}
}
private function renderFormatterView($controller, $formatter, $result) {
// Wrap the results in a model class if required by the formatter
if (isset($formatter['model'])) {
$cm = $controller['models'][$formatter['model']];
$model_arguments = isset($cm['arguments']) ? $cm['arguments'] : array();
$model_class = new ReflectionClass($cm['class']);
$result = $model_class
->newInstanceArgs(array(
$result,
$model_arguments,
));
}
$view_class = new ReflectionClass($formatter['view']);
$view_arguments = isset($formatter['view arguments']) ? $formatter['view arguments'] : array();
$view = $view_class
->newInstanceArgs(array(
$result,
$view_arguments,
));
return $view
->render();
}
/**
* Get best match parser for $controller based on $mime type.
*/
private function requestParser($mime, $controller = NULL) {
// Check if the controller has declared support for parsing the mime type.
if ($controller && !empty($controller['rest request parsers'])) {
$parser = $this
->matchParser($mime, $controller['rest request parsers']);
if ($parser) {
return $parser;
}
}
$parsers = rest_server_request_parsers();
// Remove parsers that have been disabled for this endpoint.
foreach (array_keys($parsers) as $key) {
if (!$this->settings['parsers'][$key]) {
unset($parsers[$key]);
}
}
return $this
->matchParser($mime, $parsers);
}
/**
* Create a instance of the Mimeparse utility class.
*
* @return Mimeparse
*/
private function mimeParse() {
static $mimeparse;
if (!$mimeparse) {
module_load_include('php', 'rest_server', 'lib/mimeparse');
$mimeparse = new Mimeparse();
}
return $mimeparse;
}
private function matchParser($mime, $parsers) {
$mimeparse = $this
->mimeParse();
$mime_type = $mimeparse
->best_match(array_keys($parsers), $mime);
return $mime_type ? $parsers[$mime_type] : FALSE;
}
public static function parseURLEncoded($handle) {
parse_str(self::contentFromStream($handle), $data);
return $data;
}
public static function parsePHP($handle) {
return unserialize(self::contentFromStream($handle));
}
public static function parseFile($handle) {
return self::contentFromStream($handle);
}
public static function parseMultipart() {
return $_POST;
}
public static function parseJSON($handle) {
return json_decode(self::contentFromStream($handle), TRUE);
}
public static function parseYAML($handle) {
include_once _rest_server_get_spyc_location();
return Spyc::YAMLLoadString(self::contentFromStream($handle));
}
private function responseFormatters($format = NULL) {
$formatters = rest_server_response_formatters();
// Remove formatters that have been disabled for this endpoint.
foreach (array_keys($formatters) as $key) {
if (!$this->settings['formatters'][$key]) {
unset($formatters[$key]);
}
}
if ($format) {
return isset($formatters[$format]) ? $formatters[$format] : FALSE;
}
return $formatters;
}
private function supportedControllerModel($controller, $format) {
if (isset($format['model']) && isset($controller['models']) && isset($controller['models'][$format['model']])) {
return $controller['models'][$format['model']];
}
}
function handleException($e) {
$code = $e
->getCode();
switch ($code) {
case 204:
drupal_set_header('HTTP/1.0 204 No Content: ' . $e
->getMessage());
break;
case 304:
drupal_set_header('HTTP/1.0 304 Not Modified: ' . $e
->getMessage());
break;
case 401:
drupal_set_header('HTTP/1.0 401 Unauthorized: ' . $e
->getMessage());
break;
case 404:
drupal_set_header('HTTP/1.0 404 Not found: ' . $e
->getMessage());
break;
case 406:
drupal_set_header('HTTP/1.0 406 Not Acceptable: ' . $e
->getMessage());
break;
default:
if ($code >= 400 && $code < 600) {
drupal_set_header('HTTP/1.0 ' . $code . ' ' . $e
->getMessage());
}
else {
drupal_set_header('HTTP/1.0 500 An error occurred: (' . $code . ') ' . $e
->getMessage());
}
break;
}
if ($this->endpoint->debug) {
watchdog('services', 'Exception throw: <pre>@arguments</pre>', array(
'@arguments' => print_r($e, TRUE),
), WATCHDOG_DEBUG);
}
if (method_exists($e, 'getData')) {
return $e
->getData();
}
}
private function resolveController($resource, $method, $path) {
$pc = count($path);
$class = NULL;
$operation = NULL;
// Use the index handler for all empty path GET-requests
if (!$pc && $method == 'GET') {
$class = 'operations';
$operation = 'index';
}
elseif ($pc == 1 && ($method == 'GET' || $method == 'PUT' || $method == 'DELETE') || $pc == 0 && $method == 'POST') {
$action_mapping = array(
'GET' => 'retrieve',
'POST' => 'create',
'PUT' => 'update',
'DELETE' => 'delete',
);
$class = 'operations';
$operation = $action_mapping[$method];
}
elseif ($pc >= 2 && $method == 'GET') {
$class = 'relationships';
$operation = $path[1];
}
elseif ($pc == 1 && $method == 'POST') {
$class = 'actions';
$operation = $path[0];
}
elseif ($pc >= 2 && $method == 'POST') {
$class = 'targeted_actions';
$operation = $path[1];
}
$controller = FALSE;
if (!empty($class) && !empty($operation) && !empty($resource[$class][$operation])) {
$controller = $resource[$class][$operation];
if (isset($resource['file']) && empty($controller['file'])) {
$controller['file'] = $resource['file'];
}
}
return $controller;
}
}
Members
Name | Modifiers | Type | Description | Overrides |
---|---|---|---|---|
RESTServer:: |
private | property | ||
RESTServer:: |
public static | function | ||
RESTServer:: |
public static | function | ||
RESTServer:: |
private | function | Parses controller arguments from request | |
RESTServer:: |
public | function | Handles the call to the REST server | |
RESTServer:: |
function | |||
RESTServer:: |
private | function | ||
RESTServer:: |
private | function | Create a instance of the Mimeparse utility class. | |
RESTServer:: |
public static | function | ||
RESTServer:: |
public static | function | ||
RESTServer:: |
public static | function | ||
RESTServer:: |
public static | function | ||
RESTServer:: |
public static | function | ||
RESTServer:: |
private | function | ||
RESTServer:: |
private | function | ||
RESTServer:: |
public static | function | ||
RESTServer:: |
public static | function | ||
RESTServer:: |
private | function | ||
RESTServer:: |
private | function | Get best match parser for $controller based on $mime type. | |
RESTServer:: |
private | function | ||
RESTServer:: |
private | function | ||
RESTServer:: |
public static | function | ||
RESTServer:: |
public static | function | ||
RESTServer:: |
private | function | ||
RESTServer:: |
public | function | Formats a resource uri |