mirror of
https://github.com/beestat/app.git
synced 2025-05-24 02:14:03 -04:00
Removed "json_" prefixes from all columns and converted columns to actual JSON types. Also removed all converged columns and converted contents to regular columns.
1058 lines
35 KiB
PHP
1058 lines
35 KiB
PHP
<?php
|
|
|
|
namespace cora;
|
|
|
|
/**
|
|
* Workhorse for processing an API request. This has all of the core
|
|
* functionality.
|
|
*
|
|
* @author Jon Ziebell
|
|
*/
|
|
final class cora {
|
|
|
|
/**
|
|
* The singleton.
|
|
*/
|
|
private static $instance;
|
|
|
|
/**
|
|
* The timestamp when processing of the API request started.
|
|
*
|
|
* @var int
|
|
*/
|
|
private $start_timestamp;
|
|
|
|
/**
|
|
* The timestamp (microseconds) when processing of the API request started.
|
|
*
|
|
* @var int
|
|
*/
|
|
private $start_timestamp_microtime;
|
|
|
|
/**
|
|
* The original request passed to this object, usually $_REQUEST. Stored
|
|
* right away so logging and error functions have access to it.
|
|
*
|
|
* @var array
|
|
*/
|
|
private $request;
|
|
|
|
/**
|
|
* A list of all of the API calls extracted from the request. This is stored
|
|
* so that logging and error functions have access to it.
|
|
*
|
|
* @var array
|
|
*/
|
|
private $api_calls;
|
|
|
|
/**
|
|
* The current API user.
|
|
*
|
|
* @var array
|
|
*/
|
|
private $api_user;
|
|
|
|
/**
|
|
* An array of the API responses. For single API calls, count() == 1, for
|
|
* batch calls there will be one row per call.
|
|
*
|
|
* @var array
|
|
*/
|
|
private $response_data = [];
|
|
|
|
/**
|
|
* The actual response in array form. It is stored here so the shutdown
|
|
* handler has access to it.
|
|
*
|
|
* @var array
|
|
*/
|
|
private $response;
|
|
|
|
/**
|
|
* This is necessary because of the shutdown handler. According to the PHP
|
|
* documentation and various bug reports, when the shutdown function
|
|
* executes the current working directory changes back to root.
|
|
* https://bugs.php.net/bug.php?id=36529. This is cool and all but it breaks
|
|
* the autoloader. My solution for this is to just change the working
|
|
* directory back to what it was when the script originally ran.
|
|
*
|
|
* Obviuosly I could hardcode this but then users would have to configure
|
|
* the cwd when installing Cora. This handles it automatically and seems to
|
|
* work just fine. Note that if the class that the autoloader needs is
|
|
* already loaded, the shutdown handler won't break. So it's usually not a
|
|
* problem but this is a good thing to fix.
|
|
*
|
|
* @var string
|
|
*/
|
|
private $current_working_directory;
|
|
|
|
/**
|
|
* A list of the response times for each API call. This does not reflect the
|
|
* response time for the entire request, nor does it include the time it
|
|
* took for overhead like rate limit checking.
|
|
*
|
|
* @var array
|
|
*/
|
|
private $response_times = [];
|
|
|
|
/**
|
|
* A list of the query counts for each API call. This does not reflect the
|
|
* query count for the entire request, nor does it include the queries for
|
|
* overhead like rate limit checking.
|
|
*
|
|
* @var array
|
|
*/
|
|
private $response_query_counts = [];
|
|
|
|
/**
|
|
* A list of the query times for each API call. This does not reflect the
|
|
* query time for the entire request, nor does it include the times for
|
|
* overhead like rate limit checking.
|
|
*
|
|
* @var array
|
|
*/
|
|
private $response_query_times = [];
|
|
|
|
/**
|
|
* Whether or not each API call was returned from the cache.
|
|
*
|
|
* @var array
|
|
*/
|
|
private $from_cache = [];
|
|
|
|
/**
|
|
* How long the API call is cached for. Used when setting the
|
|
* beestat-cached-until header.
|
|
*
|
|
* @var array
|
|
*/
|
|
private $cached_until = [];
|
|
|
|
/**
|
|
* This stores the currently executing API call. If that API call were to
|
|
* fail, I need to know which one I was running in order to propery log the
|
|
* error.
|
|
*
|
|
* @var array
|
|
*/
|
|
private $current_api_call = null;
|
|
|
|
/**
|
|
* Database object.
|
|
*
|
|
* @var database
|
|
*/
|
|
private $database;
|
|
|
|
/**
|
|
* Setting object.
|
|
*
|
|
* @var setting
|
|
*/
|
|
private $setting;
|
|
|
|
/**
|
|
* The headers to output in the shutdown handler.
|
|
*
|
|
* @var array
|
|
*/
|
|
private $headers;
|
|
|
|
/**
|
|
* Whether or not this is a custom response. If true, none of the Cora data
|
|
* like 'success' and 'data' is returned; only the actual data from the
|
|
* single API call is returned.
|
|
*
|
|
* @var bool
|
|
*/
|
|
// private $custom_response;
|
|
|
|
/**
|
|
* Extra information for errors. For example, the database class puts
|
|
* additional information into this variable if the query fails. The
|
|
* error_message remains the same but has this additional data to help the
|
|
* developer (if debug is enabled).
|
|
*
|
|
* @var mixed
|
|
*/
|
|
private $error_extra_info = null;
|
|
|
|
/**
|
|
* Save the request variables for use later on. If unset, they are defaulted
|
|
* to null. Any of these values being null will throw an exception as soon
|
|
* as you try to process the request. The reason that doesn't happen here is
|
|
* so that I can store exactly what was sent to me for logging purposes.
|
|
*/
|
|
private function __construct() {
|
|
$this->start_timestamp = microtime(true);
|
|
$this->start_timestamp_microtime = $this->microtime();
|
|
|
|
// See class variable documentation for reasoning.
|
|
$this->current_working_directory = getcwd();
|
|
}
|
|
|
|
/**
|
|
* Use this function to instantiate this class instead of calling new cora()
|
|
* (which isn't allowed anyways). This is necessary so that the API class
|
|
* can have access to Cora.
|
|
*
|
|
* @return cora A new cora object or the already created one.
|
|
*/
|
|
public static function get_instance() {
|
|
if(isset(self::$instance) === false) {
|
|
self::$instance = new self();
|
|
}
|
|
return self::$instance;
|
|
}
|
|
|
|
/**
|
|
* Execute the request. It is run through the rate limiter, checked for
|
|
* errors, then processed. Requests sent after the rate limit is reached are
|
|
* not logged.
|
|
*
|
|
* @param array $request Basically just $_REQUEST or a slight mashup of it
|
|
* for batch requests.
|
|
*
|
|
* @throws \Exception If the rate limit threshhold is reached.
|
|
* @throws \Exception If SSL is required but not used.
|
|
* @throws \Exception If the API key is not provided.
|
|
* @throws \Exception If the API key is invalid.
|
|
* @throws \Exception If the session is expired.
|
|
* @throws \Exception If a resource is not provided.
|
|
* @throws \Exception If a method is not provided.
|
|
* @throws \Exception If the requested method does not exist.
|
|
*/
|
|
public function process_request($request) {
|
|
// This is necessary in order for the shutdown handler/log function to have
|
|
// access to this data, but it's not used anywhere else.
|
|
$this->request = $request;
|
|
|
|
// Setting class for getting settings. Anything that extends cora\api gets
|
|
// this automatically.
|
|
$this->setting = setting::get_instance();
|
|
|
|
// Used to have this in the constructor, but the database uses this class
|
|
// which causes a dependency loop in the constructors.
|
|
$this->database = database::get_instance();
|
|
|
|
// A couple quick error checks
|
|
if($this->is_over_rate_limit() === true) {
|
|
throw new \Exception('Rate limit reached.', 1005);
|
|
}
|
|
if($this->setting->get('force_ssl') === true && empty($_SERVER['HTTPS']) === true) {
|
|
throw new \Exception('Request must be sent over HTTPS.', 1006);
|
|
}
|
|
|
|
// Make sure the API key that was sent is present and valid.
|
|
if(isset($request['api_key']) === false) {
|
|
throw new \Exception('API Key is required.', 1000);
|
|
}
|
|
|
|
$api_user_resource = new api_user();
|
|
$api_users = $api_user_resource->read(['api_key' => $request['api_key']]);
|
|
if(count($api_users) !== 1) {
|
|
throw new \Exception('Invalid API key.', 1003);
|
|
} else {
|
|
$this->api_user = $api_users[0];
|
|
}
|
|
|
|
// Build a list of API calls.
|
|
$this->build_api_call_list($request);
|
|
|
|
// Check the API request for errors.
|
|
$this->check_api_request_for_errors();
|
|
|
|
// Set the default headers as a catch-all. Most API calls won't touch these,
|
|
// but it is possible for them to override headers as desired.
|
|
$this->set_default_headers();
|
|
|
|
// Get this every time. It's only used for session API calls. Non-session
|
|
// API calls don't bother with this.
|
|
$session = session::get_instance();
|
|
$session_is_valid = $session->touch($this->api_user['session_key']);
|
|
|
|
// Process each request.
|
|
foreach($this->api_calls as $api_call) {
|
|
// Store the currently running API call for tracking if an error occurs.
|
|
$this->current_api_call = $api_call;
|
|
|
|
// These are required before we can move on with any more processing or
|
|
// error checking.
|
|
if(isset($api_call['resource']) === false) {
|
|
throw new \Exception('Resource is required.', 1001);
|
|
}
|
|
if(isset($api_call['method']) === false) {
|
|
throw new \Exception('Method is required.', 1002);
|
|
}
|
|
|
|
// Sets $call_type to 'public' or 'private'
|
|
$call_type = $this->get_api_call_type($api_call);
|
|
|
|
// If the request requires a session, make sure it's valid.
|
|
if($call_type === 'private') {
|
|
if($session_is_valid === false) {
|
|
throw new \Exception('Session is expired.', 1004);
|
|
}
|
|
}
|
|
|
|
// If the resource doesn't exist, spl_autoload_register() will throw a
|
|
// fatal error. The shutdown handler will "catch" it. It is not possible
|
|
// to catch exceptions directly from the autoloader using try/catch.
|
|
$resource_instance = new $api_call['resource']();
|
|
|
|
// If the method doesn't exist
|
|
if(method_exists($resource_instance, $api_call['method']) === false) {
|
|
throw new \Exception('Method does not exist.', 1009);
|
|
}
|
|
|
|
$arguments = $this->get_arguments($api_call);
|
|
|
|
// Process the request and save some statistics.
|
|
$start_time = microtime(true);
|
|
$start_query_count = $this->database->get_query_count();
|
|
$start_query_time = $this->database->get_query_time();
|
|
|
|
if(isset($api_call['alias']) === true) {
|
|
$index = $api_call['alias'];
|
|
}
|
|
else {
|
|
$index = count($this->response_data);
|
|
}
|
|
|
|
// Caching! If this API call is configured for caching,
|
|
// $cache_config = $this->setting->get('cache');
|
|
if( // Is cacheable
|
|
isset($api_call['resource']::$cache) === true &&
|
|
isset($api_call['resource']::$cache[$api_call['method']]) === true
|
|
) {
|
|
$api_cache_instance = new api_cache();
|
|
$api_cache = $api_cache_instance->retrieve($api_call);
|
|
|
|
if($api_cache !== null) {
|
|
// If there was a cache entry available, use that.
|
|
$this->response_data[$index] = $api_cache['response_data'];
|
|
$this->from_cache[$index] = true;
|
|
$this->cached_until[$index] = date('c', strtotime($api_cache['expires_at']));
|
|
} else {
|
|
// Else just run the API call, then cache it.
|
|
$this->response_data[$index] = call_user_func_array(
|
|
[$resource_instance, $api_call['method']],
|
|
$arguments
|
|
);
|
|
$this->from_cache[$index] = false;
|
|
|
|
$api_cache = $api_cache_instance->cache(
|
|
$api_call,
|
|
$this->response_data[$index],
|
|
$api_call['resource']::$cache[$api_call['method']]
|
|
);
|
|
$this->cached_until[$index] = date('c', strtotime($api_cache['expires_at']));
|
|
}
|
|
}
|
|
else { // Not cacheable
|
|
$this->response_data[$index] = call_user_func_array(
|
|
[$resource_instance, $api_call['method']],
|
|
$arguments
|
|
);
|
|
$this->from_cache[$index] = false;
|
|
}
|
|
|
|
$this->response_times[$index] = (microtime(true) - $start_time);
|
|
$this->response_query_counts[$index] = $this->database->get_query_count() - $start_query_count;
|
|
$this->response_query_times[$index] = $this->database->get_query_time() - $start_query_time;
|
|
}
|
|
|
|
$this->set_cached_until_header();
|
|
}
|
|
|
|
/**
|
|
* Build a list of API calls from the request. For a single request, it's
|
|
* just the request. For batch requests, add each item in the batch
|
|
* parameter to this array.
|
|
*
|
|
* @param array $request The original request.
|
|
*
|
|
* @throws \Exception If this is a batch request and the batch data is not
|
|
* valid JSON
|
|
* @throws \Exception If this is a batch request and it exceeds the maximum
|
|
* number of api calls allowed in one batch.
|
|
*/
|
|
private function build_api_call_list($request) {
|
|
$this->api_calls = [];
|
|
if(isset($request['batch']) === true) {
|
|
$batch = json_decode($request['batch'], true);
|
|
if($batch === null) {
|
|
throw new \Exception('Batch is not valid JSON.', 1012);
|
|
}
|
|
$batch_limit = $this->setting->get('batch_limit');
|
|
if($batch_limit !== null && count($batch) > $batch_limit) {
|
|
throw new \Exception('Batch limit exceeded.', 1013);
|
|
}
|
|
foreach($batch as $api_call) {
|
|
// Put this on each API call for logging.
|
|
$api_call['api_key'] = $request['api_key'];
|
|
$this->api_calls[] = $api_call;
|
|
}
|
|
}
|
|
else {
|
|
$this->api_calls[] = $request;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check the API request for various errors.
|
|
*
|
|
* @throws \Exception If something other than ALL or NO aliases are set.
|
|
* @throws \Exception If Any duplicate aliases are used.
|
|
*/
|
|
private function check_api_request_for_errors() {
|
|
$aliases = [];
|
|
foreach($this->api_calls as $api_call) {
|
|
if(isset($api_call['alias']) === true) {
|
|
$aliases[] = $api_call['alias'];
|
|
}
|
|
}
|
|
|
|
// Check to make sure either all or none are set.
|
|
$number_aliases = count($aliases);
|
|
if(count($this->api_calls) !== $number_aliases && $number_aliases !== 0) {
|
|
throw new \Exception('All API calls must have an alias if at least one is set.', 1017);
|
|
}
|
|
|
|
// Check for duplicates.
|
|
$number_unique_aliases = count(array_unique($aliases));
|
|
if($number_aliases !== $number_unique_aliases) {
|
|
throw new \Exception('Duplicate alias on API call.', 1018);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns 'session' or 'non_session' depending on where the API method is
|
|
* located at. Session methods require a valid session in order to execute.
|
|
*
|
|
* @param array $api_call The API call to get the type for.
|
|
*
|
|
* @throws \Exception If the method was not found in the map.
|
|
*
|
|
* @return string The type.
|
|
*/
|
|
private function get_api_call_type($api_call) {
|
|
if(in_array($api_call['method'], $api_call['resource']::$exposed['private'])) {
|
|
return 'private';
|
|
}
|
|
else if(in_array($api_call['method'], $api_call['resource']::$exposed['public'])) {
|
|
return 'public';
|
|
}
|
|
else {
|
|
throw new \Exception('Requested method is not mapped.', 1008);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check to see if the request from the current IP address needs to be rate
|
|
* limited. If $requests_per_minute is null then there is no rate limiting.
|
|
*
|
|
* @return bool If this request puts us over the rate threshold.
|
|
*/
|
|
private function is_over_rate_limit() {
|
|
$requests_per_minute = $this->setting->get('requests_per_minute');
|
|
|
|
if($requests_per_minute === null) {
|
|
return false;
|
|
}
|
|
|
|
$api_log_resource = new api_log();
|
|
$requests_this_minute = $api_log_resource->get_number_requests_since(
|
|
$_SERVER['REMOTE_ADDR'],
|
|
time() - 60
|
|
);
|
|
|
|
return ($requests_this_minute > $requests_per_minute);
|
|
}
|
|
|
|
/**
|
|
* Fetches a list of arguments when passed an array of keys. Since the
|
|
* arguments are passed from JS to PHP in JSON, I don't need to cast any of
|
|
* the values as the data types are preserved. Since the argument order from
|
|
* the client doesn't matter, this makes sure that the arguments are placed
|
|
* in the correct order for calling the function.
|
|
*
|
|
* @param array $api_call The API call.
|
|
*
|
|
* @throws \Exception If the arguments in the api_call were not valid JSON.
|
|
*
|
|
* @return array The requested arguments.
|
|
*/
|
|
private function get_arguments($api_call) {
|
|
$arguments = [];
|
|
|
|
// Arguments are not strictly required. If a method requires them then you
|
|
// will still get an error, but they are not required by the API.
|
|
if(isset($api_call['arguments']) === true) {
|
|
// All arguments are sent in the "arguments" key as JSON.
|
|
$api_call_arguments = json_decode($api_call['arguments'], true);
|
|
|
|
if($api_call_arguments === null) {
|
|
throw new \Exception('Arguments are not valid JSON.', 1011);
|
|
}
|
|
|
|
$reflection_method = new \ReflectionMethod(
|
|
$api_call['resource'],
|
|
$api_call['method']
|
|
);
|
|
$parameters = $reflection_method->getParameters();
|
|
|
|
foreach($parameters as $parameter) {
|
|
if(isset($api_call_arguments[$parameter->getName()]) === true) {
|
|
$argument = $api_call_arguments[$parameter->getName()];
|
|
|
|
// If this is a batch request, look for JSONPath arguments.
|
|
if(isset($this->request['batch']) === true) {
|
|
$argument = $this->evaluate_json_path_argument($argument);
|
|
}
|
|
}
|
|
else {
|
|
$argument = null;
|
|
}
|
|
$arguments[] = $argument;
|
|
}
|
|
}
|
|
return $arguments;
|
|
}
|
|
|
|
/**
|
|
* Recursively check all values in an argument. If any of them are JSON
|
|
* path, evaluate them.
|
|
*
|
|
* @param mixed $argument The argument to check.
|
|
*
|
|
* @return mixed The argument with the evaluated path.
|
|
*/
|
|
private function evaluate_json_path_argument($argument) {
|
|
if(is_array($argument) === true) {
|
|
foreach($argument as $key => $value) {
|
|
$argument[$key] = $this->evaluate_json_path_argument($value);
|
|
}
|
|
}
|
|
else if(preg_match('/^{=(.*)}$/', $argument, $matches) === 1) {
|
|
$json_path_resource = new json_path();
|
|
$json_path = $matches[1];
|
|
$argument = $json_path_resource->evaluate($this->response_data, $json_path);
|
|
}
|
|
return $argument;
|
|
}
|
|
|
|
/**
|
|
* Sets error_extra_info.
|
|
*
|
|
* @param mixed $error_extra_info Whatever you want the extra info to be.
|
|
*/
|
|
public function set_error_extra_info($error_extra_info) {
|
|
$this->error_extra_info = $error_extra_info;
|
|
}
|
|
|
|
/**
|
|
* Get error_extra_info.
|
|
*
|
|
* @return mixed
|
|
*/
|
|
public function get_error_extra_info() {
|
|
return $this->error_extra_info;
|
|
}
|
|
|
|
/**
|
|
* Get the current API user.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function get_api_user() {
|
|
return $this->api_user;
|
|
}
|
|
|
|
/**
|
|
* Sets the headers that should be used for this API call. This is useful
|
|
* for doing things like returning files from the API where the content-type
|
|
* is no longer application/json. This replaces all headers; headers are not
|
|
* outputted to the browser until all API calls have completed, so the last
|
|
* call to this function will win.
|
|
*
|
|
* @param array $headers The headers to output.
|
|
* @param bool $custom_response Whether or not to wrap the response with the
|
|
* Cora data or just output the API call's return value.
|
|
*
|
|
* @throws \Exception If this is a batch request and a custom response was
|
|
* requested.
|
|
* @throws \Exception If this is a batch request and the content type was
|
|
* altered from application/json
|
|
* @throws \Exception If this is not a batch request and the content type
|
|
* was altered from application/json without a custom response.
|
|
*/
|
|
// public function set_headers($headers, $custom_response = false) {
|
|
// if(isset($this->request['batch']) === true) {
|
|
// if($custom_response === true) {
|
|
// throw new \Exception('Batch API requests can not use a custom response.', 1015);
|
|
// }
|
|
// if($this->content_type_is_json($headers) === false) {
|
|
// throw new \Exception('Batch API requests must return JSON.', 1014);
|
|
// }
|
|
// }
|
|
// else {
|
|
// // Not a batch request
|
|
// if($custom_response === false && $this->content_type_is_json($headers) === false) {
|
|
// throw new \Exception('Non-custom responses must return JSON.', 1016);
|
|
// }
|
|
// }
|
|
// $this->headers = $headers;
|
|
// $this->custom_response = $custom_response;
|
|
// }
|
|
|
|
/**
|
|
* Return whether or not the current output headers indicate that the
|
|
* content type is JSON. This is mostly just used to make sure that batch
|
|
* API calls output JSON.
|
|
*
|
|
* @param array $headers The headers to look at.
|
|
*
|
|
* @return bool Whether or not the output has a content type of
|
|
* application/json
|
|
*/
|
|
private function content_type_is_json($headers) {
|
|
return isset($headers['Content-type']) === true
|
|
&& stristr($headers['Content-type'], 'application/json') !== false;
|
|
}
|
|
|
|
/**
|
|
* Override of the default PHP error handler. Grabs the error info and sends
|
|
* it to the exception handler which returns a JSON response.
|
|
*
|
|
* @param int $error_code The error number from PHP.
|
|
* @param string $error_message The error message.
|
|
* @param string $error_file The file the error happend in.
|
|
* @param int $error_line The line of the file the error happened on.
|
|
*
|
|
* @return string The JSON response with the error details.
|
|
*/
|
|
public function error_handler($error_code, $error_message, $error_file, $error_line) {
|
|
$this->set_error_response(
|
|
$error_message,
|
|
$error_code,
|
|
$error_file,
|
|
$error_line,
|
|
debug_backtrace(false)
|
|
);
|
|
die(); // Do not continue execution; shutdown handler will now run.
|
|
}
|
|
|
|
/**
|
|
* Override of the default PHP exception handler. All unhandled exceptions
|
|
* go here.
|
|
*
|
|
* @param Exception $e The exception.
|
|
*/
|
|
public function exception_handler($e) {
|
|
$this->set_error_response(
|
|
$e->getMessage(),
|
|
$e->getCode(),
|
|
$e->getFile(),
|
|
$e->getLine(),
|
|
$e->getTrace()
|
|
);
|
|
die(); // Do not continue execution; shutdown handler will now run.
|
|
}
|
|
|
|
/**
|
|
* Handle all exceptions by generating a JSON response with the error
|
|
* details. If debugging is enabled, a bunch of other information is sent
|
|
* back to help out.
|
|
*
|
|
* @param string $error_message The error message.
|
|
* @param mixed $error_code The supplied error code.
|
|
* @param string $error_file The file the error happened in.
|
|
* @param int $error_line The line of the file the error happened on.
|
|
* @param array $error_trace The stack trace for the error.
|
|
*/
|
|
public function set_error_response($error_message, $error_code, $error_file, $error_line, $error_trace) {
|
|
// There are a few places that call this function to set an error response,
|
|
// so this can't just be done in the exception handler alone. If an error
|
|
// occurs, rollback the current transaction. Also only attempt to roll back
|
|
// the transaction if the database was successfully created/connected to.
|
|
if($this->database !== null) {
|
|
$this->database->rollback_transaction();
|
|
}
|
|
|
|
$this->response = [
|
|
'success' => false,
|
|
'data' => [
|
|
'error_message' => $error_message,
|
|
'error_code' => $error_code,
|
|
'error_file' => $error_file,
|
|
'error_line' => $error_line,
|
|
'error_trace' => $error_trace,
|
|
'error_extra_info' => $this->error_extra_info
|
|
]
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Executes when the script finishes. If there was an error that somehow
|
|
* didn't get caught, then this will find it with error_get_last and return
|
|
* appropriately. Note that error_get_last() will only get something when an
|
|
* error wasn't caught by my error/exception handlers. The default PHP error
|
|
* handler fills this in. Doesn't do anything if an exception was thrown due
|
|
* to the rate limit.
|
|
*
|
|
* @throws \Exception If a this was a batch request but one of the api calls
|
|
* changed the content-type to anything but the default.
|
|
*/
|
|
public function shutdown_handler() {
|
|
// Since the shutdown handler is rather verbose in what it has to check for
|
|
// and do, it's possible it will fail or detect an error that needs to be
|
|
// handled. For example, someone could return binary data from an API call
|
|
// which will fail a json_encode, or someone could change the headers in a
|
|
// batch API call, which isn't allowed. I can't throw an exception since I'm
|
|
// already in the shutdown handler...it will be caught but it won't execute
|
|
// a new shutdown handler and no output will be sent to the client. I just
|
|
// have to handle all problems manually.
|
|
try {
|
|
// Fix the current working directory. See documentation on this class
|
|
// variable for details.
|
|
chdir($this->current_working_directory);
|
|
|
|
// If I didn't catch an error/exception with my handlers, look here...this
|
|
// will catch fatal errors that I can't.
|
|
$error = error_get_last();
|
|
if($error !== null) {
|
|
$this->set_error_response(
|
|
$error['message'],
|
|
$error['type'],
|
|
$error['file'],
|
|
$error['line'],
|
|
debug_backtrace(false)
|
|
);
|
|
}
|
|
|
|
// If the response has already been set by one of the error handlers, end
|
|
// execution here and just log & output the response.
|
|
if(isset($this->response) === true) {
|
|
// Don't log anything for rate limit breaches.
|
|
if($this->response['data']['error_code'] !== 1005) {
|
|
$this->log();
|
|
}
|
|
|
|
// Override whatever headers might have already been set.
|
|
$this->set_default_headers();
|
|
$this->output_headers();
|
|
die($this->get_json_response());
|
|
}
|
|
else {
|
|
// If we got here, no errors have occurred.
|
|
|
|
// For non-custom responses, build the response, log it, and output it.
|
|
$this->response = ['success' => true];
|
|
|
|
if(isset($this->request['batch']) === true) {
|
|
$this->response['data'] = $this->response_data;
|
|
}
|
|
else {
|
|
// $this->response['data'] = $this->response_data[0];
|
|
$this->response['data'] = reset($this->response_data);
|
|
}
|
|
|
|
// Log all of the API calls that were made.
|
|
$this->log();
|
|
|
|
// Output the response
|
|
$this->output_headers();
|
|
die($this->get_json_response());
|
|
}
|
|
}
|
|
catch(\Exception $e) {
|
|
$this->set_error_response(
|
|
$e->getMessage(),
|
|
$e->getCode(),
|
|
$e->getFile(),
|
|
$e->getLine(),
|
|
$e->getTrace()
|
|
);
|
|
$this->set_default_headers();
|
|
$this->output_headers();
|
|
die($this->get_json_response());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the json_encoded response. This is called from the shutdown handler
|
|
* and removes debug information if debugging is disabled and then
|
|
* json_encodes the data.
|
|
*
|
|
* @return string The JSON encoded response.
|
|
*/
|
|
private function get_json_response() {
|
|
$response = $this->response;
|
|
if($this->setting->get('debug') === false && $response['success'] === false) {
|
|
unset($response['data']['error_file']);
|
|
unset($response['data']['error_line']);
|
|
unset($response['data']['error_trace']);
|
|
unset($response['data']['error_extra_info']);
|
|
}
|
|
return json_encode($response);
|
|
}
|
|
|
|
/**
|
|
* Output whatever the headers are currently set to.
|
|
*/
|
|
private function output_headers() {
|
|
foreach($this->headers as $key => $value) {
|
|
header($key . ': ' . $value);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resets the headers to default. Have to do this in case one of the API
|
|
* calls changes them and there was an error to handle.
|
|
*/
|
|
private function set_default_headers() {
|
|
$this->headers['Content-type'] = 'application/json; charset=UTF-8';
|
|
}
|
|
|
|
/**
|
|
* Set the beestat-cached-until header.
|
|
*/
|
|
private function set_cached_until_header() {
|
|
if(isset($this->request['batch']) === true) { // Batch
|
|
$beestat_cached_until = [];
|
|
foreach($this->cached_until as $index => $cached_until) {
|
|
$beestat_cached_until[$index] = $cached_until;
|
|
}
|
|
if(count($beestat_cached_until) > 0) {
|
|
$this->headers['beestat-cached-until'] = json_encode($beestat_cached_until);
|
|
}
|
|
} else { // Single
|
|
if(count($this->cached_until) === 1) {
|
|
$this->headers['beestat-cached-until'] = reset($this->cached_until);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns true for all loggable content types. Mostly JSON, XML, and other
|
|
* text-based types.
|
|
*
|
|
* @return bool Whether or not the output has a content type that can be
|
|
* logged.
|
|
*/
|
|
private function content_type_is_loggable() {
|
|
if(isset($this->headers['Content-type']) === false) {
|
|
return false;
|
|
}
|
|
else {
|
|
$loggable_content_types = [
|
|
'application/json',
|
|
'application/xml',
|
|
'application/javascript',
|
|
'text/html',
|
|
'text/xml',
|
|
'text/plain',
|
|
'text/css'
|
|
];
|
|
foreach($loggable_content_types as $loggable_content_type) {
|
|
if(stristr($this->headers['Content-type'], $loggable_content_type) !== false) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Log the request and response to the database. The logged response is
|
|
* truncated to 16kb for sanity.
|
|
*/
|
|
private function log() {
|
|
$api_log_resource = new api_log();
|
|
$session = session::get_instance();
|
|
|
|
$user_id = $session->get_user_id();
|
|
|
|
// If exception. This is lenghty because I have to check to make sure
|
|
// everything was set or else use null.
|
|
if(isset($this->response['data']['error_code']) === true) {
|
|
if(isset($this->request['api_key']) === true) {
|
|
$api_user_resource = new api_user();
|
|
$api_users = $api_user_resource->read(['api_key' => $this->request['api_key']]);
|
|
$request_api_user_id = $api_users[0]['api_user_id'];
|
|
}
|
|
else {
|
|
$request_api_user_id = null;
|
|
}
|
|
|
|
$request_resource = null;
|
|
$request_method = null;
|
|
$request_arguments = null;
|
|
if($this->current_api_call !== null) {
|
|
if(isset($this->current_api_call['resource']) === true) {
|
|
$request_resource = $this->current_api_call['resource'];
|
|
}
|
|
if(isset($this->current_api_call['method']) === true) {
|
|
$request_method = $this->current_api_call['method'];
|
|
}
|
|
if(isset($this->current_api_call['arguments']) === true) {
|
|
$request_arguments = $this->current_api_call['arguments'];
|
|
}
|
|
}
|
|
$response_error_code = $this->response['data']['error_code'];
|
|
$response_time = null;
|
|
$response_query_count = null;
|
|
$response_query_time = null;
|
|
$response_data = substr(json_encode($this->response['data']), 0, 16384);
|
|
$from_cache = null;
|
|
|
|
$api_log_resource->create(
|
|
[
|
|
'user_id' => $user_id,
|
|
'request_api_user_id' => $request_api_user_id,
|
|
'request_resource' => $request_resource,
|
|
'request_method' => $request_method,
|
|
'request_arguments' => preg_replace('/"(password)":".*"/', '"$1":"[removed]"', $request_arguments),
|
|
'response_error_code' => $response_error_code,
|
|
'response_data' => preg_replace('/"(password)":".*"/', '"$1":"[removed]"', $response_data),
|
|
'response_time' => $response_time,
|
|
'response_query_count' => $response_query_count,
|
|
'response_query_time' => $response_query_time,
|
|
'from_cache' => $from_cache
|
|
]
|
|
);
|
|
|
|
$this->log_influx(
|
|
[
|
|
'user_id' => $user_id,
|
|
'request_api_user_id' => $request_api_user_id,
|
|
'request_resource' => $request_resource,
|
|
'request_method' => $request_method,
|
|
'request_timestamp' => $this->start_timestamp_microtime,
|
|
'response_error_code' => $response_error_code,
|
|
'response_time' => $response_time,
|
|
'response_query_count' => $response_query_count,
|
|
'response_query_time' => $response_query_time,
|
|
'from_cache' => $from_cache
|
|
]
|
|
);
|
|
|
|
}
|
|
else {
|
|
$response_error_code = null;
|
|
$count_api_calls = count($this->api_calls);
|
|
for($i = 0; $i < $count_api_calls; $i++) {
|
|
$api_call = $this->api_calls[$i];
|
|
|
|
$api_user_resource = new api_user();
|
|
$api_users = $api_user_resource->read(['api_key' => $api_call['api_key']]);
|
|
$request_api_user_id = $api_users[0]['api_user_id'];
|
|
|
|
$request_resource = $api_call['resource'];
|
|
$request_method = $api_call['method'];
|
|
if(isset($api_call['arguments']) === true) {
|
|
$request_arguments = $api_call['arguments'];
|
|
}
|
|
else {
|
|
$request_arguments = null;
|
|
}
|
|
|
|
if(isset($api_call['alias']) === true) {
|
|
$index = $api_call['alias'];
|
|
}
|
|
else {
|
|
$index = $i;
|
|
}
|
|
|
|
$response_time = $this->response_times[$index];
|
|
$response_query_count = $this->response_query_counts[$index];
|
|
$response_query_time = $this->response_query_times[$index];
|
|
|
|
// The data could be an integer, an XML string, an array, etc, but let's
|
|
// just always json_encode it to keep things simple and standard.
|
|
if($this->content_type_is_loggable() === true) {
|
|
$response_data = substr(json_encode($this->response_data[$index]), 0, 16384);
|
|
}
|
|
else {
|
|
$response_data = null;
|
|
}
|
|
|
|
$from_cache = $this->from_cache[$index];
|
|
|
|
$api_log_resource->create(
|
|
[
|
|
'user_id' => $user_id,
|
|
'request_api_user_id' => $request_api_user_id,
|
|
'request_resource' => $request_resource,
|
|
'request_method' => $request_method,
|
|
'request_arguments' => preg_replace('/"(password)":".*"/', '"$1":"[removed]"', $request_arguments),
|
|
'response_error_code' => $response_error_code,
|
|
'response_data' => null, // Can't store this; uses too much disk.
|
|
// 'response_data' => preg_replace('/"(password)":".*"/', '"$1":"[removed]"', $response_data),
|
|
'response_time' => $response_time,
|
|
'response_query_count' => $response_query_count,
|
|
'response_query_time' => $response_query_time,
|
|
'from_cache' => $from_cache
|
|
]
|
|
);
|
|
|
|
$this->log_influx(
|
|
[
|
|
'user_id' => $user_id,
|
|
'request_api_user_id' => $request_api_user_id,
|
|
'request_resource' => $request_resource,
|
|
'request_method' => $request_method,
|
|
'request_timestamp' => $this->start_timestamp_microtime,
|
|
'response_error_code' => $response_error_code,
|
|
'response_time' => $response_time,
|
|
'response_query_count' => $response_query_count,
|
|
'response_query_time' => $response_query_time,
|
|
'from_cache' => $from_cache,
|
|
]
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Log to InfluxDB/Grafana.
|
|
*
|
|
* @param array $data
|
|
*/
|
|
private function log_influx($data) {
|
|
$logger_resource = new \logger();
|
|
$logger_resource->log_influx(
|
|
'api_log',
|
|
[
|
|
'request_api_user_id' => (string) $data['request_api_user_id'],
|
|
'exception' => $data['response_error_code'] === null ? '0' : '1',
|
|
'from_cache' => $data['from_cache'] === false ? '0' : '1'
|
|
],
|
|
[
|
|
'user_id' => (int) $data['user_id'],
|
|
'request_resource' => (string) $data['request_resource'],
|
|
'request_method' => (string) $data['request_method'],
|
|
'response_time' => round($data['response_time'], 4),
|
|
'response_query_count' => (int) $data['response_query_count'],
|
|
'response_error_code' => $data['response_error_code'] === null ? null : (int) $data['response_error_code'],
|
|
'response_query_time' => round($data['response_query_time'], 4)
|
|
],
|
|
$data['request_timestamp']
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get microtime for influx.
|
|
*
|
|
* @link https://github.com/influxdata/influxdb-php
|
|
*
|
|
* @return string
|
|
*/
|
|
private function microtime() {
|
|
list($usec, $sec) = explode(' ', microtime());
|
|
return sprintf('%d%06d', $sec, $usec * 1000000);
|
|
}
|
|
|
|
|
|
}
|