2097 lines
59 KiB
PHP
2097 lines
59 KiB
PHP
<?php
|
|
|
|
namespace okapi;
|
|
|
|
# OKAPI Framework -- Wojciech Rygielski <rygielski@mimuw.edu.pl>
|
|
|
|
# If you want to include_once/require_once OKAPI in your code,
|
|
# see facade.php. You should not rely on any other file, never!
|
|
|
|
use Exception;
|
|
use ErrorException;
|
|
use OAuthServerException;
|
|
use OAuthServer400Exception;
|
|
use OAuthServer401Exception;
|
|
use OAuthMissingParameterException;
|
|
use OAuthConsumer;
|
|
use OAuthToken;
|
|
use OAuthServer;
|
|
use OAuthSignatureMethod_HMAC_SHA1;
|
|
use OAuthRequest;
|
|
use okapi\cronjobs\CronJobController;
|
|
|
|
/** Return an array of email addresses which always get notified on OKAPI errors. */
|
|
function get_admin_emails()
|
|
{
|
|
$emails = array();
|
|
if (class_exists("okapi\\Settings"))
|
|
{
|
|
try
|
|
{
|
|
foreach (Settings::get('ADMINS') as $email)
|
|
if (!in_array($email, $emails))
|
|
$emails[] = $email;
|
|
}
|
|
catch (Exception $e) { /* pass */ }
|
|
}
|
|
if (count($emails) == 0)
|
|
$emails[] = 'root@localhost';
|
|
return $emails;
|
|
}
|
|
|
|
#
|
|
# Base exception types.
|
|
#
|
|
|
|
/** A base class for all bad request exceptions. */
|
|
class BadRequest extends Exception {
|
|
protected function provideExtras(&$extras) {
|
|
$extras['reason_stack'][] = 'bad_request';
|
|
$extras['status'] = 400;
|
|
}
|
|
public function getOkapiJSON() {
|
|
$extras = array(
|
|
'developer_message' => $this->getMessage(),
|
|
'reason_stack' => array(),
|
|
);
|
|
$this->provideExtras($extras);
|
|
$extras['more_info'] = Settings::get('SITE_URL')."okapi/introduction.html#errors";
|
|
return json_encode(array("error" => $extras));
|
|
}
|
|
}
|
|
|
|
/** Thrown on PHP's FATAL errors (detected in a shutdown function). */
|
|
class FatalError extends ErrorException {}
|
|
|
|
#
|
|
# We'll try to make PHP into something more decent. Exception and
|
|
# error handling.
|
|
#
|
|
|
|
/** Container for exception-handling functions. */
|
|
class OkapiExceptionHandler
|
|
{
|
|
/** Handle exception thrown while executing OKAPI request. */
|
|
public static function handle($e)
|
|
{
|
|
if ($e instanceof OAuthServerException)
|
|
{
|
|
# This is thrown on invalid OAuth requests. There are many subclasses
|
|
# of this exception. All of them result in HTTP 400 or HTTP 401 error
|
|
# code. See also: http://oauth.net/core/1.0a/#http_codes
|
|
|
|
if ($e instanceof OAuthServer400Exception)
|
|
header("HTTP/1.0 400 Bad Request");
|
|
else
|
|
header("HTTP/1.0 401 Unauthorized");
|
|
header("Access-Control-Allow-Origin: *");
|
|
header("Content-Type: application/json; charset=utf-8");
|
|
|
|
print $e->getOkapiJSON();
|
|
}
|
|
elseif ($e instanceof BadRequest)
|
|
{
|
|
# Intentionally thrown from within the OKAPI method code.
|
|
# Consumer (aka external developer) had something wrong with his
|
|
# request and we want him to know that.
|
|
|
|
header("HTTP/1.0 400 Bad Request");
|
|
header("Access-Control-Allow-Origin: *");
|
|
header("Content-Type: application/json; charset=utf-8");
|
|
|
|
print $e->getOkapiJSON();
|
|
}
|
|
else # (ErrorException, MySQL exception etc.)
|
|
{
|
|
# This one is thrown on PHP notices and warnings - usually this
|
|
# indicates an error in OKAPI method. If thrown, then something
|
|
# must be fixed on OUR part.
|
|
|
|
if (!headers_sent())
|
|
{
|
|
header("HTTP/1.0 500 Internal Server Error");
|
|
header("Access-Control-Allow-Origin: *");
|
|
header("Content-Type: text/plain; charset=utf-8");
|
|
}
|
|
|
|
print "Oops... Something went wrong on *our* part.\n\n";
|
|
print "Message was passed on to the site administrators. We'll try to fix it.\n";
|
|
print "Contact the developers if you think you can help!";
|
|
|
|
error_log($e->getMessage());
|
|
|
|
$exception_info = self::get_exception_info($e);
|
|
|
|
if (class_exists("okapi\\Settings") && (Settings::get('DEBUG')))
|
|
{
|
|
print "\n\nBUT! Since the DEBUG flag is on, then you probably ARE a developer yourself.\n";
|
|
print "Let's cut to the chase then:";
|
|
print "\n\n".$exception_info;
|
|
}
|
|
if (class_exists("okapi\\Settings") && (Settings::get('DEBUG_PREVENT_EMAILS')))
|
|
{
|
|
# Sending emails was blocked on admin's demand.
|
|
# This is possible only on development environment.
|
|
}
|
|
else
|
|
{
|
|
$subject = "OKAPI Method Error - ".substr(
|
|
$_SERVER['REQUEST_URI'], 0, strpos(
|
|
$_SERVER['REQUEST_URI'].'?', '?'));
|
|
|
|
$message = (
|
|
"OKAPI caught the following exception while executing API method request.\n".
|
|
"This is an error in OUR code and should be fixed. Please contact the\n".
|
|
"developer of the module that threw this error. Thanks!\n\n".
|
|
$exception_info
|
|
);
|
|
try
|
|
{
|
|
Okapi::mail_admins($subject, $message);
|
|
}
|
|
catch (Exception $e)
|
|
{
|
|
# Unable to use full-featured mail_admins version. We'll use a backup.
|
|
# We need to make sure we're not spamming.
|
|
|
|
$lock_file = "/tmp/okapi-fatal-error-mode";
|
|
$last_email = false;
|
|
if (file_exists($lock_file))
|
|
$last_email = filemtime($lock_file);
|
|
if ($last_email === false) {
|
|
# Assume this is the first email.
|
|
$last_email = 0;
|
|
}
|
|
if (time() - $last_email < 60) {
|
|
# Send no more than one per minute.
|
|
return;
|
|
}
|
|
@touch($lock_file);
|
|
|
|
$admin_email = implode(", ", get_admin_emails());
|
|
$sender_email = class_exists("okapi\\Settings") ? Settings::get('FROM_FIELD') : 'root@localhost';
|
|
$subject = "Fatal error mode: ".$subject;
|
|
$message = "Fatal error mode: OKAPI will send at most ONE message per minute.\n\n".$message;
|
|
$headers = (
|
|
"Content-Type: text/plain; charset=utf-8\n".
|
|
"From: OKAPI <$sender_email>\n".
|
|
"Reply-To: $sender_email\n"
|
|
);
|
|
mail($admin_email, $subject, $message, $headers);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public static function get_exception_info($e)
|
|
{
|
|
$exception_info = "===== ERROR MESSAGE =====\n".trim($e->getMessage())."\n=========================\n\n";
|
|
if ($e instanceof FatalError)
|
|
{
|
|
# This one doesn't have a stack trace. It is fed directly to OkapiExceptionHandler::handle
|
|
# by OkapiErrorHandler::handle_shutdown. Instead of printing trace, we will just print
|
|
# the file and line.
|
|
|
|
$exception_info .= "File: ".$e->getFile()."\nLine: ".$e->getLine()."\n\n";
|
|
}
|
|
else
|
|
{
|
|
$exception_info .= "--- Stack trace ---\n".$e->getTraceAsString()."\n\n";
|
|
}
|
|
|
|
$exception_info .= (isset($_SERVER['REQUEST_URI']) ? "--- OKAPI method called ---\n".
|
|
preg_replace("/([?&])/", "\n$1", $_SERVER['REQUEST_URI'])."\n\n" : "");
|
|
$exception_info .= "--- Request headers ---\n".implode("\n", array_map(
|
|
function($k, $v) { return "$k: $v"; },
|
|
array_keys(getallheaders()), array_values(getallheaders())
|
|
));
|
|
|
|
return $exception_info;
|
|
}
|
|
}
|
|
|
|
/** Container for error-handling functions. */
|
|
class OkapiErrorHandler
|
|
{
|
|
public static $treat_notices_as_errors = false;
|
|
|
|
/** Handle error encountered while executing OKAPI request. */
|
|
public static function handle($severity, $message, $filename, $lineno)
|
|
{
|
|
if ($severity == E_STRICT) return false;
|
|
if (($severity == E_NOTICE || $severity == E_DEPRECATED) &&
|
|
!self::$treat_notices_as_errors)
|
|
{
|
|
return false;
|
|
}
|
|
throw new ErrorException($message, 0, $severity, $filename, $lineno);
|
|
}
|
|
|
|
/** Use this BEFORE calling a piece of buggy code. */
|
|
public static function disable()
|
|
{
|
|
restore_error_handler();
|
|
}
|
|
|
|
/** Use this AFTER calling a piece of buggy code. */
|
|
public static function reenable()
|
|
{
|
|
set_error_handler(array('\okapi\OkapiErrorHandler', 'handle'));
|
|
}
|
|
|
|
/** Handle FATAL errors (not catchable, report only). */
|
|
public static function handle_shutdown()
|
|
{
|
|
$error = error_get_last();
|
|
|
|
# We don't know whether this error has been already handled. The error_get_last
|
|
# function will return E_NOTICE or E_STRICT errors if the stript has shut down
|
|
# correctly. The only error which cannot be recovered from is E_ERROR, we have
|
|
# to check the type then.
|
|
|
|
if (($error !== null) && ($error['type'] == E_ERROR))
|
|
{
|
|
$e = new FatalError($error['message'], 0, $error['type'], $error['file'], $error['line']);
|
|
OkapiExceptionHandler::handle($e);
|
|
}
|
|
}
|
|
}
|
|
|
|
# Setting handlers. Errors will now throw exceptions, and all exceptions
|
|
# will be properly handled. (Unfortunetelly, only SOME errors can be caught
|
|
# this way, PHP limitations...)
|
|
|
|
set_exception_handler(array('\okapi\OkapiExceptionHandler', 'handle'));
|
|
set_error_handler(array('\okapi\OkapiErrorHandler', 'handle'));
|
|
register_shutdown_function(array('\okapi\OkapiErrorHandler', 'handle_shutdown'));
|
|
|
|
#
|
|
# Extending exception types (introducing some convenient shortcuts for
|
|
# the developer).
|
|
#
|
|
|
|
class Http404 extends BadRequest {}
|
|
|
|
/** Common type of BadRequest: Required parameter is missing. */
|
|
class ParamMissing extends BadRequest
|
|
{
|
|
private $paramName;
|
|
protected function provideExtras(&$extras) {
|
|
parent::provideExtras($extras);
|
|
$extras['reason_stack'][] = 'missing_parameter';
|
|
$extras['parameter'] = $this->paramName;
|
|
}
|
|
public function __construct($paramName)
|
|
{
|
|
parent::__construct("Required parameter '$paramName' is missing.");
|
|
$this->paramName = $paramName;
|
|
}
|
|
}
|
|
|
|
/** Common type of BadRequest: Parameter has invalid value. */
|
|
class InvalidParam extends BadRequest
|
|
{
|
|
public $paramName;
|
|
|
|
/** What was wrong about the param? */
|
|
public $whats_wrong_about_it;
|
|
|
|
protected function provideExtras(&$extras) {
|
|
parent::provideExtras($extras);
|
|
$extras['reason_stack'][] = 'invalid_parameter';
|
|
$extras['parameter'] = $this->paramName;
|
|
$extras['whats_wrong_about_it'] = $this->whats_wrong_about_it;
|
|
}
|
|
public function __construct($paramName, $whats_wrong_about_it = "", $code = 0)
|
|
{
|
|
$this->paramName = $paramName;
|
|
$this->whats_wrong_about_it = $whats_wrong_about_it;
|
|
if ($whats_wrong_about_it)
|
|
parent::__construct("Parameter '$paramName' has invalid value: ".$whats_wrong_about_it, $code);
|
|
else
|
|
parent::__construct("Parameter '$paramName' has invalid value.", $code);
|
|
}
|
|
}
|
|
|
|
/** Thrown on invalid SQL queries. */
|
|
class DbException extends Exception {}
|
|
|
|
#
|
|
# Database access layer.
|
|
#
|
|
|
|
/** Database access class. Use this instead of mysql_query, sql or sqlValue. */
|
|
class Db
|
|
{
|
|
private static $connected = false;
|
|
|
|
public static function connect()
|
|
{
|
|
if (mysql_connect(Settings::get('DB_SERVER'), Settings::get('DB_USERNAME'), Settings::get('DB_PASSWORD')))
|
|
{
|
|
mysql_select_db(Settings::get('DB_NAME'));
|
|
mysql_query("set names 'utf8'");
|
|
self::$connected = true;
|
|
}
|
|
else
|
|
throw new Exception("Could not connect to MySQL: ".mysql_error());
|
|
}
|
|
|
|
public static function select_row($query)
|
|
{
|
|
$rows = self::select_all($query);
|
|
switch (count($rows))
|
|
{
|
|
case 0: return null;
|
|
case 1: return $rows[0];
|
|
default:
|
|
throw new DbException("Invalid query. Db::select_row returned more than one row for:\n\n".$query."\n");
|
|
}
|
|
}
|
|
|
|
public static function select_all($query)
|
|
{
|
|
$rows = array();
|
|
self::select_and_push($query, $rows);
|
|
return $rows;
|
|
}
|
|
|
|
public static function select_and_push($query, & $arr, $keyField = null)
|
|
{
|
|
$rs = self::query($query);
|
|
while (true)
|
|
{
|
|
$row = mysql_fetch_assoc($rs);
|
|
if ($row === false)
|
|
break;
|
|
if ($keyField == null)
|
|
$arr[] = $row;
|
|
else
|
|
$arr[$row[$keyField]] = $row;
|
|
}
|
|
mysql_free_result($rs);
|
|
}
|
|
|
|
public static function select_value($query)
|
|
{
|
|
$column = self::select_column($query);
|
|
if ($column == null)
|
|
return null;
|
|
if (count($column) == 1)
|
|
return $column[0];
|
|
throw new DbException("Invalid query. Db::select_value returned more than one row for:\n\n".$query."\n");
|
|
}
|
|
|
|
public static function select_column($query)
|
|
{
|
|
$column = array();
|
|
$rs = self::query($query);
|
|
while (true)
|
|
{
|
|
$values = mysql_fetch_array($rs);
|
|
if ($values === false)
|
|
break;
|
|
array_push($column, $values[0]);
|
|
}
|
|
mysql_free_result($rs);
|
|
return $column;
|
|
}
|
|
|
|
public static function last_insert_id()
|
|
{
|
|
return mysql_insert_id();
|
|
}
|
|
|
|
public static function execute($query)
|
|
{
|
|
$rs = self::query($query);
|
|
if ($rs !== true)
|
|
throw new DbException("Db::execute returned a result set for your query. ".
|
|
"You should use Db::select_* or Db::query for SELECT queries!");
|
|
}
|
|
|
|
public static function query($query)
|
|
{
|
|
if (!self::$connected)
|
|
self::connect();
|
|
$rs = mysql_query($query);
|
|
if (!$rs)
|
|
{
|
|
throw new DbException("SQL Error ".mysql_errno().": ".mysql_error()."\n\nThe query was:\n".$query."\n");
|
|
}
|
|
return $rs;
|
|
}
|
|
|
|
public static function field_exists($table, $field)
|
|
{
|
|
if (!preg_match("/[a-z0-9_]+/", $table.$field))
|
|
return false;
|
|
try {
|
|
$spec = self::select_all("desc ".$table.";");
|
|
} catch (Exception $e) {
|
|
/* Table doesn't exist, probably. */
|
|
return false;
|
|
}
|
|
foreach ($spec as &$row_ref) {
|
|
if (strtoupper($row_ref['Field']) == strtoupper($field))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
#
|
|
# Including OAuth internals. Preparing OKAPI Consumer and Token classes.
|
|
#
|
|
|
|
require_once($GLOBALS['rootpath']."okapi/oauth.php");
|
|
|
|
class OkapiConsumer extends OAuthConsumer
|
|
{
|
|
public $name;
|
|
public $url;
|
|
public $email;
|
|
|
|
public function __construct($key, $secret, $name, $url, $email)
|
|
{
|
|
$this->key = $key;
|
|
$this->secret = $secret;
|
|
$this->name = $name;
|
|
$this->url = $url;
|
|
$this->email = $email;
|
|
}
|
|
|
|
public function __toString()
|
|
{
|
|
return "OkapiConsumer[key=$this->key,name=$this->name]";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Use this when calling OKAPI methods internally from OKAPI code. (If you want call
|
|
* OKAPI from other OC code, you must use Facade class - see facade.php)
|
|
*/
|
|
class OkapiInternalConsumer extends OkapiConsumer
|
|
{
|
|
public function __construct()
|
|
{
|
|
$admins = get_admin_emails();
|
|
parent::__construct('internal', null, "Internal OKAPI jobs", null, $admins[0]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Used when debugging methods using DEBUG_AS_USERNAME flag.
|
|
*/
|
|
class OkapiDebugConsumer extends OkapiConsumer
|
|
{
|
|
public function __construct()
|
|
{
|
|
$admins = get_admin_emails();
|
|
parent::__construct('debug', null, "DEBUG_AS_USERNAME Debugger", null, $admins[0]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Used by calls made via Facade class. SHOULD NOT be referenced anywhere else from
|
|
* within OKAPI code.
|
|
*/
|
|
class OkapiFacadeConsumer extends OkapiConsumer
|
|
{
|
|
public function __construct()
|
|
{
|
|
$admins = get_admin_emails();
|
|
parent::__construct('facade', null, "Internal usage via Facade", null, $admins[0]);
|
|
}
|
|
}
|
|
|
|
class OkapiToken extends OAuthToken
|
|
{
|
|
public $consumer_key;
|
|
public $token_type;
|
|
|
|
public function __construct($key, $secret, $consumer_key, $token_type)
|
|
{
|
|
parent::__construct($key, $secret);
|
|
$this->consumer_key = $consumer_key;
|
|
$this->token_type = $token_type;
|
|
}
|
|
}
|
|
class OkapiRequestToken extends OkapiToken
|
|
{
|
|
public $callback_url;
|
|
public $authorized_by_user_id;
|
|
public $verifier;
|
|
|
|
public function __construct($key, $secret, $consumer_key, $callback_url,
|
|
$authorized_by_user_id, $verifier)
|
|
{
|
|
parent::__construct($key, $secret, $consumer_key, 'request');
|
|
$this->callback_url = $callback_url;
|
|
$this->authorized_by_user_id = $authorized_by_user_id;
|
|
$this->verifier = $verifier;
|
|
}
|
|
}
|
|
|
|
class OkapiAccessToken extends OkapiToken
|
|
{
|
|
public $user_id;
|
|
|
|
public function __construct($key, $secret, $consumer_key, $user_id)
|
|
{
|
|
parent::__construct($key, $secret, $consumer_key, 'access');
|
|
$this->user_id = $user_id;
|
|
}
|
|
}
|
|
|
|
/** Use this in conjunction with OkapiInternalConsumer. */
|
|
class OkapiInternalAccessToken extends OkapiAccessToken
|
|
{
|
|
public function __construct($user_id)
|
|
{
|
|
parent::__construct('internal-'.$user_id, null, 'internal', $user_id);
|
|
}
|
|
}
|
|
|
|
/** Use this in conjunction with OkapiFacadeConsumer. */
|
|
class OkapiFacadeAccessToken extends OkapiAccessToken
|
|
{
|
|
public function __construct($user_id)
|
|
{
|
|
parent::__construct('facade-'.$user_id, null, 'facade', $user_id);
|
|
}
|
|
}
|
|
|
|
/** Used when debugging with DEBUG_AS_USERNAME. */
|
|
class OkapiDebugAccessToken extends OkapiAccessToken
|
|
{
|
|
public function __construct($user_id)
|
|
{
|
|
parent::__construct('debug-'.$user_id, null, 'debug', $user_id);
|
|
}
|
|
}
|
|
|
|
/** Default OAuthServer with some OKAPI-specific methods added. */
|
|
class OkapiOAuthServer extends OAuthServer
|
|
{
|
|
public function __construct($data_store)
|
|
{
|
|
parent::__construct($data_store);
|
|
# We want HMAC_SHA1 authorization method only.
|
|
$this->add_signature_method(new OAuthSignatureMethod_HMAC_SHA1());
|
|
}
|
|
|
|
/**
|
|
* By default, works like verify_request, but it does support some additional
|
|
* options. If $token_required == false, it doesn't throw an exception when
|
|
* there is no token specified. You may also change the token_type required
|
|
* for this request.
|
|
*/
|
|
public function verify_request2(&$request, $token_type = 'access', $token_required = true)
|
|
{
|
|
$this->get_version($request);
|
|
$consumer = $this->get_consumer($request);
|
|
try {
|
|
$token = $this->get_token($request, $consumer, $token_type);
|
|
} catch (OAuthMissingParameterException $e) {
|
|
# Note, that exception will be different if token is supplied
|
|
# and is invalid. We catch only a completely MISSING token parameter.
|
|
if (($e->getParamName() == 'oauth_token') && (!$token_required))
|
|
$token = null;
|
|
else
|
|
throw $e;
|
|
}
|
|
$this->check_signature($request, $consumer, $token);
|
|
return array($consumer, $token);
|
|
}
|
|
}
|
|
|
|
# Including local datastore and settings (connecting SQL database etc.).
|
|
|
|
require_once($GLOBALS['rootpath']."okapi/settings.php");
|
|
require_once($GLOBALS['rootpath']."okapi/datastore.php");
|
|
|
|
class OkapiHttpResponse
|
|
{
|
|
public $status = "200 OK";
|
|
public $cache_control = "no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0";
|
|
public $content_type = "text/plain; charset=utf-8";
|
|
public $content_disposition = null;
|
|
public $allow_gzip = true;
|
|
public $connection_close = false;
|
|
public $etag = null;
|
|
|
|
/** Use this only as a setter, use get_body or print_body for reading! */
|
|
public $body;
|
|
|
|
/** This could be set in case when body is a stream of known length. */
|
|
public $stream_length = null;
|
|
|
|
public function get_length()
|
|
{
|
|
if (is_resource($this->body))
|
|
return $this->stream_length;
|
|
return strlen($this->body);
|
|
}
|
|
|
|
/** Note: You can call this only once! */
|
|
public function print_body()
|
|
{
|
|
if (is_resource($this->body))
|
|
{
|
|
while (!feof($this->body))
|
|
print fread($this->body, 1024*1024);
|
|
}
|
|
else
|
|
print $this->body;
|
|
}
|
|
|
|
/**
|
|
* Note: You can call this only once! The result might be huge (a stream),
|
|
* it is usually better to print it directly with ->print_body().
|
|
*/
|
|
public function get_body()
|
|
{
|
|
if (is_resource($this->body))
|
|
{
|
|
ob_start();
|
|
fpassthru($this->body);
|
|
return ob_get_clean();
|
|
}
|
|
else
|
|
return $this->body;
|
|
}
|
|
|
|
/**
|
|
* Print the headers and the body. This should be the last thing your script does.
|
|
*/
|
|
public function display()
|
|
{
|
|
header("HTTP/1.1 ".$this->status);
|
|
header("Access-Control-Allow-Origin: *");
|
|
header("Content-Type: ".$this->content_type);
|
|
header("Cache-Control: ".$this->cache_control);
|
|
if ($this->connection_close)
|
|
header("Connection: close");
|
|
if ($this->content_disposition)
|
|
header("Content-Disposition: ".$this->content_disposition);
|
|
if ($this->etag)
|
|
header("ETag: $this->etag");
|
|
|
|
# Make sure that gzip is supported by the client.
|
|
$try_gzip = $this->allow_gzip;
|
|
if (empty($_SERVER["HTTP_ACCEPT_ENCODING"]) || (strpos($_SERVER["HTTP_ACCEPT_ENCODING"], "gzip") === false))
|
|
$try_gzip = false;
|
|
|
|
# We will gzip the data ourselves, while disabling gziping by Apache. This way, we can
|
|
# set the Content-Length correctly which is handy in some scenarios.
|
|
|
|
if ($try_gzip && is_string($this->body))
|
|
{
|
|
header("Content-Encoding: gzip");
|
|
$gzipped = gzencode($this->body, 5);
|
|
header("Content-Length: ".strlen($gzipped));
|
|
print $gzipped;
|
|
}
|
|
else
|
|
{
|
|
$length = $this->get_length();
|
|
if ($length)
|
|
header("Content-Length: ".$length);
|
|
$this->print_body();
|
|
}
|
|
}
|
|
}
|
|
|
|
class OkapiRedirectResponse extends OkapiHttpResponse
|
|
{
|
|
public $url;
|
|
public function __construct($url) { $this->url = $url; }
|
|
public function display()
|
|
{
|
|
header("HTTP/1.1 303 See Other");
|
|
header("Location: ".$this->url);
|
|
}
|
|
}
|
|
|
|
class OkapiLock
|
|
{
|
|
private $lockfile;
|
|
private $lock;
|
|
|
|
/** Note: This does NOT tell you if someone currently locked it! */
|
|
public static function exists($name)
|
|
{
|
|
$lockfile = Okapi::get_var_dir()."/okapi-lock-".$name;
|
|
return file_exists($lockfile);
|
|
}
|
|
|
|
public static function get($name)
|
|
{
|
|
return new OkapiLock($name);
|
|
}
|
|
|
|
private function __construct($name)
|
|
{
|
|
if (Settings::get('DEBUG_PREVENT_SEMAPHORES'))
|
|
{
|
|
# Using semaphores is forbidden on this server by its admin.
|
|
# This is possible only on development environment.
|
|
$this->lock = null;
|
|
}
|
|
else
|
|
{
|
|
$this->lockfile = Okapi::get_var_dir()."/okapi-lock-".$name;
|
|
if (!file_exists($this->lockfile))
|
|
{
|
|
$fp = fopen($this->lockfile, "wb");
|
|
fclose($fp);
|
|
}
|
|
$this->lock = sem_get(fileinode($this->lockfile));
|
|
}
|
|
}
|
|
|
|
public function acquire()
|
|
{
|
|
if ($this->lock !== null)
|
|
sem_acquire($this->lock);
|
|
}
|
|
|
|
public function release()
|
|
{
|
|
if ($this->lock !== null)
|
|
sem_release($this->lock);
|
|
}
|
|
|
|
public function remove()
|
|
{
|
|
if ($this->lock !== null)
|
|
{
|
|
sem_remove($this->lock);
|
|
unlink($this->lockfile);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Container for various OKAPI functions. */
|
|
class Okapi
|
|
{
|
|
public static $data_store;
|
|
public static $server;
|
|
public static $revision = 651; # This gets replaced in automatically deployed packages
|
|
private static $okapi_vars = null;
|
|
|
|
/** Get a variable stored in okapi_vars. If variable not found, return $default. */
|
|
public static function get_var($varname, $default = null)
|
|
{
|
|
if (self::$okapi_vars === null)
|
|
{
|
|
$rs = Db::query("
|
|
select var, value
|
|
from okapi_vars
|
|
");
|
|
self::$okapi_vars = array();
|
|
while ($row = mysql_fetch_assoc($rs))
|
|
self::$okapi_vars[$row['var']] = $row['value'];
|
|
}
|
|
if (isset(self::$okapi_vars[$varname]))
|
|
return self::$okapi_vars[$varname];
|
|
return $default;
|
|
}
|
|
|
|
/**
|
|
* Save a variable to okapi_vars. WARNING: The entire content of okapi_vars table
|
|
* is loaded on EVERY execution. Do not store data in this table, unless it's
|
|
* frequently needed.
|
|
*/
|
|
public static function set_var($varname, $value)
|
|
{
|
|
Okapi::get_var($varname);
|
|
Db::execute("
|
|
replace into okapi_vars (var, value)
|
|
values (
|
|
'".mysql_real_escape_string($varname)."',
|
|
'".mysql_real_escape_string($value)."');
|
|
");
|
|
self::$okapi_vars[$varname] = $value;
|
|
}
|
|
|
|
/** Send an email message to local OKAPI administrators. */
|
|
public static function mail_admins($subject, $message)
|
|
{
|
|
# First, make sure we're not spamming.
|
|
|
|
$cache_key = 'mail_admins_counter/'.(floor(time() / 3600) * 3600).'/'.md5($subject);
|
|
try {
|
|
$counter = Cache::get($cache_key);
|
|
} catch (DbException $e) {
|
|
# Why catching exceptions here? See bug#156.
|
|
$counter = null;
|
|
}
|
|
if ($counter === null)
|
|
$counter = 0;
|
|
$counter++;
|
|
try {
|
|
Cache::set($cache_key, $counter, 3600);
|
|
} catch (DbException $e) {
|
|
# Why catching exceptions here? See bug#156.
|
|
}
|
|
if ($counter <= 5)
|
|
{
|
|
# We're not spamming yet.
|
|
|
|
self::mail_from_okapi(get_admin_emails(), $subject, $message);
|
|
}
|
|
else
|
|
{
|
|
# We are spamming. Prevent sending more emails.
|
|
|
|
$content_cache_key_prefix = 'mail_admins_spam/'.(floor(time() / 3600) * 3600).'/';
|
|
$timeout = 86400;
|
|
if ($counter == 6)
|
|
{
|
|
self::mail_from_okapi(get_admin_emails(), "Anti-spam mode activated for '$subject'",
|
|
"OKAPI has activated an \"anti-spam\" mode for the following subject:\n\n".
|
|
"\"$subject\"\n\n".
|
|
"Anti-spam mode is activiated when more than 5 messages with\n".
|
|
"the same subject are sent within one hour.\n\n".
|
|
"Additional debug information:\n".
|
|
"- counter cache key: $cache_key\n".
|
|
"- content prefix: $content_cache_key_prefix<n>\n".
|
|
"- content timeout: $timeout\n"
|
|
);
|
|
}
|
|
$content_cache_key = $content_cache_key_prefix.$counter;
|
|
Cache::set($content_cache_key, $message, $timeout);
|
|
}
|
|
}
|
|
|
|
/** Send an email message from OKAPI to the given recipients. */
|
|
public static function mail_from_okapi($email_addresses, $subject, $message)
|
|
{
|
|
if (class_exists("okapi\\Settings") && (Settings::get('DEBUG_PREVENT_EMAILS')))
|
|
{
|
|
# Sending emails was blocked on admin's demand.
|
|
# This is possible only on development environment.
|
|
return;
|
|
}
|
|
if (!is_array($email_addresses))
|
|
$email_addresses = array($email_addresses);
|
|
$sender_email = class_exists("okapi\\Settings") ? Settings::get('FROM_FIELD') : 'root@localhost';
|
|
mail(implode(", ", $email_addresses), $subject, $message,
|
|
"Content-Type: text/plain; charset=utf-8\n".
|
|
"From: OKAPI <$sender_email>\n".
|
|
"Reply-To: $sender_email\n"
|
|
);
|
|
}
|
|
|
|
/** Get directory to store dynamic (cache or temporary) files. No trailing slash included. */
|
|
public static function get_var_dir()
|
|
{
|
|
$dir = Settings::get('VAR_DIR');
|
|
if ($dir != null)
|
|
return rtrim($dir, "/");
|
|
throw new Exception("You need to set a valid VAR_DIR.");
|
|
}
|
|
|
|
/**
|
|
* Get an array of all site-specific attributes in the following format:
|
|
* $arr[<id_of_the_attribute>][<language_code>] = <attribute_name>.
|
|
*/
|
|
public static function get_all_atribute_names()
|
|
{
|
|
if (Settings::get('OC_BRANCH') == 'oc.pl')
|
|
{
|
|
# OCPL branch uses cache_attrib table to store attribute names. It has
|
|
# different structure than the OCDE cache_attrib table. OCPL does not
|
|
# have translation tables.
|
|
|
|
$rs = Db::query("select id, language, text_long from cache_attrib order by id");
|
|
}
|
|
else
|
|
{
|
|
# OCDE branch uses translation tables. Let's make a select which will
|
|
# produce results compatible with the one above.
|
|
|
|
$rs = Db::query("
|
|
select
|
|
ca.id,
|
|
stt.lang as language,
|
|
stt.text as text_long
|
|
from
|
|
cache_attrib ca,
|
|
sys_trans_text stt
|
|
where ca.trans_id = stt.trans_id
|
|
order by ca.id
|
|
");
|
|
}
|
|
|
|
$dict = array();
|
|
while ($row = mysql_fetch_assoc($rs)) {
|
|
$dict[$row['id']][strtolower($row['language'])] = $row['text_long'];
|
|
}
|
|
return $dict;
|
|
}
|
|
|
|
/** Returns something like "OpenCaching.PL" or "OpenCaching.DE". */
|
|
public static function get_normalized_site_name($site_url = null)
|
|
{
|
|
if ($site_url == null)
|
|
$site_url = Settings::get('SITE_URL');
|
|
$matches = null;
|
|
if (preg_match("#^https?://(www.)?opencaching.([a-z.]+)/$#", $site_url, $matches)) {
|
|
return "OpenCaching.".strtoupper($matches[2]);
|
|
} else {
|
|
return "DEVELSITE";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Pick text from $langdict based on language preference $langpref.
|
|
*
|
|
* Example:
|
|
* pick_best_language(
|
|
* array('pl' => 'X', 'de' => 'Y', 'en' => 'Z'),
|
|
* array('sp', 'de', 'en')
|
|
* ) == 'Y'.
|
|
*
|
|
* @param array $langdict - assoc array of lang-code => text.
|
|
* @param array $langprefs - list of lang codes, in order of preference.
|
|
*/
|
|
public static function pick_best_language($langdict, $langprefs)
|
|
{
|
|
foreach ($langprefs as $pref)
|
|
if (isset($langdict[$pref]))
|
|
return $langdict[$pref];
|
|
foreach ($langdict as &$text_ref)
|
|
return $text_ref;
|
|
return "";
|
|
}
|
|
|
|
/**
|
|
* Split the array into groups of max. $size items.
|
|
*/
|
|
public static function make_groups($array, $size)
|
|
{
|
|
$i = 0;
|
|
$groups = array();
|
|
while ($i < count($array))
|
|
{
|
|
$groups[] = array_slice($array, $i, $size);
|
|
$i += $size;
|
|
}
|
|
return $groups;
|
|
}
|
|
|
|
/**
|
|
* Check if any pre-request cronjobs are scheduled to execute and execute
|
|
* them if needed. Reschedule for new executions.
|
|
*/
|
|
public static function execute_prerequest_cronjobs()
|
|
{
|
|
$nearest_event = Okapi::get_var("cron_nearest_event");
|
|
if ($nearest_event + 0 <= time())
|
|
{
|
|
require_once($GLOBALS['rootpath']."okapi/cronjobs.php");
|
|
$nearest_event = CronJobController::run_jobs('pre-request');
|
|
Okapi::set_var("cron_nearest_event", $nearest_event);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if any cron-5 cronjobs are scheduled to execute and execute
|
|
* them if needed. Reschedule for new executions.
|
|
*/
|
|
public static function execute_cron5_cronjobs()
|
|
{
|
|
$nearest_event = Okapi::get_var("cron_nearest_event");
|
|
if ($nearest_event + 0 <= time())
|
|
{
|
|
set_time_limit(0);
|
|
ignore_user_abort(true);
|
|
require_once($GLOBALS['rootpath']."okapi/cronjobs.php");
|
|
$nearest_event = CronJobController::run_jobs('cron-5');
|
|
Okapi::set_var("cron_nearest_event", $nearest_event);
|
|
}
|
|
}
|
|
|
|
private static function gettext_set_lang($langprefs)
|
|
{
|
|
static $gettext_last_used_langprefs = null;
|
|
static $gettext_last_set_locale = null;
|
|
|
|
# We remember the last $langprefs argument which we've been called with.
|
|
# This way, we don't need to call the actual locale-switching code most
|
|
# of the times.
|
|
|
|
if ($gettext_last_used_langprefs != $langprefs)
|
|
{
|
|
$gettext_last_set_locale = call_user_func(Settings::get("GETTEXT_INIT"), $langprefs);
|
|
$gettext_last_used_langprefs = $langprefs;
|
|
textdomain(Settings::get("GETTEXT_DOMAIN"));
|
|
}
|
|
return $gettext_last_set_locale;
|
|
}
|
|
|
|
private static $gettext_original_domain = null;
|
|
private static $gettext_langprefs_stack = array();
|
|
|
|
/**
|
|
* Attempt to switch the language based on the preference array given.
|
|
* Previous language settings will be remembered (in a stack). You SHOULD
|
|
* restore them later by calling gettext_domain_restore.
|
|
*/
|
|
public static function gettext_domain_init($langprefs = null)
|
|
{
|
|
# Put the langprefs on the stack.
|
|
|
|
if ($langprefs == null)
|
|
$langprefs = array(Settings::get('SITELANG'));
|
|
self::$gettext_langprefs_stack[] = $langprefs;
|
|
|
|
if (count(self::$gettext_langprefs_stack) == 1)
|
|
{
|
|
# This is the first time gettext_domain_init is called. In order to
|
|
# properly reinitialize the original settings after gettext_domain_restore
|
|
# is called for the last time, we need to save current textdomain (which
|
|
# should be different than the one which we use - Settings::get("GETTEXT_DOMAIN")).
|
|
|
|
self::$gettext_original_domain = textdomain(null);
|
|
}
|
|
|
|
# Attempt to change the language. Acquire the actual locale code used
|
|
# (might differ from expected when language was not found).
|
|
|
|
$locale_code = self::gettext_set_lang($langprefs);
|
|
return $locale_code;
|
|
}
|
|
public static function gettext_domain_restore()
|
|
{
|
|
# Dismiss the last element on the langpref stack. This is the language
|
|
# which we've been actualy using until now. We want it replaced with
|
|
# the language below it.
|
|
|
|
array_pop(self::$gettext_langprefs_stack);
|
|
|
|
$size = count(self::$gettext_langprefs_stack);
|
|
if ($size > 0)
|
|
{
|
|
$langprefs = self::$gettext_langprefs_stack[$size - 1];
|
|
self::gettext_set_lang($langprefs);
|
|
}
|
|
else
|
|
{
|
|
# The stack is empty. This means we're going out of OKAPI code and
|
|
# we want the original textdomain reestablished.
|
|
|
|
textdomain(self::$gettext_original_domain);
|
|
self::$gettext_original_domain = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Internal. This is called always when OKAPI core is included.
|
|
*/
|
|
public static function init_internals($allow_cronjobs = true)
|
|
{
|
|
static $init_made = false;
|
|
if ($init_made)
|
|
return;
|
|
ini_set('memory_limit', '128M');
|
|
Db::connect();
|
|
if (Settings::get('TIMEZONE') !== null)
|
|
date_default_timezone_set(Settings::get('TIMEZONE'));
|
|
if (!self::$data_store)
|
|
self::$data_store = new OkapiDataStore();
|
|
if (!self::$server)
|
|
self::$server = new OkapiOAuthServer(self::$data_store);
|
|
if ($allow_cronjobs)
|
|
self::execute_prerequest_cronjobs();
|
|
$init_made = true;
|
|
}
|
|
|
|
/**
|
|
* Generate a string of random characters, suitable for keys as passwords.
|
|
* Troublesome characters like '0', 'O', '1', 'l' will not be used.
|
|
* If $user_friendly=true, then it will consist from numbers only.
|
|
*/
|
|
public static function generate_key($length, $user_friendly = false)
|
|
{
|
|
if ($user_friendly)
|
|
$chars = "0123456789";
|
|
else
|
|
$chars = "abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
$max = strlen($chars);
|
|
$key = "";
|
|
for ($i=0; $i<$length; $i++)
|
|
{
|
|
$key .= $chars[rand(0, $max-1)];
|
|
}
|
|
return $key;
|
|
}
|
|
|
|
/**
|
|
* Register new OKAPI Consumer, send him an email with his key-pair, etc.
|
|
* This method does not verify parameter values, check if they are in
|
|
* a correct format prior the execution.
|
|
*/
|
|
public static function register_new_consumer($appname, $appurl, $email)
|
|
{
|
|
require_once($GLOBALS['rootpath']."okapi/service_runner.php");
|
|
$consumer = new OkapiConsumer(Okapi::generate_key(20), Okapi::generate_key(40),
|
|
$appname, $appurl, $email);
|
|
$sample_cache = OkapiServiceRunner::call("services/caches/search/all",
|
|
new OkapiInternalRequest($consumer, null, array('limit', 1)));
|
|
if (count($sample_cache['results']) > 0)
|
|
$sample_cache_code = $sample_cache['results'][0];
|
|
else
|
|
$sample_cache_code = "CACHECODE";
|
|
|
|
# Message for the Consumer.
|
|
ob_start();
|
|
print "This is the key-pair we've generated for your application:\n\n";
|
|
print "Consumer Key: $consumer->key\n";
|
|
print "Consumer Secret: $consumer->secret\n\n";
|
|
print "Note: Consumer Secret is needed only when you intend to use OAuth.\n";
|
|
print "You don't need Consumer Secret for Level 1 Authentication.\n\n";
|
|
print "Now you may easily access Level 1 methods of OKAPI! For example:\n";
|
|
print Settings::get('SITE_URL')."okapi/services/caches/geocache?cache_code=$sample_cache_code&consumer_key=$consumer->key\n\n";
|
|
print "If you plan on using OKAPI for a longer time, then you should subscribe\n";
|
|
print "to the OKAPI News blog to stay up-to-date. Check it out here:\n";
|
|
print "http://opencaching-api.blogspot.com/\n\n";
|
|
print "Have fun!";
|
|
Okapi::mail_from_okapi($email, "Your OKAPI Consumer Key", ob_get_clean());
|
|
|
|
# Message for the Admins.
|
|
|
|
ob_start();
|
|
print "Name: $consumer->name\n";
|
|
print "Developer: $consumer->email\n";
|
|
print ($consumer->url ? "URL: $consumer->url\n" : "");
|
|
print "Consumer Key: $consumer->key\n";
|
|
Okapi::mail_admins("New OKAPI app registered!", ob_get_clean());
|
|
|
|
Db::execute("
|
|
insert into okapi_consumers (`key`, name, secret, url, email, date_created)
|
|
values (
|
|
'".mysql_real_escape_string($consumer->key)."',
|
|
'".mysql_real_escape_string($consumer->name)."',
|
|
'".mysql_real_escape_string($consumer->secret)."',
|
|
'".mysql_real_escape_string($consumer->url)."',
|
|
'".mysql_real_escape_string($consumer->email)."',
|
|
now()
|
|
);
|
|
");
|
|
}
|
|
|
|
/** Return the distance between two geopoints, in meters. */
|
|
public static function get_distance($lat1, $lon1, $lat2, $lon2)
|
|
{
|
|
$x1 = (90-$lat1) * 3.14159 / 180;
|
|
$x2 = (90-$lat2) * 3.14159 / 180;
|
|
$d = acos(cos($x1) * cos($x2) + sin($x1) * sin($x2) * cos(($lon1-$lon2) * 3.14159 / 180)) * 6371000;
|
|
if ($d < 0) $d = 0;
|
|
return $d;
|
|
}
|
|
|
|
/**
|
|
* Return an SQL formula for calculating distance between two geopoints.
|
|
* Parameters should be either numberals or strings (SQL field references).
|
|
*/
|
|
public function get_distance_sql($lat1, $lon1, $lat2, $lon2)
|
|
{
|
|
$x1 = "(90-$lat1) * 3.14159 / 180";
|
|
$x2 = "(90-$lat2) * 3.14159 / 180";
|
|
$d = "acos(cos($x1) * cos($x2) + sin($x1) * sin($x2) * cos(($lon1-$lon2) * 3.14159 / 180)) * 6371000";
|
|
return $d;
|
|
}
|
|
|
|
/** Return bearing (float 0..360) from geopoint 1 to 2. */
|
|
public function get_bearing($lat1, $lon1, $lat2, $lon2)
|
|
{
|
|
if ($lat1 == $lat2 && $lon1 == $lon2)
|
|
return null;
|
|
if ($lat1 == $lat2) $lat1 += 0.0000166;
|
|
if ($lon1 == $lon2) $lon1 += 0.0000166;
|
|
|
|
$rad_lat1 = $lat1 / 180.0 * 3.14159;
|
|
$rad_lon1 = $lon1 / 180.0 * 3.14159;
|
|
$rad_lat2 = $lat2 / 180.0 * 3.14159;
|
|
$rad_lon2 = $lon2 / 180.0 * 3.14159;
|
|
|
|
$delta_lon = $rad_lon2 - $rad_lon1;
|
|
$bearing = atan2(sin($delta_lon) * cos($rad_lat2),
|
|
cos($rad_lat1) * sin($rad_lat2) - sin($rad_lat1) * cos($rad_lat2) * cos($delta_lon));
|
|
$bearing = 180.0 * $bearing / 3.14159;
|
|
if ( $bearing < 0.0 ) $bearing = $bearing + 360.0;
|
|
|
|
return $bearing;
|
|
}
|
|
|
|
/** Transform bearing (float 0..360) to simple 2-letter string (N, NE, E, SE, etc.) */
|
|
function bearing_as_two_letters($b)
|
|
{
|
|
static $names = array('N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW');
|
|
if ($b === null) return 'n/a';
|
|
return $names[round(($b / 360.0) * 8.0) % 8];
|
|
}
|
|
|
|
/** Transform bearing (float 0..360) to simple 3-letter string (N, NNE, NE, ESE, etc.) */
|
|
function bearing_as_three_letters($b)
|
|
{
|
|
static $names = array('N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE',
|
|
'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW');
|
|
if ($b === null) return 'n/a';
|
|
return $names[round(($b / 360.0) * 16.0) % 16];
|
|
}
|
|
|
|
/** Escape string for use with XML. See issue 169. */
|
|
public static function xmlescape($string)
|
|
{
|
|
static $pattern = '/[^\x{0009}\x{000a}\x{000d}\x{0020}-\x{D7FF}\x{E000}-\x{FFFD}]+/u';
|
|
$string = preg_replace($pattern, '', $string);
|
|
return strtr($string, array("<" => "<", ">" => ">", "\"" => """, "'" => "'", "&" => "&"));
|
|
}
|
|
|
|
/**
|
|
* Return object as a standard OKAPI response. The $object will be formatted
|
|
* using one of the default formatters (JSON, JSONP, XML, etc.). Formatter is
|
|
* auto-detected by peeking on the $request's 'format' parameter. In some
|
|
* specific cases, this method can also return the $object itself, instead
|
|
* of OkapiResponse - this allows nesting methods within other methods.
|
|
*/
|
|
public static function formatted_response(OkapiRequest $request, &$object)
|
|
{
|
|
if ($request instanceof OkapiInternalRequest && ($request->i_want_okapi_response == false))
|
|
{
|
|
# If you call a method internally, then you probably expect to get
|
|
# the actual object instead of it's formatted representation.
|
|
return $object;
|
|
}
|
|
$format = $request->get_parameter('format');
|
|
if ($format == null) $format = 'json';
|
|
if (!in_array($format, array('json', 'jsonp', 'xmlmap', 'xmlmap2')))
|
|
throw new InvalidParam('format', "'$format'");
|
|
$callback = $request->get_parameter('callback');
|
|
if ($callback && $format != 'jsonp')
|
|
throw new BadRequest("The 'callback' parameter is reserved to be used with the JSONP output format.");
|
|
if ($format == 'json')
|
|
{
|
|
$response = new OkapiHttpResponse();
|
|
$response->content_type = "application/json; charset=utf-8";
|
|
$response->body = json_encode($object);
|
|
return $response;
|
|
}
|
|
elseif ($format == 'jsonp')
|
|
{
|
|
if (!$callback)
|
|
throw new BadRequest("'callback' parameter is required for JSONP calls");
|
|
if (!preg_match("/^[a-zA-Z_][a-zA-Z0-9_]*$/", $callback))
|
|
throw new InvalidParam('callback', "'$callback' doesn't seem to be a valid JavaScript function name (should match /^[a-zA-Z_][a-zA-Z0-9_]*\$/).");
|
|
$response = new OkapiHttpResponse();
|
|
$response->content_type = "application/javascript; charset=utf-8";
|
|
$response->body = $callback."(".json_encode($object).");";
|
|
return $response;
|
|
}
|
|
elseif ($format == 'xmlmap')
|
|
{
|
|
# Deprecated (see issue 128). Keeping this for backward-compatibility.
|
|
$response = new OkapiHttpResponse();
|
|
$response->content_type = "text/xml; charset=utf-8";
|
|
$response->body = self::xmlmap_dumps($object);
|
|
return $response;
|
|
}
|
|
elseif ($format == 'xmlmap2')
|
|
{
|
|
$response = new OkapiHttpResponse();
|
|
$response->content_type = "text/xml; charset=utf-8";
|
|
$response->body = self::xmlmap2_dumps($object);
|
|
return $response;
|
|
}
|
|
else
|
|
{
|
|
# Should not happen (as we do a proper check above).
|
|
throw new Exception();
|
|
}
|
|
}
|
|
|
|
private static function _xmlmap_add(&$chunks, &$obj)
|
|
{
|
|
if (is_string($obj))
|
|
{
|
|
$chunks[] = "<string>";
|
|
$chunks[] = self::xmlescape($obj);
|
|
$chunks[] = "</string>";
|
|
}
|
|
elseif (is_int($obj))
|
|
{
|
|
$chunks[] = "<int>$obj</int>";
|
|
}
|
|
elseif (is_float($obj))
|
|
{
|
|
$chunks[] = "<float>$obj</float>";
|
|
}
|
|
elseif (is_bool($obj))
|
|
{
|
|
$chunks[] = $obj ? "<bool>true</bool>" : "<bool>false</bool>";
|
|
}
|
|
elseif (is_null($obj))
|
|
{
|
|
$chunks[] = "<null/>";
|
|
}
|
|
elseif (is_array($obj))
|
|
{
|
|
# Have to check if this is associative or not! Shit. I hate PHP.
|
|
if (array_keys($obj) === range(0, count($obj) - 1))
|
|
{
|
|
# Not assoc.
|
|
$chunks[] = "<list>";
|
|
foreach ($obj as &$item_ref)
|
|
{
|
|
$chunks[] = "<item>";
|
|
self::_xmlmap_add($chunks, $item_ref);
|
|
$chunks[] = "</item>";
|
|
}
|
|
$chunks[] = "</list>";
|
|
}
|
|
else
|
|
{
|
|
# Assoc.
|
|
$chunks[] = "<dict>";
|
|
foreach ($obj as $key => &$item_ref)
|
|
{
|
|
$chunks[] = "<item key=\"".self::xmlescape($key)."\">";
|
|
self::_xmlmap_add($chunks, $item_ref);
|
|
$chunks[] = "</item>";
|
|
}
|
|
$chunks[] = "</dict>";
|
|
}
|
|
}
|
|
else
|
|
{
|
|
# That's a bug.
|
|
throw new Exception("Cannot encode as xmlmap: " + print_r($obj, true));
|
|
}
|
|
}
|
|
|
|
private static function _xmlmap2_add(&$chunks, &$obj, $key)
|
|
{
|
|
$attrs = ($key !== null) ? " key=\"".self::xmlescape($key)."\"" : "";
|
|
if (is_string($obj))
|
|
{
|
|
$chunks[] = "<string$attrs>";
|
|
$chunks[] = self::xmlescape($obj);
|
|
$chunks[] = "</string>";
|
|
}
|
|
elseif (is_int($obj))
|
|
{
|
|
$chunks[] = "<number$attrs>$obj</number>";
|
|
}
|
|
elseif (is_float($obj))
|
|
{
|
|
$chunks[] = "<number$attrs>$obj</number>";
|
|
}
|
|
elseif (is_bool($obj))
|
|
{
|
|
$chunks[] = $obj ? "<boolean$attrs>true</boolean>" : "<boolean$attrs>false</boolean>";
|
|
}
|
|
elseif (is_null($obj))
|
|
{
|
|
$chunks[] = "<null$attrs/>";
|
|
}
|
|
elseif (is_array($obj))
|
|
{
|
|
# Have to check if this is associative or not! Shit. I hate PHP.
|
|
if (array_keys($obj) === range(0, count($obj) - 1))
|
|
{
|
|
# Not assoc.
|
|
$chunks[] = "<array$attrs>";
|
|
foreach ($obj as &$item_ref)
|
|
{
|
|
self::_xmlmap2_add($chunks, $item_ref, null);
|
|
}
|
|
$chunks[] = "</array>";
|
|
}
|
|
else
|
|
{
|
|
# Assoc.
|
|
$chunks[] = "<object$attrs>";
|
|
foreach ($obj as $key => &$item_ref)
|
|
{
|
|
self::_xmlmap2_add($chunks, $item_ref, $key);
|
|
}
|
|
$chunks[] = "</object>";
|
|
}
|
|
}
|
|
else
|
|
{
|
|
# That's a bug.
|
|
throw new Exception("Cannot encode as xmlmap2: " + print_r($obj, true));
|
|
}
|
|
}
|
|
|
|
/** Return the object in a serialized version, in the (deprecated) "xmlmap" format. */
|
|
public static function xmlmap_dumps(&$obj)
|
|
{
|
|
$chunks = array();
|
|
self::_xmlmap_add($chunks, $obj);
|
|
return implode('', $chunks);
|
|
}
|
|
|
|
/** Return the object in a serialized version, in the "xmlmap2" format. */
|
|
public static function xmlmap2_dumps(&$obj)
|
|
{
|
|
$chunks = array();
|
|
self::_xmlmap2_add($chunks, $obj, null);
|
|
return implode('', $chunks);
|
|
}
|
|
|
|
private static $cache_types = array(
|
|
#
|
|
# OKAPI does not expose type IDs. Instead, it uses the following
|
|
# "code words". Only the "primary" cache types are documented.
|
|
# This means that all other types may (in theory) be altered.
|
|
# Cache type may become "primary" ONLY when *all* OC servers recognize
|
|
# that type.
|
|
#
|
|
# Changing this may introduce nasty bugs (e.g. in the replicate module).
|
|
# CONTACT ME BEFORE YOU MODIFY THIS!
|
|
#
|
|
'oc.pl' => array(
|
|
# Primary types (documented, cannot change)
|
|
'Traditional' => 2, 'Multi' => 3, 'Quiz' => 7, 'Virtual' => 4,
|
|
'Event' => 6,
|
|
# Additional types (may get changed)
|
|
'Other' => 1, 'Webcam' => 5,
|
|
'Moving' => 8, 'Podcast' => 9, 'Own' => 10,
|
|
),
|
|
'oc.de' => array(
|
|
# Primary types (documented, cannot change)
|
|
'Traditional' => 2, 'Multi' => 3, 'Quiz' => 7, 'Virtual' => 4,
|
|
'Event' => 6,
|
|
# Additional types (might get changed)
|
|
'Other' => 1, 'Webcam' => 5,
|
|
'Math/Physics' => 8, 'Moving' => 9, 'Drive-In' => 10,
|
|
)
|
|
);
|
|
|
|
/** E.g. 'Traditional' => 2. For unknown names throw an Exception. */
|
|
public static function cache_type_name2id($name)
|
|
{
|
|
$ref = &self::$cache_types[Settings::get('OC_BRANCH')];
|
|
if (isset($ref[$name]))
|
|
return $ref[$name];
|
|
throw new Exception("Method cache_type_name2id called with unsupported cache ".
|
|
"type name '$name'. You should not allow users to submit caches ".
|
|
"of non-primary type.");
|
|
}
|
|
|
|
/** E.g. 2 => 'Traditional'. For unknown ids returns "Other". */
|
|
public static function cache_type_id2name($id)
|
|
{
|
|
static $reversed = null;
|
|
if ($reversed == null)
|
|
{
|
|
$reversed = array();
|
|
foreach (self::$cache_types[Settings::get('OC_BRANCH')] as $key => $value)
|
|
$reversed[$value] = $key;
|
|
}
|
|
if (isset($reversed[$id]))
|
|
return $reversed[$id];
|
|
return "Other";
|
|
}
|
|
|
|
private static $cache_statuses = array(
|
|
'Available' => 1, 'Temporarily unavailable' => 2, 'Archived' => 3
|
|
);
|
|
|
|
/** E.g. 'Available' => 1. For unknown names throws an Exception. */
|
|
public static function cache_status_name2id($name)
|
|
{
|
|
if (isset(self::$cache_statuses[$name]))
|
|
return self::$cache_statuses[$name];
|
|
throw new Exception("Method cache_status_name2id called with invalid name '$name'.");
|
|
}
|
|
|
|
/** E.g. 1 => 'Available'. For unknown ids returns 'Archived'. */
|
|
public static function cache_status_id2name($id)
|
|
{
|
|
static $reversed = null;
|
|
if ($reversed == null)
|
|
{
|
|
$reversed = array();
|
|
foreach (self::$cache_statuses as $key => $value)
|
|
$reversed[$value] = $key;
|
|
}
|
|
if (isset($reversed[$id]))
|
|
return $reversed[$id];
|
|
return 'Archived';
|
|
}
|
|
|
|
private static $cache_sizes = array(
|
|
'none' => 7,
|
|
'nano' => 8,
|
|
'micro' => 2,
|
|
'small' => 3,
|
|
'regular' => 4,
|
|
'large' => 5,
|
|
'xlarge' => 6,
|
|
'other' => 1,
|
|
);
|
|
|
|
/** E.g. 'micro' => 2. For unknown names throw an Exception. */
|
|
public static function cache_size2_to_sizeid($size2)
|
|
{
|
|
if (isset(self::$cache_sizes[$size2]))
|
|
return self::$cache_sizes[$size2];
|
|
throw new Exception("Method cache_size2_to_sizeid called with invalid size2 '$size2'.");
|
|
}
|
|
|
|
/** E.g. 2 => 'micro'. For unknown ids returns "other". */
|
|
public static function cache_sizeid_to_size2($id)
|
|
{
|
|
static $reversed = null;
|
|
if ($reversed == null)
|
|
{
|
|
$reversed = array();
|
|
foreach (self::$cache_sizes as $key => $value)
|
|
$reversed[$value] = $key;
|
|
}
|
|
if (isset($reversed[$id]))
|
|
return $reversed[$id];
|
|
return "other";
|
|
}
|
|
|
|
/** Maps OKAPI's 'size2' values to opencaching.com (OX) size codes. */
|
|
private static $cache_OX_sizes = array(
|
|
'none' => null,
|
|
'nano' => 1.3,
|
|
'micro' => 2.0,
|
|
'small' => 3.0,
|
|
'regular' => 3.8,
|
|
'large' => 4.6,
|
|
'xlarge' => 4.9,
|
|
'other' => null,
|
|
);
|
|
|
|
/**
|
|
* E.g. 'micro' => 2.0, 'other' => null. For unknown names throw an
|
|
* Exception. Note, that this is not a bijection ('none' are 'other' are
|
|
* both null).
|
|
*/
|
|
public static function cache_size2_to_oxsize($size2)
|
|
{
|
|
if (array_key_exists($size2, self::$cache_OX_sizes))
|
|
return self::$cache_OX_sizes[$size2];
|
|
throw new Exception("Method cache_size2_to_oxsize called with invalid size2 '$size2'.");
|
|
}
|
|
|
|
/**
|
|
* E.g. 'Found it' => 1. For unsupported names throws Exception.
|
|
*/
|
|
public static function logtypename2id($name)
|
|
{
|
|
if ($name == 'Found it') return 1;
|
|
if ($name == "Didn't find it") return 2;
|
|
if ($name == 'Comment') return 3;
|
|
if ($name == 'Attended') return 7;
|
|
if ($name == 'Will attend') return 8;
|
|
if (($name == 'Needs maintenance') && (Settings::get('SUPPORTS_LOGTYPE_NEEDS_MAINTENANCE')))
|
|
return 5;
|
|
throw new Exception("logtype2id called with invalid log type argument: $name");
|
|
}
|
|
|
|
/** E.g. 1 => 'Found it'. For unknown ids returns 'Comment'. */
|
|
public static function logtypeid2name($id)
|
|
{
|
|
# Various OC nodes use different English names, even for primary
|
|
# log types. OKAPI needs to have them the same across *all* OKAPI
|
|
# installations. That's why these 3 are hardcoded (and should
|
|
# NEVER be changed).
|
|
|
|
if ($id == 1) return "Found it";
|
|
if ($id == 2) return "Didn't find it";
|
|
if ($id == 3) return "Comment";
|
|
if ($id == 7) return "Attended";
|
|
if ($id == 8) return "Will attend";
|
|
|
|
static $other_types = null;
|
|
if ($other_types === null)
|
|
{
|
|
# All the other log types are non-standard ones. Their names have to
|
|
# be delivered from database tables. In general, OKAPI threat such
|
|
# non-standard log entries as comments, but - perhaps - external
|
|
# applications can use it in some other way. We decided to expose
|
|
# ENGLISH (and ONLY English) names of such log entry types. We also
|
|
# advise external developers to treat unknown log entry types as
|
|
# comments inside their application.
|
|
|
|
if (Settings::get('OC_BRANCH') == 'oc.pl')
|
|
{
|
|
# OCPL uses log_types table to store log type names.
|
|
$rs = Db::query("select id, en from log_types");
|
|
}
|
|
else
|
|
{
|
|
# OCDE uses log_types with translation tables.
|
|
|
|
$rs = Db::query("
|
|
select
|
|
lt.id,
|
|
stt.text as en
|
|
from
|
|
log_types lt,
|
|
sys_trans_text stt
|
|
where
|
|
lt.trans_id = stt.trans_id
|
|
and stt.lang = 'en'
|
|
");
|
|
}
|
|
$other_types = array();
|
|
while ($row = mysql_fetch_assoc($rs))
|
|
$other_types[$row['id']] = $row['en'];
|
|
}
|
|
|
|
if (isset($other_types[$id]))
|
|
return $other_types[$id];
|
|
|
|
return "Comment";
|
|
}
|
|
}
|
|
|
|
/** A data caching layer. For slow SQL queries etc. */
|
|
class Cache
|
|
{
|
|
/**
|
|
* Save object $value under the key $key. Store this object for
|
|
* $timeout seconds. $key must be a string of max 64 characters in length.
|
|
* $value might be any serializable PHP object.
|
|
*
|
|
* If $timeout is null, then the object will be treated as persistent
|
|
* (the Cache will do its best to NEVER remove it).
|
|
*/
|
|
public static function set($key, $value, $timeout)
|
|
{
|
|
if ($timeout == null)
|
|
{
|
|
# The current cache implementation is ALWAYS persistent, so we will
|
|
# just replace it with a big value.
|
|
$timeout = 100*365*86400;
|
|
}
|
|
Db::execute("
|
|
replace into okapi_cache (`key`, value, expires)
|
|
values (
|
|
'".mysql_real_escape_string($key)."',
|
|
'".mysql_real_escape_string(gzdeflate(serialize($value)))."',
|
|
date_add(now(), interval '".mysql_real_escape_string($timeout)."' second)
|
|
);
|
|
");
|
|
}
|
|
|
|
/**
|
|
* Scored version of set. Elements set up this way will expire when they're
|
|
* not used.
|
|
*/
|
|
public static function set_scored($key, $value)
|
|
{
|
|
Db::execute("
|
|
replace into okapi_cache (`key`, value, expires, score)
|
|
values (
|
|
'".mysql_real_escape_string($key)."',
|
|
'".mysql_real_escape_string(gzdeflate(serialize($value)))."',
|
|
date_add(now(), interval 120 day),
|
|
1.0
|
|
);
|
|
");
|
|
}
|
|
|
|
/** Do 'set' on many keys at once. */
|
|
public static function set_many($dict, $timeout)
|
|
{
|
|
if (count($dict) == 0)
|
|
return;
|
|
if ($timeout == null)
|
|
{
|
|
# The current cache implementation is ALWAYS persistent, so we will
|
|
# just replace it with a big value.
|
|
$timeout = 100*365*86400;
|
|
}
|
|
$entries = array();
|
|
foreach ($dict as $key => $value)
|
|
{
|
|
$entries[] = "(
|
|
'".mysql_real_escape_string($key)."',
|
|
'".mysql_real_escape_string(gzdeflate(serialize($value)))."',
|
|
date_add(now(), interval '".mysql_real_escape_string($timeout)."' second)
|
|
)";
|
|
}
|
|
Db::execute("
|
|
replace into okapi_cache (`key`, value, expires)
|
|
values ".implode(", ", $entries)."
|
|
");
|
|
}
|
|
|
|
/**
|
|
* Retrieve object stored under the key $key. If object does not
|
|
* exist or timeout expired, return null.
|
|
*/
|
|
public static function get($key)
|
|
{
|
|
$rs = Db::query("
|
|
select value, score
|
|
from okapi_cache
|
|
where
|
|
`key` = '".mysql_real_escape_string($key)."'
|
|
and expires > now()
|
|
");
|
|
list($blob, $score) = mysql_fetch_array($rs);
|
|
if (!$blob)
|
|
return null;
|
|
if ($score != null) # Only non-null entries are scored.
|
|
{
|
|
Db::execute("
|
|
insert into okapi_cache_reads (`cache_key`)
|
|
values ('".mysql_real_escape_string($key)."')
|
|
");
|
|
}
|
|
return unserialize(gzinflate($blob));
|
|
}
|
|
|
|
/** Do 'get' on many keys at once. */
|
|
public static function get_many($keys)
|
|
{
|
|
$dict = array();
|
|
$rs = Db::query("
|
|
select `key`, value
|
|
from okapi_cache
|
|
where
|
|
`key` in ('".implode("','", array_map('mysql_real_escape_string', $keys))."')
|
|
and expires > now()
|
|
");
|
|
while ($row = mysql_fetch_assoc($rs))
|
|
{
|
|
try
|
|
{
|
|
$dict[$row['key']] = unserialize(gzinflate($row['value']));
|
|
}
|
|
catch (ErrorException $e)
|
|
{
|
|
unset($dict[$row['key']]);
|
|
Okapi::mail_admins("Debug: Unserialize error",
|
|
"Could not unserialize key '".$row['key']."' from Cache.\n".
|
|
"Probably something REALLY big was put there and data has been truncated.\n".
|
|
"Consider upgrading cache table to LONGBLOB.\n\n".
|
|
"Length of data, compressed: ".strlen($row['value']));
|
|
}
|
|
}
|
|
if (count($dict) < count($keys))
|
|
foreach ($keys as $key)
|
|
if (!isset($dict[$key]))
|
|
$dict[$key] = null;
|
|
return $dict;
|
|
}
|
|
|
|
/**
|
|
* Delete key $key from the cache.
|
|
*/
|
|
public static function delete($key)
|
|
{
|
|
self::delete_many(array($key));
|
|
}
|
|
|
|
/** Do 'delete' on many keys at once. */
|
|
public static function delete_many($keys)
|
|
{
|
|
if (count($keys) == 0)
|
|
return;
|
|
Db::execute("
|
|
delete from okapi_cache
|
|
where `key` in ('".implode("','", array_map('mysql_real_escape_string', $keys))."')
|
|
");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sometimes it is desireable to get the cached contents in a file,
|
|
* instead in a string (i.e. for imagecreatefromgd2). In such cases, you
|
|
* may use this class instead of the Cache class.
|
|
*/
|
|
class FileCache
|
|
{
|
|
public static function get_file_path($key)
|
|
{
|
|
$filename = Okapi::get_var_dir()."/okapi_filecache_".md5($key);
|
|
if (!file_exists($filename))
|
|
return null;
|
|
return $filename;
|
|
}
|
|
|
|
/**
|
|
* Note, there is no $timeout (time to live) parameter. Currently,
|
|
* OKAPI will delete every old file after certain amount of time.
|
|
* See CacheCleanupCronJob for details.
|
|
*/
|
|
public static function set($key, $value)
|
|
{
|
|
$filename = Okapi::get_var_dir()."/okapi_filecache_".md5($key);
|
|
file_put_contents($filename, $value);
|
|
return $filename;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Represents an OKAPI web method request.
|
|
*
|
|
* Use this class to get parameters from your request and access
|
|
* Consumer and Token objects. Please note, that request method
|
|
* SHOULD be irrelevant to you: GETs and POSTs are interchangable
|
|
* within OKAPI, and it's up to the caller which one to choose.
|
|
* If you think using GET is "unsafe", then probably you forgot to
|
|
* add OAuth signature requirement (consumer=required) - this way,
|
|
* all the "unsafety" issues of using GET vanish.
|
|
*/
|
|
abstract class OkapiRequest
|
|
{
|
|
public $consumer;
|
|
public $token;
|
|
public $etag; # see: http://en.wikipedia.org/wiki/HTTP_ETag
|
|
|
|
/**
|
|
* Set this to true, for some method to allow you to set higher "limit"
|
|
* parameter than usually allowed. This should be used ONLY by trusted,
|
|
* fast and *cacheable* code!
|
|
*/
|
|
public $skip_limits = false;
|
|
|
|
/**
|
|
* Return request parameter, or NULL when not found. Use this instead of
|
|
* $_GET or $_POST or $_REQUEST.
|
|
*/
|
|
public abstract function get_parameter($name);
|
|
|
|
/**
|
|
* Return the list of all request parameters. You should use this method
|
|
* ONLY when you use <import-params/> in your documentation and you want
|
|
* to pass all unknown parameters onto the other method.
|
|
*/
|
|
public abstract function get_all_parameters_including_unknown();
|
|
|
|
/** Return true, if this requests is to be logged as HTTP request in okapi_stats. */
|
|
public abstract function is_http_request();
|
|
}
|
|
|
|
class OkapiInternalRequest extends OkapiRequest
|
|
{
|
|
private $parameters;
|
|
|
|
/**
|
|
* Set this to true, if you want this request to be considered as HTTP request
|
|
* in okapi_stats tables. This is useful when running requests through Facade
|
|
* (we want them logged and displayed in weekly report).
|
|
*/
|
|
public $perceive_as_http_request = false;
|
|
|
|
/**
|
|
* Set this to true, if you want to receive OkapiResponse instead of
|
|
* the actual object.
|
|
*/
|
|
public $i_want_okapi_response = false;
|
|
|
|
/**
|
|
* You may use "null" values in parameters if you want them skipped
|
|
* (null-ized keys will be removed from parameters).
|
|
*/
|
|
public function __construct($consumer, $token, $parameters)
|
|
{
|
|
$this->consumer = $consumer;
|
|
$this->token = $token;
|
|
$this->parameters = array();
|
|
foreach ($parameters as $key => $value)
|
|
if ($value !== null)
|
|
$this->parameters[$key] = $value;
|
|
}
|
|
|
|
public function get_parameter($name)
|
|
{
|
|
if (isset($this->parameters[$name]))
|
|
return $this->parameters[$name];
|
|
else
|
|
return null;
|
|
}
|
|
|
|
public function get_all_parameters_including_unknown()
|
|
{
|
|
return $this->parameters;
|
|
}
|
|
|
|
public function is_http_request() { return $this->perceive_as_http_request; }
|
|
}
|
|
|
|
class OkapiHttpRequest extends OkapiRequest
|
|
{
|
|
private $request; /* @var OAuthRequest */
|
|
private $opt_min_auth_level; # 0..3
|
|
private $opt_token_type = 'access'; # "access" or "request"
|
|
|
|
public function __construct($options)
|
|
{
|
|
Okapi::init_internals();
|
|
$this->init_request();
|
|
#
|
|
# Parsing options.
|
|
#
|
|
$DEBUG_AS_USERNAME = null;
|
|
foreach ($options as $key => $value)
|
|
{
|
|
switch ($key)
|
|
{
|
|
case 'min_auth_level':
|
|
if (!in_array($value, array(0, 1, 2, 3)))
|
|
{
|
|
throw new Exception("'min_auth_level' option has invalid value: $value");
|
|
}
|
|
$this->opt_min_auth_level = $value;
|
|
break;
|
|
case 'token_type':
|
|
if (!in_array($value, array("request", "access")))
|
|
{
|
|
throw new Exception("'token_type' option has invalid value: $value");
|
|
}
|
|
$this->opt_token_type = $value;
|
|
break;
|
|
case 'DEBUG_AS_USERNAME':
|
|
$DEBUG_AS_USERNAME = $value;
|
|
break;
|
|
default:
|
|
throw new Exception("Unknown option: $key");
|
|
break;
|
|
}
|
|
}
|
|
if ($this->opt_min_auth_level === null) throw new Exception("Required 'min_auth_level' option is missing.");
|
|
|
|
if ($DEBUG_AS_USERNAME != null)
|
|
{
|
|
# Enables debugging Level 2 and Level 3 methods. Should not be committed
|
|
# at any time! If run on production server, make it an error.
|
|
|
|
if (!Settings::get('DEBUG'))
|
|
{
|
|
throw new Exception("Attempted to use DEBUG_AS_USERNAME in ".
|
|
"non-debug environment. Accidental commit?");
|
|
}
|
|
|
|
# Lower required authentication to Level 0, to pass the checks.
|
|
|
|
$this->opt_min_auth_level = 0;
|
|
}
|
|
|
|
#
|
|
# Let's see if the request is signed. If it is, verify the signature.
|
|
# It it's not, check if it isn't against the rules defined in the $options.
|
|
#
|
|
|
|
if ($this->get_parameter('oauth_signature'))
|
|
{
|
|
# User is using OAuth. There is a cronjob scheduled to run every 5 minutes and
|
|
# delete old Request Tokens and Nonces. We may assume that cleanup was executed
|
|
# not more than 5 minutes ago.
|
|
|
|
list($this->consumer, $this->token) = Okapi::$server->
|
|
verify_request2($this->request, $this->opt_token_type, $this->opt_min_auth_level == 3);
|
|
if ($this->get_parameter('consumer_key') && $this->get_parameter('consumer_key') != $this->get_parameter('oauth_consumer_key'))
|
|
throw new BadRequest("Inproper mixing of authentication types. You used both 'consumer_key' ".
|
|
"and 'oauth_consumer_key' parameters (Level 1 and Level 2), but they do not match with ".
|
|
"each other. Were you trying to hack me? ;)");
|
|
if ($this->opt_min_auth_level == 3 && !$this->token)
|
|
{
|
|
throw new BadRequest("This method requires a valid Token to be included (Level 3 ".
|
|
"Authentication). You didn't provide one.");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if ($this->opt_min_auth_level >= 2)
|
|
{
|
|
throw new BadRequest("This method requires OAuth signature (Level ".
|
|
$this->opt_min_auth_level." Authentication). You didn't sign your request.");
|
|
}
|
|
else
|
|
{
|
|
$consumer_key = $this->get_parameter('consumer_key');
|
|
if ($consumer_key)
|
|
{
|
|
$this->consumer = Okapi::$data_store->lookup_consumer($consumer_key);
|
|
if (!$this->consumer)
|
|
throw new InvalidParam('consumer_key', "Consumer does not exist.");
|
|
}
|
|
if (($this->opt_min_auth_level == 1) && (!$this->consumer))
|
|
throw new BadRequest("This method requires the 'consumer_key' argument (Level 1 ".
|
|
"Authentication). You didn't provide one.");
|
|
}
|
|
}
|
|
|
|
#
|
|
# Prevent developers from accessing request parameters with PHP globals.
|
|
# Remember, that OKAPI requests can be nested within other OKAPI requests!
|
|
# Search the code for "new OkapiInternalRequest" to see examples.
|
|
#
|
|
|
|
$_GET = $_POST = $_REQUEST = null;
|
|
|
|
# When debugging, simulate as if been run using a proper Level 3 Authentication.
|
|
|
|
if ($DEBUG_AS_USERNAME != null)
|
|
{
|
|
# Note, that this will override any other valid authentication the
|
|
# developer might have issued.
|
|
|
|
$debug_user_id = Db::select_value("select user_id from user where username='".
|
|
mysql_real_escape_string($options['DEBUG_AS_USERNAME'])."'");
|
|
if ($debug_user_id == null)
|
|
throw new Exception("Invalid user name in DEBUG_AS_USERNAME: '".$options['DEBUG_AS_USERNAME']."'");
|
|
$this->consumer = new OkapiDebugConsumer();
|
|
$this->token = new OkapiDebugAccessToken($debug_user_id);
|
|
}
|
|
|
|
# Read the ETag.
|
|
|
|
if (isset($_SERVER['HTTP_IF_NONE_MATCH']))
|
|
$this->etag = $_SERVER['HTTP_IF_NONE_MATCH'];
|
|
}
|
|
|
|
private function init_request()
|
|
{
|
|
$this->request = OAuthRequest::from_request();
|
|
if (!in_array($this->request->get_normalized_http_method(),
|
|
array('GET', 'POST')))
|
|
{
|
|
throw new BadRequest("Use GET and POST methods only.");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return request parameter, or NULL when not found. Use this instead of
|
|
* $_GET or $_POST or $_REQUEST.
|
|
*/
|
|
public function get_parameter($name)
|
|
{
|
|
$value = $this->request->get_parameter($name);
|
|
|
|
# Default implementation of OAuthRequest allows arrays to be passed with
|
|
# multiple references to the same variable ("a=1&a=2&a=3"). This is invalid
|
|
# in OKAPI and should be reported back. See issue 85:
|
|
# http://code.google.com/p/opencaching-api/issues/detail?id=85
|
|
|
|
if (is_array($value))
|
|
throw new InvalidParam($name, "Make sure you are using '$name' no more than ONCE in your URL.");
|
|
return $value;
|
|
}
|
|
|
|
public function get_all_parameters_including_unknown()
|
|
{
|
|
return $this->request->get_parameters();
|
|
}
|
|
|
|
public function is_http_request() { return true; }
|
|
}
|