Files
cm-app-services2/application/libraries/REST_Controller.php

734 lines
26 KiB
PHP

<?php defined('BASEPATH') or exit('No direct script access allowed');
/**
* REST_controller V 2.5.x
*
* @see https://github.com/philsturgeon/codeigniter-restserver
*
*/
class REST_Controller extends CI_Controller
{
protected $rest_format = null; // Set this in a controller to use a default format
protected $methods = array(); // contains a list of method properties such as limit, log and level
protected $request = null; // Stores accept, language, body, headers, etc
protected $response = null; // What is gonna happen in output?
public $rest = null; // Stores DB, keys, key level, etc
protected $_get_args = array();
protected $_post_args = array();
protected $_put_args = array();
protected $_delete_args = array();
protected $_args = array();
protected $_allow = true;
// List all supported methods, the first will be the default format
protected $_supported_formats = array(
'xml' => 'application/xml',
'rawxml' => 'application/xml',
'json' => 'application/json',
'jsonp' => 'application/javascript',
'serialized' => 'application/vnd.php.serialized',
'php' => 'text/plain',
'html' => 'text/html',
'csv' => 'application/csv',
);
// Constructor function
public function __construct()
{
parent::__construct();
// Lets grab the config and get ready to party
$this->load->config('rest');
if (empty($this->request)) {
$this->request = new stdClass;
}
if (empty($this->rest)) {
$this->rest = new stdClass;
}
// How is this request being made? POST, DELETE, GET, PUT?
$this->request->method = $this->_detect_method();
// Set up our GET variables
$this->_get_args = array_merge($this->_get_args, $this->uri->ruri_to_assoc());
//$this->load->library('security');
// This library is bundled with REST_Controller 2.5+, but will eventually be part of CodeIgniter itself
$this->load->library('format');
// Try to find a format for the request (means we have a request body)
$this->request->format = $this->_detect_input_format();
// Some Methods cant have a body
$this->request->body = null;
switch ($this->request->method) {
case 'get':
// Grab proper GET variables
parse_str(parse_url($_SERVER['REQUEST_URI'], PHP_URL_QUERY), $get);
// If there are any, populate $this->_get_args
empty($get) or $this->_get_args = $get;
break;
case 'post':
$this->_post_args = $_POST;
$this->request->format and $this->request->body = file_get_contents('php://input');
break;
case 'put':
// It might be a HTTP body
if ($this->request->format) {
$this->request->body = file_get_contents('php://input');
}
// If no file type is provided, this is probably just arguments
else {
parse_str(file_get_contents('php://input'), $this->_put_args);
}
break;
case 'delete':
// Set up out DELETE variables (which shouldn't really exist, but sssh!)
parse_str(file_get_contents('php://input'), $this->_delete_args);
break;
}
// Now we know all about our request, let's try and parse the body if it exists
if ($this->request->format and $this->request->body) {
$this->request->body = $this->format->factory($this->request->body, $this->request->format)->to_data();
}
// Merge both for one mega-args variable
$this->_args = array_merge($this->_get_args, $this->_put_args, $this->_post_args, $this->_delete_args);
// Which format should the data be returned in?
if (empty($this->response)) {
$this->response = new stdClass;
}
$this->response->format = $this->_detect_output_format();
// Which format should the data be returned in?
$this->response->lang = $this->_detect_lang();
// Check if there is a specific auth type for the current class/method
$this->auth_override = $this->_auth_override_check();
// When there is no specific override for the current class/method, use the default auth value set in the config
if ($this->auth_override !== true) {
if ($this->config->item('rest_auth') == 'basic') {
$this->_prepare_basic_auth();
} elseif ($this->config->item('rest_auth') == 'digest') {
$this->_prepare_digest_auth();
}
}
// Load DB if its enabled
// if (config_item('rest_database_group') AND (config_item('rest_enable_keys') OR config_item('rest_enable_logging')))
// {
$this->rest->db = $this->load->database(config_item('rest_database_group'), true);
// }
// Checking for keys? GET TO WORK!
if (config_item('rest_enable_keys')) {
$this->_allow = $this->_detect_api_key();
}
// only allow ajax requests
if (!$this->input->is_ajax_request() and config_item('rest_ajax_only')) {
$this->response(array('status' => false, 'error' => 'Only AJAX requests are accepted.'), 505);
}
}
/*
* Remap
*
* Requests are not made to methods directly The request will be for an "object".
* this simply maps the object and method to the correct Controller method.
*/
public function _remap($object_called, $arguments)
{
$pattern = '/^(.*)\.(' . implode('|', array_keys($this->_supported_formats)) . ')$/';
if (preg_match($pattern, $object_called, $matches)) {
$object_called = $matches[1];
}
$controller_method = $object_called . '_' . $this->request->method;
// Do we want to log this method (if allowed by config)?
$log_method = !(isset($this->methods[$controller_method]['log']) and $this->methods[$controller_method]['log'] == false);
// Use keys for this method?
$use_key = !(isset($this->methods[$controller_method]['key']) and $this->methods[$controller_method]['key'] == false);
// Get that useless shitty key out of here
if (config_item('rest_enable_keys') and $use_key and $this->_allow === false) {
if (config_item('rest_enable_logging') and $log_method) {
$this->_log_request();
}
$this->response(array('status' => false, 'error' => 'Invalid API Key.'), 403);
}
// Sure it exists, but can they do anything with it?
if (!method_exists($this, $controller_method)) {
$this->response(array('status' => false, 'error' => 'Unknown method.'), 404);
}
// Doing key related stuff? Can only do it if they have a key right?
if (config_item('rest_enable_keys') and !empty($this->rest->key)) {
// Check the limit
if (config_item('rest_enable_limits') and !$this->_check_limit($controller_method)) {
$this->response(array('status' => false, 'error' => 'This API key has reached the hourly limit for this method.'), 401);
}
// If no level is set use 0, they probably aren't using permissions
$level = isset($this->methods[$controller_method]['level']) ? $this->methods[$controller_method]['level'] : 0;
// If no level is set, or it is lower than/equal to the key's level
$authorized = $level <= $this->rest->level;
// IM TELLIN!
if (config_item('rest_enable_logging') and $log_method) {
$this->_log_request($authorized);
}
// They don't have good enough perms
$authorized or $this->response(array('status' => false, 'error' => 'This API key does not have enough permissions.'), 401);
}
// No key stuff, but record that stuff is happening
else if (config_item('rest_enable_logging') and $log_method) {
$this->_log_request($authorized = true);
}
// And...... GO!
call_user_func_array(array($this, $controller_method), $arguments);
}
/*
* response
*
* Takes pure data and optionally a status code, then creates the response
*/
public function response($data = array(), $http_code = null)
{
// If data is empty and not code provide, error and bail
if (empty($data) && $http_code === null) {
$http_code = 404;
}
// Otherwise (if no data but 200 provided) or some data, carry on camping!
else {
is_numeric($http_code) or $http_code = 200;
// If the format method exists, call and return the output in that format
if (method_exists($this, '_format_' . $this->response->format)) {
// Set the correct format header
header('Content-Type: ' . $this->_supported_formats[$this->response->format]);
$output = $this->{'_format_' . $this->response->format}($data);
}
// If the format method exists, call and return the output in that format
elseif (method_exists($this->format, 'to_' . $this->response->format)) {
// Set the correct format header
header('Content-Type: ' . $this->_supported_formats[$this->response->format]);
$output = $this->format->factory($data)->{'to_' . $this->response->format}();
}
// Format not supported, output directly
else {
$output = $data;
}
}
header('HTTP/1.1: ' . $http_code);
header('Status: ' . $http_code);
header('Content-Length: ' . strlen($output));
exit($output);
}
protected function res($code = 200, $message = 'Success', $data = null)
{
$this->response(array('code' => $code, 'message' => $message, 'data' => $data), 200);
}
/*
* Detect input format
*
* Detect which format the HTTP Body is provided in
*/
protected function _detect_input_format()
{
if ($this->input->server('CONTENT_TYPE')) {
// Check all formats against the HTTP_ACCEPT header
foreach ($this->_supported_formats as $format => $mime) {
if (strpos($match = $this->input->server('CONTENT_TYPE'), ';')) {
$match = current(explode(';', $match));
}
if ($match == $mime) {
return $format;
}
}
}
return null;
}
/*
* Detect format
*
* Detect which format should be used to output the data
*/
protected function _detect_output_format()
{
$pattern = '/\.(' . implode('|', array_keys($this->_supported_formats)) . ')$/';
// Check if a file extension is used
if (preg_match($pattern, $this->uri->uri_string(), $matches)) {
return $matches[1];
}
// Check if a file extension is used
elseif ($this->_get_args and !is_array(end($this->_get_args)) and preg_match($pattern, end($this->_get_args), $matches)) {
// The key of the last argument
$last_key = end(array_keys($this->_get_args));
// Remove the extension from arguments too
$this->_get_args[$last_key] = preg_replace($pattern, '', $this->_get_args[$last_key]);
$this->_args[$last_key] = preg_replace($pattern, '', $this->_args[$last_key]);
return $matches[1];
}
// A format has been passed as an argument in the URL and it is supported
if (isset($this->_get_args['format']) and array_key_exists($this->_get_args['format'], $this->_supported_formats)) {
return $this->_get_args['format'];
}
// Otherwise, check the HTTP_ACCEPT (if it exists and we are allowed)
if ($this->config->item('rest_ignore_http_accept') === false and $this->input->server('HTTP_ACCEPT')) {
// Check all formats against the HTTP_ACCEPT header
foreach (array_keys($this->_supported_formats) as $format) {
// Has this format been requested?
if (strpos($this->input->server('HTTP_ACCEPT'), $format) !== false) {
// If not HTML or XML assume its right and send it on its way
if ($format != 'html' and $format != 'xml') {
return $format;
}
// HTML or XML have shown up as a match
else {
// If it is truely HTML, it wont want any XML
if ($format == 'html' and strpos($this->input->server('HTTP_ACCEPT'), 'xml') === false) {
return $format;
}
// If it is truely XML, it wont want any HTML
elseif ($format == 'xml' and strpos($this->input->server('HTTP_ACCEPT'), 'html') === false) {
return $format;
}
}
}
}
} // End HTTP_ACCEPT checking
// Well, none of that has worked! Let's see if the controller has a default
if (!empty($this->rest_format)) {
return $this->rest_format;
}
// Just use the default format
return config_item('rest_default_format');
}
/*
* Detect method
*
* Detect which method (POST, PUT, GET, DELETE) is being used
*/
protected function _detect_method()
{
$method = strtolower($this->input->server('REQUEST_METHOD'));
if ($this->config->item('enable_emulate_request') && $this->input->post('_method')) {
$method = $this->input->post('_method');
}
if (in_array($method, array('get', 'delete', 'post', 'put'))) {
return $method;
}
return 'get';
}
/*
* Detect API Key
*
* See if the user has provided an API key
*/
protected function _detect_api_key()
{
// Get the api key name variable set in the rest config file
$api_key_variable = config_item('rest_key_name');
// Work out the name of the SERVER entry based on config
$key_name = 'HTTP_' . strtoupper(str_replace('-', '_', $api_key_variable));
$this->rest->key = null;
$this->rest->level = null;
$this->rest->ignore_limits = false;
// Find the key from server or arguments
if ($key = isset($this->_args[$api_key_variable]) ? $this->_args[$api_key_variable] : $this->input->server($key_name)) {
if (!$row = $this->rest->db->where('key', $key)->get(config_item('rest_keys_table'))->row()) {
return false;
}
$this->rest->key = $row->key;
isset($row->level) and $this->rest->level = $row->level;
isset($row->ignore_limits) and $this->rest->ignore_limits = $row->ignore_limits;
return true;
}
// No key has been sent
return false;
}
/*
* Detect language(s)
*
* What language do they want it in?
*/
protected function _detect_lang()
{
if (!$lang = $this->input->server('HTTP_ACCEPT_LANGUAGE')) {
return null;
}
// They might have sent a few, make it an array
if (strpos($lang, ',') !== false) {
$langs = explode(',', $lang);
$return_langs = array();
$i = 1;
foreach ($langs as $lang) {
// Remove weight and strip space
list($lang) = explode(';', $lang);
$return_langs[] = trim($lang);
}
return $return_langs;
}
// Nope, just return the string
return $lang;
}
/*
* Log request
*
* Record the entry for awesomeness purposes
*/
protected function _log_request($authorized = false)
{
return $this->rest->db->insert(config_item('rest_logs_table'), array(
'uri' => $this->uri->uri_string(),
'method' => $this->request->method,
'params' => serialize($this->_args),
'api_key' => isset($this->rest->key) ? $this->rest->key : '',
'ip_address' => $this->input->ip_address(),
'time' => function_exists('now') ? now() : time(),
'authorized' => $authorized,
));
}
/*
* Log request
*
* Record the entry for awesomeness purposes
*/
protected function _check_limit($controller_method)
{
// They are special, or it might not even have a limit
if (!empty($this->rest->ignore_limits) or !isset($this->methods[$controller_method]['limit'])) {
// On your way sonny-jim.
return true;
}
// How many times can you get to this method an hour?
$limit = $this->methods[$controller_method]['limit'];
// Get data on a keys usage
$result = $this->rest->db
->where('uri', $this->uri->uri_string())
->where('api_key', $this->rest->key)
->get(config_item('rest_limits_table'))
->row();
// No calls yet, or been an hour since they called
if (!$result or $result->hour_started < time() - (60 * 60)) {
// Right, set one up from scratch
$this->rest->db->insert(config_item('rest_limits_table'), array(
'uri' => $this->uri->uri_string(),
'api_key' => isset($this->rest->key) ? $this->rest->key : '',
'count' => 1,
'hour_started' => time(),
));
}
// They have called within the hour, so lets update
else {
// Your luck is out, you've called too many times!
if ($result->count >= $limit) {
return false;
}
$this->rest->db
->where('uri', $this->uri->uri_string())
->where('api_key', $this->rest->key)
->set('count', 'count + 1', false)
->update(config_item('rest_limits_table'));
}
return true;
}
/*
* Auth override check
*
* Check if there is a specific auth type set for the current class/method being called
*/
protected function _auth_override_check()
{
// Assign the class/method auth type override array from the config
$this->overrides_array = $this->config->item('auth_override_class_method');
// Check to see if the override array is even populated, otherwise return false
if (empty($this->overrides_array)) {
return false;
}
// Check to see if there's an override value set for the current class/method being called
if (empty($this->overrides_array[$this->router->class][$this->router->method])) {
return false;
}
// None auth override found, prepare nothing but send back a true override flag
if ($this->overrides_array[$this->router->class][$this->router->method] == 'none') {
return true;
}
// Basic auth override found, prepare basic
if ($this->overrides_array[$this->router->class][$this->router->method] == 'basic') {
$this->_prepare_basic_auth();
return true;
}
// Digest auth override found, prepare digest
if ($this->overrides_array[$this->router->class][$this->router->method] == 'digest') {
$this->_prepare_digest_auth();
return true;
}
// Return false when there is an override value set but it doesn't match 'basic', 'digest', or 'none'. (the value was misspelled)
return false;
}
// INPUT FUNCTION --------------------------------------------------------------
public function get($key = null, $xss_clean = true)
{
if ($key === null) {
return $this->_get_args;
}
return array_key_exists($key, $this->_get_args) ? $this->_xss_clean($this->_get_args[$key], $xss_clean) : false;
}
public function post($key = null, $xss_clean = true)
{
if ($key === null) {
return $this->_post_args;
}
return $this->input->post($key, $xss_clean);
}
public function put($key = null, $xss_clean = true)
{
if ($key === null) {
return $this->_put_args;
}
return array_key_exists($key, $this->_put_args) ? $this->_xss_clean($this->_put_args[$key], $xss_clean) : false;
}
public function delete($key = null, $xss_clean = true)
{
if ($key === null) {
return $this->_delete_args;
}
return array_key_exists($key, $this->_delete_args) ? $this->_xss_clean($this->_delete_args[$key], $xss_clean) : false;
}
protected function _xss_clean($val, $bool)
{
if (CI_VERSION < 2) {
return $bool ? $this->input->xss_clean($val) : $val;
} else {
return $bool ? $this->security->xss_clean($val) : $val;
}
}
public function validation_errors()
{
$string = strip_tags($this->form_validation->error_string());
return explode("\n", trim($string, "\n"));
}
// SECURITY FUNCTIONS ---------------------------------------------------------
protected function _check_login($username = '', $password = null)
{
if (empty($username)) {
return false;
}
$valid_logins = &$this->config->item('rest_valid_logins');
if (!array_key_exists($username, $valid_logins)) {
return false;
}
// If actually NULL (not empty string) then do not check it
if ($password !== null and $valid_logins[$username] != $password) {
return false;
}
return true;
}
protected function _prepare_basic_auth()
{
$username = null;
$password = null;
// mod_php
if ($this->input->server('PHP_AUTH_USER')) {
$username = $this->input->server('PHP_AUTH_USER');
$password = $this->input->server('PHP_AUTH_PW');
}
// most other servers
elseif ($this->input->server('HTTP_AUTHENTICATION')) {
if (strpos(strtolower($this->input->server('HTTP_AUTHENTICATION')), 'basic') === 0) {
list($username, $password) = explode(':', base64_decode(substr($this->input->server('HTTP_AUTHORIZATION'), 6)));
}
}
if (!$this->_check_login($username, $password)) {
$this->_force_login();
}
}
protected function _prepare_digest_auth()
{
$uniqid = uniqid(""); // Empty argument for backward compatibility
// We need to test which server authentication variable to use
// because the PHP ISAPI module in IIS acts different from CGI
if ($this->input->server('PHP_AUTH_DIGEST')) {
$digest_string = $this->input->server('PHP_AUTH_DIGEST');
} elseif ($this->input->server('HTTP_AUTHORIZATION')) {
$digest_string = $this->input->server('HTTP_AUTHORIZATION');
} else {
$digest_string = "";
}
/* The $_SESSION['error_prompted'] variabile is used to ask
the password again if none given or if the user enters
a wrong auth. informations. */
if (empty($digest_string)) {
$this->_force_login($uniqid);
}
// We need to retrieve authentication informations from the $auth_data variable
preg_match_all('@(username|nonce|uri|nc|cnonce|qop|response)=[\'"]?([^\'",]+)@', $digest_string, $matches);
$digest = array_combine($matches[1], $matches[2]);
if (!array_key_exists('username', $digest) or !$this->_check_login($digest['username'])) {
$this->_force_login($uniqid);
}
$valid_logins = &$this->config->item('rest_valid_logins');
$valid_pass = $valid_logins[$digest['username']];
// This is the valid response expected
$A1 = md5($digest['username'] . ':' . $this->config->item('rest_realm') . ':' . $valid_pass);
$A2 = md5(strtoupper($this->request->method) . ':' . $digest['uri']);
$valid_response = md5($A1 . ':' . $digest['nonce'] . ':' . $digest['nc'] . ':' . $digest['cnonce'] . ':' . $digest['qop'] . ':' . $A2);
if ($digest['response'] != $valid_response) {
header('HTTP/1.0 401 Unauthorized');
header('HTTP/1.1 401 Unauthorized');
exit;
}
}
protected function _force_login($nonce = '')
{
if ($this->config->item('rest_auth') == 'basic') {
header('WWW-Authenticate: Basic realm="' . $this->config->item('rest_realm') . '"');
} elseif ($this->config->item('rest_auth') == 'digest') {
header('WWW-Authenticate: Digest realm="' . $this->config->item('rest_realm') . '" qop="auth" nonce="' . $nonce . '" opaque="' . md5($this->config->item('rest_realm')) . '"');
}
$this->response(array('status' => false, 'error' => 'Not authorized'), 401);
}
// Force it into an array
protected function _force_loopable($data)
{
// Force it to be something useful
if (!is_array($data) and !is_object($data)) {
$data = (array) $data;
}
return $data;
}
// FORMATING FUNCTIONS ---------------------------------------------------------
// Many of these have been moved to the Format class for better separation, but these methods will be checked too
// Encode as JSONP
protected function _format_jsonp($data = array())
{
return $this->get('callback') . '(' . json_encode($data) . ')';
}
}