aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--endpoints/rest.php109
-rw-r--r--engine/lib/api.php544
-rw-r--r--engine/schema/mysql.sql29
3 files changed, 682 insertions, 0 deletions
diff --git a/endpoints/rest.php b/endpoints/rest.php
new file mode 100644
index 000000000..483971eb4
--- /dev/null
+++ b/endpoints/rest.php
@@ -0,0 +1,109 @@
+<?php
+ /**
+ * Rest endpoint.
+ * The API REST endpoint.
+ *
+ * @package Elgg
+ * @subpackage API
+ * @license http://www.gnu.org/licenses/old-licenses/gpl-2.0.html GNU Public License version 2
+ * @author Marcus Povey <marcus@dushka.co.uk>
+ * @copyright Curverider Ltd 2008
+ * @link http://elgg.org/
+ */
+
+ // Include required files
+ require_once('../engine/start.php');
+ global $ApiEnvironment;
+
+ // Register the error handler
+ error_reporting(E_ALL);
+ set_error_handler('__php_error_handler');
+
+ // Register a default exception handler
+ set_exception_handler('__php_exception_handler');
+
+ // Get parameter variables
+ $format = get_input('format', 'php');
+ $method = get_input('method');
+ $result = null;
+
+
+ // See if we have a session
+ /**
+ * If we have a session then we can assume that this is being called by AJAX from
+ * within an already logged on browser.
+ *
+ * NB. This may be a gaping security hole, but hey ho.
+ */
+ if (!isloggedin())
+ {
+ // Get api header
+ $api_header = get_and_validate_api_headers();
+ $ApiEnvironment->api_header = $api_header;
+
+ // Pull API user details
+ $ApiEnvironment->api_user = get_api_user($api_header->api_key);
+
+ if ($ApiEnvironment->api_user)
+ {
+ // Get the secret key
+ $secret_key = $ApiEnvironment->api_user->secret;
+
+ // Validate HMAC
+ $hmac = calculate_hmac($api_header->hmac_algo,
+ $api_header->time,
+ $api_header->api_key,
+ $secret_key,
+ $api_header->get_variables,
+ $api_header->method == 'POST' ? $api_header->posthash : "");
+
+ if (strcmp(
+ $api_header->hmac,
+ $hmac
+ )==0)
+ {
+ // Now make sure this is not a replay
+ if (!cache_hmac_check_replay($hmac))
+ {
+ $postdata = "";
+ $token = "";
+ $params = $_REQUEST;
+
+ // Validate post data
+ if ($api_header->method=="POST")
+ {
+ $postdata = get_post_data();
+ $calculated_posthash = calculate_posthash($postdata, $api_header->posthash_algo);
+
+ if (strcmp($api_header->posthash, $calculated_posthash)!=0)
+ throw new SecurityException("POST data hash is invalid - Expected $calculated_posthash but got {$api_header->posthash}.");
+ }
+
+ // Execute
+ if (isset($params['auth_token']))
+ $result = execute_method($method, $params, $token);
+ }
+ else
+ throw new SecurityException("Packet signature already seen.");
+ }
+ else
+ throw new SecurityException("HMAC is invalid. {$api_header->hmac} != [calc]$hmac = {$api_header->hmac_algo}(**SECRET KEY**, time:{$api_header->time}, apikey:{$api_header->api_key}, get_vars:{$api_header->get_variables}" . ($api_header->method=="POST"? "posthash:$api_header->posthash}" : ")"));
+ }
+ else
+ throw new SecurityException("Invalid or missing API Key.",ErrorResult::$RESULT_FAIL_APIKEY_INVALID);
+ }
+ else
+ {
+ // User is logged in, just execute
+ if (isset($params['auth_token'])) $token = $params['auth_token'];
+ $result = execute_method($method, $params, $token);
+ }
+
+
+ // Finally output
+ if (!($result instanceof GenericResult))
+ throw new APIException("API Result is of an unknown type, this should never happen.");
+
+ output_result($result, $format);
+
+?> \ No newline at end of file
diff --git a/engine/lib/api.php b/engine/lib/api.php
new file mode 100644
index 000000000..b446c5d63
--- /dev/null
+++ b/engine/lib/api.php
@@ -0,0 +1,544 @@
+<?php
+ /**
+ * Elgg API
+ * Functions and objects which make up the API engine.
+ *
+ * @package Elgg
+ * @subpackage Core
+ * @license http://www.gnu.org/licenses/old-licenses/gpl-2.0.html GNU Public License version 2
+ * @author Marcus Povey <marcus@dushka.co.uk>
+ * @copyright Curverider Ltd 2008
+ * @link http://elgg.org/
+ */
+
+ // Result classes /////////////////////////////////////////////////////////////////////////
+
+ /**
+ * @class GenericResult Result superclass.
+ * @author Marcus Povey <marcus@dushka.co.uk>
+ */
+ abstract class GenericResult
+ {
+ /**
+ * The status of the result.
+ * @var int
+ */
+ private $status_code;
+
+ /**
+ * Message returned along with the status which is almost always an error message.
+ * This must be human readable, understandable and localised.
+ * @var string
+ */
+ private $message;
+
+ /**
+ * Result store.
+ * Attach result specific informaton here.
+ *
+ * @var mixed. Should probably be an object of some sort.
+ */
+ private $result;
+
+ /**
+ * Set a status code and optional message.
+ *
+ * @param int $status The status code.
+ * @param string $message The message.
+ */
+ protected function setStatusCode($status, $message = "")
+ {
+ $this->status_code = $status;
+ $this->message = $message;
+ }
+
+ /**
+ * Set the result.
+ *
+ * @param mixed $result
+ */
+ protected function setResult($result) { $this->result = $result; }
+
+ protected function getStatusCode() { return $this->status_code; }
+ protected function getStatusMessage() { return $this->message; }
+ protected function getResult() { return $this->result; }
+
+ /**
+ * Serialise to a standard class.
+ *
+ * DEVNOTE: The API is only interested in data, we can not easily serialise
+ * custom classes without the need for 1) the other side being PHP, 2) you need to have the class
+ * definition installed, 3) its the right version!
+ *
+ * Therefore, I'm not bothering.
+ *
+ * Override this to include any more specific information, however api results should be attached to the
+ * class using setResult().
+ *
+ * @return stdClass Object containing the serialised result.
+ */
+ public function toStdClass()
+ {
+ global $ERRORS;
+
+ $result = new stdClass;
+
+ $result->status = $this->getStatusCode();
+ if ($this->getStatusMessage()!="") $result->message = $this->getStatusMessage();
+
+ $resultdata = $this->getResult();
+ if (isset($resultdata)) $result->result = $resultdata;
+
+ if (count($ERRORS))
+ $result->runtime_errors = $ERRORS;
+
+ return $result;
+ }
+ }
+
+ /**
+ * @class SuccessResult
+ * Generic success result class, extend if you want to do something special.
+ * @author Marcus Povey <marcus@dushka.co.uk>
+ */
+ class SuccessResult extends GenericResult
+ {
+ public static $RESULT_SUCCESS = 0; // Do not change this from 0
+
+ public function SuccessResult($result)
+ {
+ $this->setResult($result);
+ $this->setStatusCode(SuccessResult::$RESULT_SUCCESS);
+ }
+
+ public static function getInstance($result)
+ {
+ // Return a new error object.
+ return new SuccessResult($result);
+ }
+ }
+
+ /**
+ * @class ErrorResult
+ * The error result class.
+ * @author Marcus Povey <marcus@dushka.co.uk>
+ */
+ class ErrorResult extends GenericResult
+ {
+ public static $RESULT_FAIL = -1 ; // Fail with no specific code
+
+ public static $RESULT_FAIL_APIKEY_DISABLED = -30;
+ public static $RESULT_FAIL_APIKEY_INACTIVE = -31;
+ public static $RESULT_FAIL_APIKEY_INVALID = -32;
+
+ public static $RESULT_FAIL_AUTHTOKEN = -20; // Invalid, expired or missing auth token
+
+ public function ErrorResult($message, $code = "", Exception $exception = NULL)
+ {
+ if ($code == "")
+ $code = GenericResult::$RESULT_FAIL;
+
+ if ($exception!=NULL)
+ $this->setResult($exception->__toString());
+
+ $this->setStatusCode($code, $message);
+ }
+
+ /**
+ * Get a new instance of the ErrorResult.
+ *
+ * @param string $message
+ * @param int $code
+ * @param Exception $exception Optional exception for generating a stack trace.
+ */
+ public static function getInstance($message, $code = "", Exception $exception = NULL)
+ {
+ // Return a new error object.
+ return new ErrorResult($message, $code, $exception);
+ }
+ }
+
+ // API functions //////////////////////////////////////////////////////////////////////////
+
+ /** Create the environment for API Calls */
+ $ApiEnvironment = new stdClass;
+
+
+ /**
+ * An array holding methods.
+ * The structure of this is
+ * $METHODS = array (
+ * "api.method" => array (
+ * "function" = 'my_function_callback'
+ * "parameters" = array (
+ * "variable" = array ( // NB, the order is the same as defined by your function callback
+ * type => 'int' | 'bool' | 'float' | 'string'
+ * required => true (default) | false
+ * )
+ * )
+ * "require_auth" => true (default) | false
+ * )
+ * )
+ */
+ $METHODS = array();
+
+
+ // export function
+
+ /**
+ * Expose an arbitrary function as an api call.
+ *
+ * Limitations: Currently can not expose functions which expect objects or arrays.
+ *
+ * @param string $method The api name to expose this as, eg "myapi.dosomething"
+ * @param string $function Your function callback.
+ * @param array $parameters Optional list of parameters in the same order as in your function, with optional parameters last.
+ * @param bool $require_auth Whether this requires a user authentication token or not (default is true)
+ * @return bool
+ */
+ function expose_function($method, $function, array $parameters = NULL, $require_auth = true)
+ {
+ }
+
+ /**
+ * Executes a method.
+ * A method is a function which you have previously exposed using expose_function.
+ *
+ * @param string $method Method, e.g. "foo.bar"
+ * @param array $parameters Array of parameters in the format "variable" => "value", thse will be sanitised before being fed to your handler.
+ * @param string $token The authentication token to authorise this method call.
+ * @return GenericResult The result of the execution.
+ * @throws APIException, SecurityException
+ */
+ function execute_method($method, array $parameters, $token = "")
+ {
+
+ // TODO: If auth token, validate user and set session
+
+
+ // Return an error if not found
+ throw new APIException("Method call '$method' has not been implemented.");
+ }
+
+ /**
+ * This function looks at the super-global variable $_SERVER and extracts the various
+ * header variables needed to pass to the validation functions after performing basic validation.
+ *
+ * @return stdClass Containing all the values.
+ * @throws APIException Detailing the error.
+ */
+ function get_and_validate_api_headers()
+ {
+ $result = new stdClass;
+
+ $result->method = trim($_SERVER['REQUEST_METHOD']);
+ if (($result->method != "GET") && ($result->method!= "POST")) // Only allow these methods
+ throw new APIException("Request method must be GET or POST");
+
+ $result->api_key = trim($_SERVER['HTTP_X_ELGG_APIKEY']);
+ if ($result->api_key == "")
+ throw new APIException("Missing X-Elgg-apikey HTTP header");
+
+ $result->hmac = trim($_SERVER['HTTP_X_ELGG_HMAC']);
+ if ($result->hmac == "")
+ throw new APIException("Missing X-Elgg-hmac header");
+
+ $result->hmac_algo = trim($_SERVER['HTTP_X_ELGG_HMAC_ALGO']);
+ if ($result->hmac_algo == "")
+ throw new APIException("Missing X-Elgg-hmac-algo header");
+
+ $result->time = trim($_SERVER['HTTP_X_ELGG_TIME']);
+ if ($result->time == "")
+ throw new APIException("Missing X-Elgg-time header");
+ if (($result->time<(microtime(true)-86400.00)) || ($result->time>(microtime(true)+86400.00))) // Basic timecheck, think about making this smaller if we get loads of users and the cache gets really big.
+ throw new APIException("X-Elgg-time is too far in the past or future");
+
+ $result->get_variables = trim($_SERVER['QUERY_STRING']);
+ if ($result->get_variables == "")
+ throw new APIException("No data on the query string");
+
+ if ($result->method=="POST")
+ {
+ $result->posthash = trim($_SERVER['HTTP_X_ELGG_POSTHASH']);
+ if ($result->posthash == "")
+ throw new APIException("Missing X-Elgg-posthash header");
+
+ $result->posthash_algo = trim($_SERVER['HTTP_X_ELGG_POSTHASH_ALGO']);
+ if ($result->posthash_algo == "")
+ throw new APIException("Missing X-Elgg-posthash_algo header");
+
+ $result->content_type = trim($_SERVER['CONTENT_TYPE']);
+ if ($result->content_type == "")
+ throw new APIException("Missing content type for post data");
+ }
+
+ return $result;
+ }
+
+ /**
+ * Find an API User's details based on the provided public api key.
+ *
+ * @param string $api_key The API Key
+ * @return mixed stdClass representing the database row or false.
+ */
+ function get_api_user($api_key)
+ {
+ global $CONFIG;
+
+ $api_key = sanitise_string($api_key);
+
+ return get_data_row("SELECT * from {$CONFIG->dbprefix}api_users where api_key='$api_key'");
+ }
+
+ /**
+ * Calculate the HMAC for the query.
+ * This function signs an api request using the information provided and is then verified by
+ * searunner.
+ *
+ * @param $algo string The HMAC algorithm used as stored in X-Searunner-hmac-algo.
+ * @param $time string String representation of unix time as stored in X-Searunner-time.
+ * @param $api_key string Your api key.
+ * @param $secret string Your secret key.
+ * @param $get_variables string URLEncoded string representation of the get variable parameters, eg "format=php&method=searunner.test".
+ * @param $post_hash string Optional sha1 hash of the post data.
+ * @return string The HMAC string.
+ */
+ function calculate_hmac($algo, $time, $api_key, $secret_key, $get_variables, $post_hash = "")
+ {
+ $ctx = hash_init($algo, HASH_HMAC, $secret_key);
+
+ hash_update($ctx, trim($time));
+ hash_update($ctx, trim($api_key));
+ hash_update($ctx, trim($get_variables));
+ if (trim($post_hash)!="") hash_update($ctx, trim($post_hash));
+
+ return hash_final($ctx);
+ }
+
+ /**
+ * Calculate a hash for some post data.
+ *
+ * TODO: Work out how to handle really large bits of data.
+ *
+ * @param $postdata string The post data.
+ * @param $algo string The algorithm used.
+ * @return string The hash.
+ */
+ function calculate_posthash($postdata, $algo)
+ {
+ $ctx = hash_init($algo);
+
+ hash_update($ctx, $postdata);
+
+ return hash_final($ctx);
+ }
+
+ /**
+ * This function will do two things. Firstly it verifys that a $hmac hasn't been seen before, and
+ * secondly it will add the given hmac to the cache.
+ *
+ * TODO : REWRITE TO NOT USE ZEND
+ *
+ * @param $hmac The hmac string.
+ * @return bool True if replay detected, false if not.
+ */
+ function cache_hmac_check_replay($hmac)
+ {
+ global $CONFIG;
+
+ throw new NotImplementedException("Writeme!");
+
+ return true;
+ }
+
+
+
+ // XML functions //////////////////////////////////////////////////////////////////////////
+
+ /**
+ * This function serialises an object recursively into an XML representation.
+ * @param $data object The object to serialise.
+ * @param $n int Level, only used for recursion.
+ * @return string The serialised XML output.
+ */
+ function serialise_object_to_xml($data, $name = "", $n = 0)
+ {
+ $classname = ($name=="" ? get_class($data) : $name);
+
+ $vars = get_object_vars($data);
+
+ $output = "";
+
+ if ($n==0) $output = "<$classname>";
+
+ foreach ($vars as $key => $value)
+ {
+ $output .= "<$key type=\"".gettype($value)."\">";
+
+ if (is_object($value))
+ $output .= serialise_object_to_xml($value, $key, $n+1);
+ else if (is_array($value))
+ $output .= serialise_array_to_xml($value, $n+1);
+ else
+ $output .= htmlentities($value);
+
+ $output .= "</$key>\n";
+ }
+
+ if ($n==0) $output .= "</$classname>\n";
+
+ return $output;
+ }
+
+ /**
+ * Serialise an array.
+ *
+ * @param array $data
+ * @param int $n Used for recursion
+ * @return string
+ */
+ function serialise_array_to_xml(array $data, $n = 0)
+ {
+ $output = "";
+
+ if ($n==0) $output = "<array>\n";
+
+ foreach ($data as $key => $value)
+ {
+ $item = "array_item";
+
+ if (is_numeric($key))
+ $output .= "<$item name=\"$key\" type=\"".gettype($value)."\">";
+ else
+ {
+ $item = $key;
+ $output .= "<$item type=\"".gettype($value)."\">";
+ }
+
+ if (is_object($value))
+ $output .= serialise_object_to_xml($value, $item, $n+1);
+ else if (is_array($value))
+ $output .= serialise_array_to_xml($value, $n+1);
+ else
+ $output .= htmlentities($value);
+
+ $output .= "</$item>\n";
+ }
+
+ if ($n==0) $output = "</array>\n";
+
+ return $output;
+ }
+
+ // Output functions ///////////////////////////////////////////////////////////////////////
+
+ /**
+ * Get output for a result in one of a number of formats.
+ *
+ * @param GenericResult $result
+ * @param string $format Optional format, if not specified or invalid, PHP is assumed.
+ * @return mixed The serialised output, or false.
+ */
+ function get_serialised_result(GenericResult $result, $format = "php")
+ {
+ $format = trim(strtolower($format));
+
+ if ($result)
+ {
+ // Echo
+ switch ($format)
+ {
+ case 'xml' : return serialise_object_to_xml($result->toStdClass(), "Elgg");
+
+ case 'json' : return json_encode($result->toStdClass());
+
+ case 'php' :
+ default: return serialize($result->toStdClass());
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Output a result, altering headers and mime-types as necessary.
+ *
+ * @param GenericResult $result
+ * @param string $format Optional format, if not specified or invalid, PHP is assumed.
+ */
+ function output_result(GenericResult $result, $format = 'php')
+ {
+ switch ($format)
+ {
+ case 'xml' : header('Content-Type: text/xml');
+ }
+
+ echo get_serialised_result($result, $format);
+ }
+
+ // Error handler functions ////////////////////////////////////////////////////////////////
+
+
+ /** Define a global array of errors */
+ $ERRORS = array();
+
+ /**
+ * PHP Error handler function.
+ * This function acts as a wrapper to catch and report PHP error messages.
+ *
+ * @see http://uk3.php.net/set-error-handler
+ * @param unknown_type $errno
+ * @param unknown_type $errmsg
+ * @param unknown_type $filename
+ * @param unknown_type $linenum
+ * @param unknown_type $vars
+ */
+ function __php_api_error_handler($errno, $errmsg, $filename, $linenum, $vars)
+ {
+ global $ERRORS;
+
+ $error = date("Y-m-d H:i:s (T)") . ": \"" . $errmsg . "\" in file " . $filename . " (line " . $linenum . ")";
+
+ switch ($errno) {
+ case E_USER_ERROR:
+ error_log("ERROR: " . $error);
+ $ERRORS[] = "ERROR: " .$error;
+
+ // Since this is a fatal error, we want to stop any further execution but do so gracefully.
+ throw new Exception("ERROR: " . $error);
+ break;
+
+ case E_WARNING :
+ case E_USER_WARNING :
+ error_log("WARNING: " . $error);
+ $ERRORS[] = "WARNING: " .$error;
+ break;
+
+ default:
+ error_log("DEBUG: " . $error);
+ $ERRORS[] = "DEBUG: " .$error;
+ }
+ }
+
+ /**
+ * PHP Exception handler.
+ * This is a generic exception handler for PHP exceptions. This will catch any
+ * uncaught exception and return it as an ErrorResult in the requested format.
+ *
+ * @param Exception $exception
+ */
+ function __php_api_exception_handler($exception) {
+
+ error_log("*** FATAL EXCEPTION (API) *** : " . $exception);
+
+ output_result(
+ ErrorResult::getInstance(
+ $exception->getMessage(),
+ $exception->getCode() == 0 ? ErrorResult::$RESULT_FAIL : $exception->getCode(),
+ $exception),
+
+ get_input('format','php') // Attempt to get the requested format if passed.
+ );
+ }
+
+?> \ No newline at end of file
diff --git a/engine/schema/mysql.sql b/engine/schema/mysql.sql
index 1361f7ebc..2ce3f71e4 100644
--- a/engine/schema/mysql.sql
+++ b/engine/schema/mysql.sql
@@ -185,3 +185,32 @@ CREATE TABLE `prefix_metadata` (
UNIQUE KEY (`object_id`,`object_type`, `name`)
) ENGINE=MyISAM;
+
+--
+-- API Users - Users who have access to the api (may not be real users)
+--
+CREATE TABLE api_users (
+ id int(11) auto_increment,
+
+ email_address varchar(128),
+
+ api_key varchar(40),
+ secret varchar(40) NOT NULL,
+ active int default 1,
+
+ unique key (email_address),
+ unique key (api_key),
+ primary key (id)
+);
+
+--
+-- Configuration settings
+--
+CREATE TABLE configuration (
+ id int(11) NOT NULL auto_increment,
+ name varchar(50) not null default '',
+ `value` varchar(255) not null default '',
+
+ primary key (id),
+ unique key (name)
+); \ No newline at end of file