OKAPI Project update (r483).

This commit is contained in:
Wojciech Rygielski
2012-10-27 15:18:30 +02:00
parent b298bfccc0
commit 8eb0201947
109 changed files with 2799 additions and 316 deletions

View File

@@ -4,6 +4,7 @@ namespace okapi;
use Exception;
use okapi\Okapi;
use okapi\Settings;
use okapi\views\menu\OkapiMenu;
#
@@ -14,26 +15,12 @@ use okapi\views\menu\OkapiMenu;
# To learn more about OKAPI, see core.php.
#
$GLOBALS['rootpath'] = '../'; # this is for OC-code compatibility, OC requires this
$GLOBALS['no-session'] = true; # turn off OC-code session starting
$GLOBALS['no-ob'] = true; # turn off OC-code GZIP output buffering
$GLOBALS['rootpath'] = '../'; # this is for OC-code compatibility
require_once($GLOBALS['rootpath'].'okapi/core.php');
OkapiErrorHandler::$treat_notices_as_errors = true;
require_once($GLOBALS['rootpath'].'okapi/urls.php');
# OKAPI does not use sessions. The following statement will allow concurrent
# requests to be fired from browser.
if (session_id())
{
# WRTODO: Move this to some kind of cronjob, to prevent admin-spamming in case on an error.
throw new Exception("Session started when should not be! You have to patch your OC installation. ".
"You have to check \"if ((!isset(\$GLOBALS['no-session'])) || (\$GLOBALS['no-session'] == false))\" ".
"before executing session_start.");
}
# Make sure OC did not start anything suspicious, like ob_start('ob_gzhandler').
# OKAPI makes it's own decisions whether "to gzip or not to gzip".
if (ob_list_handlers() == array('default output handler'))
{
# We will assume that this one comes from "output_buffering" being turned on
@@ -43,13 +30,6 @@ if (ob_list_handlers() == array('default output handler'))
ob_end_clean();
}
if (count(ob_list_handlers()) > 0)
{
# WRTODO: Move this to some kind of cronjob, to prevent admin-spamming in case on an error.
throw new Exception("Output buffering started while it should not be! You have to patch you OC ".
"installation (probable lib/common.inc.php file). You have to check \"if ((!isset(\$GLOBALS['no-ob'])) ".
"|| (\$GLOBALS['no-ob'] == false))\" before executing ob_start. Refer to installation docs.");
}
class OkapiScriptEntryPointController
{
@@ -92,7 +72,7 @@ class OkapiScriptEntryPointController
# Pattern matched! Moving on to the proper View...
array_shift($matches);
require_once "views/$namespace.php";
require_once($GLOBALS['rootpath']."okapi/views/$namespace.php");
$response = call_user_func_array(array('\\okapi\\views\\'.
str_replace('/', '\\', $namespace).'\\View', 'call'), $matches);
if ($response)
@@ -108,7 +88,7 @@ class OkapiScriptEntryPointController
# None of the patterns matched OR method threw the Http404 exception.
require_once "views/http404.php";
require_once($GLOBALS['rootpath']."okapi/views/http404.php");
$response = \okapi\views\http404\View::call();
$response->display();
}

View File

@@ -4,19 +4,8 @@ namespace okapi;
# OKAPI Framework -- Wojciech Rygielski <rygielski@mimuw.edu.pl>
# Including this file will initialize OKAPI Framework with its default
# exception and error handlers. OKAPI is strict about PHP warnings and
# notices. You might need to temporarily disable the error handler in
# order to get it to work in some legacy code. Do this by calling
# OkapiErrorHandler::disable() BEFORE calling the "buggy" code, and
# OkapiErrorHandler::reenable() AFTER returning from it.
# When I was installing it for the first time on the opencaching.pl
# site, I needed to change some minor things in order to get it to work
# properly, i.e., turn some "die()"-like statements into exceptions.
# Contact me for details.
# I hope this will come in handy... - WR.
# 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;
@@ -34,19 +23,19 @@ use okapi\cronjobs\CronJobController;
/** Return an array of email addresses which always get notified on OKAPI errors. */
function get_admin_emails()
{
$emails = array(
isset($GLOBALS['sql_errormail']) ? $GLOBALS['sql_errormail'] : 'root@localhost',
);
$emails = array();
if (class_exists("okapi\\Settings"))
{
try
{
foreach (Settings::get('EXTRA_ADMINS') as $email)
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;
}
@@ -66,7 +55,7 @@ class BadRequest extends Exception {
'reason_stack' => array(),
);
$this->provideExtras($extras);
$extras['more_info'] = $GLOBALS['absolute_server_URI']."okapi/introduction.html#errors";
$extras['more_info'] = Settings::get('SITE_URL')."okapi/introduction.html#errors";
return json_encode(array("error" => $extras));
}
}
@@ -96,7 +85,7 @@ class OkapiExceptionHandler
else
header("HTTP/1.0 401 Unauthorized");
header("Access-Control-Allow-Origin: *");
header("Content-Type: text/plain; charset=utf-8");
header("Content-Type: application/json; charset=utf-8");
print $e->getOkapiJSON();
}
@@ -108,7 +97,7 @@ class OkapiExceptionHandler
header("HTTP/1.0 400 Bad Request");
header("Access-Control-Allow-Origin: *");
header("Content-Type: text/plain; charset=utf-8");
header("Content-Type: application/json; charset=utf-8");
print $e->getOkapiJSON();
}
@@ -133,7 +122,7 @@ class OkapiExceptionHandler
$exception_info = self::get_exception_info($e);
if (isset($GLOBALS['debug_page']) && $GLOBALS['debug_page'])
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:";
@@ -146,22 +135,50 @@ class OkapiExceptionHandler
}
else
{
$admin_email = implode(", ", get_admin_emails());
$sender_email = isset($GLOBALS['emailaddr']) ? $GLOBALS['emailaddr'] : 'root@localhost';
mail(
$admin_email,
"OKAPI Method Error - ".(
isset($GLOBALS['absolute_server_URI'])
? $GLOBALS['absolute_server_URI'] : "unknown location"
),
$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,
"Content-Type: text/plain; charset=utf-8\n".
"From: OKAPI <$sender_email>\n".
"Reply-To: $sender_email\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);
}
}
}
}
@@ -304,6 +321,20 @@ class DbException extends Exception {}
/** 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);
@@ -379,6 +410,8 @@ class Db
public static function query($query)
{
if (!self::$connected)
self::connect();
$rs = mysql_query($query);
if (!$rs)
{
@@ -392,7 +425,7 @@ class Db
# Including OAuth internals. Preparing OKAPI Consumer and Token classes.
#
require_once('oauth.php');
require_once($GLOBALS['rootpath']."okapi/oauth.php");
class OkapiConsumer extends OAuthConsumer
{
@@ -556,16 +589,18 @@ class OkapiOAuthServer extends OAuthServer
# Including local datastore and settings (connecting SQL database etc.).
require_once('settings.php');
require_once('datastore.php');
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;
@@ -616,11 +651,13 @@ class OkapiHttpResponse
header("HTTP/1.1 ".$this->status);
header("Access-Control-Allow-Origin: *");
header("Content-Type: ".$this->content_type);
header("Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0");
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;
@@ -660,8 +697,16 @@ class OkapiRedirectResponse extends OkapiHttpResponse
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);
@@ -677,13 +722,13 @@ class OkapiLock
}
else
{
$lockfile = Okapi::get_var_dir()."/okapi-lock-".$name;
if (!file_exists($lockfile))
$this->lockfile = Okapi::get_var_dir()."/okapi-lock-".$name;
if (!file_exists($this->lockfile))
{
$fp = fopen($lockfile, "wb");
$fp = fopen($this->lockfile, "wb");
fclose($fp);
}
$this->lock = sem_get(fileinode($lockfile));
$this->lock = sem_get(fileinode($this->lockfile));
}
}
@@ -698,6 +743,15 @@ class OkapiLock
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. */
@@ -705,7 +759,7 @@ class Okapi
{
public static $data_store;
public static $server;
public static $revision = 424; # This gets replaced in automatically deployed packages
public static $revision = 483; # 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. */
@@ -743,16 +797,54 @@ class Okapi
self::$okapi_vars[$varname] = $value;
}
/** Return true if the server is running in a debug mode. */
public static function debug_mode()
{
return (isset($GLOBALS['debug_page']) && $GLOBALS['debug_page']);
}
/** Send an email message to local OKAPI administrators. */
public static function mail_admins($subject, $message)
{
self::mail_from_okapi(get_admin_emails(), $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. */
@@ -766,7 +858,7 @@ class Okapi
}
if (!is_array($email_addresses))
$email_addresses = array($email_addresses);
$sender_email = isset($GLOBALS['emailaddr']) ? $GLOBALS['emailaddr'] : 'root@localhost';
$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".
@@ -780,7 +872,7 @@ class Okapi
$dir = Settings::get('VAR_DIR');
if ($dir != null)
return rtrim($dir, "/");
return isset($GLOBALS['dynbasepath']) ? $GLOBALS['dynbasepath'] : "/tmp";
throw new Exception("You need to set a valid VAR_DIR.");
}
/**
@@ -826,7 +918,7 @@ class Okapi
public static function get_normalized_site_name($site_url = null)
{
if ($site_url == null)
$site_url = $GLOBALS['absolute_server_URI'];
$site_url = Settings::get('SITE_URL');
$matches = null;
if (preg_match("#^https?://(www.)?opencaching.([a-z.]+)/$#", $site_url, $matches)) {
return "OpenCaching.".strtoupper($matches[2]);
@@ -881,7 +973,7 @@ class Okapi
$nearest_event = Okapi::get_var("cron_nearest_event");
if ($nearest_event + 0 <= time())
{
require_once 'cronjobs.php';
require_once($GLOBALS['rootpath']."okapi/cronjobs.php");
$nearest_event = CronJobController::run_jobs('pre-request');
Okapi::set_var("cron_nearest_event", $nearest_event);
}
@@ -898,7 +990,7 @@ class Okapi
{
set_time_limit(0);
ignore_user_abort(true);
require_once 'cronjobs.php';
require_once($GLOBALS['rootpath']."okapi/cronjobs.php");
$nearest_event = CronJobController::run_jobs('cron-5');
Okapi::set_var("cron_nearest_event", $nearest_event);
}
@@ -1023,7 +1115,7 @@ class Okapi
*/
public static function register_new_consumer($appname, $appurl, $email)
{
include_once 'service_runner.php';
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",
@@ -1032,7 +1124,6 @@ class Okapi
$sample_cache_code = $sample_cache['results'][0];
else
$sample_cache_code = "CACHECODE";
$sender_email = isset($GLOBALS['emailaddr']) ? $GLOBALS['emailaddr'] : 'root@localhost';
# Message for the Consumer.
ob_start();
@@ -1042,7 +1133,7 @@ class Okapi
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 $GLOBALS['absolute_server_URI']."okapi/services/caches/geocache?cache_code=$sample_cache_code&consumer_key=$consumer->key\n\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";
@@ -1347,14 +1438,14 @@ class Okapi
'Traditional' => 2, 'Multi' => 3, 'Quiz' => 7, 'Virtual' => 4,
# Additional types (may get changed)
'Other' => 1, 'Webcam' => 5, 'Event' => 6,
'Moving' => 8, 'Own' => 9,
'Moving' => 8, 'Podcast' => 9, 'Own' => 10,
),
'oc.de' => array(
# Primary types (documented, cannot change)
'Traditional' => 2, 'Multi' => 3, 'Quiz' => 7, 'Virtual' => 4,
# Additional types (might get changed)
'Other' => 1, 'Webcam' => 5, 'Event' => 6,
'Math/Physics-Cache' => 8, 'Moving' => 9, 'Drive-In' => 10,
'Math/Physics' => 8, 'Moving' => 9, 'Drive-In' => 10,
)
);
@@ -1485,7 +1576,7 @@ class Cache
{
/**
* Save object $value under the key $key. Store this object for
* $timeout seconds. $key must be a string of max 32 characters in length.
* $timeout seconds. $key must be a string of max 64 characters in length.
* $value might be any serializable PHP object.
*/
public static function set($key, $value, $timeout)
@@ -1500,6 +1591,23 @@ class Cache
");
}
/**
* 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)
{
@@ -1526,15 +1634,23 @@ class Cache
*/
public static function get($key)
{
$blob = Db::select_value("
select value
$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));
}
@@ -1592,6 +1708,34 @@ class Cache
}
}
/**
* 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.
*
@@ -1607,6 +1751,14 @@ 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
@@ -1614,6 +1766,13 @@ abstract class OkapiRequest
*/
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();
}
@@ -1657,6 +1816,11 @@ class OkapiInternalRequest extends OkapiRequest
return null;
}
public function get_all_parameters_including_unknown()
{
return $this->parameters;
}
public function is_http_request() { return $this->perceive_as_http_request; }
}
@@ -1707,7 +1871,7 @@ class OkapiHttpRequest extends OkapiRequest
# 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 (!Okapi::debug_mode())
if (!Settings::get('DEBUG'))
{
throw new Exception("Attempted to set DEBUG_AS_USERNAME set in ".
"non-debug environment. Accidental commit?");
@@ -1785,6 +1949,11 @@ class OkapiHttpRequest extends OkapiRequest
$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()
@@ -1815,5 +1984,10 @@ class OkapiHttpRequest extends OkapiRequest
return $value;
}
public function get_all_parameters_including_unknown()
{
return $this->request->get_parameters();
}
public function is_http_request() { return true; }
}

View File

@@ -16,6 +16,7 @@ namespace okapi\cronjobs;
use Exception;
use okapi\Okapi;
use okapi\Settings;
use okapi\OkapiLock;
use okapi\OkapiExceptionHandler;
use okapi\Db;
@@ -45,6 +46,7 @@ class CronJobController
new AdminStatsSender(),
new LocaleChecker(),
new FulldumpGeneratorJob(),
new TileTreeUpdater(),
);
foreach ($cache as $cronjob)
if (!in_array($cronjob->get_type(), array('pre-request', 'cron-5')))
@@ -59,7 +61,7 @@ class CronJobController
*/
public static function run_jobs($type)
{
require_once $GLOBALS['rootpath'].'okapi/service_runner.php';
require_once($GLOBALS['rootpath'].'okapi/service_runner.php');
# We don't want other cronjobs of the same time to run simultanously.
$lock = OkapiLock::get('cronjobs-'.$type);
@@ -108,6 +110,8 @@ class CronJobController
*/
public static function force_run($job_name)
{
require_once($GLOBALS['rootpath'].'okapi/service_runner.php');
foreach (self::get_enabled_cronjobs() as $cronjob)
{
if (($cronjob->get_name() == $job_name) || ($cronjob->get_name() == "okapi\\cronjobs\\".$job_name))
@@ -241,16 +245,75 @@ class OAuthCleanupCronJob extends PrerequestCronJob
}
}
/** Deletes all expired cache elements, once per hour. */
/** Clean up the cache, once per hour. */
class CacheCleanupCronJob extends Cron5Job
{
public function get_period() { return 3600; } # 1 hour
public function get_period() { return 3600; }
public function execute()
{
# Delete all expired elements.
Db::execute("
delete from okapi_cache
where expires < now()
");
# Update the "score" stats.
$multiplier = 0.9; # Every hour, all scores are multiplied by this.
$limit = 0.01; # When a score reaches this limit, the entry is deleted.
# Every time the entry is read, its score is incread by 1. If an entry
# is saved, but never read, it will be deleted after log(L,M) hours
# (log(0.01, 0.9) = 43h). If an entry is read 1000000 times and then
# never read anymore, it will be deleted after log(1000000/L, 1/M)
# hours (log(1000000/0.01, 1/0.9) = 174h = 7 days).
Db::execute("
update okapi_cache
set score = score * $multiplier
where score is not null
");
Db::execute("
update
okapi_cache c,
(
select cache_key, count(*) as count
from okapi_cache_reads
group by cache_key
) cr
set c.score = c.score + cr.count
where
c.`key` = cr.cache_key
and c.score is not null
");
Db::execute("truncate okapi_cache_reads");
# Delete elements with the lowest score. Entries which have been set
# but never read will be removed after 36 hours (0.9^36 < 0.02 < 0.9^35).
Db::execute("
delete from okapi_cache
where
score is not null
and score < $limit
");
Db::query("optimize table okapi_cache");
# FileCache does not have an expiry date. We will delete all files older
# than 24 hours.
$dir = Okapi::get_var_dir();
if ($dh = opendir($dir)) {
while (($file = readdir($dh)) !== false) {
if (strpos($file, "okapi_filecache_") === 0) {
if (filemtime("$dir/$file") < time() - 86400) {
unlink("$dir/$file");
}
}
}
closedir($dh);
}
}
}
@@ -355,7 +418,7 @@ class CheckCronTab2 extends PrerequestCronJob
"Hello. OKAPI detected, that it's crontab is not working properly.\n".
"Please check your configuration or contact OKAPI developers.\n\n".
"This line should be present among your crontab entries:\n\n".
"*/5 * * * * wget -O - -q -t 1 ".$GLOBALS['absolute_server_URI']."okapi/cron5\n\n".
"*/5 * * * * wget -O - -q -t 1 ".Settings::get('SITE_URL')."okapi/cron5\n\n".
"If you're receiving this in Virtual Machine development environment, then\n".
"ignore it. Probably you just paused (or switched off) your VM for some time\n".
"(which would be considered an error in production environment)."
@@ -378,7 +441,7 @@ class ChangeLogWriterJob extends Cron5Job
public function get_period() { return 300; }
public function execute()
{
require_once 'services/replicate/replicate_common.inc.php';
require_once($GLOBALS['rootpath']."okapi/services/replicate/replicate_common.inc.php");
ReplicateCommon::update_clog_table();
}
}
@@ -391,18 +454,53 @@ class FulldumpGeneratorJob extends Cron5Job
public function get_period() { return 7*86400; }
public function execute()
{
require_once 'services/replicate/replicate_common.inc.php';
require_once($GLOBALS['rootpath']."okapi/services/replicate/replicate_common.inc.php");
ReplicateCommon::generate_fulldump();
}
}
/**
* Listen for changelog updates. Update okapi_tile_caches accordingly.
*/
class TileTreeUpdater extends Cron5Job
{
public function get_period() { return 5*60; }
public function execute()
{
$current_clog_revision = Okapi::get_var('clog_revision', 0);
$tiletree_revision = Okapi::get_var('clog_followup_revision', 0);
if ($tiletree_revision === $current_clog_revision) {
# No update necessary.
} elseif ($tiletree_revision < $current_clog_revision) {
require_once($GLOBALS['rootpath']."okapi/services/caches/map/replicate_listener.inc.php");
if ($current_clog_revision - $tiletree_revision < 100000) # In the middle of 2012, OCPL generated 30000 entries per week
{
for ($i=0; $i<100; $i++) # This gives us no more than 20000 (?) at a time.
{
$response = OkapiServiceRunner::call('services/replicate/changelog', new OkapiInternalRequest(
new OkapiInternalConsumer(), null, array('since' => $tiletree_revision)));
\okapi\services\caches\map\ReplicateListener::receive($response['changelog']);
$tiletree_revision = $response['revision'];
Okapi::set_var('clog_followup_revision', $tiletree_revision);
if (!$response['more'])
break;
}
} else {
# Some kind of bigger update. Resetting TileTree might be a better option.
\okapi\services\caches\map\ReplicateListener::reset();
Okapi::set_var('clog_followup_revision', $current_clog_revision);
}
}
}
}
/** Once per day, removes all revisions older than 10 days from okapi_clog table. */
class ChangeLogCleanerJob extends Cron5Job
{
public function get_period() { return 86400; }
public function execute()
{
require_once 'services/replicate/replicate_common.inc.php';
require_once($GLOBALS['rootpath']."okapi/services/replicate/replicate_common.inc.php");
$max_revision = ReplicateCommon::get_revision();
$cache_key = 'clog_revisions_daily';
$data = Cache::get($cache_key);
@@ -423,6 +521,7 @@ class ChangeLogCleanerJob extends Cron5Job
where id < '".mysql_real_escape_string($new_min_revision)."'
");
Cache::set($cache_key, $new_data, 10*86400);
Db::query("optimize table okapi_clog");
}
}
@@ -566,7 +665,7 @@ class LocaleChecker extends Cron5Job
public function get_period() { return 7*86400; }
public function execute()
{
require_once 'locale/locales.php';
require_once($GLOBALS['rootpath']."okapi/locale/locales.php");
$required = Locales::get_required_locales();
$installed = Locales::get_installed_locales();
$missing = array();

View File

@@ -4,8 +4,6 @@ namespace okapi;
use OAuthDataStore;
require_once($rootpath.'lib/common.inc.php');
class OkapiDataStore extends OAuthDataStore
{
public function lookup_consumer($consumer_key)

View File

@@ -2,23 +2,38 @@
namespace okapi;
# OKAPI Framework -- Wojciech Rygielski <rygielski@mimuw.edu.pl>
# Include this file if you want to use OKAPI's services with any
# external code (your service calls will appear under the name "Facade"
# in the weekly OKAPI usage report).
# Note, that his is the *ONLY* internal OKAPI file that is guaranteed
# to stay backward-compatible (I'm speaking about INTERNAL files here,
# all OKAPI methods will stay compatible forever). If you want to use
# something that has not been exposed through the Facade class, contact
# OKAPI developers, we will add it here.
# Including this file will initialize OKAPI Framework with its default
# exception and error handlers. OKAPI is strict about PHP warnings and
# notices. You might need to temporarily disable the error handler in
# order to get it to work with some legacy code. Do this by calling
# OkapiErrorHandler::disable() BEFORE calling the "buggy" code, and
# OkapiErrorHandler::reenable() AFTER returning from it.
use Exception;
use okapi\OkapiServiceRunner;
use okapi\OkapiInternalRequest;
use okapi\OkapiFacadeConsumer;
use okapi\OkapiFacadeAccessToken;
require_once('core.php');
require_once('service_runner.php');
require_once($GLOBALS['rootpath']."okapi/core.php");
OkapiErrorHandler::$treat_notices_as_errors = true;
require_once($GLOBALS['rootpath']."okapi/service_runner.php");
/**
* Use this class to access OKAPI from OC code. This is the *ONLY* internal OKAPI file that is
* guaranteed to stay backward-compatible*! You SHOULD NOT include any other okapi file in your
* code. If you want to use something that has not been exposed through the Facade class,
* inform OKAPI developers, we will add it.
*
* * - notice that we are talking about INTERNAL files here. Of course, all OKAPI methods
* (accessed via HTTP) will stay compatible forever (if possible).
* Use this class to access OKAPI's services from external code (i.e. OC code).
*/
class Facade
{
@@ -30,7 +45,6 @@ class Facade
*/
public static function service_call($service_name, $user_id_or_null, $parameters)
{
// WRTODO: make this count as HTTP call?
$request = new OkapiInternalRequest(
new OkapiFacadeConsumer(),
($user_id_or_null !== null) ? new OkapiFacadeAccessToken($user_id_or_null) : null,
@@ -39,4 +53,24 @@ class Facade
$request->perceive_as_http_request = true;
return OkapiServiceRunner::call($service_name, $request);
}
}
/**
* This works like service_call with two exceptions: 1. It passes all your
* current HTTP request headers to OKAPI (which can make use of them in
* terms of caching), 2. It outputs the service response directly, instead
* of returning it.
*/
public static function service_display($service_name, $user_id_or_null, $parameters)
{
$request = new OkapiInternalRequest(
new OkapiFacadeConsumer(),
($user_id_or_null !== null) ? new OkapiFacadeAccessToken($user_id_or_null) : null,
$parameters
);
$request->perceive_as_http_request = true;
if (isset($_SERVER['HTTP_IF_NONE_MATCH']))
$request->etag = $_SERVER['HTTP_IF_NONE_MATCH'];
$response = OkapiServiceRunner::call($service_name, $request);
$response->display();
}
}

View File

@@ -0,0 +1,29 @@
<?
namespace okapi;
/**
* Use this class to access OC session variables. This is especially useful if
* you want to determine which user is currently logged in to OC.
*/
class OCSession
{
/** Return ID of currently logged in user or NULL if no user is logged in. */
public static function get_user_id()
{
static $cached_result = false;
if ($cached_result !== false)
return $cached_result;
$cookie_name = Settings::get('OC_COOKIE_NAME');
if (!isset($_COOKIE[$cookie_name]))
return null;
$OC_data = unserialize(base64_decode($_COOKIE[$cookie_name]));
$OC_sessionid = $OC_data['sessionid'];
if (!$OC_sessionid)
return null;
return Db::select_value("select user_id from sys_sessions where uuid='".mysql_real_escape_string($OC_sessionid)."'");
}
}

View File

@@ -35,6 +35,7 @@ class OkapiServiceRunner
'services/caches/geocaches',
'services/caches/formatters/gpx',
'services/caches/formatters/garmin',
'services/caches/map/tile',
'services/logs/entries',
'services/logs/entry',
'services/logs/logs',
@@ -62,7 +63,7 @@ class OkapiServiceRunner
{
if (!self::exists($service_name))
throw new Exception();
require_once "$service_name.php";
require_once($GLOBALS['rootpath']."okapi/$service_name.php");
try
{
return call_user_func(array('\\okapi\\'.
@@ -123,7 +124,7 @@ class OkapiServiceRunner
Okapi::gettext_domain_init();
try
{
require_once "$service_name.php";
require_once($GLOBALS['rootpath']."okapi/$service_name.php");
$response = call_user_func(array('\\okapi\\'.
str_replace('/', '\\', $service_name).'\\WebService', 'call'), $request);
Okapi::gettext_domain_restore();
@@ -140,19 +141,35 @@ class OkapiServiceRunner
return $response;
}
private static function save_stats($service_name, OkapiRequest $request, $runtime)
/**
* For internal use only. The stats table can be used to store any kind of
* runtime-stats data, i.e. not only regarding services. This is a special
* version of save_stats which saves runtime stats under the name of $extra_name.
* Note, that $request can be null.
*/
public static function save_stats_extra($extra_name, $request, $runtime)
{
self::save_stats("extra/".$extra_name, $request, $runtime);
}
private static function save_stats($service_name, $request, $runtime)
{
# Getting rid of nulls. MySQL PRIMARY keys cannot contain nullable columns.
# Temp table doesn't have primary key, but other stats tables (which are
# dependant on stats table) - do.
$consumer_key = ($request->consumer != null) ? $request->consumer->key : 'anonymous';
$user_id = (($request->token != null) && ($request->token instanceof OkapiAccessToken)) ? $request->token->user_id : -1;
if ($request->is_http_request())
$calltype = 'http';
else
if ($request !== null) {
$consumer_key = ($request->consumer != null) ? $request->consumer->key : 'anonymous';
$user_id = (($request->token != null) && ($request->token instanceof OkapiAccessToken)) ? $request->token->user_id : -1;
if ($request->is_http_request() && ($service_name[0] == 's')) # 's' for "services/", we don't want "extra/" included
$calltype = 'http';
else
$calltype = 'internal';
} else {
$consumer_key = 'internal';
$user_id = -1;
$calltype = 'internal';
}
Db::execute("
insert into okapi_stats_temp (`datetime`, consumer_key, user_id, service_name, calltype, runtime)

View File

@@ -4,6 +4,7 @@ namespace okapi\services\apiref\method;
use Exception;
use okapi\Okapi;
use okapi\Settings;
use okapi\OkapiRequest;
use okapi\ParamMissing;
use okapi\InvalidParam;
@@ -59,7 +60,7 @@ class WebService
$result = array(
'name' => $methodname,
'short_name' => end($exploded),
'ref_url' => $GLOBALS['absolute_server_URI']."okapi/$methodname.html",
'ref_url' => Settings::get('SITE_URL')."okapi/$methodname.html",
'auth_options' => array(
'min_auth_level' => $options['min_auth_level'],
'oauth_consumer' => $options['min_auth_level'] >= 2,
@@ -93,12 +94,23 @@ class WebService
$referenced_methodname = $attrs['method'];
$referenced_method_info = OkapiServiceRunner::call('services/apiref/method',
new OkapiInternalRequest(new OkapiInternalConsumer(), null, array('name' => $referenced_methodname)));
$include_list = isset($attrs['params']) ? explode("|", $attrs['params']) : null;
foreach ($referenced_method_info['arguments'] as $arg)
{
if ($arg['class'] == 'common-formatting')
continue;
$arg['description'] = "<i>Inherited from <a href='".$referenced_method_info['ref_url'].
"'>".$referenced_method_info['name']."</a> method.</i>";
if ($include_list === null)
{
$arg['description'] = "<i>Inherited from <a href='".$referenced_method_info['ref_url'].
"'>".$referenced_method_info['name']."</a> method.</i>";
}
elseif (in_array($arg['name'], $include_list))
{
$arg['description'] = "<i>Same as in the <a href='".$referenced_method_info['ref_url'].
"'>".$referenced_method_info['name']."</a> method.</i>";
} else {
continue;
}
$arg['class'] = 'inherited';
$result['arguments'][] = $arg;
}
@@ -109,13 +121,13 @@ class WebService
'name' => 'format',
'is_required' => false,
'class' => 'common-formatting',
'description' => "<i>Standard <a href='".$GLOBALS['absolute_server_URI']."okapi/introduction.html#common-formatting'>common formatting</a> argument.</i>"
'description' => "<i>Standard <a href='".Settings::get('SITE_URL')."okapi/introduction.html#common-formatting'>common formatting</a> argument.</i>"
);
$result['arguments'][] = array(
'name' => 'callback',
'is_required' => false,
'class' => 'common-formatting',
'description' => "<i>Standard <a href='".$GLOBALS['absolute_server_URI']."okapi/introduction.html#common-formatting'>common formatting</a> argument.</i>"
'description' => "<i>Standard <a href='".Settings::get('SITE_URL')."okapi/introduction.html#common-formatting'>common formatting</a> argument.</i>"
);
}
if (!$docs->returns)

View File

@@ -4,6 +4,7 @@ namespace okapi\services\apisrv\installation;
use Exception;
use okapi\Okapi;
use okapi\Settings;
use okapi\OkapiRequest;
use okapi\ParamMissing;
use okapi\InvalidParam;
@@ -22,7 +23,7 @@ class WebService
public static function call(OkapiRequest $request)
{
$result = array();
$result['site_url'] = $GLOBALS['absolute_server_URI'];
$result['site_url'] = Settings::get('SITE_URL');
$result['okapi_base_url'] = $result['site_url']."okapi/";
$result['site_name'] = Okapi::get_normalized_site_name();
$result['okapi_revision'] = Okapi::$revision;

View File

@@ -5,6 +5,7 @@ namespace okapi\services\apisrv\installations;
use Exception;
use ErrorException;
use okapi\Okapi;
use okapi\Settings;
use okapi\Cache;
use okapi\OkapiRequest;
use okapi\ParamMissing;
@@ -61,9 +62,9 @@ class WebService
$results = array(
array(
'site_url' => $GLOBALS['absolute_server_URI'],
'site_url' => Settings::get('SITE_URL'),
'site_name' => "Unable to retrieve!",
'okapi_base_url' => $GLOBALS['absolute_server_URI']."okapi/",
'okapi_base_url' => Settings::get('SITE_URL')."okapi/",
)
);
Cache::set($cachekey, $results, 12 * 3600); # so to retry no earlier than after 12 hours
@@ -89,7 +90,7 @@ class WebService
'site_name' => $site_name,
'okapi_base_url' => $okapi_base_url,
);
if ($site_url == $GLOBALS['absolute_server_URI'])
if ($site_url == Settings::get('SITE_URL'))
$i_was_included = true;
}
@@ -99,9 +100,9 @@ class WebService
if (!$i_was_included)
{
$results[] = array(
'site_url' => $GLOBALS['absolute_server_URI'],
'site_url' => Settings::get('SITE_URL'),
'site_name' => "DEVELSITE",
'okapi_base_url' => $GLOBALS['absolute_server_URI']."okapi/",
'okapi_base_url' => Settings::get('SITE_URL')."okapi/",
);
# Contact OKAPI developers in order to get added to the official sites list!
}

View File

@@ -3,6 +3,7 @@
namespace okapi\services\caches\formatters\garmin;
use okapi\Okapi;
use okapi\Settings;
use okapi\OkapiRequest;
use okapi\OkapiHttpResponse;
use okapi\OkapiInternalRequest;
@@ -119,7 +120,7 @@ class WebService
# Note: Oliver Dietz (oc.de) replied that images with 'local' set to 0 could not
# be accessed locally. But all the files have 'local' set to 1 anyway.
$syspath = $GLOBALS['picdir']."/".$img['uuid'].".jpg";
$syspath = Settings::get('IMAGES_DIR')."/".$img['uuid'].".jpg";
if (!file_exists($syspath))
continue;
$file = file_get_contents($syspath);
@@ -140,7 +141,7 @@ class WebService
set_time_limit(600);
$response = new OkapiHttpResponse();
$response->content_type = "application/zip";
$response->content_disposition = 'Content-Disposition: attachment; filename="results.zip"';
$response->content_disposition = 'attachment; filename="results.zip"';
$response->stream_length = filesize($tempfilename);
$response->body = fopen($tempfilename, "rb");
$response->allow_gzip = false;

View File

@@ -153,7 +153,7 @@ class WebService
$response = new OkapiHttpResponse();
$response->content_type = "text/xml; charset=utf-8";
$response->content_disposition = 'Content-Disposition: attachment; filename="results.gpx"';
$response->content_disposition = 'attachment; filename="results.gpx"';
ob_start();
Okapi::gettext_domain_init(explode("|", $langpref)); # Consumer gets properly localized GPX file.
include 'gpxfile.tpl.php';

View File

@@ -23,6 +23,7 @@ class WebService
{
$cache_code = $request->get_parameter('cache_code');
if (!$cache_code) throw new ParamMissing('cache_code');
if (strpos($cache_code, "|") !== false) throw new InvalidParam('cache_code');
$langpref = $request->get_parameter('langpref');
if (!$langpref) $langpref = "en";
$fields = $request->get_parameter('fields');

View File

@@ -45,7 +45,7 @@ class WebService
else
$cache_codes = explode("|", $cache_codes);
if (count($cache_codes) > 500)
if ((count($cache_codes) > 500) && (!$request->skip_limits))
throw new InvalidParam('cache_codes', "Maximum allowed number of referenced ".
"caches is 500. You provided ".count($cache_codes)." cache codes.");
if (count($cache_codes) != count(array_unique($cache_codes)))
@@ -121,7 +121,7 @@ class WebService
select
c.cache_id, c.name, c.longitude, c.latitude, c.last_modified,
c.date_created, c.type, c.status, c.date_hidden, c.size, c.difficulty,
c.terrain, c.wp_oc, c.logpw, u.uuid as user_uuid, u.username, u.user_id,
c.terrain, c.wp_oc, c.logpw, c.user_id,
ifnull(sc.toprating, 0) as topratings,
ifnull(sc.found, 0) as founds,
@@ -131,10 +131,9 @@ class WebService
-- SEE ALSO OC.PL BRANCH BELOW
from
caches c
inner join user u on c.user_id = u.user_id
left join stat_caches as sc on c.cache_id = sc.cache_id
where
binary wp_oc in ('".implode("','", array_map('mysql_real_escape_string', $cache_codes))."')
wp_oc in ('".implode("','", array_map('mysql_real_escape_string', $cache_codes))."')
and status in (1,2,3)
");
}
@@ -148,7 +147,7 @@ class WebService
select
c.cache_id, c.name, c.longitude, c.latitude, c.last_modified,
c.date_created, c.type, c.status, c.date_hidden, c.size, c.difficulty,
c.terrain, c.wp_oc, c.logpw, u.uuid as user_uuid, u.username, u.user_id,
c.terrain, c.wp_oc, c.logpw, c.user_id,
c.topratings,
c.founds,
@@ -157,17 +156,16 @@ class WebService
c.votes, c.score
-- SEE ALSO OC.DE BRANCH ABOVE
from
caches c,
user u
caches c
where
binary wp_oc in ('".implode("','", array_map('mysql_real_escape_string', $cache_codes))."')
and c.user_id = u.user_id
wp_oc in ('".implode("','", array_map('mysql_real_escape_string', $cache_codes))."')
and c.status in (1,2,3)
");
}
$results = array();
$cacheid2wptcode = array();
$owner_ids = array();
while ($row = mysql_fetch_assoc($rs))
{
$entry = array();
@@ -182,13 +180,10 @@ class WebService
case 'location': $entry['location'] = round($row['latitude'], 6)."|".round($row['longitude'], 6); break;
case 'type': $entry['type'] = Okapi::cache_type_id2name($row['type']); break;
case 'status': $entry['status'] = Okapi::cache_status_id2name($row['status']); break;
case 'url': $entry['url'] = $GLOBALS['absolute_server_URI']."viewcache.php?wp=".$row['wp_oc']; break;
case 'url': $entry['url'] = Settings::get('SITE_URL')."viewcache.php?wp=".$row['wp_oc']; break;
case 'owner':
$entry['owner'] = array(
'uuid' => $row['user_uuid'],
'username' => $row['username'],
'profile_url' => $GLOBALS['absolute_server_URI']."viewprofile.php?userid=".$row['user_id']
);
$owner_ids[$row['wp_oc']] = $row['user_id'];
/* continued later */
break;
case 'distance':
$entry['distance'] = (int)Okapi::get_distance($center_lat, $center_lon, $row['latitude'], $row['longitude']);
@@ -246,6 +241,29 @@ class WebService
}
mysql_free_result($rs);
# owner
if (in_array('owner', $fields) && (count($results) > 0))
{
$rs = Db::query("
select user_id, uuid, username
from user
where user_id in ('".implode("','", array_map('mysql_real_escape_string', array_values($owner_ids)))."')
");
$tmp = array();
while ($row = mysql_fetch_assoc($rs))
$tmp[$row['user_id']] = $row;
foreach ($results as $cache_code => &$result_ref)
{
$row = $tmp[$owner_ids[$cache_code]];
$result_ref['owner'] = array(
'uuid' => $row['uuid'],
'username' => $row['username'],
'profile_url' => Settings::get('SITE_URL')."viewprofile.php?userid=".$row['user_id']
);
}
}
# is_found
if (in_array('is_found', $fields))
@@ -466,7 +484,7 @@ class WebService
'user' => array(
'uuid' => $row['user_uuid'],
'username' => $row['username'],
'profile_url' => $GLOBALS['absolute_server_URI']."viewprofile.php?userid=".$row['user_id'],
'profile_url' => Settings::get('SITE_URL')."viewprofile.php?userid=".$row['user_id'],
),
'type' => Okapi::logtypeid2name($row['type']),
'comment' => $row['text']
@@ -706,7 +724,7 @@ class WebService
public static function get_cache_attribution_note($cache_id, $lang)
{
$site_url = $GLOBALS['absolute_server_URI'];
$site_url = Settings::get('SITE_URL');
$site_name = Okapi::get_normalized_site_name();
$cache_url = $site_url."viewcache.php?cacheid=$cache_id";

View File

@@ -0,0 +1,260 @@
<?php
namespace okapi\services\caches\map;
use Exception;
use okapi\Okapi;
use okapi\Settings;
use okapi\Cache;
use okapi\Db;
use okapi\OkapiRequest;
use okapi\OkapiHttpResponse;
use okapi\ParamMissing;
use okapi\InvalidParam;
use okapi\BadRequest;
use okapi\DoesNotExist;
use okapi\OkapiInternalRequest;
use okapi\OkapiInternalConsumer;
use okapi\OkapiServiceRunner;
use okapi\services\caches\map\TileTree;
require_once 'tiletree.inc.php';
class ReplicateListener
{
public static function receive($changelog)
{
# This will be called every time new items arrive from replicate module's
# changelog. The format of $changelog is described in the replicate module
# (NOT the entire response, just the "changelog" key).
foreach ($changelog as $c)
{
if ($c['object_type'] == 'geocache')
{
if ($c['change_type'] == 'replace')
self::handle_geocache_replace($c);
else
self::handle_geocache_delete($c);
}
}
}
public static function reset()
{
# This will be called when there are "too many" entries in the changelog
# and the replicate module thinks it better to just reset the entire TileTree.
# For the first hours after such reset maps may work very slow!
Okapi::mail_admins("OKAPI TileMap database reset",
"Hello,\n\n".
"OKAPI's 'replicate' module detected a big database update. As result\n".
"of this, OKAPI decided to reset the TileMap cache. This may\n".
"temporarily influence TileMap performance. The map may work much\n".
"slower during the next few hours or days, while the cache is being\n".
"rebuilt.\n\n".
"If this happens frequently, please contact OKAPI developers. It may\n".
"indicate a bug in OKAPI's 'replicate' module or cronjob settings.\n\n".
"Thanks!"
);
Db::execute("delete from okapi_tile_status");
Db::execute("delete from okapi_tile_caches");
}
private static function handle_geocache_replace($c)
{
# Check if any relevant geocache attributes have changed.
# We will pick up "our" copy of the cache from zero-zoom level.
try {
$cache = OkapiServiceRunner::call("services/caches/geocache", new OkapiInternalRequest(new OkapiInternalConsumer(), null, array(
'cache_code' => $c['object_key']['code'],
'fields' => 'internal_id|code|name|location|type|status|rating|recommendations|founds|trackables_count'
)));
} catch (InvalidParam $e) {
# Unprobable, but possible. Ignore changelog entry.
return;
}
$theirs = TileTree::generate_short_row($cache);
$ours = mysql_fetch_row(Db::query("
select cache_id, z21x, z21y, status, type, rating, flags
from okapi_tile_caches
where
z=0
and cache_id = '".mysql_real_escape_string($cache['internal_id'])."'
"));
if (!$ours)
{
# Aaah, a new geocache! How nice... ;)
self::add_geocache_to_cached_tiles($theirs);
}
elseif (($ours[1] != $theirs[1]) || ($ours[2] != $theirs[2])) # z21x & z21y fields
{
# Location changed.
self::remove_geocache_from_cached_tiles($ours[0]);
self::add_geocache_to_cached_tiles($theirs);
}
elseif ($ours != $theirs)
{
self::update_geocache_attributes_in_cached_tiles($theirs);
}
else
{
# No need to update anything. This is very common (i.e. when the
# cache was simply found, not actually changed). Replicate module generates
# many updates which do not influence our cache.
}
}
private static function remove_geocache_from_cached_tiles($cache_id)
{
# Simply remove all traces of this geocache from all tiles.
# This includes all references along tiles' borders, etc.
Db::execute("
delete from okapi_tile_caches
where cache_id = '".mysql_real_escape_string($cache_id)."'
");
# Note, that after this operation, okapi_tile_status may be out-of-date.
# There might exist some rows with status==2, but they should be in status==1.
# Currently, we can ignore this, because status==1 is just a shortcut to
# avoid making unnecessary queries.
}
private static function add_geocache_to_cached_tiles(&$row)
{
# This one is the most complicated. We need to identify all tiles
# where the cache should be present. This include 22 obvious "exact match"
# tiles (one per each zoom level), *and* all "just outside the border"
# tiles (one geocache can be present in up to 4 tiles per zoom level).
# This gives us max. 88 tiles to add the geocache to.
$tiles_to_update = array();
# We will begin at zoom 21 and then go down to zoom 0.
$z21x = $row[1];
$z21y = $row[2];
$ex = $z21x >> 8; # initially, z21x / <tile width>
$ey = $z21y >> 8; # initially, z21y / <tile height>
for ($zoom = 21; $zoom >= 0; $zoom--, $ex >>= 1, $ey >>= 1)
{
# ($ex, $ey) points to the "exact match" tile. We need to determine
# tile-range to check for "just outside the border" tiles. We will
# go with the simple approach and check all 1+8 bordering tiles.
$tiles_in_this_region = array();
for ($x=$ex-1; $x<=$ex+1; $x++)
for ($y=$ey-1; $y<=$ey+1; $y++)
if (($x >= 0) && ($x < 1<<$zoom) && ($y >= 0) && ($y < 1<<$zoom))
$tiles_in_this_region[] = array($x, $y);
foreach ($tiles_in_this_region as $coords)
{
list($x, $y) = $coords;
$scale = 8 + 21 - $zoom;
$margin = 1 << ($scale - 3); # 32px of current $zoom level, measured in z21 pixels.
$left_z21x = ($x << $scale) - $margin;
$right_z21x = (($x + 1) << $scale) + $margin;
$top_z21y = ($y << $scale) - $margin;
$bottom_z21y = (($y + 1) << $scale) + $margin;
if ($z21x < $left_z21x)
continue;
if ($z21x > $right_z21x)
continue;
if ($z21y < $top_z21y)
continue;
if ($z21y > $bottom_z21y)
continue;
# We found a match. Store it for later.
$tiles_to_update[] = array($zoom, $x, $y);
}
}
# We have a list of all possible tiles that need updating.
# Most of these tiles aren't cached at all. We need to update
# only the cached ones.
$alternatives = array();
foreach ($tiles_to_update as $coords)
{
list($z, $x, $y) = $coords;
$alternatives[] = "(
z = '".mysql_real_escape_string($z)."'
and x = '".mysql_real_escape_string($x)."'
and y = '".mysql_real_escape_string($y)."'
)";
}
Db::execute("
replace into okapi_tile_caches (
z, x, y, cache_id, z21x, z21y, status, type, rating, flags
)
select
z, x, y,
'".mysql_real_escape_string($row[0])."',
'".mysql_real_escape_string($row[1])."',
'".mysql_real_escape_string($row[2])."',
'".mysql_real_escape_string($row[3])."',
'".mysql_real_escape_string($row[4])."',
".(($row[5] === null) ? "null" : $row[5]).",
'".mysql_real_escape_string($row[6])."'
from okapi_tile_status
where
(".implode(" or ", $alternatives).")
and status in (1,2)
");
# We might have just filled some empty tiles (status 1) with data.
# We need to update their status to 2.
Db::execute("
update okapi_tile_status
set status=2
where
(".implode(" or ", $alternatives).")
and status=1
");
# And that's all. That should do the trick.
}
private static function update_geocache_attributes_in_cached_tiles(&$row)
{
# Update all attributes (for all levels). Note, that we don't need to
# update location ($row[1] and $row[2]) - this method is called ONLY
# when location stayed untouched!
Db::execute("
update okapi_tile_caches
set
status = '".mysql_real_escape_string($row[3])."',
type = '".mysql_real_escape_string($row[4])."',
rating = ".(($row[5] === null) ? "null" : $row[5]).",
flags = '".mysql_real_escape_string($row[6])."'
where
cache_id = '".mysql_real_escape_string($row[0])."'
");
}
private static function handle_geocache_delete($c)
{
# Simply delete the cache at all zoom levels.
$cache_id = Db::select_value("
select cache_id
from caches
where wp_oc='".mysql_real_escape_string($c['object_key']['code'])."'
");
self::remove_geocache_from_cached_tiles($cache_id);
}
}

View File

@@ -0,0 +1,391 @@
<?php
namespace okapi\services\caches\map\tile;
use Exception;
use okapi\Okapi;
use okapi\Settings;
use okapi\Cache;
use okapi\FileCache;
use okapi\Db;
use okapi\OkapiRequest;
use okapi\OkapiHttpResponse;
use okapi\ParamMissing;
use okapi\InvalidParam;
use okapi\BadRequest;
use okapi\DoesNotExist;
use okapi\OkapiInternalRequest;
use okapi\OkapiInternalConsumer;
use okapi\OkapiServiceRunner;
use okapi\services\caches\map\TileTree;
use okapi\services\caches\map\DefaultTileRenderer;
require_once('tiletree.inc.php');
require_once('tilerenderer.inc.php');
class WebService
{
/**
* Should be always true. You may temporarily set it to false, when you're
* testing/debugging the tile renderer.
*/
private static $USE_ETAGS_CACHE = true;
/**
* Should be always true. You may temporarily set it to false, when you're
* testing/debugging the tile renderer.
*/
private static $USE_IMAGE_CACHE = true;
/**
* Should be always true. You may temporarily set it to false, when you're
* testing/debugging. Grep the code to check when this flag is used.
*/
private static $USE_OTHER_CACHE = true;
public static function options()
{
return array(
'min_auth_level' => 1
);
}
private static function require_uint($request, $name, $min_value = 0)
{
$val = $request->get_parameter($name);
if ($val === null)
throw new ParamMissing($name);
$ret = intval($val);
if ($ret < 0 || ("$ret" !== $val))
throw new InvalidParam($name, "Expecting non-negative integer.");
return $ret;
}
public static function call(OkapiRequest $request)
{
$checkpointA_started = microtime(true);
# Make sure the request is internal.
if (!in_array($request->consumer->key, array('internal', 'facade')))
throw new BadRequest("Your Consumer Key has not been allowed to access this method.");
# zoom, x, y - required tile-specific parameters.
$zoom = self::require_uint($request, 'z');
if ($zoom > 21)
throw new InvalidParam('z', "Maximum value for this parameter is 21.");
$x = self::require_uint($request, 'x');
$y = self::require_uint($request, 'y');
if ($x >= 1<<$zoom)
throw new InvalidParam('x', "Should be in 0..".((1<<$zoom) - 1).".");
if ($y >= 1<<$zoom)
throw new InvalidParam('y', "Should be in 0..".((1<<$zoom) - 1).".");
# status
$filter_conds = array();
$tmp = $request->get_parameter('status');
if ($tmp == null) $tmp = "Available";
$allowed_status_codes = array();
foreach (explode("|", $tmp) as $name)
{
try
{
$allowed_status_codes[] = Okapi::cache_status_name2id($name);
}
catch (Exception $e)
{
throw new InvalidParam('status', "'$name' is not a valid cache status.");
}
}
sort($allowed_status_codes);
if (count($allowed_status_codes) == 0)
throw new InvalidParam('status');
if (count($allowed_status_codes) < 3)
$filter_conds[] = "status in ('".implode("','", array_map('mysql_real_escape_string', $allowed_status_codes))."')";
# type
if ($tmp = $request->get_parameter('type'))
{
$operator = "in";
if ($tmp[0] == '-')
{
$tmp = substr($tmp, 1);
$operator = "not in";
}
$types = array();
foreach (explode("|", $tmp) as $name)
{
try
{
$id = Okapi::cache_type_name2id($name);
$types[] = $id;
}
catch (Exception $e)
{
throw new InvalidParam('type', "'$name' is not a valid cache type.");
}
}
sort($types);
# Check if all cache types were selected. Since we're running
# on various OC installations, we don't know which caches types
# are "all" here. We have to check.
$all = self::$USE_OTHER_CACHE ? Cache::get('all_cache_types') : null;
if ($all === null)
{
$all = Db::select_column("
select distinct type
from caches
where status in (1,2,3)
");
Cache::set('all_cache_types', $all, 86400);
}
$all_included = true;
foreach ($all as $type)
if (!in_array($type, $types))
{
$all_included = false;
break;
}
if ($all_included && ($operator == "in"))
{
# All cache types are to be included. This is common.
}
else
{
$filter_conds[] = "type $operator ('".implode("','", array_map('mysql_real_escape_string', $types))."')";
}
}
# User-specific geocaches (cached together).
$cache_key = "tileuser/".$request->token->user_id;
$user = self::$USE_OTHER_CACHE ? Cache::get($cache_key) : null;
if ($user === null)
{
$user = array();
# Ignored caches.
$rs = Db::query("
select cache_id
from cache_ignore
where user_id = '".mysql_real_escape_string($request->token->user_id)."'
");
$user['ignored'] = array();
while (list($cache_id) = mysql_fetch_row($rs))
$user['ignored'][$cache_id] = true;
# Found caches.
$rs = Db::query("
select distinct cache_id
from cache_logs
where
user_id = '".mysql_real_escape_string($request->token->user_id)."'
and type = 1
and ".((Settings::get('OC_BRANCH') == 'oc.pl') ? "deleted = 0" : "true")."
");
$user['found'] = array();
while (list($cache_id) = mysql_fetch_row($rs))
$user['found'][$cache_id] = true;
# Own caches.
$rs = Db::query("
select distinct cache_id
from caches
where user_id = '".mysql_real_escape_string($request->token->user_id)."'
");
$user['own'] = array();
while (list($cache_id) = mysql_fetch_row($rs))
$user['own'][$cache_id] = true;
Cache::set($cache_key, $user, 30);
}
# exclude_ignored
$tmp = $request->get_parameter('exclude_ignored');
if ($tmp === null) $tmp = "false";
if (!in_array($tmp, array('true', 'false'), true))
throw new InvalidParam('exclude_ignored', "'$tmp'");
if ($tmp == 'true')
{
$excluded_dict = $user['ignored'];
} else {
$excluded_dict = array();
}
# exclude_my_own
if ($tmp = $request->get_parameter('exclude_my_own'))
{
if (!in_array($tmp, array('true', 'false'), 1))
throw new InvalidParam('exclude_my_own', "'$tmp'");
if (($tmp == 'true') && (count($user['own']) > 0))
{
foreach ($user['own'] as $cache_id => $v)
$excluded_dict[$cache_id] = true;
}
}
# found_status
if ($tmp = $request->get_parameter('found_status'))
{
if (!in_array($tmp, array('found_only', 'notfound_only', 'either')))
throw new InvalidParam('found_status', "'$tmp'");
if ($tmp == 'either') {
# Do nothing.
} elseif ($tmp == 'notfound_only') {
# Easy.
foreach ($user['found'] as $cache_id => $v)
$excluded_dict[$cache_id] = true;
} else {
# Found only. This will slow down queries somewhat. But it is rare.
$filter_conds[] = "cache_id in ('".implode("','", array_map('mysql_real_escape_string', array_keys($user['found'])))."')";
}
}
# with_trackables_only
if ($tmp = $request->get_parameter('with_trackables_only'))
{
if (!in_array($tmp, array('true', 'false'), 1))
throw new InvalidParam('with_trackables_only', "'$tmp'");
if ($tmp == 'true')
{
$filter_conds[] = "flags & ".TileTree::$FLAG_HAS_TRACKABLES;
}
}
# not_yet_found_only
if ($tmp = $request->get_parameter('not_yet_found_only')) # ftf hunter
{
if (!in_array($tmp, array('true', 'false'), 1))
throw new InvalidParam('not_yet_found_only', "'$tmp'");
if ($tmp == 'true')
{
$filter_conds[] = "flags & ".TileTree::$FLAG_NOT_YET_FOUND;
}
}
# rating
if ($tmp = $request->get_parameter('rating'))
{
if (!preg_match("/^[1-5]-[1-5](\|X)?$/", $tmp))
throw new InvalidParam('rating', "'$tmp'");
list($min, $max) = explode("-", $tmp);
if (strpos($max, "|X") !== false)
{
$max = $max[0];
$allow_null = true;
} else {
$allow_null = false;
}
if ($min > $max)
throw new InvalidParam('rating', "'$tmp'");
if (($min == 1) && ($max == 5) && $allow_null) {
/* no extra condition necessary */
} else {
$filter_conds[] = "(rating between $min and $max)".
($allow_null ? " or rating is null" : "");
}
}
# Filter out caches in $excluded_dict.
if (count($excluded_dict) > 0)
{
$filter_conds[] = "cache_id not in ('".implode("','", array_keys($excluded_dict))."')";
}
# Get caches within the tile (+ those around the borders). All filtering
# options need to be applied here.
$rs = TileTree::query_fast($zoom, $x, $y, $filter_conds);
OkapiServiceRunner::save_stats_extra("caches/map/tile/checkpointA", null,
microtime(true) - $checkpointA_started);
$checkpointB_started = microtime(true);
# Read the rows and add extra flags to them.
$rows = array();
if ($rs !== null)
{
while ($row = mysql_fetch_row($rs))
{
# Add the "found" flag, to indicate that this cache needs
# to be drawn as found.
if (isset($user['found'][$row[0]]))
$row[6] |= TileTree::$FLAG_FOUND; # $row[6] is "flags"
if (isset($user['own'][$row[0]]))
$row[6] |= TileTree::$FLAG_OWN; # $row[6] is "flags"
$rows[] = $row;
}
unset($row);
}
# Compute a fast image fingerprint. This will be used both for ETags
# and internal cache ($cache_key).
$tile = new DefaultTileRenderer($zoom, $rows);
$image_fingerprint = $tile->get_unique_hash();
# Start creating response.
$response = new OkapiHttpResponse();
$response->content_type = $tile->get_content_type();
$response->cache_control = "Cache-Control: private, max-age=600";
$response->etag = 'W/"'.$image_fingerprint.'"';
# Check if the request didn't include the same ETag.
OkapiServiceRunner::save_stats_extra("caches/map/tile/checkpointB", null,
microtime(true) - $checkpointB_started);
$checkpointC_started = microtime(true);
if (self::$USE_ETAGS_CACHE && ($request->etag == $response->etag))
{
# Hit. Report the content was unmodified.
$response->etag = null;
$response->status = "304 Not Modified";
return $response;
}
# Check if the image was recently rendered and is kept in image cache.
$cache_key = "tile/".$image_fingerprint;
$response->body = self::$USE_IMAGE_CACHE ? Cache::get($cache_key) : null;
OkapiServiceRunner::save_stats_extra("caches/map/tile/checkpointC", null,
microtime(true) - $checkpointC_started);
$checkpointD_started = microtime(true);
if ($response->body !== null)
{
# Hit. We will use the cached version of the image.
return $response;
}
# Miss. Render the image. Cache the result.
$response->body = $tile->render();
Cache::set_scored($cache_key, $response->body);
OkapiServiceRunner::save_stats_extra("caches/map/tile/checkpointD", null,
microtime(true) - $checkpointD_started);
return $response;
}
}

View File

@@ -0,0 +1,25 @@
<xml>
<brief>Get cache map tile</brief>
<issue-id>150</issue-id>
<desc>
<p><b>CLOSED BETA</b> version. Due to long-term performance tests, currently
this method is not publicly accessible. Let us know if you're interested
in using it.</p>
<p>Use this method to retrieve a tile-map of all caches included in your search
result.</p>
</desc>
<req name='z'>Zoom level (0..21).</req>
<req name='x'>Tile number on the X axis.</req>
<req name='y'>Tile number on the Y axis.</req>
<opt name='with_trackables_only' default='false'>
All geocaches without trackables will be skipped.
</opt>
<opt name='not_yet_found_only' default='false'>
All geocaches which have been already found by someone, will be skipped.
</opt>
<import-params method="services/caches/search/all"
params="status|type|exclude_ignored|exclude_my_own|found_status|rating"/>
<returns>
The PNG image with the requested map tile.
</returns>
</xml>

View File

@@ -0,0 +1,601 @@
<?php
namespace okapi\services\caches\map;
use Exception;
use okapi\Okapi;
use okapi\Settings;
use okapi\Cache;
use okapi\Db;
use okapi\FileCache; // WRTODO
interface TileRenderer
{
/**
* Return the unique hash of the tile being rendered. This method will be
* called only once, prior the render method. You may (but don't have to)
* throw an Exception on every subsequent call.
*/
public function get_unique_hash();
/** Get the content type of the data returned by the render method. */
public function get_content_type();
/**
* Render the image. This function will be called only once, after calling
* get_unique_hash method.
*/
public function render();
}
class DefaultTileRenderer implements TileRenderer
{
/**
* Changing this will affect all generated hashes. You should increment it
* whenever you alter anything in the drawing algorithm.
*/
private static $VERSION = 41;
/**
* Should be always true. You may temporarily set it to false, when you're
* testing/debugging a new set of static icons.
*/
private static $USE_STATIC_IMAGE_CACHE = true;
/**
* Should be always true. You may temporarily set it to false, when you're
* testing/debugging a new captions renderer.
*/
private static $USE_CAPTIONS_CACHE = true;
private $zoom;
private $rows_ref;
private $im;
/**
* Takes the zoom level and the list of geocache descriptions. Note, that
* $rows_ref can be altered after calling render. If you don't want it to,
* you should pass a deep copy.
*/
public function __construct($zoom, &$rows_ref)
{
$this->zoom = $zoom;
$this->rows_ref = &$rows_ref;
}
public function get_unique_hash()
{
return md5(json_encode(array(
"DefaultTileRenderer",
self::$VERSION,
$this->zoom,
$this->rows_ref
)));
}
public function get_content_type()
{
return "image/png";
}
public function render()
{
# Preprocess the rows.
if ($this->zoom >= 5)
$this->decide_which_get_captions();
# Make a background.
$this->im = imagecreatetruecolor(256, 256);
imagealphablending($this->im, false);
if ($this->zoom >= 13) $opacity = 15;
elseif ($this->zoom <= 12) $opacity = max(0, $this->zoom * 2 - 14);
$transparent = imagecolorallocatealpha($this->im, 0, 0, 0, 127 - $opacity);
imagefilledrectangle($this->im, 0, 0, 256, 256, $transparent);
imagealphablending($this->im, true);
# Draw the caches.
foreach ($this->rows_ref as &$row_ref)
$this->draw_cache($row_ref);
# Return the result.
ob_start();
imagesavealpha($this->im, true);
imagepng($this->im);
imagedestroy($this->im);
return ob_get_clean();
}
private static function get_image($name, $opacity=1, $brightness=0,
$contrast=0, $r=0, $g=0, $b=0)
{
static $locmem_cache = array();
# Check locmem cache.
$key = "$name/$opacity/$brightness/$contrast/$r/$g/$b";
if (!isset($locmem_cache[$key]))
{
# Miss. Check default cache. WRTODO: upgrade to normal Cache?
$cache_key = "tilesrc/".Okapi::$revision."/".self::$VERSION."/".$key;
$gd2_path = self::$USE_STATIC_IMAGE_CACHE
? FileCache::get_file_path($cache_key) : null;
if ($gd2_path === null)
{
# Miss again. Read the image from PNG.
$locmem_cache[$key] = imagecreatefrompng($GLOBALS['rootpath']."okapi/static/tilemap/$name.png");
# Apply all wanted effects.
if ($opacity != 1)
self::change_opacity($locmem_cache[$key], $opacity);
if ($contrast != 0)
imagefilter($locmem_cache[$key], IMG_FILTER_CONTRAST, $contrast);
if ($brightness != 0)
imagefilter($locmem_cache[$key], IMG_FILTER_BRIGHTNESS, $brightness);
if (($r != 0) || ($g != 0) || ($b != 0))
{
imagefilter($locmem_cache[$key], IMG_FILTER_GRAYSCALE);
imagefilter($locmem_cache[$key], IMG_FILTER_COLORIZE, $r, $g, $b);
}
# Cache the result.
ob_start();
imagegd2($locmem_cache[$key]);
$gd2 = ob_get_clean();
FileCache::set($cache_key, $gd2);
}
else
{
# File cache hit. GD2 files are much faster to read than PNGs.
$locmem_cache[$key] = imagecreatefromgd2($gd2_path);
}
}
return $locmem_cache[$key];
}
/**
* Extremely slow! Remember to cache the result!
*/
private static function change_opacity($im, $ratio)
{
imagealphablending($im, false);
$w = imagesx($im);
$h = imagesy($im);
for($x = 0; $x < $w; $x++)
{
for($y = 0; $y < $h; $y++)
{
$color = imagecolorat($im, $x, $y);
$new_color = ((max(0, floor(127 - ((127 - (($color >> 24) & 0x7f)) * $ratio))) & 0x7f) << 24) | ($color & 0x80ffffff);
imagesetpixel($im, $x, $y, $new_color);
}
}
imagealphablending($im, true);
}
private function draw_cache(&$cache_struct)
{
if ($this->zoom <= 8)
$this->draw_cache_tiny($cache_struct);
elseif ($this->zoom <= 12)
$this->draw_cache_medium($cache_struct);
else
$this->draw_cache_large($cache_struct);
# Put caption (this flag is set only when there is plenty of space around).
if ($cache_struct[6] & TileTree::$FLAG_DRAW_CAPTION)
{
$caption = $this->get_caption($cache_struct[0]);
imagecopy($this->im, $caption, $cache_struct[1] - 32, $cache_struct[2] + 6, 0, 0, 64, 26);
}
}
private function draw_cache_large(&$cache_struct)
{
list($cache_id, $px, $py, $status, $type, $rating, $flags, $count) = $cache_struct;
$found = $flags & TileTree::$FLAG_FOUND;
$own = $flags & TileTree::$FLAG_OWN;
$new = $flags & TileTree::$FLAG_NEW;
# Prepare vars.
if ($own) {
$key = 'large_outer_own';
$a = 1; $br = 0; $c = 0;
$r = 0; $g = 0; $b = 0;
} elseif ($found) {
$key = 'large_outer_found';
$a = 1; $br = 40; $c = 20;
//$a = 0.5; $br = 0; $c = 0;
$r = 0; $g = 0; $b = 0;
} elseif ($new) {
$key = 'large_outer_new';
$a = 1; $br = 0; $c = 0;
$r = 0; $g = 0; $b = 0;
} else {
$key = 'large_outer';
$a = 1; $br = 0; $c = 0;
$r = 0; $g = 0; $b = 0;
}
# Put the outer marker (indicates the found/new/own status).
$outer_marker = self::get_image($key, $a);
$width = 40;
$height = 32;
$center_x = 12;
$center_y = 26;
$markercenter_x = 12;
$markercenter_y = 12;
if ($count > 1)
imagecopy($this->im, $outer_marker, $px - $center_x + 3, $py - $center_y - 2, 0, 0, $width, $height);
imagecopy($this->im, $outer_marker, $px - $center_x, $py - $center_y, 0, 0, $width, $height);
# Put the inner marker (indicates the type).
$inner_marker = self::get_image("large_inner_".self::get_type_suffix(
$type, true), $a, $br, $c, $r, $g, $b);
imagecopy($this->im, $inner_marker, $px - 7, $py - 22, 0, 0, 16, 16);
# If the cache is unavailable, mark it with X.
if (($status != 1) && ($count == 1))
{
$icon = self::get_image(($status == 2) ? "status_unavailable"
: "status_archived", $a);
imagecopy($this->im, $icon, $px - 1, $py - $center_y - 4, 0, 0, 16, 16);
}
# Put the rating smile. :)
if ($status == 1)
{
if ($rating >= 4.2)
{
if ($flags & TileTree::$FLAG_STAR) {
$icon = self::get_image("rating_grin", $a, $br, $c, $r, $g, $b);
imagecopy($this->im, $icon, $px - 7 - 6, $py - $center_y - 8, 0, 0, 16, 16);
$icon = self::get_image("rating_star", $a, $br, $c, $r, $g, $b);
imagecopy($this->im, $icon, $px - 7 + 6, $py - $center_y - 8, 0, 0, 16, 16);
} else {
$icon = self::get_image("rating_grin", $a, $br, $c, $r, $g, $b);
imagecopy($this->im, $icon, $px - 7, $py - $center_y - 8, 0, 0, 16, 16);
}
}
# This was commented out because users complained about too many smiles ;)
// elseif ($rating >= 3.6) {
// $icon = self::get_image("rating_smile", $a, $br, $c, $r, $g, $b);
// imagecopy($this->im, $icon, $px - 7, $py - $center_y - 8, 0, 0, 16, 16);
// }
}
# Mark found caches with V.
if ($found)
{
$icon = self::get_image("found", 0.7, $br, $c, $r, $g, $b);
imagecopy($this->im, $icon, $px - 2, $py - $center_y - 3, 0, 0, 16, 16);
}
}
/**
* Split lines so that they fit inside the specified width.
*/
private static function wordwrap($font, $size, $maxWidth, $text)
{
$words = explode(" ", $text);
$lines = array();
$line = "";
$nextBonus = "";
for ($i=0; ($i<count($words)) || (mb_strlen($nextBonus)>0); $i++) {
$word = isset($words[$i])?$words[$i]:"";
if (mb_strlen($nextBonus) > 0)
$word = $nextBonus." ".$word;
$nextBonus = "";
while (true) {
$bbox = imagettfbbox($size, 0, $font, $line.$word);
$width = $bbox[2]-$bbox[0];
if ($width <= $maxWidth) {
$line .= $word." ";
continue 2;
}
if (mb_strlen($line) > 0) {
$lines[] = trim($line);
$line = "";
continue;
}
$nextBonus = $word[mb_strlen($word)-1].$nextBonus;
$word = mb_substr($word, 0, mb_strlen($word)-1);
continue;
}
}
if (mb_strlen($line) > 0)
$lines[] = trim($line);
return implode("\n", $lines);
}
/**
* Return 64x26 bitmap with the caption (name) for the given geocache.
*/
private function get_caption($cache_id)
{
# Check cache.
$cache_key = "tilecaption/".self::$VERSION."/".$cache_id;
$gd2 = self::$USE_CAPTIONS_CACHE ? Cache::get($cache_key) : null;
if ($gd2 === null)
{
# We'll work with 16x bigger image to get smoother interpolation.
$im = imagecreatetruecolor(64*4, 26*4);
imagealphablending($im, false);
$transparent = imagecolorallocatealpha($im, 255, 255, 255, 127);
imagefilledrectangle($im, 0, 0, 64*4, 26*4, $transparent);
imagealphablending($im, true);
# Get the name of the cache.
$name = Db::select_value("
select name
from caches
where cache_id = '".mysql_real_escape_string($cache_id)."'
");
# Split the name into a couple of lines.
//$font = $GLOBALS['rootpath'].'util.sec/bt.ttf';
$font = $GLOBALS['rootpath'].'okapi/static/tilemap/tahoma.ttf';
$size = 25;
$lines = explode("\n", self::wordwrap($font, $size, 64*4 - 6*2, $name));
# For each line, compute its (x, y) so that the text is centered.
$y = 0;
$positions = array();
foreach ($lines as $line)
{
$bbox = imagettfbbox($size, 0, $font, $line);
$width = $bbox[2]-$bbox[0];
$x = 128 - ($width >> 1);
$positions[] = array($x, $y);
$y += 36;
}
$drawer = function($x, $y, $color) use (&$lines, &$positions, &$im, &$size, &$font)
{
$len = count($lines);
for ($i=0; $i<$len; $i++)
{
$line = $lines[$i];
list($offset_x, $offset_y) = $positions[$i];
imagettftext($im, $size, 0, $offset_x + $x, $offset_y + $y, $color, $font, $line);
}
};
# Draw an outline.
$outline_color = imagecolorallocatealpha($im, 255, 255, 255, 80);
for ($x=0; $x<=12; $x+=3)
for ($y=$size-3; $y<=$size+9; $y+=3)
$drawer($x, $y, $outline_color);
# Add a slight shadow effect (on top of the outline).
$drawer(9, $size + 3, imagecolorallocatealpha($im, 0, 0, 0, 110));
# Draw the caption.
$drawer(6, $size + 3, imagecolorallocatealpha($im, 150, 0, 0, 40));
# Resample.
imagealphablending($im, false);
$small = imagecreatetruecolor(64, 26);
imagealphablending($small, false);
imagecopyresampled($small, $im, 0, 0, 0, 0, 64, 26, 64*4, 26*4);
# Cache it!
ob_start();
imagegd2($small);
$gd2 = ob_get_clean();
Cache::set_scored($cache_key, $gd2);
}
return imagecreatefromstring($gd2);
}
private function draw_cache_medium(&$cache_struct)
{
list($cache_id, $px, $py, $status, $type, $rating, $flags, $count) = $cache_struct;
$found = $flags & TileTree::$FLAG_FOUND;
$own = $flags & TileTree::$FLAG_OWN;
$new = $flags & TileTree::$FLAG_NEW;
# Put the marker (indicates the type).
$marker = self::get_image("medium_".self::get_type_suffix($type, false));
$width = 14;
$height = 14;
$center_x = 7;
$center_y = 8;
$markercenter_x = 7;
$markercenter_y = 8;
if ($count > 1)
{
imagecopy($this->im, $marker, $px - $center_x + 3, $py - $center_y - 2, 0, 0, $width, $height);
imagecopy($this->im, $marker, $px - $center_x, $py - $center_y, 0, 0, $width, $height);
}
elseif ($status == 1) # don't put the marker for unavailable caches (X only)
{
imagecopy($this->im, $marker, $px - $center_x, $py - $center_y, 0, 0, $width, $height);
}
# If the cache is unavailable, mark it with X.
if (($status != 1) && ($count == 1))
{
$icon = self::get_image(($status == 2) ? "status_unavailable"
: "status_archived");
imagecopy($this->im, $icon, $px - ($center_x - $markercenter_x) - 6,
$py - ($center_y - $markercenter_y) - 8, 0, 0, 16, 16);
}
if ($own)
{
# Mark own caches with additional overlay.
$overlay = self::get_image("medium_overlay_own");
imagecopy($this->im, $overlay, $px - $center_x, $py - $center_y, 0, 0, $width, $height);
}
elseif ($found)
{
# Mark found caches with V.
$icon = self::get_image("found", 0.7);
imagecopy($this->im, $icon, $px - ($center_x - $markercenter_x) - 7,
$py - ($center_y - $markercenter_y) - 9, 0, 0, 16, 16);
}
elseif ($new)
{
# Mark new caches with additional overlay.
$overlay = self::get_image("medium_overlay_new");
imagecopy($this->im, $overlay, $px - $center_x, $py - $center_y, 0, 0, $width, $height);
}
}
private static function get_type_suffix($type, $extended_set)
{
switch ($type) {
case 2: return 'traditional';
case 3: return 'multi';
case 6: return 'event';
case 7: return 'quiz';
case 4: return 'virtual';
case 1: return 'unknown';
}
if ($extended_set)
{
switch ($type) {
case 10: return 'own';
case 8: return 'moving';
case 5: return 'webcam';
}
}
return 'other';
}
private function draw_cache_tiny(&$cache_struct)
{
list($cache_id, $px, $py, $status, $type, $rating, $flags, $count) = $cache_struct;
$found = $flags & TileTree::$FLAG_FOUND;
$own = $flags & TileTree::$FLAG_OWN;
$new = $flags & TileTree::$FLAG_NEW;
$marker = self::get_image("tiny_".self::get_type_suffix($type, false));
$width = 10;
$height = 10;
$center_x = 5;
$center_y = 6;
$markercenter_x = 5;
$markercenter_y = 6;
# Put the marker. If cache covers more caches, then put two markers instead of one.
if ($count > 1)
{
imagecopy($this->im, $marker, $px - $center_x + 3, $py - $center_y - 2, 0, 0, $width, $height);
imagecopy($this->im, $marker, $px - $center_x, $py - $center_y, 0, 0, $width, $height);
}
elseif ($status == 1)
{
imagecopy($this->im, $marker, $px - $center_x, $py - $center_y, 0, 0, $width, $height);
}
# If the cache is unavailable, mark it with X.
if (($status != 1) && ($count == 1))
{
$icon = self::get_image(($status == 2) ? "status_unavailable"
: "status_archived");
imagecopy($this->im, $icon, $px - ($center_x - $markercenter_x) - 6,
$py - ($center_y - $markercenter_y) - 8, 0, 0, 16, 16);
}
}
/**
* Examine all rows and decide which of them will get captions.
* Mark selected rows with TileTree::$FLAG_DRAW_CAPTION.
*
* Note: Calling this will alter the rows!
*/
private function decide_which_get_captions()
{
# We will split the tile (along with its margins) into 12x12 squares.
# A single geocache placed in square (x, y) gets the caption only
# when there are no other geocaches in any of the adjacent squares.
# This is efficient and yields acceptable results.
$matrix = array();
for ($i=0; $i<12; $i++)
$matrix[] = array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
foreach ($this->rows_ref as &$row_ref)
{
$mx = ($row_ref[1] + 64) >> 5;
$my = ($row_ref[2] + 64) >> 5;
if (($mx >= 12) || ($my >= 12)) continue;
if (($matrix[$mx][$my] === 0) && ($row_ref[7] == 1)) # 7 is count
$matrix[$mx][$my] = $row_ref[0]; # 0 is cache_id
else
$matrix[$mx][$my] = -1;
}
$selected_cache_ids = array();
for ($mx=1; $mx<11; $mx++)
{
for ($my=1; $my<11; $my++)
{
if ($matrix[$mx][$my] > 0) # cache_id
{
# Check all adjacent squares.
if ( ($matrix[$mx-1][$my-1] === 0)
&& ($matrix[$mx-1][$my ] === 0)
&& ($matrix[$mx-1][$my+1] === 0)
&& ($matrix[$mx ][$my-1] === 0)
&& ($matrix[$mx ][$my+1] === 0)
&& ($matrix[$mx+1][$my-1] === 0)
&& ($matrix[$mx+1][$my ] === 0)
&& ($matrix[$mx+1][$my+1] === 0)
)
$selected_cache_ids[] = $matrix[$mx][$my];
}
}
}
foreach ($this->rows_ref as &$row_ref)
if (in_array($row_ref[0], $selected_cache_ids))
$row_ref[6] |= TileTree::$FLAG_DRAW_CAPTION;
}
}

View File

@@ -0,0 +1,310 @@
<?php
namespace okapi\services\caches\map;
use Exception;
use okapi\Okapi;
use okapi\Settings;
use okapi\Cache;
use okapi\Db;
use okapi\OkapiRequest;
use okapi\OkapiHttpResponse;
use okapi\ParamMissing;
use okapi\InvalidParam;
use okapi\BadRequest;
use okapi\DoesNotExist;
use okapi\OkapiInternalRequest;
use okapi\OkapiInternalConsumer;
use okapi\OkapiServiceRunner;
use okapi\OkapiLock;
class TileTree
{
# Static flags (stored in the database).
public static $FLAG_STAR = 0x01;
public static $FLAG_HAS_TRACKABLES = 0x02;
public static $FLAG_NOT_YET_FOUND = 0x04;
# Dynamic flags (added at runtime).
public static $FLAG_FOUND = 0x0100;
public static $FLAG_OWN = 0x0200;
public static $FLAG_NEW = 0x0400;
public static $FLAG_DRAW_CAPTION = 0x0800;
/**
* Return null if not computed, 1 if computed and empty, 2 if computed and not empty.
*/
public static function get_tile_status($zoom, $x, $y)
{
return Db::select_value("
select status
from okapi_tile_status
where
z = '".mysql_real_escape_string($zoom)."'
and x = '".mysql_real_escape_string($x)."'
and y = '".mysql_real_escape_string($y)."'
");
}
/**
* Return MySQL's result set iterator over all caches matched by your query.
*
* Each row is an array of the following format:
* list(cache_id, $pixel_x, $pixel_y, status, type, rating, flags, count).
*
* Note that $pixels can also be negative or >=256 (up to a margin of 32px).
* Count is the number of other caches "eclipsed" by this geocache (such
* eclipsed geocaches are not included in the result).
*/
public static function query_fast($zoom, $x, $y, $filter_conds)
{
$time_started = microtime(true);
# First, we check if the cache-set for this tile was already computed
# (and if it was, was it empty).
$status = self::get_tile_status($zoom, $x, $y);
if ($status === null) # Not yet computed.
{
# Note, that computing the tile does not involve taking any
# filtering parameters.
$status = self::compute_tile($zoom, $x, $y);
OkapiServiceRunner::save_stats_extra("tiletree/query_fast/preparetile-miss",
null, microtime(true) - $time_started);
}
else
{
OkapiServiceRunner::save_stats_extra("tiletree/query_fast/preparetile-hit",
null, microtime(true) - $time_started);
}
if ($status === 1) # Computed and empty.
{
# This tile was already computed and it is empty.
return null;
}
# If we got here, then the tile is computed and not empty (status 2).
# Since all search parameters are aggregated, we just need a simple
# SQL query to get the filtered result.
$tile_upper_x = $x << 8;
$tile_leftmost_y = $y << 8;
if (count($filter_conds) == 0)
$filter_conds[] = "true";
return Db::query("
select
cache_id,
cast(z21x >> (21 - $zoom) as signed) - $tile_upper_x as px,
cast(z21y >> (21 - $zoom) as signed) - $tile_leftmost_y as py,
status, type, rating, flags, count(*)
from okapi_tile_caches
where
z = '".mysql_real_escape_string($zoom)."'
and x = '".mysql_real_escape_string($x)."'
and y = '".mysql_real_escape_string($y)."'
and (".implode(") and (", $filter_conds).")
group by
z21x >> (3 + (21 - $zoom)),
z21y >> (3 + (21 - $zoom))
order by
z21y >> (3 + (21 - $zoom)),
z21x >> (3 + (21 - $zoom))
");
}
/**
* Precache the ($zoom, $x, $y) slot in the okapi_tile_caches table.
*/
public static function compute_tile($zoom, $x, $y)
{
$time_started = microtime(true);
# Note, that multiple threads may try to compute tiles simulatanously.
# For low-level tiles, this can be expensive. WRTODO: Think of some
# appropriate locks.
$status = self::get_tile_status($zoom, $x, $y);
if ($status !== null)
return $status;
if ($zoom === 0)
{
# When computing zoom zero, we don't have a parent to speed up
# the computation. We need to use the caches table. Note, that
# zoom level 0 contains *entire world*, so we don't have to use
# any WHERE condition in the following query.
# This can be done a little faster (without the use of internal requests),
# but there is *no need* to - this query is run seldom and is cached.
$params = array();
$params['status'] = "Available|Temporarily unavailable|Archived"; # we want them all
$params['limit'] = "10000000"; # no limit
$internal_request = new OkapiInternalRequest(new OkapiInternalConsumer(), null, $params);
$internal_request->skip_limits = true;
$response = OkapiServiceRunner::call("services/caches/search/all", $internal_request);
$cache_codes = $response['results'];
$internal_request = new OkapiInternalRequest(new OkapiInternalConsumer(), null, array(
'cache_codes' => implode('|', $cache_codes),
'fields' => 'internal_id|code|name|location|type|status|rating|recommendations|founds|trackables_count'
));
$internal_request->skip_limits = true;
$caches = OkapiServiceRunner::call("services/caches/geocaches", $internal_request);
foreach ($caches as $cache)
{
$row = self::generate_short_row($cache);
Db::execute("
replace into okapi_tile_caches (
z, x, y, cache_id, z21x, z21y, status, type, rating, flags
) values (
0, 0, 0,
'".mysql_real_escape_string($row[0])."',
'".mysql_real_escape_string($row[1])."',
'".mysql_real_escape_string($row[2])."',
'".mysql_real_escape_string($row[3])."',
'".mysql_real_escape_string($row[4])."',
".(($row[5] === null) ? "null" : $row[5]).",
'".mysql_real_escape_string($row[6])."'
);
");
}
$status = 2;
OkapiServiceRunner::save_stats_extra("tiletree/compute_tile/rebuild-0",
null, microtime(true) - $time_started);
}
else
{
# We will use the parent tile to compute the contents of this tile.
$parent_zoom = $zoom - 1;
$parent_x = $x >> 1;
$parent_y = $y >> 1;
$status = self::get_tile_status($parent_zoom, $parent_x, $parent_y);
if ($status === null) # Not computed.
{
$time_started = microtime(true);
$status = self::compute_tile($parent_zoom, $parent_x, $parent_y);
OkapiServiceRunner::save_stats_extra("tiletree/compute_tile/build",
null, microtime(true) - $time_started);
}
else
{
OkapiServiceRunner::save_stats_extra("tiletree/compute_tile/hit",
null, microtime(true) - $time_started);
}
if ($status === 1) # Computed and empty.
{
# No need to check.
}
else # Computed, not empty.
{
$scale = 8 + 21 - $zoom;
$parentcenter_z21x = (($parent_x << 1) | 1) << $scale;
$parentcenter_z21y = (($parent_y << 1) | 1) << $scale;
$margin = 1 << ($scale - 2);
$left_z21x = (($parent_x << 1) << $scale) - $margin;
$right_z21x = ((($parent_x + 1) << 1) << $scale) + $margin;
$top_z21y = (($parent_y << 1) << $scale) - $margin;
$bottom_z21y = ((($parent_y + 1) << 1) << $scale) + $margin;
# Choose the right quarter.
# |1 2|
# |3 4|
if ($x & 1) # 2 or 4
$left_z21x = $parentcenter_z21x - $margin;
else # 1 or 3
$right_z21x = $parentcenter_z21x + $margin;
if ($y & 1) # 3 or 4
$top_z21y = $parentcenter_z21y - $margin;
else # 1 or 2
$bottom_z21y = $parentcenter_z21y + $margin;
# Cache the result.
Db::execute("
replace into okapi_tile_caches (
z, x, y, cache_id, z21x, z21y, status, type, rating, flags
)
select
'".mysql_real_escape_string($zoom)."',
'".mysql_real_escape_string($x)."',
'".mysql_real_escape_string($y)."',
cache_id, z21x, z21y, status, type, rating, flags
from okapi_tile_caches
where
z = '".mysql_real_escape_string($parent_zoom)."'
and x = '".mysql_real_escape_string($parent_x)."'
and y = '".mysql_real_escape_string($parent_y)."'
and z21x between $left_z21x and $right_z21x
and z21y between $top_z21y and $bottom_z21y
");
$test = Db::select_value("
select 1
from okapi_tile_caches
where
z = '".mysql_real_escape_string($zoom)."'
and x = '".mysql_real_escape_string($x)."'
and y = '".mysql_real_escape_string($y)."'
limit 1;
");
if ($test)
$status = 2;
else
$status = 1;
}
}
# Mark tile as computed.
Db::execute("
replace into okapi_tile_status (z, x, y, status)
values (
'".mysql_real_escape_string($zoom)."',
'".mysql_real_escape_string($x)."',
'".mysql_real_escape_string($y)."',
'".mysql_real_escape_string($status)."'
);
");
return $status;
}
/**
* Convert OKAPI's cache object to a short database row to be inserted
* into okapi_tile_caches table. Returns the list of the following attributes:
* cache_id, z21x, z21y, status, type, rating, flags (rating might be null!).
*/
public static function generate_short_row($cache)
{
list($lat, $lon) = explode("|", $cache['location']);
list($z21x, $z21y) = self::latlon_to_z21xy($lat, $lon);
$flags = 0;
if (($cache['founds'] > 6) && (($cache['recommendations'] / $cache['founds']) > 0.3))
$flags |= self::$FLAG_STAR;
if ($cache['trackables_count'] > 0)
$flags |= self::$FLAG_HAS_TRACKABLES;
if ($cache['founds'] == 0)
$flags |= self::$FLAG_NOT_YET_FOUND;
return array($cache['internal_id'], $z21x, $z21y, Okapi::cache_status_name2id($cache['status']),
Okapi::cache_type_name2id($cache['type']), $cache['rating'], $flags);
}
private static function latlon_to_z21xy($lat, $lon)
{
$offset = 128 << 21;
$x = round($offset + ($offset * $lon / 180));
$y = round($offset - $offset/pi() * log((1 + sin($lat * pi() / 180)) / (1 - sin($lat * pi() / 180))) / 2);
return array($x, $y);
}
}

View File

@@ -16,7 +16,7 @@ use okapi\ParamMissing;
use okapi\InvalidParam;
use okapi\services\caches\search\SearchAssistant;
require_once 'searching.inc.php';
require_once('searching.inc.php');
class WebService
{

View File

@@ -62,14 +62,17 @@
Only caches with size attribute between these numbers (inclusive) will be returned
(1 - micro, 5 - very big).</p>
<p><b>Notice:</b> If you use this parameter, all caches which do not have a container
(like Event or Virtual caches) will be excluded from the results.</p>
(like Event or Virtual caches) will be excluded from the results. If you want caches
with no container included, append "|X" suffix to the value of this parameter
(e.g. "3-5|X").</p>
</opt>
<opt name='rating'>
<p>A string "X-Y", where X and Y are integers between 1 and 5, and X &lt;= Y.
Only caches with an overall rating between these numbers (inclusive) will be returned
(1 - poor, 5 - excellent).</p>
<p><b>Notice:</b> If you use this parameter, all caches with too few votes will be
excluded from results.</p>
excluded from results. If you still want unrated caches included, append "|X" suffix
to the value of this parameter (e.g. "3-5|X").</p>
<p><b>Notice:</b> Some OC installations do not provide ratings for their geocaches.
On such installation, this parameter will be ignored.</p>
</opt>
@@ -118,6 +121,12 @@
<p>User UUID. If given, the response will only include geocaches not found by
the given user.</p>
</opt>
<opt name='exclude_ignored' default='false'>
<p><b>Notice:</b> This parameter may be used only for requests signed
with an Access Token (you will need to use <b>Level 3</b> Authentication).</p>
<p>Boolean. If set to <b>true</b>, caches which the user has marked as ignored
will not be included in the result.</p>
</opt>
<opt name='exclude_my_own' default='false'>
<p><b>Notice:</b> This parameter may be used only for requests signed
with an Access Token (you will need to use <b>Level 3</b> Authentication).

View File

@@ -2,7 +2,7 @@
namespace okapi\services\caches\search\bbox;
require_once 'searching.inc.php';
require_once('searching.inc.php');
use okapi\Okapi;
use okapi\OkapiRequest;

View File

@@ -2,9 +2,10 @@
namespace okapi\services\caches\search\by_urls;
require_once 'searching.inc.php';
require_once('searching.inc.php');
use okapi\Okapi;
use okapi\Settings;
use okapi\OkapiRequest;
use okapi\ParamMissing;
use okapi\InvalidParam;
@@ -31,7 +32,7 @@ class WebService
static $length = null;
if ($host == null)
{
$host = parse_url($GLOBALS['absolute_server_URI'], PHP_URL_HOST);
$host = parse_url(Settings::get('SITE_URL'), PHP_URL_HOST);
if (strpos($host, "www.") === 0)
$host = substr($host, 4);
$length = strlen($host);
@@ -46,6 +47,13 @@ class WebService
return null;
if ((!isset($uri['host'])) || (substr($uri['host'], -$length) != $host))
return null;
if (!isset($uri['path']))
return null;
if (preg_match("#^/(O[A-Z][A-Z0-9]{4,5})$#", $uri['path'], $matches))
{
# Some servers allow "http://oc.xx/<cache_code>" shortcut.
return array('cache_code', $matches[1]);
}
$parts = array();
if (isset($uri['query']))
$parts = array_merge($parts, explode('&', $uri['query']));

View File

@@ -2,7 +2,7 @@
namespace okapi\services\caches\search\nearest;
require_once 'searching.inc.php';
require_once('searching.inc.php');
use okapi\Okapi;
use okapi\OkapiRequest;

View File

@@ -152,30 +152,58 @@ class SearchAssistant
{
if ($tmp = $request->get_parameter($param_name))
{
if (!preg_match("/^[1-5]-[1-5]$/", $tmp))
if (!preg_match("/^[1-5]-[1-5](\|X)?$/", $tmp))
throw new InvalidParam($param_name, "'$tmp'");
list($min, $max) = explode("-", $tmp);
if (strpos($max, "|X") !== false)
{
$max = $max[0];
$allow_null = true;
} else {
$allow_null = false;
}
if ($min > $max)
throw new InvalidParam($param_name, "'$tmp'");
switch ($param_name)
{
case 'terrain':
$where_conds[] = "caches.terrain between 2*$min and 2*$max";
if ($allow_null)
throw new InvalidParam($param_name, "The '|X' suffix is not allowed here.");
if (($min == 1) && ($max == 5)) {
/* no extra condition necessary */
} else {
$where_conds[] = "caches.terrain between 2*$min and 2*$max";
}
break;
case 'difficulty':
$where_conds[] = "caches.difficulty between 2*$min and 2*$max";
if ($allow_null)
throw new InvalidParam($param_name, "The '|X' suffix is not allowed here.");
if (($min == 1) && ($max == 5)) {
/* no extra condition necessary */
} else {
$where_conds[] = "caches.difficulty between 2*$min and 2*$max";
}
break;
case 'size':
$where_conds[] = "caches.size between $min+1 and $max+1";
if (($min == 1) && ($max == 5) && $allow_null) {
/* no extra condition necessary */
} else {
$where_conds[] = "(caches.size between $min+1 and $max+1)".
($allow_null ? " or caches.size=7" : "");
}
break;
case 'rating':
if (Settings::get('OC_BRANCH') == 'oc.pl')
{
$divisors = array(-3.0, -1.0, 0.1, 1.4, 2.2, 3.0);
$min = $divisors[$min - 1];
$max = $divisors[$max];
$where_conds[] = "$X_SCORE between $min and $max";
$where_conds[] = "$X_VOTES > 3";
if (($min == 1) && ($max == 5) && $allow_null) {
/* no extra condition necessary */
} else {
$divisors = array(-3.0, -1.0, 0.1, 1.4, 2.2, 3.0);
$min = $divisors[$min - 1];
$max = $divisors[$max];
$where_conds[] = "(($X_SCORE between $min and $max) and ($X_VOTES >= 3))".
($allow_null ? " or ($X_VOTES < 3)" : "");
}
}
else
{
@@ -294,6 +322,26 @@ class SearchAssistant
$where_conds[] = "caches.cache_id not in ('".implode("','", array_map('mysql_real_escape_string', $found_cache_ids))."')";
}
#
# exclude_ignored
#
if ($tmp = $request->get_parameter('exclude_ignored'))
{
if ($request->token == null)
throw new InvalidParam('exclude_ignored', "Might be used only for requests signed with an Access Token.");
if (!in_array($tmp, array('true', 'false')))
throw new InvalidParam('exclude_ignored', "'$tmp'");
if ($tmp == 'true') {
$ignored_cache_ids = Db::select_column("
select cache_id
from cache_ignore
where user_id = '".mysql_real_escape_string($request->token->user_id)."'
");
$where_conds[] = "cache_id not in ('".implode("','", array_map('mysql_real_escape_string', $ignored_cache_ids))."')";
}
}
#
# exclude_my_own
#
@@ -331,7 +379,7 @@ class SearchAssistant
if ($limit == null) $limit = "100";
if (!is_numeric($limit))
throw new InvalidParam('limit', "'$limit'");
if ($limit < 1 || $limit > 500)
if ($limit < 1 || (($limit > 500) && (!$request->skip_limits)))
throw new InvalidParam('limit', "Has to be between 1 and 500.");
#
@@ -342,7 +390,7 @@ class SearchAssistant
if ($offset == null) $offset = "0";
if (!is_numeric($offset))
throw new InvalidParam('offset', "'$offset'");
if ($offset + $limit > 500)
if (($offset + $limit > 500) && (!$request->skip_limits))
throw new BadRequest("The sum of offset and limit may not exceed 500.");
if ($offset < 0 || $offset > 499)
throw new InvalidParam('offset', "Has to be between 0 and 499.");
@@ -381,7 +429,7 @@ class SearchAssistant
$order_clauses[] = "($cl) $dir";
}
}
$ret_array = array(
'where_conds' => $where_conds,
'offset' => (int)$offset,

View File

@@ -68,7 +68,7 @@ class WebService
'user' => array(
'uuid' => $row['user_uuid'],
'username' => $row['username'],
'profile_url' => $GLOBALS['absolute_server_URI']."viewprofile.php?userid=".$row['user_id'],
'profile_url' => Settings::get('SITE_URL')."viewprofile.php?userid=".$row['user_id'],
),
'type' => Okapi::logtypeid2name($row['type']),
'comment' => $row['text']

View File

@@ -95,7 +95,7 @@ class WebService
if (!in_array($needs_maintenance, array('true', 'false')))
throw new InvalidParam('needs_maintenance', "Unknown option: '$needs_maintenance'.");
$needs_maintenance = ($needs_maintenance == 'true');
if (!Settings::get('SUPPORTS_LOGTYPE_NEEDS_MAINTENANCE'))
if ($needs_maintenance && (!Settings::get('SUPPORTS_LOGTYPE_NEEDS_MAINTENANCE')))
{
# If not supported, just ignore it.
self::$success_message .= " ".sprintf(_("However, your \"needs maintenance\" flag was ignored, because %s does not support this feature."),
@@ -115,15 +115,6 @@ class WebService
# Various integrity checks.
if (!in_array($cache['status'], array("Available", "Temporarily unavailable")))
{
# Only admins and cache owners may publish comments for Archived caches.
if ($user['is_admin'] || ($user['uuid'] == $cache['owner']['uuid'])) {
/* pass */
} else {
throw new CannotPublishException(_("This cache is archived. Only admins and the owner are allowed to add a log entry."));
}
}
if ($cache['type'] == 'Event' && $logtype != 'Comment')
throw new CannotPublishException(_('This cache is an Event cache. You cannot "Find it"! (But - you may "Comment" on it.)'));
if ($logtype == 'Comment' && strlen(trim($comment)) == 0)
@@ -381,11 +372,13 @@ class WebService
");
}
# Call OC's event handler.
require_once($GLOBALS['rootpath'].'lib/eventhandler.inc.php');
event_new_log($cache['internal_id'], $user['internal_id']);
# We need to delete the copy of stats-picture for this user. Otherwise,
# the legacy OC code won't detect that the picture needs to be refreshed.
$filepath = Settings::get('VAR_DIR').'images/statpics/statpic'.$user['internal_id'].'.jpg';
if (file_exists($filepath))
unlink($filepath);
# Success. Return the uuid.
return $log_uuid;
@@ -500,9 +493,20 @@ class WebService
}
}
private static function create_uuid()
{
return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
mt_rand(0, 0xffff), mt_rand(0, 0xffff),
mt_rand(0, 0xffff),
mt_rand(0, 0x0fff) | 0x4000,
mt_rand(0, 0x3fff) | 0x8000,
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
);
}
private static function insert_log_row($consumer_key, $cache_internal_id, $user_internal_id, $logtype, $when, $PSEUDOENCODED_comment)
{
$log_uuid = create_uuid();
$log_uuid = self::create_uuid();
Db::execute("
insert into cache_logs (uuid, cache_id, user_id, type, date, text, last_modified, date_created, node)
values (
@@ -514,7 +518,7 @@ class WebService
'".mysql_real_escape_string($PSEUDOENCODED_comment)."',
now(),
now(),
'".mysql_real_escape_string($GLOBALS['oc_nodeid'])."'
'".mysql_real_escape_string(Settings::get('OC_NODE_ID'))."'
);
");
$log_internal_id = Db::last_insert_id();

View File

@@ -3,6 +3,7 @@
namespace okapi\services\oauth\authorize;
use okapi\Okapi;
use okapi\Settings;
use okapi\OkapiRedirectResponse;
use okapi\OkapiRequest;
use okapi\ParamMissing;
@@ -33,7 +34,7 @@ class WebService
# done in the "services" folder. 2) "services" don't display
# any interactive webpages, they just return the result.
return new OkapiRedirectResponse($GLOBALS['absolute_server_URI']."okapi/apps/authorize".
return new OkapiRedirectResponse(Settings::get('SITE_URL')."okapi/apps/authorize".
"?oauth_token=".$token_key.(($langpref != null) ? "&langpref=".$langpref : "").
"&interactivity=".$interactivity);
}

View File

@@ -26,7 +26,7 @@ class WebService
public static function call(OkapiRequest $request)
{
require_once 'replicate_common.inc.php';
require_once('replicate_common.inc.php');
$since = $request->get_parameter('since');
if ($since === null) throw new ParamMissing('since');

View File

@@ -46,7 +46,7 @@ class WebService
public static function call(OkapiRequest $request)
{
require_once 'replicate_common.inc.php';
require_once('replicate_common.inc.php');
$data = Cache::get("last_fulldump");
if ($data == null)
@@ -71,7 +71,7 @@ class WebService
$response = new OkapiHttpResponse();
$response->content_type = $data['meta']['content_type'];
$response->content_disposition = 'Content-Disposition: attachment; filename="'.$data['meta']['public_filename'].'"';
$response->content_disposition = 'attachment; filename="'.$data['meta']['public_filename'].'"';
$response->stream_length = $data['meta']['compressed_size'];
$response->body = fopen($data['meta']['filepath'], "rb");
$response->allow_gzip = false;

View File

@@ -27,7 +27,7 @@ class WebService
public static function call(OkapiRequest $request)
{
require_once 'replicate_common.inc.php';
require_once('replicate_common.inc.php');
$result = array();
$result['changelog'] = array(

View File

@@ -140,7 +140,7 @@ class ReplicateCommon
'uuid', $DELETED_uuids, self::$logged_log_entry_fields, false, true, 3600);
}
# Update state variables and release DB lock.
# Update state variables.
Okapi::set_var("last_clog_update", $now);
$revision = Db::select_value("select max(id) from okapi_clog");
@@ -173,7 +173,7 @@ class ReplicateCommon
# Get the current values for objects. Compare them with their previous versions
# and generate changelog entries.
require_once $GLOBALS['rootpath'].'okapi/service_runner.php';
require_once($GLOBALS['rootpath'].'okapi/service_runner.php');
$current_values = OkapiServiceRunner::call($feeder_method, new OkapiInternalRequest(
new OkapiInternalConsumer(), null, array($feeder_keys_param => implode("|", $key_values),
'fields' => $fields)));
@@ -379,9 +379,7 @@ class ReplicateCommon
if ($entry['change_type'] == 'replace')
$filtered[] = $entry;
unset($entries);
$fp = fopen("$dir/$basename.json", "wb");
fwrite($fp, json_encode($filtered));
fclose($fp);
file_put_contents("$dir/$basename.json", json_encode($filtered));
unset($filtered);
$i++;
}
@@ -416,9 +414,7 @@ class ReplicateCommon
if ($entry['change_type'] == 'replace')
$filtered[] = $entry;
unset($entries);
$fp = fopen("$dir/$basename.json", "wb");
fwrite($fp, json_encode($filtered));
fclose($fp);
file_put_contents("$dir/$basename.json", json_encode($filtered));
unset($filtered);
$i++;
}
@@ -435,9 +431,7 @@ class ReplicateCommon
'generated_at' => $generated_at,
),
);
$fp = fopen("$dir/index.json", "wb");
fwrite($fp, json_encode($metadata));
fclose($fp);
file_put_contents("$dir/index.json", json_encode($metadata));
# Compute uncompressed size.

View File

@@ -4,11 +4,11 @@ namespace okapi\services\users\users;
use Exception;
use okapi\Okapi;
use okapi\Settings;
use okapi\Db;
use okapi\OkapiRequest;
use okapi\ParamMissing;
use okapi\InvalidParam;
use okapi\Settings;
use okapi\services\caches\search\SearchAssistant;
class WebService
@@ -57,7 +57,7 @@ class WebService
{
case 'uuid': $entry['uuid'] = $row['uuid']; break;
case 'username': $entry['username'] = $row['username']; break;
case 'profile_url': $entry['profile_url'] = $GLOBALS['absolute_server_URI']."viewprofile.php?userid=".$row['user_id']; break;
case 'profile_url': $entry['profile_url'] = Settings::get('SITE_URL')."viewprofile.php?userid=".$row['user_id']; break;
case 'is_admin':
if (!$request->token) {
$entry['is_admin'] = null;

View File

@@ -9,14 +9,8 @@ use okapi\Locales;
# http://code.google.com/p/opencaching-api/source/browse/trunk/okapi/settings.php
#
# HOW TO MODIFY OKAPI SETTINGS: If you want a setting X to have a value of Y,
# add following lines to your OC's lib/settings.inc.php file:
#
# $OKAPI_SETTINGS = array(
# 'X' => 'Y',
# // ... etc ...
# );
#
# // E.g. $OKAPI_SETTINGS = array('OC_BRANCH' => 'oc.de', 'SITELANG' => 'de');
# create/edit the "<rootpath>/okapi_settings.php" file. See example here:
# http://code.google.com/p/opencaching-pl/source/browse/trunk/okapi_settings.php
#
# This file provides documentation and DEFAULT values for those settings.
#
@@ -28,6 +22,15 @@ final class Settings
/** Default values for setting keys. */
private static $DEFAULT_SETTINGS = array(
/**
* List of administrator email addresses. OKAPI will send important messages
* to this addresses. You should replace this with your true email address.
*/
'ADMINS' => array(),
/** Set this to true on development machines. */
'DEBUG' => false,
/**
* Currently there are two mainstream branches of OpenCaching code.
* Which branch is you installation using?
@@ -46,6 +49,9 @@ final class Settings
*/
'SITELANG' => "en",
/** Email address to use in the "From:" when sending messages. */
'FROM_FIELD' => 'root@localhost',
/**
* All OKAPI documentation pages should remain English-only, but some
* other pages (and results) might be translated to their localized
@@ -64,13 +70,6 @@ final class Settings
*/
'GETTEXT_DOMAIN' => 'okapi_messages',
/**
* By default, OKAPI sends messages to email address defined in $GLOBALS['sql_errormail'].
* However, there can be only one address defined there. If you want to add more, you may
* use this setting to provide a list of additional email addresses.
*/
'EXTRA_ADMINS' => array(),
/**
* Where should OKAPI store dynamically generated cache files? If you leave it at null,
* OKAPI will try to guess (not recommended). If you move this directory, it's better
@@ -78,6 +77,19 @@ final class Settings
*/
'VAR_DIR' => null,
/**
* Where to store uploaded images? This directory needs to be shared among
* both OKAPI and OC code (see $picdir in your settings.inc.php).
*/
'IMAGES_DIR' => null,
/**
* Name of the cookie within which OC stores serialized session id, etc.
* OKAPI requires to access this in order to make sure which user is logged
* in.
*/
'OC_COOKIE_NAME' => null,
/**
* Set to true, if your installation supports "Needs maintenance" log type (with
* log type id == 5). If your users are not allowed to submit "Needs maintenance"
@@ -98,6 +110,25 @@ final class Settings
* performance and data integrity!
*/
'DEBUG_PREVENT_SEMAPHORES' => false,
/* Database settings */
'DB_SERVER' => 'localhost',
'DB_NAME' => null,
'DB_USERNAME' => null,
'DB_PASSWORD' => null,
/** URL of the OC site (with slash, and without the "/okapi" part). */
'SITE_URL' => null,
/** OKAPI needs this when inserting new data to cache_logs table. */
'OC_NODE_ID' => null,
/**
* Your OC sites data licencing document. All OKAPI Consumers will be
* required to accept this.
*/
'DATA_LICENSE_URL' => null,
);
/**
@@ -111,44 +142,50 @@ final class Settings
*/
private static function load_settings()
{
# Check the settings.inc.php for overrides.
try {
require_once($GLOBALS['rootpath']."okapi_settings.php");
$ref = get_okapi_settings();
} catch (Exception $e) {
throw new Exception("Could not import <rootpath>/okapi_settings.php:\n".$e->getMessage());
}
self::$SETTINGS = self::$DEFAULT_SETTINGS;
$ref = null;
if (isset($GLOBALS['OKAPI_SETTINGS']))
{
$ref = &$GLOBALS['OKAPI_SETTINGS'];
}
else
{
throw new Exception("Could not locate OKAPI settings! Put your settings array in ".
"\$GLOBALS['OKAPI_SETTINGS']]. Settings may be empty, but must exist.");
}
foreach (self::$SETTINGS as $key => $_)
{
if (isset($ref[$key]))
{
self::$SETTINGS[$key] = $ref[$key];
self::verify($key, self::$SETTINGS[$key]);
}
}
self::verify(self::$SETTINGS);
}
/** Throw an exception, if given $value is invalid for the given $key. */
private static function verify($key, $value)
private static function verify($dict)
{
$debug = (isset($GLOBALS['debug_page']) && $GLOBALS['debug_page']);
if (($key == 'OC_BRANCH') && (!in_array($value, array('oc.pl', 'oc.de'))))
if (!in_array($dict['OC_BRANCH'], array('oc.pl', 'oc.de')))
throw new Exception("Currently, OC_BRANCH has to be either 'oc.pl' or 'oc.de'. Hint: Whom did you get your code from?");
$boolean_keys = array('SUPPORTS_LOGTYPE_NEEDS_MAINTENANCE', 'DEBUG_PREVENT_EMAILS', 'DEBUG_PREVENT_SEMAPHORES');
if (in_array($key, $boolean_keys) && (!in_array($value, array(true, false))))
throw new Exception("Invalid value for $key.");
if (($key == 'DEBUG_PREVENT_EMAILS') and ($value == true) and ($debug == false))
throw new Exception("DEBUG_PREVENT_EMAILS might be used only when debugging. Sending emails is vital in production environment.");
if (($key == 'DEBUG_PREVENT_SEMAPHORES') and ($value == true) and ($debug == false))
throw new Exception("USE_SEMAPHORES might be used only when debugging. Semaphores are vital in production environment.");
$boolean_keys = array('SUPPORTS_LOGTYPE_NEEDS_MAINTENANCE', 'DEBUG', 'DEBUG_PREVENT_EMAILS', 'DEBUG_PREVENT_SEMAPHORES');
foreach ($boolean_keys as $key)
if (!in_array($dict[$key], array(true, false)))
throw new Exception("Invalid value for $key.");
if (count($dict['ADMINS']) == 0)
throw new Exception("ADMINS array has to filled (e.g. array('root@localhost')).");
if ($dict['DEBUG'] == false)
foreach ($dict as $k => $v)
if ((strpos($k, 'DEBUG_') === 0) && $v == true)
throw new Exception("When DEBUG is false, $k has to be false too.");
if ($dict['VAR_DIR'] == null)
throw new Exception("VAR_DIR cannot be null. Please provide a valid directory.");
if ($dict['IMAGES_DIR'] == null)
throw new Exception("IMAGES_DIR cannot be null. Please provide a valid directory.");
foreach ($dict as $k => $v)
if ((strpos($k, '_DIR') !== false) && ($k[strlen($k) - 1] == '/'))
throw new Exception("All *_DIR settings may not end with a slash. Check $k.");
$notnull = array('OC_COOKIE_NAME', 'DB_SERVER', 'DB_NAME', 'DB_USERNAME', 'SITE_URL', 'OC_NODE_ID');
foreach ($notnull as $k)
if ($dict[$k] === null)
throw new Exception("$k cannot be null.");
if ($dict['SITE_URL'][strlen($dict['SITE_URL']) - 1] != '/')
throw new Exception("SITE_URL must end with a slash.");
}
/**
@@ -178,7 +215,7 @@ final class Settings
*/
public static function default_gettext_init($langprefs)
{
require_once 'locale/locales.php';
require_once($GLOBALS['rootpath']."okapi/locale/locales.php");
$locale = Locales::get_best_locale($langprefs);
putenv("LC_ALL=$locale");
setlocale(LC_ALL, $locale);

Binary file not shown.

After

Width:  |  Height:  |  Size: 558 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 506 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 428 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 469 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 618 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 496 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 531 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 622 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 516 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 803 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 744 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 369 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 365 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 387 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 375 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 616 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 580 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 865 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 535 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 572 B

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 B

View File

@@ -25,5 +25,9 @@ class OkapiUrls
'^devel/attrlist$' => 'devel/attrlist',
'^devel/dbstruct$' => 'devel/dbstruct',
'^devel/cronreport$' => 'devel/cronreport',
'^devel/tilereport$' => 'devel/tilereport',
# For debugging TileMap performance only.
// '^tilestress$' => 'tilestress',
);
}

View File

@@ -10,6 +10,7 @@ use okapi\OkapiHttpRequest;
use okapi\OkapiRedirectResponse;
use okapi\Settings;
use okapi\Locales;
use okapi\OCSession;
class View
{
@@ -53,7 +54,7 @@ class View
# by the user, who knows nothing on tokens and OAuth. Let's be nice then!
$vars = array(
'okapi_base_url' => $GLOBALS['absolute_server_URI']."okapi/",
'okapi_base_url' => Settings::get('SITE_URL')."okapi/",
'token' => $token,
'token_expired' => true,
'site_name' => Okapi::get_normalized_site_name(),
@@ -69,9 +70,14 @@ class View
return $response;
}
# Determine which user is logged in to OC.
require_once($GLOBALS['rootpath']."okapi/lib/oc_session.php");
$OC_user_id = OCSession::get_user_id();
# Ensure a user is logged in (or force re-login).
if ($force_relogin || ($GLOBALS['usr'] == false))
if ($force_relogin || ($OC_user_id == null))
{
if ($force_relogin)
{
@@ -88,7 +94,7 @@ class View
# We should be logged out now. Let's login again.
$after_login = "okapi/apps/authorize?oauth_token=$token_key".(($langpref != Settings::get('SITELANG'))?"&langpref=".$langpref:"");
$login_url = $GLOBALS['absolute_server_URI']."login.php?target=".urlencode($after_login)
$login_url = Settings::get('SITE_URL')."login.php?target=".urlencode($after_login)
."&langpref=".$langpref;
return new OkapiRedirectResponse($login_url);
}
@@ -101,7 +107,7 @@ class View
select 1
from okapi_authorizations
where
user_id = '".mysql_real_escape_string($GLOBALS['usr']['userid'])."'
user_id = '".mysql_real_escape_string($OC_user_id)."'
and consumer_key = '".mysql_real_escape_string($token['consumer_key'])."'
", 0);
@@ -118,7 +124,7 @@ class View
insert into okapi_authorizations (consumer_key, user_id)
values (
'".mysql_real_escape_string($token['consumer_key'])."',
'".mysql_real_escape_string($GLOBALS['usr']['userid'])."'
'".mysql_real_escape_string($OC_user_id)."'
);
");
$authorized = true;
@@ -133,7 +139,7 @@ class View
} else {
# Consumer did not provide a callback URL (oauth_callback=oob).
# We'll have to redirect to the OpenCaching main page then...
return new OkapiRedirectResponse($GLOBALS['absolute_server_URI']."index.php");
return new OkapiRedirectResponse(Settings::get('SITE_URL')."index.php");
}
}
}
@@ -141,7 +147,7 @@ class View
{
# Not yet authorized. Display an authorization request.
$vars = array(
'okapi_base_url' => $GLOBALS['absolute_server_URI']."okapi/",
'okapi_base_url' => Settings::get('SITE_URL')."okapi/",
'token' => $token,
'site_name' => Okapi::get_normalized_site_name(),
'locales' => $locales,
@@ -161,7 +167,7 @@ class View
Db::execute("
update okapi_tokens
set user_id = '".mysql_real_escape_string($GLOBALS['usr']['userid'])."'
set user_id = '".mysql_real_escape_string($OC_user_id)."'
where `key` = '".mysql_real_escape_string($token_key)."';
");
@@ -172,7 +178,7 @@ class View
} else {
# Consumer did not provide a callback URL (probably the user is using a desktop
# or mobile application). We'll just have to display the verifier to the user.
return new OkapiRedirectResponse($GLOBALS['absolute_server_URI']."okapi/apps/authorized?oauth_token=".$token_key
return new OkapiRedirectResponse(Settings::get('SITE_URL')."okapi/apps/authorized?oauth_token=".$token_key
."&oauth_verifier=".$token['verifier']."&langpref=".$langpref);
}
}

View File

@@ -37,11 +37,11 @@ class View
{
# Probably Request Token has expired or it was already used. We'll
# just redirect to the OpenCaching main page.
return new OkapiRedirectResponse($GLOBALS['absolute_server_URI']);
return new OkapiRedirectResponse(Settings::get('SITE_URL'));
}
$vars = array(
'okapi_base_url' => $GLOBALS['absolute_server_URI']."okapi/",
'okapi_base_url' => Settings::get('SITE_URL')."okapi/",
'token' => $token,
'verifier' => $verifier,
'site_name' => Okapi::get_normalized_site_name()

View File

@@ -9,6 +9,7 @@ use okapi\OkapiHttpResponse;
use okapi\OkapiHttpRequest;
use okapi\OkapiRedirectResponse;
use okapi\Settings;
use okapi\OCSession;
class View
{
@@ -17,12 +18,15 @@ class View
$langpref = isset($_GET['langpref']) ? $_GET['langpref'] : Settings::get('SITELANG');
$langprefs = explode("|", $langpref);
# Ensure a user is logged in.
if ($GLOBALS['usr'] == false)
# Determine which user is logged in to OC.
require_once($GLOBALS['rootpath']."okapi/lib/oc_session.php");
$OC_user_id = OCSession::get_user_id();
if ($OC_user_id == null)
{
$after_login = "okapi/apps/".(($langpref != Settings::get('SITELANG'))?"?langpref=".$langpref:"");
$login_url = $GLOBALS['absolute_server_URI']."login.php?target=".urlencode($after_login);
$login_url = Settings::get('SITE_URL')."login.php?target=".urlencode($after_login);
return new OkapiRedirectResponse($login_url);
}
@@ -34,12 +38,12 @@ class View
okapi_consumers c,
okapi_authorizations a
where
a.user_id = '".mysql_real_escape_string($GLOBALS['usr']['userid'])."'
a.user_id = '".mysql_real_escape_string($OC_user_id)."'
and c.`key` = a.consumer_key
order by c.name
");
$vars = array();
$vars['okapi_base_url'] = $GLOBALS['absolute_server_URI']."okapi/";
$vars['okapi_base_url'] = Settings::get('SITE_URL')."okapi/";
$vars['site_name'] = Okapi::get_normalized_site_name();
$vars['apps'] = array();
while ($row = mysql_fetch_assoc($rs))

View File

@@ -4,21 +4,28 @@ namespace okapi\views\apps\revoke_access;
use Exception;
use okapi\Okapi;
use okapi\Settings;
use okapi\Db;
use okapi\OkapiHttpResponse;
use okapi\OkapiHttpRequest;
use okapi\OkapiRedirectResponse;
use okapi\OCSession;
class View
{
public static function call()
{
# Determine which user is logged in to OC.
require_once($GLOBALS['rootpath']."okapi/lib/oc_session.php");
$OC_user_id = OCSession::get_user_id();
# Ensure a user is logged in.
if ($GLOBALS['usr'] == false)
if ($OC_user_id == null)
{
$after_login = "okapi/apps/"; # it is correct, if you're wondering
$login_url = $GLOBALS['absolute_server_URI']."login.php?target=".urlencode($after_login);
$login_url = Settings::get('SITE_URL')."login.php?target=".urlencode($after_login);
return new OkapiRedirectResponse($login_url);
}
@@ -29,18 +36,18 @@ class View
Db::execute("
delete from okapi_tokens
where
user_id = '".mysql_real_escape_string($GLOBALS['usr']['userid'])."'
user_id = '".mysql_real_escape_string($OC_user_id)."'
and consumer_key = '".mysql_real_escape_string($consumer_key)."'
");
Db::execute("
delete from okapi_authorizations
where
user_id = '".mysql_real_escape_string($GLOBALS['usr']['userid'])."'
user_id = '".mysql_real_escape_string($OC_user_id)."'
and consumer_key = '".mysql_real_escape_string($consumer_key)."'
");
# Redirect back to the apps page.
return new OkapiRedirectResponse($GLOBALS['absolute_server_URI']."okapi/apps/");
return new OkapiRedirectResponse(Settings::get('SITE_URL')."okapi/apps/");
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace okapi\views\attrlist;
use Exception;
use okapi\Okapi;
use okapi\Cache;
use okapi\Db;
use okapi\OkapiRequest;
use okapi\OkapiRedirectResponse;
use okapi\OkapiHttpResponse;
use okapi\ParamMissing;
use okapi\InvalidParam;
use okapi\OkapiServiceRunner;
use okapi\OkapiInternalRequest;
class View
{
public static function call()
{
# This is a hidden page for OKAPI developers. It will list all
# attributes defined in this OC installation.
$rs = Db::query("select id, language, text_long from cache_attrib order by id");
$dict = array();
while ($row = mysql_fetch_assoc($rs))
$dict[$row['id']][strtolower($row['language'])] = $row['text_long'];
$chunks = array();
foreach ($dict as $internal_id => $langs)
{
$chunks[] = "<attr code='...' internal_id='$internal_id'";
$langkeys = array_keys($langs);
sort($langkeys);
foreach ($langkeys as $langkey)
$chunks[] = " $langkey='".$langs[$langkey]."'";
$chunks[] = " />\n";
}
$response = new OkapiHttpResponse();
$response->content_type = "text/plain; charset=utf-8";
$response->body = implode("", $chunks);
return $response;
}
}

View File

@@ -31,7 +31,7 @@ class View
# every 5 minutes (run 'crontab -e' to change or disable it) AND additionally
# every time you visit http://yoursite/okapi/cron5
# require_once 'cronjobs.php'; CronJobController::force_run("JOB_NAME"); die();
# require_once($GLOBALS['rootpath']."okapi/cronjobs.php"); CronJobController::force_run("JOB_NAME"); die();
Okapi::execute_cron5_cronjobs();
}

View File

@@ -26,7 +26,7 @@ class View
$response->content_type = "text/plain; charset=utf-8";
ob_start();
require_once 'cronjobs.php';
require_once($GLOBALS['rootpath']."okapi/cronjobs.php");
$schedule = Cache::get("cron_schedule");
if ($schedule == null)
$schedule = array();

View File

@@ -4,6 +4,7 @@ namespace okapi\views\devel\dbstruct;
use Exception;
use okapi\Okapi;
use okapi\Settings;
use okapi\Cache;
use okapi\Db;
use okapi\OkapiRequest;
@@ -22,9 +23,9 @@ class View
# structure of the database. This is useful for making OKAPI compatible
# across different OC installations.
$user = $GLOBALS['dbusername'];
$password = $GLOBALS['dbpasswd'];
$dbname = $GLOBALS['dbname'];
$user = Settings::get('DB_USERNAME');
$password = Settings::get('DB_PASSWORD');
$dbname = Settings::get('DB_NAME');
$struct = shell_exec("mysqldump --no-data -u$user -p$password $dbname");
# Remove the "AUTO_INCREMENT=..." values. They break the diffs.
@@ -51,13 +52,13 @@ class View
"-- Note: The following script has some limitations. It will render database\n".
"-- structure compatible, but not necessarilly EXACTLY the same. It might be\n".
"-- better to use manual diff instead.\n\n";
require_once 'comparator.inc.php';
require_once("comparator.inc.php");
$updater = new \dbStructUpdater();
if (isset($_GET['reverse']) && ($_GET['reverse'] == 'true'))
{
$response->body .=
"-- REVERSE MODE. The following will alter [2], so that it has the structure of [1].\n".
"-- 1. ".$GLOBALS['absolute_server_URI']."okapi/devel/dbstruct (".md5($struct).")\n".
"-- 1. ".Settings::get('SITE_URL')."okapi/devel/dbstruct (".md5($struct).")\n".
"-- 2. ".$_GET['compare_to']." (".md5($alternate_struct).")\n\n";
$alters = $updater->getUpdates($alternate_struct, $struct);
}
@@ -65,7 +66,7 @@ class View
{
$response->body .=
"-- The following will alter [1], so that it has the structure of [2].\n".
"-- 1. ".$GLOBALS['absolute_server_URI']."okapi/devel/dbstruct (".md5($struct).")\n".
"-- 1. ".Settings::get('SITE_URL')."okapi/devel/dbstruct (".md5($struct).")\n".
"-- 2. ".$_GET['compare_to']." (".md5($alternate_struct).")\n\n";
$alters = $updater->getUpdates($struct, $alternate_struct);
}

View File

@@ -0,0 +1,162 @@
<?php
namespace okapi\views\devel\tilereport;
use Exception;
use okapi\Okapi;
use okapi\Cache;
use okapi\Db;
use okapi\OkapiRequest;
use okapi\OkapiRedirectResponse;
use okapi\OkapiHttpResponse;
use okapi\ParamMissing;
use okapi\InvalidParam;
use okapi\OkapiServiceRunner;
use okapi\OkapiInternalRequest;
use okapi\cronjobs\CronJobController;
class View
{
public static function call()
{
# Flush the stats, so the page is fresh upon every request.
require_once($GLOBALS['rootpath']."okapi/cronjobs.php");
CronJobController::force_run("StatsWriterCronJob");
# When services/caches/map/tile method is called, it writes some extra
# stats in the okapi_stats_hourly table. This page retrieves and
# formats these stats in a readable manner (for debugging).
$response = new OkapiHttpResponse();
$response->content_type = "text/plain; charset=utf-8";
ob_start();
$start = isset($_GET['start']) ? $_GET['start'] : date(
"Y-m-d 00:00:00", time() - 7*86400);
$end = isset($_GET['end']) ? $_GET['end'] : date("Y-m-d 23:59:59");
print "From: $start\n";
print " To: $end\n\n";
$rs = Db::query("
select
service_name,
sum(total_calls),
sum(total_runtime)
from okapi_stats_hourly
where
period_start >= '".mysql_real_escape_string($start)."'
and period_start < '".mysql_real_escape_string($end)."'
and service_name like '%caches/map/tile%'
group by service_name
");
$total_calls = 0;
$total_runtime = 0.0;
$calls = array('A' => 0, 'B' => 0, 'C' => 0, 'D' => 0);
$runtime = array('A' => 0.0, 'B' => 0.0, 'C' => 0.0, 'D' => 0.0);
while (list($name, $c, $r) = mysql_fetch_array($rs))
{
if ($name == 'services/caches/map/tile')
{
$total_calls = $c;
$total_runtime = $r;
}
elseif (strpos($name, 'extra/caches/map/tile/checkpoint') === 0)
{
$calls[$name[32]] = $c;
$runtime[$name[32]] = $r;
}
}
if ($total_calls != $calls['A'])
{
print "Partial results. Only ".$calls['A']." out of $total_calls are covered.\n";
print "All other will count as \"unaccounted for\".\n\n";
$total_calls = $calls['A'];
}
$calls_left = $total_calls;
$runtime_left = $total_runtime;
$perc = function($a, $b) { return ($b > 0) ? sprintf("%.1f", 100 * $a / $b)."%" : "(?)"; };
$avg = function($a, $b) { return ($b > 0) ? sprintf("%.4f", $a / $b)."s" : "(?)"; };
$get_stats = function() use (&$calls_left, &$runtime_left, &$total_calls, &$total_runtime, &$perc)
{
return (
str_pad($perc($calls_left, $total_calls), 6, " ", STR_PAD_LEFT).
str_pad($perc($runtime_left, $total_runtime), 7, " ", STR_PAD_LEFT)
);
};
print "%CALLS %TIME Description\n";
print "====== ====== ======================================================================\n";
print $get_stats()." $total_calls responses served. Total runtime: ".sprintf("%.2f", $total_runtime)."s\n";
print "\n";
print " All of these requests needed a TileTree build/lookup. The average runtime of\n";
print " these lookups was ".$avg($runtime['A'], $total_calls).". ".$perc($runtime['A'], $total_runtime)." of total runtime was spent here.\n";
print "\n";
$runtime_left -= $runtime['A'];
print $get_stats()." All calls passed here after ~".$avg($runtime['A'], $total_calls)."\n";
print "\n";
print " Lookup result was then processed and \"image description\" was created. It was\n";
print " passed on to the TileRenderer to compute the ETag hash string. The average runtime\n";
print " of this part was ".$avg($runtime['B'], $total_calls).". ".$perc($runtime['B'], $total_runtime)." of total runtime was spent here.\n";
print "\n";
$runtime_left -= $runtime['B'];
print $get_stats()." All calls passed here after ~".$avg($runtime['A'] + $runtime['B'], $total_calls)."\n";
$etag_hits = $calls['B'] - $calls['C'];
print "\n";
print " $etag_hits of the requests matched the ETag and were served a HTTP 304 response.\n";
print "\n";
$calls_left = $calls['C'];
print $get_stats()." $calls_left calls passed here after ~".$avg($runtime['A'] + $runtime['B'], $total_calls)."\n";
$imagecache_hits = $calls['C'] - $calls['D'];
print "\n";
print " $imagecache_hits of these calls hit the server image cache.\n";
print " ".$perc($runtime['C'], $total_runtime)." of total runtime was spent to find these.\n";
print "\n";
$calls_left = $calls['D'];
$runtime_left -= $runtime['C'];
print $get_stats()." $calls_left calls passed here after ~".$avg($runtime['A'] + $runtime['B'] + $runtime['C'], $total_calls)."\n";
print "\n";
print " These calls required the tile to be rendered. On average, it took\n";
print " ".$avg($runtime['D'], $calls['D'])." to *render* a tile.\n";
print " ".$perc($runtime['D'], $total_runtime)." of total runtime was spent here.\n";
print "\n";
$runtime_left -= $runtime['D'];
print $perc($runtime_left, $total_runtime)." of runtime was unaccounted for (other processing).\n";
print "Average response time was ".$avg($total_runtime, $total_calls).".\n\n";
print "Current okapi_cache score distribution:\n";
$rs = Db::query("
select floor(log2(score)), count(*), sum(length(value))
from okapi_cache
where score is not null
group by floor(log2(score))
");
while (list($log2, $count, $size) = mysql_fetch_array($rs))
{
print $count." elements ($size bytes) with score between ".pow(2, $log2)." and ".pow(2, $log2 + 1).".\n";
}
$response->body = ob_get_clean();
return $response;
}
}

View File

@@ -4,6 +4,7 @@ namespace okapi\views\examples;
use Exception;
use okapi\Okapi;
use okapi\Settings;
use okapi\OkapiRequest;
use okapi\OkapiHttpResponse;
use okapi\ParamMissing;
@@ -16,13 +17,13 @@ class View
{
public static function call()
{
require_once $GLOBALS['rootpath'].'okapi/service_runner.php';
require_once $GLOBALS['rootpath'].'okapi/views/menu.inc.php';
require_once($GLOBALS['rootpath'].'okapi/service_runner.php');
require_once($GLOBALS['rootpath'].'okapi/views/menu.inc.php');
$vars = array(
'menu' => OkapiMenu::get_menu_html("examples.html"),
'okapi_base_url' => $GLOBALS['absolute_server_URI']."okapi/",
'site_url' => $GLOBALS['absolute_server_URI'],
'okapi_base_url' => Settings::get('SITE_URL')."okapi/",
'site_url' => Settings::get('SITE_URL'),
'installations' => OkapiMenu::get_installations(),
'okapi_rev' => Okapi::$revision,
'site_name' => Okapi::get_normalized_site_name(),

View File

@@ -4,6 +4,7 @@ namespace okapi\views\http404;
use Exception;
use okapi\Okapi;
use okapi\Settings;
use okapi\OkapiHttpResponse;
use okapi\views\menu\OkapiMenu;
@@ -11,10 +12,10 @@ class View
{
public static function call()
{
require_once 'menu.inc.php';
require_once('menu.inc.php');
$vars = array(
'okapi_base_url' => $GLOBALS['absolute_server_URI']."okapi/",
'okapi_base_url' => Settings::get('SITE_URL')."okapi/",
'menu' => OkapiMenu::get_menu_html(),
'installations' => OkapiMenu::get_installations(),
'okapi_rev' => Okapi::$revision,

View File

@@ -4,6 +4,7 @@ namespace okapi\views\index;
use Exception;
use okapi\Okapi;
use okapi\Settings;
use okapi\OkapiRequest;
use okapi\OkapiRedirectResponse;
use okapi\ParamMissing;
@@ -18,7 +19,7 @@ class View
# This is called when someone displays "http://../okapi/" (with no
# html path at the end). We will redirect to the introduction page.
return new OkapiRedirectResponse($GLOBALS['absolute_server_URI'].
return new OkapiRedirectResponse(Settings::get('SITE_URL').
"okapi/introduction.html");
}
}

Some files were not shown because too many files have changed in this diff Show More