commit
f118da11e8
39
.gitignore
vendored
39
.gitignore
vendored
@ -1,20 +1,21 @@
|
||||
# common exclude files list
|
||||
#
|
||||
# for local exclude files, see .git/info/exclude
|
||||
|
||||
htdocs/*/settings.inc.php
|
||||
htdocs/*/*/settings.inc.php
|
||||
htdocs/cache/*
|
||||
htdocs/cache2/*
|
||||
htdocs/download/*
|
||||
htdocs/images/statpics/*
|
||||
htdocs/images/uploads/*
|
||||
htdocs/resource2/tinymce/tiny_mce_*.gz
|
||||
htdocs/sitemap*xml*
|
||||
htdocs/util-local/ocxml11client/data-files
|
||||
htdocs/util-local/ocxml11client/tmp
|
||||
|
||||
lib/htmlpurifier-4.2.0/library/HTMLPurifier/DefinitionCache/Serializer/CSS/*
|
||||
lib/htmlpurifier-4.2.0/library/HTMLPurifier/DefinitionCache/Serializer/HTML/*
|
||||
lib/htmlpurifier-4.2.0/library/HTMLPurifier/DefinitionCache/Serializer/URI/*
|
||||
# Commonly ignored files list. For locally ignored files (e.g. local IDE
|
||||
# settings), use .git/info/exclude file. Keep the lines in this file sorted.
|
||||
|
||||
/lib/htmlpurifier-4.2.0/library/HTMLPurifier/DefinitionCache/Serializer/CSS/*
|
||||
/lib/htmlpurifier-4.2.0/library/HTMLPurifier/DefinitionCache/Serializer/HTML/*
|
||||
/lib/htmlpurifier-4.2.0/library/HTMLPurifier/DefinitionCache/Serializer/URI/*
|
||||
/htdocs/cache/*
|
||||
/htdocs/cache2/*
|
||||
/htdocs/config2/settings.inc.php
|
||||
/htdocs/download/*
|
||||
/htdocs/images/statpics/*
|
||||
/htdocs/images/uploads/*
|
||||
/htdocs/lib/settings.inc.php
|
||||
/htdocs/resource2/tinymce/tiny_mce_*.gz
|
||||
/htdocs/util-local/ocxml11client/data-files
|
||||
/htdocs/util-local/ocxml11client/tmp
|
||||
/htdocs/util/mysql_root/settings.inc.php
|
||||
/htdocs/util/notification/settings.inc.php
|
||||
/htdocs/util/publish_caches/settings.inc.php
|
||||
/htdocs/util/watchlist/settings.inc.php
|
||||
/htdocs/var/*
|
||||
|
@ -231,5 +231,11 @@
|
||||
// (e.g. xml-interface and mapserver-results)
|
||||
// you can use -1 to use the master (not recommended, because replicated to slaves)
|
||||
$opt['db']['slave']['primary'] = -1;
|
||||
|
||||
|
||||
$OKAPI_SETTINGS = array(
|
||||
'OC_BRANCH' => 'oc.de',
|
||||
'SITELANG' => 'de',
|
||||
'VAR_DIR' => $opt['rootpath'].'var',
|
||||
);
|
||||
|
||||
?>
|
||||
|
@ -297,4 +297,11 @@ function post_config()
|
||||
'filename' => 'http://geokrety.org/index.php?lang=de_DE.UTF-8'
|
||||
);
|
||||
}
|
||||
|
||||
$OKAPI_SETTINGS = array(
|
||||
'OC_BRANCH' => 'oc.de',
|
||||
'SITELANG' => 'de',
|
||||
'VAR_DIR' => $opt['rootpath'].'var',
|
||||
);
|
||||
|
||||
?>
|
||||
|
5
htdocs/okapi/.htaccess
Normal file
5
htdocs/okapi/.htaccess
Normal file
@ -0,0 +1,5 @@
|
||||
Options -Indexes
|
||||
|
||||
RewriteEngine on
|
||||
RewriteCond %{REQUEST_URI} !/okapi/static/
|
||||
RewriteRule ^(.*)$ controller.php [L,QSA,E=no-gzip:1,E=dont-vary:1]
|
119
htdocs/okapi/controller.php
Normal file
119
htdocs/okapi/controller.php
Normal file
@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace okapi;
|
||||
|
||||
use Exception;
|
||||
use okapi\Okapi;
|
||||
use okapi\views\menu\OkapiMenu;
|
||||
|
||||
#
|
||||
# All HTTP requests within the /okapi/ path are redirected through this
|
||||
# controller. From here we'll pass them to the right entry point (or
|
||||
# display an appropriate error message).
|
||||
#
|
||||
# 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
|
||||
|
||||
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
|
||||
# in PHP config. This is very common and probably is good for most other OC
|
||||
# pages. But we don't need it in OKAPI. We will just turn this off.
|
||||
|
||||
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
|
||||
{
|
||||
public static function dispatch_request($uri)
|
||||
{
|
||||
# Chop off the ?args=... part.
|
||||
|
||||
if (strpos($uri, '?') !== false)
|
||||
$uri = substr($uri, 0, strpos($uri, '?'));
|
||||
|
||||
# Chop off everything before "/okapi/". This should work for okay for most "weird"
|
||||
# server configurations. It will also address a more subtle issue described here:
|
||||
# http://stackoverflow.com/questions/8040461/request-uri-unexpectedly-contains-fqdn
|
||||
|
||||
if (strpos($uri, "/okapi/") !== false)
|
||||
$uri = substr($uri, strpos($uri, "/okapi/"));
|
||||
|
||||
# Make sure we're in the right directory (.htaccess should make sure of that).
|
||||
|
||||
if (strpos($uri, "/okapi/") !== 0)
|
||||
throw new Exception("'$uri' is outside of the /okapi/ path.");
|
||||
$uri = substr($uri, 7);
|
||||
|
||||
# Initializing internals and running pre-request cronjobs (we don't want
|
||||
# cronjobs to be run before "okapi/update", for example before database
|
||||
# was installed).
|
||||
|
||||
$allow_cronjobs = ($uri != "update");
|
||||
Okapi::init_internals($allow_cronjobs);
|
||||
|
||||
# Checking for allowed patterns...
|
||||
|
||||
try
|
||||
{
|
||||
foreach (OkapiUrls::$mapping as $pattern => $namespace)
|
||||
{
|
||||
$matches = null;
|
||||
if (preg_match("#$pattern#", $uri, $matches))
|
||||
{
|
||||
# Pattern matched! Moving on to the proper View...
|
||||
|
||||
array_shift($matches);
|
||||
require_once "views/$namespace.php";
|
||||
$response = call_user_func_array(array('\\okapi\\views\\'.
|
||||
str_replace('/', '\\', $namespace).'\\View', 'call'), $matches);
|
||||
if ($response)
|
||||
$response->display();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Http404 $e)
|
||||
{
|
||||
/* pass */
|
||||
}
|
||||
|
||||
# None of the patterns matched OR method threw the Http404 exception.
|
||||
|
||||
require_once "views/http404.php";
|
||||
$response = \okapi\views\http404\View::call();
|
||||
$response->display();
|
||||
}
|
||||
}
|
||||
|
||||
Okapi::gettext_domain_init();
|
||||
OkapiScriptEntryPointController::dispatch_request($_SERVER['REQUEST_URI']);
|
||||
Okapi::gettext_domain_restore();
|
1799
htdocs/okapi/core.php
Normal file
1799
htdocs/okapi/core.php
Normal file
File diff suppressed because it is too large
Load Diff
613
htdocs/okapi/cronjobs.php
Normal file
613
htdocs/okapi/cronjobs.php
Normal file
@ -0,0 +1,613 @@
|
||||
<?
|
||||
|
||||
namespace okapi\cronjobs;
|
||||
|
||||
# If you want to debug ONE specific cronjob - see views/cron5.php file!
|
||||
|
||||
# If you want to debug the entire "system", then you should know that OKAPI
|
||||
# uses two cache layers in order to decide if the cronjob has to be run,
|
||||
# or even if the cronjobs.php file should be included. If you want to force OKAPI
|
||||
# to run ALL cronjobs, then you should run these two queries on your database:
|
||||
#
|
||||
# - delete from okapi_cache where `key`='cron_schedule';
|
||||
# - delete from okapi_vars where var='cron_nearest_event';
|
||||
#
|
||||
# Then, visit http://yoursite/okapi/cron5.
|
||||
|
||||
use Exception;
|
||||
use okapi\Okapi;
|
||||
use okapi\OkapiLock;
|
||||
use okapi\OkapiExceptionHandler;
|
||||
use okapi\Db;
|
||||
use okapi\Cache;
|
||||
use okapi\Locales;
|
||||
use okapi\OkapiServiceRunner;
|
||||
use okapi\OkapiInternalRequest;
|
||||
use okapi\OkapiInternalConsumer;
|
||||
use okapi\services\replicate\ReplicateCommon;
|
||||
|
||||
class CronJobController
|
||||
{
|
||||
/** Return the list of all currently enabled cronjobs. */
|
||||
public static function get_enabled_cronjobs()
|
||||
{
|
||||
static $cache = null;
|
||||
if ($cache == null)
|
||||
{
|
||||
$cache = array(
|
||||
new OAuthCleanupCronJob(),
|
||||
new CacheCleanupCronJob(),
|
||||
new StatsWriterCronJob(),
|
||||
new CheckCronTab1(),
|
||||
new CheckCronTab2(),
|
||||
new ChangeLogWriterJob(),
|
||||
new ChangeLogCleanerJob(),
|
||||
new AdminStatsSender(),
|
||||
new LocaleChecker(),
|
||||
new FulldumpGeneratorJob(),
|
||||
);
|
||||
foreach ($cache as $cronjob)
|
||||
if (!in_array($cronjob->get_type(), array('pre-request', 'cron-5')))
|
||||
throw new Exception("Cronjob '".$cronjob->get_name()."' has an invalid (unsupported) type.");
|
||||
}
|
||||
return $cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute all scheduled cronjobs of given type, reschedule, and return
|
||||
* UNIX timestamp of the nearest scheduled event.
|
||||
*/
|
||||
public static function run_jobs($type)
|
||||
{
|
||||
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);
|
||||
$lock->acquire();
|
||||
|
||||
$schedule = Cache::get("cron_schedule");
|
||||
if ($schedule == null)
|
||||
$schedule = array();
|
||||
foreach (self::get_enabled_cronjobs() as $cronjob)
|
||||
{
|
||||
$name = $cronjob->get_name();
|
||||
if ((!isset($schedule[$name])) || ($schedule[$name] <= time()))
|
||||
{
|
||||
if ($cronjob->get_type() != $type)
|
||||
{
|
||||
$next_run = isset($schedule[$name]) ? $schedule[$name] : (time() - 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
$cronjob->execute();
|
||||
}
|
||||
catch (Exception $e)
|
||||
{
|
||||
Okapi::mail_admins("Cronjob error: ".$cronjob->get_name(),
|
||||
OkapiExceptionHandler::get_exception_info($e));
|
||||
}
|
||||
$next_run = $cronjob->get_next_scheduled_run(isset($schedule[$name]) ? $schedule[$name] : time());
|
||||
}
|
||||
$schedule[$name] = $next_run;
|
||||
}
|
||||
}
|
||||
$nearest = time() + 3600;
|
||||
foreach ($schedule as $name => $time)
|
||||
if ($time < $nearest)
|
||||
$nearest = $time;
|
||||
Cache::set("cron_schedule", $schedule, 30*86400);
|
||||
$lock->release();
|
||||
return $nearest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Force a specified cronjob to run. Throw an exception if cronjob not found.
|
||||
* $job_name mast equal one of the names returned by ->get_name() method.
|
||||
*/
|
||||
public static function force_run($job_name)
|
||||
{
|
||||
foreach (self::get_enabled_cronjobs() as $cronjob)
|
||||
{
|
||||
if (($cronjob->get_name() == $job_name) || ($cronjob->get_name() == "okapi\\cronjobs\\".$job_name))
|
||||
{
|
||||
$cronjob->execute();
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new Exception("CronJob $job_name not found.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the schedule of a specified cronjob. This will force the job to
|
||||
* run on nearest occasion (but not NOW).
|
||||
*/
|
||||
public static function reset_job_schedule($job_name)
|
||||
{
|
||||
$thejob = null;
|
||||
foreach (self::get_enabled_cronjobs() as $tmp)
|
||||
if (($tmp->get_name() == $job_name) || ($tmp->get_name() == "okapi\\cronjobs\\".$job_name))
|
||||
$thejob = $tmp;
|
||||
if ($thejob == null)
|
||||
throw new Exception("Could not reset schedule for job $job_name. $jon_name not found.");
|
||||
|
||||
# We have to acquire lock on the schedule. This might take some time if cron-5 jobs are
|
||||
# currently being run.
|
||||
|
||||
$type = $thejob->get_type();
|
||||
$lock = OkapiLock::get('cronjobs-'.$type);
|
||||
$lock->acquire();
|
||||
|
||||
$schedule = Cache::get("cron_schedule");
|
||||
if ($schedule != null)
|
||||
{
|
||||
if (isset($schedule[$thejob->get_name()]))
|
||||
unset($schedule[$thejob->get_name()]);
|
||||
Cache::set("cron_schedule", $schedule, 30*86400);
|
||||
}
|
||||
|
||||
$lock->release();
|
||||
}
|
||||
}
|
||||
|
||||
abstract class CronJob
|
||||
{
|
||||
/** Run the job. */
|
||||
public abstract function execute();
|
||||
|
||||
/** Get unique name for this cronjob. */
|
||||
public function get_name() { return get_class($this); }
|
||||
|
||||
/**
|
||||
* Get the type of this cronjob. Currently there are two: 'pre-request'
|
||||
* and 'cron-5'. The first can be executed before every request, the second
|
||||
* is executed from system's crontab, as a separate process. 'cron-5' can be
|
||||
* executed every 5 minutes, or every 10, 15 etc. minutes. 'pre-request'
|
||||
* can be executed before each HTTP request, AND additionally every 5 minutes
|
||||
* (before 'cron-5' runs).
|
||||
*/
|
||||
public abstract function get_type();
|
||||
|
||||
/**
|
||||
* Get the next scheduled run (unix timestamp). You may assume this function
|
||||
* will be called ONLY directly after the job was run. You may use this to say,
|
||||
* for example, "run the job before first request made after midnight".
|
||||
*/
|
||||
public abstract function get_next_scheduled_run($previously_scheduled_run);
|
||||
}
|
||||
|
||||
/**
|
||||
* CronJob which is run before requests. All implenatations specify a *minimum* time period
|
||||
* that should pass between running a job. If job was run at time X, then it will
|
||||
* be run again just before the first request made after X+period. The job also
|
||||
* will be run after server gets updated.
|
||||
*/
|
||||
abstract class PrerequestCronJob extends CronJob
|
||||
{
|
||||
/**
|
||||
* Always returns 'pre-request'.
|
||||
*/
|
||||
public final function get_type() { return 'pre-request'; }
|
||||
|
||||
/**
|
||||
* Return number of seconds - a *minimum* time period that should pass between
|
||||
* running the job.
|
||||
*/
|
||||
public abstract function get_period();
|
||||
|
||||
public function get_next_scheduled_run($previously_scheduled_run)
|
||||
{
|
||||
return time() + $this->get_period();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* CronJob which is run from crontab. It may be invoked every 5 minutes, or
|
||||
* every 10, 15 etc. Hence the name - cron-5.
|
||||
*/
|
||||
abstract class Cron5Job extends CronJob
|
||||
{
|
||||
/**
|
||||
* Always returns 'cron-5'.
|
||||
*/
|
||||
public final function get_type() { return 'cron-5'; }
|
||||
|
||||
/**
|
||||
* Return number of seconds - period of time after which cronjob execution
|
||||
* should be repeated. This should be dividable be 300 (5 minutes).
|
||||
*/
|
||||
public abstract function get_period();
|
||||
|
||||
public function get_next_scheduled_run($previously_scheduled_run)
|
||||
{
|
||||
$t = time() + $this->get_period();
|
||||
return ($t - ($t % 300));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Deletes old Request Tokens and Nonces every 5 minutes. This is required for
|
||||
* OAuth to run safely.
|
||||
*/
|
||||
class OAuthCleanupCronJob extends PrerequestCronJob
|
||||
{
|
||||
public function get_period() { return 300; } # 5 minutes
|
||||
public function execute()
|
||||
{
|
||||
if (Okapi::$data_store)
|
||||
Okapi::$data_store->cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
/** Deletes all expired cache elements, once per hour. */
|
||||
class CacheCleanupCronJob extends Cron5Job
|
||||
{
|
||||
public function get_period() { return 3600; } # 1 hour
|
||||
public function execute()
|
||||
{
|
||||
Db::execute("
|
||||
delete from okapi_cache
|
||||
where expires < now()
|
||||
");
|
||||
}
|
||||
}
|
||||
|
||||
/** Reads temporary (fast) stats-tables and reformats them into more permanent structures. */
|
||||
class StatsWriterCronJob extends PrerequestCronJob
|
||||
{
|
||||
public function get_period() { return 60; } # 1 minute
|
||||
public function execute()
|
||||
{
|
||||
if (Okapi::get_var('db_version', 0) + 0 < 32)
|
||||
return;
|
||||
Db::query("lock tables okapi_stats_hourly write, okapi_stats_temp write;");
|
||||
$rs = Db::query("
|
||||
select
|
||||
consumer_key,
|
||||
user_id,
|
||||
concat(substr(`datetime`, 1, 13), ':00:00') as period_start,
|
||||
service_name,
|
||||
calltype,
|
||||
count(*) as calls,
|
||||
sum(runtime) as runtime
|
||||
from okapi_stats_temp
|
||||
group by substr(`datetime`, 1, 13), consumer_key, user_id, service_name, calltype
|
||||
");
|
||||
while ($row = mysql_fetch_assoc($rs))
|
||||
{
|
||||
Db::execute("
|
||||
insert into okapi_stats_hourly (consumer_key, user_id, period_start, service_name,
|
||||
total_calls, http_calls, total_runtime, http_runtime)
|
||||
values (
|
||||
'".mysql_real_escape_string($row['consumer_key'])."',
|
||||
'".mysql_real_escape_string($row['user_id'])."',
|
||||
'".mysql_real_escape_string($row['period_start'])."',
|
||||
'".mysql_real_escape_string($row['service_name'])."',
|
||||
".$row['calls'].",
|
||||
".(($row['calltype'] == 'http') ? $row['calls'] : 0).",
|
||||
".$row['runtime'].",
|
||||
".(($row['calltype'] == 'http') ? $row['runtime'] : 0)."
|
||||
)
|
||||
on duplicate key update
|
||||
".(($row['calltype'] == 'http') ? "
|
||||
http_calls = http_calls + ".$row['calls'].",
|
||||
http_runtime = http_runtime + ".$row['runtime'].",
|
||||
" : "")."
|
||||
total_calls = total_calls + ".$row['calls'].",
|
||||
total_runtime = total_runtime + ".$row['runtime']."
|
||||
");
|
||||
}
|
||||
Db::execute("delete from okapi_stats_temp;");
|
||||
Db::execute("unlock tables;");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Once per hour, puts a test entry in the database. This is to make sure
|
||||
* that crontab is set up properly.
|
||||
*/
|
||||
class CheckCronTab1 extends Cron5Job
|
||||
{
|
||||
public function get_period() { return 3600; }
|
||||
public function execute()
|
||||
{
|
||||
Cache::set('crontab_last_ping', time(), 86400);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Twice an hour, upon request, checks if the test entry (previously put by
|
||||
* CheckCronTab1 job) is up-to-date (the one which was saved by CheckCronTab1 job).
|
||||
*/
|
||||
class CheckCronTab2 extends PrerequestCronJob
|
||||
{
|
||||
public function get_period() { return 30 * 60; }
|
||||
public function execute()
|
||||
{
|
||||
$last_ping = Cache::get('crontab_last_ping');
|
||||
if ($last_ping === null)
|
||||
$last_ping = time() - 86400; # if not set, assume 1 day ago.
|
||||
if ($last_ping > time() - 3600)
|
||||
{
|
||||
# There was a ping during the last hour. Everything is okay.
|
||||
# Reset the counter and return.
|
||||
|
||||
Cache::set('crontab_check_counter', 3, 86400);
|
||||
return;
|
||||
}
|
||||
|
||||
# There was no ping. Decrement the counter. When reached zero, alert.
|
||||
|
||||
$counter = Cache::get('crontab_check_counter');
|
||||
if ($counter === null)
|
||||
$counter = 3;
|
||||
$counter--;
|
||||
if ($counter > 0)
|
||||
{
|
||||
Cache::set('crontab_check_counter', $counter, 86400);
|
||||
}
|
||||
elseif ($counter == 0)
|
||||
{
|
||||
Okapi::mail_admins(
|
||||
"Crontab not working.",
|
||||
"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".
|
||||
"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)."
|
||||
);
|
||||
|
||||
# Schedule the next admin-nagging. Each subsequent notification will be sent
|
||||
# with a greater delay.
|
||||
|
||||
$since_last = time() - $last_ping;
|
||||
Cache::set('crontab_check_counter', (int)($since_last / $this->get_period()), 86400);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Once per 5 minutes, searches for changes in the database and updates the changelog.
|
||||
*/
|
||||
class ChangeLogWriterJob extends Cron5Job
|
||||
{
|
||||
public function get_period() { return 300; }
|
||||
public function execute()
|
||||
{
|
||||
require_once 'services/replicate/replicate_common.inc.php';
|
||||
ReplicateCommon::update_clog_table();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Once per week, generates the fulldump archive.
|
||||
*/
|
||||
class FulldumpGeneratorJob extends Cron5Job
|
||||
{
|
||||
public function get_period() { return 7*86400; }
|
||||
public function execute()
|
||||
{
|
||||
require_once 'services/replicate/replicate_common.inc.php';
|
||||
ReplicateCommon::generate_fulldump();
|
||||
}
|
||||
}
|
||||
|
||||
/** 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';
|
||||
$max_revision = ReplicateCommon::get_revision();
|
||||
$cache_key = 'clog_revisions_daily';
|
||||
$data = Cache::get($cache_key);
|
||||
if ($data == null)
|
||||
$data = array();
|
||||
$data[time()] = $max_revision;
|
||||
$new_min_revision = 1;
|
||||
$new_data = array();
|
||||
foreach ($data as $time => $r)
|
||||
{
|
||||
if ($time < time() - 10*86400)
|
||||
$new_min_revision = max($new_min_revision, $r);
|
||||
else
|
||||
$new_data[$time] = $r;
|
||||
}
|
||||
Db::execute("
|
||||
delete from okapi_clog
|
||||
where id < '".mysql_real_escape_string($new_min_revision)."'
|
||||
");
|
||||
Cache::set($cache_key, $new_data, 10*86400);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Once per week, sends simple OKAPI usage stats to the admins.
|
||||
*/
|
||||
class AdminStatsSender extends Cron5Job
|
||||
{
|
||||
public function get_period() { return 7*86400; }
|
||||
public function execute()
|
||||
{
|
||||
ob_start();
|
||||
$apisrv_stats = OkapiServiceRunner::call('services/apisrv/stats', new OkapiInternalRequest(
|
||||
new OkapiInternalConsumer(), null, array()));
|
||||
$active_apps_count = Db::select_value("
|
||||
select count(distinct s.consumer_key)
|
||||
from
|
||||
okapi_stats_hourly s,
|
||||
okapi_consumers c
|
||||
where
|
||||
s.consumer_key = c.`key`
|
||||
and s.period_start > date_add(now(), interval -7 day)
|
||||
");
|
||||
$weekly_stats = Db::select_row("
|
||||
select
|
||||
sum(s.http_calls) as total_http_calls,
|
||||
sum(s.http_runtime) as total_http_runtime
|
||||
from okapi_stats_hourly s
|
||||
where
|
||||
s.consumer_key != 'internal' -- we don't want to exclude 'anonymous' nor 'facade'
|
||||
and s.period_start > date_add(now(), interval -7 day)
|
||||
");
|
||||
print "Hello! This is your weekly summary of OKAPI usage.\n\n";
|
||||
print "Apps active this week: ".$active_apps_count." out of ".$apisrv_stats['apps_count'].".\n";
|
||||
print "Total of ".$weekly_stats['total_http_calls']." requests were made (".sprintf("%01.1f", $weekly_stats['total_http_runtime'])." seconds).\n\n";
|
||||
$consumers = Db::select_all("
|
||||
select
|
||||
s.consumer_key,
|
||||
c.name,
|
||||
sum(s.http_calls) as http_calls,
|
||||
sum(s.http_runtime) as http_runtime
|
||||
from
|
||||
okapi_stats_hourly s
|
||||
left join okapi_consumers c
|
||||
on s.consumer_key = c.`key`
|
||||
where s.period_start > date_add(now(), interval -7 day)
|
||||
group by s.consumer_key
|
||||
having sum(s.http_calls) > 0
|
||||
order by sum(s.http_calls) desc
|
||||
");
|
||||
print "== Consumers ==\n\n";
|
||||
print "Consumer name Calls Runtime\n";
|
||||
print "----------------------------------- ------- -----------\n";
|
||||
foreach ($consumers as $row)
|
||||
{
|
||||
$name = $row['name'];
|
||||
if ($row['consumer_key'] == 'anonymous')
|
||||
$name = "Anonymous (Level 0 Authentication)";
|
||||
elseif ($row['consumer_key'] == 'facade')
|
||||
$name = "Internal usage via Facade";
|
||||
if (mb_strlen($name) > 35)
|
||||
$name = mb_substr($name, 0, 32)."...";
|
||||
print self::mb_str_pad($name, 35, " ", STR_PAD_RIGHT);
|
||||
print str_pad($row['http_calls'], 8, " ", STR_PAD_LEFT);
|
||||
print str_pad(sprintf("%01.2f", $row['http_runtime']), 11, " ", STR_PAD_LEFT)."s\n";
|
||||
}
|
||||
print "\n";
|
||||
$methods = Db::select_all("
|
||||
select
|
||||
s.service_name,
|
||||
sum(s.http_calls) as http_calls,
|
||||
sum(s.http_runtime) as http_runtime
|
||||
from okapi_stats_hourly s
|
||||
where s.period_start > date_add(now(), interval -7 day)
|
||||
group by s.service_name
|
||||
having sum(s.http_calls) > 0
|
||||
order by sum(s.http_calls) desc
|
||||
");
|
||||
print "== Methods ==\n\n";
|
||||
print "Service name Calls Runtime Avg\n";
|
||||
print "----------------------------------- ------- ----------- --------\n";
|
||||
foreach ($methods as $row)
|
||||
{
|
||||
$name = $row['service_name'];
|
||||
if (mb_strlen($name) > 35)
|
||||
$name = mb_substr($name, 0, 32)."...";
|
||||
print self::mb_str_pad($name, 35, " ", STR_PAD_RIGHT);
|
||||
print str_pad($row['http_calls'], 8, " ", STR_PAD_LEFT);
|
||||
print str_pad(sprintf("%01.2f", $row['http_runtime']), 11, " ", STR_PAD_LEFT)."s";
|
||||
print str_pad(sprintf("%01.4f", (
|
||||
($row['http_calls'] > 0) ? ($row['http_runtime'] / $row['http_calls']) : 0
|
||||
)), 8, " ", STR_PAD_LEFT)."s\n";
|
||||
}
|
||||
print "\n";
|
||||
$oauth_users = Db::select_all("
|
||||
select
|
||||
c.name,
|
||||
count(*) as users
|
||||
from
|
||||
okapi_authorizations a,
|
||||
okapi_consumers c
|
||||
where a.consumer_key = c.`key`
|
||||
group by a.consumer_key
|
||||
order by count(*) desc;
|
||||
");
|
||||
print "== Current OAuth usage by Consumers ==\n\n";
|
||||
print "Consumer name Users\n";
|
||||
print "----------------------------------- -------\n";
|
||||
foreach ($oauth_users as $row)
|
||||
{
|
||||
$name = $row['name'];
|
||||
if (mb_strlen($name) > 35)
|
||||
$name = mb_substr($name, 0, 32)."...";
|
||||
print self::mb_str_pad($name, 35, " ", STR_PAD_RIGHT);
|
||||
print str_pad($row['users'], 8, " ", STR_PAD_LEFT)."\n";
|
||||
}
|
||||
print "\n";
|
||||
|
||||
print "This report includes requests from external consumers and those made via\n";
|
||||
print "Facade class (used by OC code). It does not include methods used by OKAPI\n";
|
||||
print "internally (i.e. while running cronjobs). Runtimes do not include HTTP\n";
|
||||
print "request handling overhead.\n";
|
||||
|
||||
$message = ob_get_clean();
|
||||
Okapi::mail_admins("Weekly OKAPI usage report", $message);
|
||||
}
|
||||
|
||||
private static function mb_str_pad($input, $pad_length, $pad_string, $pad_style)
|
||||
{
|
||||
return str_pad($input, strlen($input) - mb_strlen($input) + $pad_length,
|
||||
$pad_string, $pad_style);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Once per week, check if all required locales are installed. If not,
|
||||
* keep nagging the admins to do so.
|
||||
*/
|
||||
class LocaleChecker extends Cron5Job
|
||||
{
|
||||
public function get_period() { return 7*86400; }
|
||||
public function execute()
|
||||
{
|
||||
require_once 'locale/locales.php';
|
||||
$required = Locales::get_required_locales();
|
||||
$installed = Locales::get_installed_locales();
|
||||
$missing = array();
|
||||
foreach ($required as $locale)
|
||||
if (!in_array($locale, $installed))
|
||||
$missing[] = $locale;
|
||||
if (count($missing) == 0)
|
||||
return; # okay!
|
||||
ob_start();
|
||||
print "Hi!\n\n";
|
||||
print "Your system is missing some locales required by OKAPI for proper\n";
|
||||
print "internationalization support. OKAPI comes with support for different\n";
|
||||
print "languages. This number (hopefully) will be growing.\n\n";
|
||||
print "Please take a moment to install the following missing locales:\n\n";
|
||||
$prefixes = array();
|
||||
foreach ($missing as $locale)
|
||||
{
|
||||
print " - ".$locale."\n";
|
||||
$prefixes[substr($locale, 0, 2)] = true;
|
||||
}
|
||||
$prefixes = array_keys($prefixes);
|
||||
print "\n";
|
||||
if ((count($missing) == 1) && ($missing[0] == 'POSIX'))
|
||||
{
|
||||
# I don't remember how to install POSIX, probably everyone has it anyway.
|
||||
}
|
||||
else
|
||||
{
|
||||
print "On Debian, try the following:\n\n";
|
||||
foreach ($prefixes as $lang)
|
||||
{
|
||||
if ($lang != 'PO') # Two first letters cut from POSIX.
|
||||
print "sudo apt-get install language-pack-".$lang."-base\n";
|
||||
}
|
||||
print "sudo service apache2 restart\n";
|
||||
print "\n";
|
||||
}
|
||||
print "Thanks!\n\n";
|
||||
print "-- \n";
|
||||
print "OKAPI Team";
|
||||
Okapi::mail_admins("Additional setup needed: Missing locales.", ob_get_clean());
|
||||
}
|
||||
}
|
||||
|
180
htdocs/okapi/datastore.php
Normal file
180
htdocs/okapi/datastore.php
Normal file
@ -0,0 +1,180 @@
|
||||
<?php
|
||||
|
||||
namespace okapi;
|
||||
|
||||
use OAuthDataStore;
|
||||
|
||||
require_once($rootpath.'lib/common.inc.php');
|
||||
|
||||
class OkapiDataStore extends OAuthDataStore
|
||||
{
|
||||
public function lookup_consumer($consumer_key)
|
||||
{
|
||||
$row = Db::select_row("
|
||||
select `key`, secret, name, url, email
|
||||
from okapi_consumers
|
||||
where `key` = '".mysql_real_escape_string($consumer_key)."'
|
||||
");
|
||||
if (!$row)
|
||||
return null;
|
||||
return new OkapiConsumer($row['key'], $row['secret'], $row['name'],
|
||||
$row['url'], $row['email']);
|
||||
}
|
||||
|
||||
public function lookup_token(OkapiConsumer $consumer, $token_type, $token)
|
||||
{
|
||||
$row = Db::select_row("
|
||||
select `key`, consumer_key, secret, token_type, user_id, verifier, callback
|
||||
from okapi_tokens
|
||||
where
|
||||
consumer_key = '".mysql_real_escape_string($consumer->key)."'
|
||||
and token_type = '".mysql_real_escape_string($token_type)."'
|
||||
and `key` = '".mysql_real_escape_string($token)."'
|
||||
");
|
||||
if (!$row)
|
||||
return null;
|
||||
switch ($row['token_type'])
|
||||
{
|
||||
case 'request':
|
||||
return new OkapiRequestToken($row['key'], $row['secret'],
|
||||
$row['consumer_key'], $row['callback'], $row['user_id'],
|
||||
$row['verifier']);
|
||||
case 'access':
|
||||
return new OkapiAccessToken($row['key'], $row['secret'],
|
||||
$row['consumer_key'], $row['user_id']);
|
||||
default:
|
||||
throw new Exception();
|
||||
}
|
||||
}
|
||||
|
||||
public function lookup_nonce(OkapiConsumer $consumer, $token, $nonce, $timestamp)
|
||||
{
|
||||
# First, see if it exists. Note, that old nonces are periodically deleted.
|
||||
|
||||
$exists = Db::select_value("
|
||||
select 1
|
||||
from okapi_nonces
|
||||
where
|
||||
consumer_key = '".mysql_real_escape_string($consumer->key)."'
|
||||
and `key` = '".mysql_real_escape_string($nonce)."'
|
||||
and timestamp = '".mysql_real_escape_string($timestamp)."'
|
||||
");
|
||||
if ($exists)
|
||||
return $nonce;
|
||||
|
||||
# It didn't exist. We have to remember it.
|
||||
|
||||
Db::execute("
|
||||
insert into okapi_nonces (consumer_key, `key`, timestamp)
|
||||
values (
|
||||
'".mysql_real_escape_string($consumer->key)."',
|
||||
'".mysql_real_escape_string($nonce)."',
|
||||
'".mysql_real_escape_string($timestamp)."'
|
||||
);
|
||||
");
|
||||
return null;
|
||||
}
|
||||
|
||||
public function new_request_token(OkapiConsumer $consumer, $callback)
|
||||
{
|
||||
if ((preg_match("#^[a-z][a-z0-9_.-]*://#", $callback) > 0) ||
|
||||
$callback == "oob")
|
||||
{ /* ok */ }
|
||||
else { throw new BadRequest("oauth_callback should begin with <scheme>://, or should equal 'oob'."); }
|
||||
$token = new OkapiRequestToken(Okapi::generate_key(20), Okapi::generate_key(40),
|
||||
$consumer->key, $callback, null, Okapi::generate_key(8, true));
|
||||
Db::execute("
|
||||
insert into okapi_tokens
|
||||
(`key`, secret, token_type, timestamp,
|
||||
user_id, consumer_key, verifier, callback)
|
||||
values (
|
||||
'".mysql_real_escape_string($token->key)."',
|
||||
'".mysql_real_escape_string($token->secret)."',
|
||||
'request',
|
||||
unix_timestamp(),
|
||||
null,
|
||||
'".mysql_real_escape_string($consumer->key)."',
|
||||
'".mysql_real_escape_string($token->verifier)."',
|
||||
".(($token->callback_url == 'oob')
|
||||
? "null"
|
||||
: "'".mysql_real_escape_string($token->callback_url)."'"
|
||||
)."
|
||||
);
|
||||
");
|
||||
return $token;
|
||||
}
|
||||
|
||||
public function new_access_token(OkapiRequestToken $token, $consumer, $verifier)
|
||||
{
|
||||
if ($token->consumer_key != $consumer->key)
|
||||
throw new BadRequest("Request Token given is not associated with the Consumer who signed the request.");
|
||||
if (!$token->authorized_by_user_id)
|
||||
throw new BadRequest("Request Token given has not been authorized.");
|
||||
if ($token->verifier != $verifier)
|
||||
throw new BadRequest("Invalid verifier.");
|
||||
|
||||
# Invalidate the Request Token.
|
||||
|
||||
Db::execute("
|
||||
delete from okapi_tokens
|
||||
where `key` = '".mysql_real_escape_string($token->key)."'
|
||||
");
|
||||
|
||||
# In OKAPI, all Access Tokens are long lived. Therefore, we don't want
|
||||
# to generate a new one every time a Consumer wants it. We will check
|
||||
# if there is already an Access Token generated for this (Consumer, User)
|
||||
# pair and return it if there is.
|
||||
|
||||
$row = Db::select_row("
|
||||
select `key`, secret
|
||||
from okapi_tokens
|
||||
where
|
||||
token_type = 'access'
|
||||
and user_id = '".mysql_real_escape_string($token->authorized_by_user_id)."'
|
||||
and consumer_key = '".mysql_real_escape_string($consumer->key)."'
|
||||
");
|
||||
if ($row)
|
||||
{
|
||||
# Use existing Access Token
|
||||
|
||||
$access_token = new OkapiAccessToken($row['key'], $row['secret'],
|
||||
$consumer->key, $token->authorized_by_user_id);
|
||||
}
|
||||
else
|
||||
{
|
||||
# Generate a new Access Token.
|
||||
|
||||
$access_token = new OkapiAccessToken(Okapi::generate_key(20), Okapi::generate_key(40),
|
||||
$consumer->key, $token->authorized_by_user_id);
|
||||
Db::execute("
|
||||
insert into okapi_tokens
|
||||
(`key`, secret, token_type, timestamp, user_id, consumer_key)
|
||||
values (
|
||||
'".mysql_real_escape_string($access_token->key)."',
|
||||
'".mysql_real_escape_string($access_token->secret)."',
|
||||
'access',
|
||||
unix_timestamp(),
|
||||
'".mysql_real_escape_string($access_token->user_id)."',
|
||||
'".mysql_real_escape_string($consumer->key)."'
|
||||
);
|
||||
");
|
||||
}
|
||||
return $access_token;
|
||||
}
|
||||
|
||||
public function cleanup()
|
||||
{
|
||||
Db::execute("
|
||||
delete from okapi_nonces
|
||||
where
|
||||
timestamp < unix_timestamp(date_add(now(), interval -6 minute))
|
||||
or timestamp > unix_timestamp(date_add(now(), interval 6 minute))
|
||||
");
|
||||
Db::execute("
|
||||
delete from okapi_tokens
|
||||
where
|
||||
token_type = 'request'
|
||||
and timestamp < unix_timestamp(date_add(now(), interval -2 hour))
|
||||
");
|
||||
}
|
||||
}
|
42
htdocs/okapi/facade.php
Normal file
42
htdocs/okapi/facade.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?
|
||||
|
||||
namespace okapi;
|
||||
|
||||
use Exception;
|
||||
use okapi\OkapiServiceRunner;
|
||||
use okapi\OkapiInternalRequest;
|
||||
use okapi\OkapiFacadeConsumer;
|
||||
use okapi\OkapiFacadeAccessToken;
|
||||
|
||||
require_once('core.php');
|
||||
require_once('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).
|
||||
*/
|
||||
class Facade
|
||||
{
|
||||
/**
|
||||
* Perform OKAPI service call, signed by internal 'facade' consumer key, and return the result
|
||||
* (this will be PHP object or OkapiHttpResponse, depending on the method). Use this method
|
||||
* whenever you need to access OKAPI services from within OC code. If you want to simulate
|
||||
* Level 3 Authentication, you should supply user's internal ID (the second parameter).
|
||||
*/
|
||||
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,
|
||||
$parameters
|
||||
);
|
||||
$request->perceive_as_http_request = true;
|
||||
return OkapiServiceRunner::call($service_name, $request);
|
||||
}
|
||||
}
|
54
htdocs/okapi/locale/locales.php
Normal file
54
htdocs/okapi/locale/locales.php
Normal file
@ -0,0 +1,54 @@
|
||||
<?
|
||||
|
||||
namespace okapi;
|
||||
|
||||
class Locales
|
||||
{
|
||||
public static $languages = array(
|
||||
'pl' => array('lang' => 'pl', 'locale' => 'pl_PL.utf8', 'name' => 'Polish'),
|
||||
'en' => array('lang' => 'en', 'locale' => 'en_US.utf8', 'name' => 'English'),
|
||||
'nl' => array('lang' => 'nl', 'locale' => 'nl_NL.utf8', 'name' => 'Dutch'),
|
||||
);
|
||||
|
||||
/**
|
||||
* Get the list of locales that should be installed on the system in order
|
||||
* for all translations to work properly.
|
||||
*/
|
||||
public static function get_required_locales()
|
||||
{
|
||||
$arr = array('POSIX');
|
||||
foreach (self::$languages as $key => $value)
|
||||
$arr[] = $value['locale'];
|
||||
return $arr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of locales installed on the current system.
|
||||
*/
|
||||
public static function get_installed_locales()
|
||||
{
|
||||
$arr = array();
|
||||
foreach (explode("\n", shell_exec("locale -a")) as $item)
|
||||
if ($item)
|
||||
$arr[] = $item;
|
||||
return $arr;
|
||||
}
|
||||
|
||||
private static function get_locale_for_language($lang)
|
||||
{
|
||||
if (isset(self::$languages[$lang]))
|
||||
return self::$languages[$lang]['locale'];
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function get_best_locale($langprefs)
|
||||
{
|
||||
foreach ($langprefs as $lang)
|
||||
{
|
||||
$locale = self::get_locale_for_language($lang);
|
||||
if ($locale != null)
|
||||
return $locale;
|
||||
}
|
||||
return self::$languages['en']['locale'];
|
||||
}
|
||||
}
|
BIN
htdocs/okapi/locale/nl_NL/LC_MESSAGES/okapi_messages.mo
Normal file
BIN
htdocs/okapi/locale/nl_NL/LC_MESSAGES/okapi_messages.mo
Normal file
Binary file not shown.
249
htdocs/okapi/locale/nl_NL/LC_MESSAGES/okapi_messages.po
Normal file
249
htdocs/okapi/locale/nl_NL/LC_MESSAGES/okapi_messages.po
Normal file
@ -0,0 +1,249 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: OKAPI\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2012-06-14 23:42+0100\n"
|
||||
"PO-Revision-Date: 2012-06-14 23:44+0100\n"
|
||||
"Last-Translator: Wojciech Rygielski <rygielski@mimuw.edu.pl>\n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Poedit-KeywordsList: _;gettext;gettext_noop\n"
|
||||
"X-Poedit-Basepath: D:\\PRIV\\Projekty\\EclipseWorkspace\\opencaching-api\\\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
|
||||
"X-Poedit-Language: Dutch\n"
|
||||
"X-Poedit-Country: NETHERLANDS\n"
|
||||
"X-Poedit-SearchPath-0: .\n"
|
||||
|
||||
# For additional waypoints. As in "Stage 1: Parking".
|
||||
#: okapi/services/caches/geocaches.php:634
|
||||
msgid "Stage"
|
||||
msgstr "Etappe"
|
||||
|
||||
#: okapi/services/caches/formatters/gpxfile.tpl.php:27
|
||||
#: okapi/services/caches/formatters/gpxfile.tpl.php:44
|
||||
msgid "hidden by"
|
||||
msgstr "geplaatst door"
|
||||
|
||||
#: okapi/services/caches/formatters/gpxfile.tpl.php:46
|
||||
#, php-format
|
||||
msgid "%d recommendation"
|
||||
msgid_plural "%d recommendations"
|
||||
msgstr[0] "%d aanbeveling"
|
||||
msgstr[1] "%d aanbevelingen"
|
||||
|
||||
#: okapi/services/caches/formatters/gpxfile.tpl.php:47
|
||||
#, php-format
|
||||
msgid "found %d time"
|
||||
msgid_plural "found %d times"
|
||||
msgstr[0] "%d keer gevonden"
|
||||
msgstr[1] "%d keren gevonden"
|
||||
|
||||
# Generic term for trackable items (like geocoins and travelbugs).
|
||||
#: okapi/services/caches/formatters/gpxfile.tpl.php:50
|
||||
#, php-format
|
||||
msgid "%d trackable"
|
||||
msgid_plural "%d trackables"
|
||||
msgstr[0] "%d trackable"
|
||||
msgstr[1] "%d trackables"
|
||||
|
||||
#: okapi/services/caches/formatters/gpxfile.tpl.php:54
|
||||
msgid "Personal notes"
|
||||
msgstr "Persoonlijke notities"
|
||||
|
||||
#: okapi/services/caches/formatters/gpxfile.tpl.php:58
|
||||
msgid "Attributes"
|
||||
msgstr "Attributen"
|
||||
|
||||
#: okapi/services/caches/formatters/gpxfile.tpl.php:62
|
||||
msgid "Trackables"
|
||||
msgstr "Trackables"
|
||||
|
||||
#: okapi/services/caches/formatters/gpxfile.tpl.php:80
|
||||
msgid "Images"
|
||||
msgstr "Afbeeldingen"
|
||||
|
||||
#: okapi/services/caches/formatters/gpxfile.tpl.php:87
|
||||
msgid "Spoilers"
|
||||
msgstr "Spoilers"
|
||||
|
||||
#: okapi/services/caches/formatters/gpxfile.tpl.php:95
|
||||
msgid "Image descriptions"
|
||||
msgstr "Afbeelding omschrijving"
|
||||
|
||||
#: okapi/services/logs/submit.php:63
|
||||
msgid "You are trying to publish a log entry with a date in future. Cache log entries are allowed to be published in the past, but NOT in the future."
|
||||
msgstr "De datum van de log staat ingesteld op een toekomstige datum. Cache logs kunnen alleen in het verleden zijn, maar NIET in de toekomst."
|
||||
|
||||
#: okapi/services/logs/submit.php:83
|
||||
#: okapi/services/logs/submit.php:377
|
||||
msgid "Your cache log entry was posted successfully."
|
||||
msgstr "De log is succesvol verzonden."
|
||||
|
||||
#: okapi/services/logs/submit.php:84
|
||||
#, php-format
|
||||
msgid "However, your cache rating was ignored, because %s does not have a rating system."
|
||||
msgstr "De cachewaardering is genegeerd, omdat %s geen waarderingen in het systeem heeft."
|
||||
|
||||
#: okapi/services/logs/submit.php:107
|
||||
msgid "This cache is archived. Only admins and the owner are allowed to add a log entry."
|
||||
msgstr "Deze cache is gearchiveerd. Alleen admins en de eigenaar kunnen een log toevoegen."
|
||||
|
||||
# The user will see this error when he is trying to submit a "Fount it" entry on an event cache. "Attended" or "Will attend" log entries are not the same as "Found it". Currently OKAPI does not allow to submit "Attended" nor "Will attend" log entries, that's why user can only "Comment" on it.
|
||||
#: okapi/services/logs/submit.php:111
|
||||
msgid "This cache is an Event cache. You cannot \"Find it\"! (But - you may \"Comment\" on it.)"
|
||||
msgstr "Dit is een eventcache. Deze kan niet als \"Gevonden\" gelogd worden. Maar wel als \"Notitie\"."
|
||||
|
||||
#: okapi/services/logs/submit.php:113
|
||||
msgid "Your have to supply some text for your comment."
|
||||
msgstr "Er dient enige tekst ingevuld te worden."
|
||||
|
||||
#: okapi/services/logs/submit.php:126
|
||||
msgid "This cache requires a password. You didn't provide one!"
|
||||
msgstr "Deze cache vraagt om een wachtwoord. Deze is niet opgegeven!"
|
||||
|
||||
#: okapi/services/logs/submit.php:128
|
||||
msgid "Invalid password!"
|
||||
msgstr "Verkeerd wachtwoord!"
|
||||
|
||||
# This error will be shown to the user when he tries to submit a log entry which is EXACTLY the same as one he had submitted before.
|
||||
#: okapi/services/logs/submit.php:172
|
||||
msgid "You have already submitted a log entry with exactly the same contents."
|
||||
msgstr "Deze log is reeds met dezelfde tekst verzonden."
|
||||
|
||||
#: okapi/services/logs/submit.php:191
|
||||
msgid "You have already submitted a \"Found it\" log entry once. Now you may submit \"Comments\" only!"
|
||||
msgstr "Je hebt deze cache al als \"Gevonden\" gelogd. Je kunt nu wel een \"Notitie\" plaatsen."
|
||||
|
||||
# The English text was changed from "you cannot rate it" to "you cannot find it". The translation remained.
|
||||
#: okapi/services/logs/submit.php:193
|
||||
#, fuzzy
|
||||
msgid "You are the owner of this cache. You cannot \"Find it\"."
|
||||
msgstr "Als eigenaar van deze cache. Kan deze cache niet gewaardeerd worden."
|
||||
|
||||
#: okapi/services/logs/submit.php:211
|
||||
msgid "You have already rated this cache once. Your rating cannot be changed."
|
||||
msgstr "Je hebt deze cache al gewaardeerd. De waardering kan niet veranderd worden."
|
||||
|
||||
#: okapi/views/apps/authorize.tpl.php:5
|
||||
msgid "Authorization Form"
|
||||
msgstr "Autorisatiescherm"
|
||||
|
||||
#: okapi/views/apps/authorize.tpl.php:46
|
||||
msgid "Expired request"
|
||||
msgstr "Aanvraag verlopen"
|
||||
|
||||
#: okapi/views/apps/authorize.tpl.php:47
|
||||
msgid "Unfortunately, the request has expired. Please try again."
|
||||
msgstr "Helaas, de aanvraag is verlopen. Probeer het nogmaals."
|
||||
|
||||
#: okapi/views/apps/authorize.tpl.php:49
|
||||
msgid "External application is requesting access..."
|
||||
msgstr "Een externe toepassing verzoekt om toegang..."
|
||||
|
||||
#: okapi/views/apps/authorize.tpl.php:50
|
||||
#, php-format
|
||||
msgid "<b>%s</b> wants to access your <b>%s</b> account. Do you agree to grant access to this application?"
|
||||
msgstr "<b>%s</b> vraagt toegang tot jouw <b>%s</b> account. Toestemming verlenen voor deze toepassing?"
|
||||
|
||||
#: okapi/views/apps/authorize.tpl.php:53
|
||||
msgid "I agree"
|
||||
msgstr "Toestemmen"
|
||||
|
||||
#: okapi/views/apps/authorize.tpl.php:54
|
||||
msgid "Decline"
|
||||
msgstr "Afwijzen"
|
||||
|
||||
# This should begin with "\n", but you may ignore the rest of \n and \r characters.
|
||||
# This message is shown to the user when external application is trying to get user's permission to access his account.
|
||||
# Sample: http://i.imgur.com/ZCJNT.png
|
||||
#: okapi/views/apps/authorize.tpl.php:56
|
||||
#, php-format
|
||||
msgid ""
|
||||
"\n"
|
||||
"\t\t\t\t\t<p>Once permission is granted it is valid until its withdrawal on\n"
|
||||
"\t\t\t\t\tthe <a href='%s'>applications management</a> page.</p>\n"
|
||||
"\t\t\t\t\t<p>The application will access your acount via <a href='%s'>the OKAPI Framework</a>.\n"
|
||||
"\t\t\t\t\tIf you allow this request application will be able to access all methods delivered\n"
|
||||
"\t\t\t\t\tby the OKAPI Framework, i.e. post log entries on geocaches in your name.\n"
|
||||
"\t\t\t\t\tYou can revoke this permission at any moment.</p>\n"
|
||||
"\t\t\t\t"
|
||||
msgstr ""
|
||||
"\n"
|
||||
"\t\t\t\t\t<p>Wanneer toegestemd is blijft deze geldig tot intrekking op\n"
|
||||
"\t\t\t\t\tde <a href='%s'>toepassingsbeheer</a> pagina.</p>\n"
|
||||
"\t\t\t\t\t<p>De toepassing zal toegang krijgen via jouw account op <a href='%s'>the OKAPI Framework</a>.\n"
|
||||
"\t\t\t\t\tWanneer je toestemming geeft voor deze toepassing zullen de mogelijkheden\n"
|
||||
"\t\t\t\t\tvan OKAPI Framework toegepast worden, b.v. het loggen van een cache.\n"
|
||||
"\t\t\t\t\tDe toestemming kan elk moment ingetrokken worden.</p>\n"
|
||||
"\t\t\t\t"
|
||||
|
||||
#: okapi/views/apps/authorized.tpl.php:5
|
||||
msgid "Authorization Succeeded"
|
||||
msgstr "Aanmelden geslaagt"
|
||||
|
||||
#: okapi/views/apps/authorized.tpl.php:28
|
||||
msgid "Access successfully granted"
|
||||
msgstr "Met succes toegang verleend"
|
||||
|
||||
# This MIGHT be shown AFTER user grants external application permission to access his account.
|
||||
#: okapi/views/apps/authorized.tpl.php:29
|
||||
#, php-format
|
||||
msgid ""
|
||||
"\n"
|
||||
"\t\t\t\t<p><b>You've just granted %s application access to your %s account.</b>\n"
|
||||
"\t\t\t\tTo complete the operation, go back to %s and enter the following PIN code:</p>\n"
|
||||
"\t\t\t"
|
||||
msgstr ""
|
||||
"\n"
|
||||
"\t\t\t\t<p><b>Je hebt toegang verleent voor %s toepassing op jouw %s account.</b>\n"
|
||||
"\t\t\t\tOm de aktie te voltooien, ga terug naar %s en gebruik de volgende PIN code:</p>\n"
|
||||
"\t\t\t"
|
||||
|
||||
#: okapi/views/apps/index.tpl.php:5
|
||||
msgid "My Apps"
|
||||
msgstr "Mijn Apps"
|
||||
|
||||
#: okapi/views/apps/index.tpl.php:29
|
||||
msgid "Your external applications"
|
||||
msgstr "Jouw externe toepassingen"
|
||||
|
||||
# This will be shown when user visits /okapi/apps page.
|
||||
# Sample: http://i.imgur.com/ZCJNT.png
|
||||
#: okapi/views/apps/index.tpl.php:31
|
||||
#, php-format
|
||||
msgid ""
|
||||
"\n"
|
||||
"\t\t\t\t\t<p>This is the list of applications which you granted access to your <b>%s</b> account.\n"
|
||||
"\t\t\t\t\tThis page gives you the abbility to revoke all previously granted privileges.\n"
|
||||
"\t\t\t\t\tOnce you click \"remove\" the application will no longer be able to perform any\n"
|
||||
"\t\t\t\t\tactions on your behalf.</p>\n"
|
||||
"\t\t\t\t"
|
||||
msgstr ""
|
||||
"\n"
|
||||
"\t\t\t\t\t<p>Dit is een lijst met toegestane toepassingen op jouw <b>%s</b> account.\n"
|
||||
"\t\t\t\t\tOp deze pagina kun je alle toestemmingen intrekken die gegeven zijn.\n"
|
||||
"\t\t\t\t\tMet een klik op \"verwijderen\" zal de toepassing verwijderen en is dan ook niet meer beschikbaar\n"
|
||||
"\t\t\t\t\voor anderen.</p>\n"
|
||||
"\t\t\t\t"
|
||||
|
||||
#: okapi/views/apps/index.tpl.php:45
|
||||
msgid "remove"
|
||||
msgstr "verwijderen"
|
||||
|
||||
#: okapi/views/apps/index.tpl.php:50
|
||||
#, php-format
|
||||
msgid ""
|
||||
"\n"
|
||||
"\t\t\t\t\t<p>Thanks to the <a href='%s'>OKAPI Framework</a> you can grant external applications\n"
|
||||
"\t\t\t\t\taccess to your <b>%s</b> account. Currently no applications are authorized to act\n"
|
||||
"\t\t\t\t\ton your behalf. Once you start using external OpenCaching applications, they will appear here.</p>\n"
|
||||
"\t\t\t\t"
|
||||
msgstr ""
|
||||
"\n"
|
||||
"\t\t\t\t\t<p>Dankzij het <a href='%s'>OKAPI Framework</a> kun je toegang verlenen via externe\n"
|
||||
"\t\t\t\t\ttoepassingen op een <b>%s</b> account. Momenteel zijn op dit account nog geen externe\n"
|
||||
"\t\t\t\t\ttoepassingen actief. Geactiveerde OpenCaching toepassingen zullen hier getoond worden.</p>\n"
|
||||
"\t\t\t\t"
|
||||
|
BIN
htdocs/okapi/locale/pl_PL/LC_MESSAGES/okapi_messages.mo
Normal file
BIN
htdocs/okapi/locale/pl_PL/LC_MESSAGES/okapi_messages.mo
Normal file
Binary file not shown.
269
htdocs/okapi/locale/pl_PL/LC_MESSAGES/okapi_messages.po
Normal file
269
htdocs/okapi/locale/pl_PL/LC_MESSAGES/okapi_messages.po
Normal file
@ -0,0 +1,269 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: OKAPI\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2012-06-14 12:07+0100\n"
|
||||
"PO-Revision-Date: 2012-06-14 12:07+0100\n"
|
||||
"Last-Translator: Wojciech Rygielski <rygielski@mimuw.edu.pl>\n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Poedit-KeywordsList: _;gettext;gettext_noop\n"
|
||||
"X-Poedit-Basepath: D:\\PRIV\\Projekty\\EclipseWorkspace\\opencaching-api\\okapi\n"
|
||||
"Plural-Forms: nplurals=3; plural= n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
|
||||
"X-Poedit-Language: Polish\n"
|
||||
"X-Poedit-Country: POLAND\n"
|
||||
"X-Poedit-SourceCharset: utf-8\n"
|
||||
"X-Poedit-SearchPath-0: .\n"
|
||||
|
||||
#: services/caches/geocaches.php:634
|
||||
msgid "Stage"
|
||||
msgstr "Etap"
|
||||
|
||||
#: services/caches/formatters/gpxfile.tpl.php:27
|
||||
#: services/caches/formatters/gpxfile.tpl.php:44
|
||||
msgid "hidden by"
|
||||
msgstr "ukryta przez"
|
||||
|
||||
#: services/caches/formatters/gpxfile.tpl.php:46
|
||||
#, php-format
|
||||
msgid "%d recommendation"
|
||||
msgid_plural "%d recommendations"
|
||||
msgstr[0] "%d rekomendacja"
|
||||
msgstr[1] "%d rekomendacje"
|
||||
msgstr[2] "%d rekomendacji"
|
||||
|
||||
#: services/caches/formatters/gpxfile.tpl.php:47
|
||||
#, php-format
|
||||
msgid "found %d time"
|
||||
msgid_plural "found %d times"
|
||||
msgstr[0] "znaleziona %d raz"
|
||||
msgstr[1] "znaleziona %d razy"
|
||||
msgstr[2] "znaleziona %d razy"
|
||||
|
||||
#: services/caches/formatters/gpxfile.tpl.php:50
|
||||
#, php-format
|
||||
msgid "%d trackable"
|
||||
msgid_plural "%d trackables"
|
||||
msgstr[0] "%d GeoKret (lub TravelBug)"
|
||||
msgstr[1] "%d GeoKrety (lub TravelBugi)"
|
||||
msgstr[2] "%d GeoKretów (lub TravelBugów)"
|
||||
|
||||
#: services/caches/formatters/gpxfile.tpl.php:54
|
||||
msgid "Personal notes"
|
||||
msgstr "Osobiste notatki"
|
||||
|
||||
#: services/caches/formatters/gpxfile.tpl.php:58
|
||||
msgid "Attributes"
|
||||
msgstr "Atrybuty"
|
||||
|
||||
#: services/caches/formatters/gpxfile.tpl.php:62
|
||||
msgid "Trackables"
|
||||
msgstr "Geokrety, Travelbugi itp."
|
||||
|
||||
#: services/caches/formatters/gpxfile.tpl.php:80
|
||||
msgid "Images"
|
||||
msgstr "Obrazki"
|
||||
|
||||
#: services/caches/formatters/gpxfile.tpl.php:87
|
||||
msgid "Spoilers"
|
||||
msgstr "Spoilery"
|
||||
|
||||
#: services/caches/formatters/gpxfile.tpl.php:95
|
||||
msgid "Image descriptions"
|
||||
msgstr "Opisy obrazków"
|
||||
|
||||
#: services/logs/submit.php:63
|
||||
msgid "You are trying to publish a log entry with a date in future. Cache log entries are allowed to be published in the past, but NOT in the future."
|
||||
msgstr "Próbujesz opublikować wpis do logbooka używając daty w przyszłości. Wpisy mogą być publikowane z datą w przeszłości, ale NIE w przyszłości."
|
||||
|
||||
#: services/logs/submit.php:83
|
||||
#: services/logs/submit.php:377
|
||||
msgid "Your cache log entry was posted successfully."
|
||||
msgstr "Twój wpis do logbooka został opublikowany pomyślnie."
|
||||
|
||||
#: services/logs/submit.php:84
|
||||
#, php-format
|
||||
msgid "However, your cache rating was ignored, because %s does not have a rating system."
|
||||
msgstr "Niestety Twoja ocena skrzynki nie została zapisana, ponieważ %s nie prowadzi oceny skrzynek."
|
||||
|
||||
#: services/logs/submit.php:107
|
||||
msgid "This cache is archived. Only admins and the owner are allowed to add a log entry."
|
||||
msgstr "Ta skrzynka jest zarchiwizowana. Jedynie administratorzy oraz właściciel mogą dodawać komentarze."
|
||||
|
||||
#: services/logs/submit.php:111
|
||||
msgid "This cache is an Event cache. You cannot \"Find it\"! (But - you may \"Comment\" on it.)"
|
||||
msgstr "Ta skrzynka jest typu Wydarzenie. Nie możesz jej \"znaleźć\"! (Ale - możesz dodać wpis typu \"Komentarz\".)"
|
||||
|
||||
#: services/logs/submit.php:113
|
||||
msgid "Your have to supply some text for your comment."
|
||||
msgstr "Wpis typu \"Komentarz\" wymaga wpisania komentarza."
|
||||
|
||||
#: services/logs/submit.php:126
|
||||
msgid "This cache requires a password. You didn't provide one!"
|
||||
msgstr "Ta skrzynka wymaga podania hasła. Nie wpisałeś go."
|
||||
|
||||
#: services/logs/submit.php:128
|
||||
msgid "Invalid password!"
|
||||
msgstr "Niepoprawne hasło!"
|
||||
|
||||
#: services/logs/submit.php:172
|
||||
msgid "You have already submitted a log entry with exactly the same contents."
|
||||
msgstr "Już opublikowałeś wcześniej wpis z dokładnie taką samą treścią."
|
||||
|
||||
#: services/logs/submit.php:191
|
||||
msgid "You have already submitted a \"Found it\" log entry once. Now you may submit \"Comments\" only!"
|
||||
msgstr "Już opublikowałeś jeden wpis typu \"Znaleziona\" dla tej skrzynki. Teraz możesz dodawać jedynie \"Komentarze\"!"
|
||||
|
||||
#: services/logs/submit.php:193
|
||||
msgid "You are the owner of this cache. You cannot \"Find it\"."
|
||||
msgstr "Jesteś właścicielem tej skrzynki. Nie możesz jej \"Znaleźć\"."
|
||||
|
||||
#: services/logs/submit.php:211
|
||||
msgid "You have already rated this cache once. Your rating cannot be changed."
|
||||
msgstr "Już oceniłeś tę skrzynkę. Ocena nie może być zmieniona."
|
||||
|
||||
#: views/apps/authorize.tpl.php:5
|
||||
msgid "Authorization Form"
|
||||
msgstr "Formularz autoryzacyjny"
|
||||
|
||||
#: views/apps/authorize.tpl.php:46
|
||||
msgid "Expired request"
|
||||
msgstr "Przeterminowane żądanie"
|
||||
|
||||
#: views/apps/authorize.tpl.php:47
|
||||
msgid "Unfortunately, the request has expired. Please try again."
|
||||
msgstr "Niestety upłynął termin ważności żądania. Prosimy sprobować ponownie."
|
||||
|
||||
#: views/apps/authorize.tpl.php:49
|
||||
msgid "External application is requesting access..."
|
||||
msgstr "Aplikacja zewnętrzna prosi o dostęp..."
|
||||
|
||||
#: views/apps/authorize.tpl.php:50
|
||||
#, php-format
|
||||
msgid "<b>%s</b> wants to access your <b>%s</b> account. Do you agree to grant access to this application?"
|
||||
msgstr "<b>%s</b> chce uzyskać dostęp do Twojego konta <b>%s</b>. Czy zgadzasz się na udzielenie dostępu tej aplikacji?"
|
||||
|
||||
#: views/apps/authorize.tpl.php:53
|
||||
msgid "I agree"
|
||||
msgstr "Zgadzam się"
|
||||
|
||||
#: views/apps/authorize.tpl.php:54
|
||||
msgid "Decline"
|
||||
msgstr "Odmawiam"
|
||||
|
||||
#: views/apps/authorize.tpl.php:56
|
||||
#, php-format
|
||||
msgid ""
|
||||
"\n"
|
||||
"\t\t\t\t\t<p>Once permission is granted it is valid until its withdrawal on\n"
|
||||
"\t\t\t\t\tthe <a href='%s'>applications management</a> page.</p>\n"
|
||||
"\t\t\t\t\t<p>The application will access your acount via <a href='%s'>the OKAPI Framework</a>.\n"
|
||||
"\t\t\t\t\tIf you allow this request application will be able to access all methods delivered\n"
|
||||
"\t\t\t\t\tby the OKAPI Framework, i.e. post log entries on geocaches in your name.\n"
|
||||
"\t\t\t\t\tYou can revoke this permission at any moment.</p>\n"
|
||||
"\t\t\t\t"
|
||||
msgstr ""
|
||||
"\n"
|
||||
"<p>Raz udzielona zgoda jest ważna aż do momentu jej wycofania na stronie <a href='%s'>zarządzania aplikacjami</a>.</p><p>Aplikacja będzie łączyć się z Twoim kontem poprzez <a href='%s'>platformę OKAPI</a> (strona w języku angielskim). Uzyskanie zgody na dostęp pozwoli aplikacji na korzystanie ze wszystkich metod udostępnianych przez platformę OKAPI (m.in. aplikacja będzie mogła umieszczać komentarze pod znajdowanymi przez Ciebie skrzynkami). Zgodę możesz wycofać w każdym momencie.</p>"
|
||||
|
||||
#: views/apps/authorized.tpl.php:5
|
||||
msgid "Authorization Succeeded"
|
||||
msgstr "Autoryzacja powiodła się"
|
||||
|
||||
#: views/apps/authorized.tpl.php:28
|
||||
msgid "Access successfully granted"
|
||||
msgstr "Pomyślnie dałeś dostęp"
|
||||
|
||||
#: views/apps/authorized.tpl.php:29
|
||||
#, php-format
|
||||
msgid ""
|
||||
"\n"
|
||||
"\t\t\t\t<p><b>You've just granted %s application access to your %s account.</b>\n"
|
||||
"\t\t\t\tTo complete the operation, go back to %s and enter the following PIN code:</p>\n"
|
||||
"\t\t\t"
|
||||
msgstr ""
|
||||
"\n"
|
||||
"<p><b>Właśnie dałeś dostęp aplikacji %s do Twojego konta %s.</b>\n"
|
||||
"Aby zakończyć operację, wróć teraz do aplikacji %s i wpisz następujący kod PIN:</p>"
|
||||
|
||||
#: views/apps/index.tpl.php:5
|
||||
msgid "My Apps"
|
||||
msgstr "Moje aplikacje"
|
||||
|
||||
#: views/apps/index.tpl.php:29
|
||||
msgid "Your external applications"
|
||||
msgstr "Twoje zewnętrzne aplikacje"
|
||||
|
||||
#: views/apps/index.tpl.php:31
|
||||
#, php-format
|
||||
msgid ""
|
||||
"\n"
|
||||
"\t\t\t\t\t<p>This is the list of applications which you granted access to your <b>%s</b> account.\n"
|
||||
"\t\t\t\t\tThis page gives you the abbility to revoke all previously granted privileges.\n"
|
||||
"\t\t\t\t\tOnce you click \"remove\" the application will no longer be able to perform any\n"
|
||||
"\t\t\t\t\tactions on your behalf.</p>\n"
|
||||
"\t\t\t\t"
|
||||
msgstr ""
|
||||
"\n"
|
||||
"<p>Następującym aplikacjom zezwoliłeś na dostęp do swojego konta <b>%s</b>.\n"
|
||||
"Na tej stronie możesz wycofać udzielone poprzednio zezwolenia. Po kliknięciu\n"
|
||||
"\"usuń\" aplikacja nie będzie już mogła wykonywać żadnych operacji w Twoim imieniu.</p>"
|
||||
|
||||
#: views/apps/index.tpl.php:45
|
||||
msgid "remove"
|
||||
msgstr "usuń"
|
||||
|
||||
#: views/apps/index.tpl.php:50
|
||||
#, php-format
|
||||
msgid ""
|
||||
"\n"
|
||||
"\t\t\t\t\t<p>Thanks to the <a href='%s'>OKAPI Framework</a> you can grant external applications\n"
|
||||
"\t\t\t\t\taccess to your <b>%s</b> account. Currently no applications are authorized to act\n"
|
||||
"\t\t\t\t\ton your behalf. Once you start using external OpenCaching applications, they will appear here.</p>\n"
|
||||
"\t\t\t\t"
|
||||
msgstr ""
|
||||
"\n"
|
||||
"<p>Dzięki platformie <a href='%s'>OKAPI</a> możesz dawać zewnętrznym aplikacjom\n"
|
||||
"dostęp do Twojego konta <b>%s</b>. Aktualnie nie pozwalasz żadnej aplikacji na działania\n"
|
||||
"w Twoim imieniu. Jeśli kiedyś dodasz jakieś aplikacje, to ich lista pojawi się tutaj.</p>"
|
||||
|
||||
#~ msgid "from among %d vote"
|
||||
|
||||
#~ msgid_plural "from among %d votes"
|
||||
#~ msgstr[0] "spośród %d oceny"
|
||||
#~ msgstr[1] "spośród %d ocen"
|
||||
#~ msgstr[2] "spośród %d ocen"
|
||||
|
||||
#~ msgid "I don't agree"
|
||||
#~ msgstr "Odmawiam"
|
||||
|
||||
#~ msgid ""
|
||||
#~ "Once you grant access, the application has access to your account until "
|
||||
#~ "the moment you revoke this access on the <a href='%s'>applications "
|
||||
#~ "management</a> page."
|
||||
#~ msgstr ""
|
||||
#~ "Raz udzielona zgoda jest ważna aż do momentu jej wycofania na stronie <a "
|
||||
#~ "href='%s'>zarządzania aplikacjami</a>."
|
||||
|
||||
#~ msgid ""
|
||||
#~ "Application will access your acount via <a href='%s'>OKAPI Framework</a>."
|
||||
#~ msgstr ""
|
||||
#~ "Aplikacja będzie łączyć się z Twoim kontem poprzez <a href='%s'>platformę "
|
||||
#~ "OKAPI</a> (strona w języku angielskim)."
|
||||
|
||||
#~ msgid ""
|
||||
#~ "If you allow this request application will be able to access all methods "
|
||||
#~ "delivered by the OKAPI Framework, i.e. post log entries on geocaches you "
|
||||
#~ "have found."
|
||||
#~ msgstr ""
|
||||
#~ "Uzyskanie zgody na dostęp pozwoli aplikacji na korzystanie ze wszystkich "
|
||||
#~ "metod udostępnianych przez platformę OKAPI (m.in. aplikacja będzie mogła "
|
||||
#~ "umieszczać komentarze pod znajdowanymi przez Ciebie skrzynkami)."
|
||||
|
||||
#~ msgid "You can revoke this permission at any moment."
|
||||
#~ msgstr "Zgodę możesz wycofać w każdym momencie."
|
||||
|
||||
#~ msgid "Test in English"
|
||||
#~ msgstr "Test po polsku"
|
1048
htdocs/okapi/oauth.php
Normal file
1048
htdocs/okapi/oauth.php
Normal file
File diff suppressed because it is too large
Load Diff
169
htdocs/okapi/service_runner.php
Normal file
169
htdocs/okapi/service_runner.php
Normal file
@ -0,0 +1,169 @@
|
||||
<?
|
||||
|
||||
namespace okapi;
|
||||
|
||||
use Exception;
|
||||
|
||||
class OkapiServiceRunner
|
||||
{
|
||||
#
|
||||
# This the list of all available OKAPI methods. All methods on this list become
|
||||
# immediately public and all of them have to be documented. It is not possible
|
||||
# to create an invisible or undocumented OKAPI method. If you want to test your
|
||||
# methods, you should do it in your local development server. If you want to
|
||||
# create a private, "internal" method, you still have to document it properly
|
||||
# (you may describe it as "internal" and accessible to selected consumer keys only).
|
||||
#
|
||||
public static $all_names = array(
|
||||
# Valid format: ^services/[0-9a-z_/]*$ (it means you may use only alphanumeric
|
||||
# characters and the "_" sign in your method names).
|
||||
'services/apisrv/installation',
|
||||
'services/apisrv/installations',
|
||||
'services/apisrv/stats',
|
||||
'services/apiref/method',
|
||||
'services/apiref/method_index',
|
||||
'services/apiref/issue',
|
||||
'services/oauth/request_token',
|
||||
'services/oauth/authorize',
|
||||
'services/oauth/access_token',
|
||||
'services/caches/search/all',
|
||||
'services/caches/search/bbox',
|
||||
'services/caches/search/nearest',
|
||||
'services/caches/search/by_urls',
|
||||
'services/caches/shortcuts/search_and_retrieve',
|
||||
'services/caches/geocache',
|
||||
'services/caches/geocaches',
|
||||
'services/caches/formatters/gpx',
|
||||
'services/caches/formatters/garmin',
|
||||
'services/logs/entries',
|
||||
'services/logs/entry',
|
||||
'services/logs/logs',
|
||||
'services/logs/userlogs',
|
||||
'services/logs/submit',
|
||||
'services/users/user',
|
||||
'services/users/users',
|
||||
'services/users/by_usernames',
|
||||
'services/users/by_username',
|
||||
'services/users/by_internal_id',
|
||||
'services/users/by_internal_ids',
|
||||
'services/replicate/changelog',
|
||||
'services/replicate/fulldump',
|
||||
'services/replicate/info',
|
||||
);
|
||||
|
||||
/** Check if method exists. */
|
||||
public static function exists($service_name)
|
||||
{
|
||||
return in_array($service_name, self::$all_names);
|
||||
}
|
||||
|
||||
/** Get method options (is consumer required etc.). */
|
||||
public static function options($service_name)
|
||||
{
|
||||
if (!self::exists($service_name))
|
||||
throw new Exception();
|
||||
require_once "$service_name.php";
|
||||
try
|
||||
{
|
||||
return call_user_func(array('\\okapi\\'.
|
||||
str_replace('/', '\\', $service_name).'\\WebService', 'options'));
|
||||
} catch (Exception $e)
|
||||
{
|
||||
throw new Exception("Make sure you've declared your WebService class ".
|
||||
"in an valid namespace (".'okapi\\'.str_replace('/', '\\', $service_name)."); ".
|
||||
$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get method documentation file contents (stuff within the XML file).
|
||||
* If you're looking for a parsed representation, use services/apiref/method.
|
||||
*/
|
||||
public static function docs($service_name)
|
||||
{
|
||||
if (!self::exists($service_name))
|
||||
throw new Exception();
|
||||
try {
|
||||
return file_get_contents("$service_name.xml", true);
|
||||
} catch (Exception $e) {
|
||||
throw new Exception("Missing documentation file: $service_name.xml");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the method and return the result.
|
||||
*
|
||||
* OKAPI methods return OkapiHttpResponses, but some MAY also return
|
||||
* PHP objects (see OkapiRequest::construct_inside_request for details).
|
||||
*
|
||||
* If $request must be consistent with given method's options (must
|
||||
* include Consumer and Token, if they are required).
|
||||
*/
|
||||
public static function call($service_name, OkapiRequest $request)
|
||||
{
|
||||
Okapi::init_internals();
|
||||
|
||||
if (!self::exists($service_name))
|
||||
throw new Exception("Method does not exist: '$service_name'");
|
||||
|
||||
$options = self::options($service_name);
|
||||
if ($options['min_auth_level'] >= 2 && $request->consumer == null)
|
||||
{
|
||||
throw new Exception("Method '$service_name' called with mismatched OkapiRequest: ".
|
||||
"\$request->consumer MAY NOT be empty for Level 2 and Level 3 methods. Provide ".
|
||||
"a dummy Consumer if you have to.");
|
||||
}
|
||||
if ($options['min_auth_level'] >= 3 && $request->token == null)
|
||||
{
|
||||
throw new Exception("Method '$service_name' called with mismatched OkapiRequest: ".
|
||||
"\$request->token MAY NOT be empty for Level 3 methods.");
|
||||
}
|
||||
|
||||
$time_started = microtime(true);
|
||||
Okapi::gettext_domain_init();
|
||||
try
|
||||
{
|
||||
require_once "$service_name.php";
|
||||
$response = call_user_func(array('\\okapi\\'.
|
||||
str_replace('/', '\\', $service_name).'\\WebService', 'call'), $request);
|
||||
Okapi::gettext_domain_restore();
|
||||
} catch (Exception $e) {
|
||||
Okapi::gettext_domain_restore();
|
||||
throw $e;
|
||||
}
|
||||
$runtime = microtime(true) - $time_started;
|
||||
|
||||
# Log the request to the stats table. Only valid requests (these which didn't end up
|
||||
# with an exception) are logged.
|
||||
self::save_stats($service_name, $request, $runtime);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
private static function save_stats($service_name, OkapiRequest $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
|
||||
$calltype = 'internal';
|
||||
|
||||
Db::execute("
|
||||
insert into okapi_stats_temp (`datetime`, consumer_key, user_id, service_name, calltype, runtime)
|
||||
values (
|
||||
now(),
|
||||
'".mysql_real_escape_string($consumer_key)."',
|
||||
'".mysql_real_escape_string($user_id)."',
|
||||
'".mysql_real_escape_string($service_name)."',
|
||||
'".mysql_real_escape_string($calltype)."',
|
||||
'".mysql_real_escape_string($runtime)."'
|
||||
);
|
||||
");
|
||||
}
|
||||
}
|
75
htdocs/okapi/services/apiref/issue.php
Normal file
75
htdocs/okapi/services/apiref/issue.php
Normal file
@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace okapi\services\apiref\issue;
|
||||
|
||||
use Exception;
|
||||
use ErrorException;
|
||||
use okapi\Okapi;
|
||||
use okapi\OkapiRequest;
|
||||
use okapi\BadRequest;
|
||||
use okapi\ParamMissing;
|
||||
use okapi\InvalidParam;
|
||||
use okapi\OkapiServiceRunner;
|
||||
use okapi\OkapiInternalRequest;
|
||||
use okapi\Cache;
|
||||
|
||||
class WebService
|
||||
{
|
||||
public static function options()
|
||||
{
|
||||
return array(
|
||||
'min_auth_level' => 0
|
||||
);
|
||||
}
|
||||
|
||||
public static function call(OkapiRequest $request)
|
||||
{
|
||||
$issue_id = $request->get_parameter('issue_id');
|
||||
if (!$issue_id)
|
||||
throw new ParamMissing('issue_id');
|
||||
if ((!preg_match("/^[0-9]+$/", $issue_id)) || (strlen($issue_id) > 6))
|
||||
throw new InvalidParam('issue_id');
|
||||
|
||||
$cache_key = "apiref/issue#".$issue_id;
|
||||
$result = Cache::get($cache_key);
|
||||
if ($result == null)
|
||||
{
|
||||
# Download list of comments from Google Code Issue Tracker.
|
||||
|
||||
try
|
||||
{
|
||||
$opts = array(
|
||||
'http' => array(
|
||||
'method' => "GET",
|
||||
'timeout' => 2.0
|
||||
)
|
||||
);
|
||||
$context = stream_context_create($opts);
|
||||
$xml = file_get_contents("http://code.google.com/feeds/issues/p/opencaching-api/issues/$issue_id/comments/full",
|
||||
false, $context);
|
||||
}
|
||||
catch (ErrorException $e)
|
||||
{
|
||||
throw new BadRequest("Sorry, we could not retrieve issue stats from the Google Code site. ".
|
||||
"This is probably due to a temporary connection problem. Try again later or contact ".
|
||||
"us if this seems permanent.");
|
||||
}
|
||||
|
||||
$doc = simplexml_load_string($xml);
|
||||
$result = array(
|
||||
'id' => $issue_id + 0,
|
||||
'last_updated' => (string)$doc->updated,
|
||||
'title' => (string)$doc->title,
|
||||
'url' => (string)$doc->link[0]['href'],
|
||||
'comment_count' => $doc->entry->count()
|
||||
);
|
||||
|
||||
# On one hand, we want newly added comments to show up quickly.
|
||||
# On the other, we don't want OKAPI to contantly query Google Code.
|
||||
# It's difficult to choose a correct timeout for this...
|
||||
|
||||
Cache::set($cache_key, $result, 3600);
|
||||
}
|
||||
return Okapi::formatted_response($request, $result);
|
||||
}
|
||||
}
|
28
htdocs/okapi/services/apiref/issue.xml
Normal file
28
htdocs/okapi/services/apiref/issue.xml
Normal file
@ -0,0 +1,28 @@
|
||||
<xml>
|
||||
<brief>Retrieve information on given issue</brief>
|
||||
<issue-id>11</issue-id>
|
||||
<desc>
|
||||
<p>OKAPI is trying to be as <b>integrated</b> with its
|
||||
<a href='http://code.google.com/p/opencaching-api/'>Main Project Page</a> as it can.
|
||||
This method retrieves basic information on a given issue from our project
|
||||
<a href='http://code.google.com/p/opencaching-api/issues/list'>Issue Tracker</a>.
|
||||
In future, it <b>might</b> also return some of the latest comments (we're not yet
|
||||
sure if we want them displayed on our documentation pages).</p>
|
||||
</desc>
|
||||
<req name='issue_id'>
|
||||
ID of an Issue.
|
||||
</req>
|
||||
<common-format-params/>
|
||||
<returns>
|
||||
<p>A dictionary of the following structure:</p>
|
||||
<ul>
|
||||
<li><b>id</b> - number of the issue created for this method,</li>
|
||||
<li><b>last_updated</b> - date and time (ISO 8601) when the issue was last updated,</li>
|
||||
<li><b>title</b> - issue title,</li>
|
||||
<li><b>url</b> - URL of the issue page,</li>
|
||||
<li><b>comment_count</b> - total number of submitted comments.</li>
|
||||
</ul>
|
||||
<p>Note, that this will respond with HTTP 400 if we fail to retrieve data from
|
||||
the Google Code site.</p>
|
||||
</returns>
|
||||
</xml>
|
127
htdocs/okapi/services/apiref/method.php
Normal file
127
htdocs/okapi/services/apiref/method.php
Normal file
@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
namespace okapi\services\apiref\method;
|
||||
|
||||
use Exception;
|
||||
use okapi\Okapi;
|
||||
use okapi\OkapiRequest;
|
||||
use okapi\ParamMissing;
|
||||
use okapi\InvalidParam;
|
||||
use okapi\OkapiServiceRunner;
|
||||
use okapi\OkapiInternalRequest;
|
||||
use okapi\OkapiInternalConsumer;
|
||||
|
||||
class WebService
|
||||
{
|
||||
public static function options()
|
||||
{
|
||||
return array(
|
||||
'min_auth_level' => 0
|
||||
);
|
||||
}
|
||||
|
||||
private static function arg_desc($arg_node)
|
||||
{
|
||||
$attrs = $arg_node->attributes();
|
||||
return array(
|
||||
'name' => (string)$attrs['name'],
|
||||
'is_required' => $arg_node->getName() == 'req',
|
||||
'class' => 'public',
|
||||
'description' =>
|
||||
(isset($attrs['default']) ? ("<p>Default value: <b>".$attrs['default']."</b></p>") : "").
|
||||
self::get_inner_xml($arg_node),
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
private static function get_inner_xml($node)
|
||||
{
|
||||
$s = $node->asXML();
|
||||
$start = strpos($s, ">") + 1;
|
||||
$length = strlen($s) - $start - (3 + strlen($node->getName()));
|
||||
return substr($s, $start, $length);
|
||||
}
|
||||
|
||||
public static function call(OkapiRequest $request)
|
||||
{
|
||||
$methodname = $request->get_parameter('name');
|
||||
if (!$methodname)
|
||||
throw new ParamMissing('name');
|
||||
if (!preg_match("#^services/[0-9a-z_/]*$#", $methodname))
|
||||
throw new InvalidParam('name');
|
||||
if (!OkapiServiceRunner::exists($methodname))
|
||||
throw new InvalidParam('name', "Method does not exist: '$methodname'.");
|
||||
$options = OkapiServiceRunner::options($methodname);
|
||||
if (!isset($options['min_auth_level']))
|
||||
throw new Exception("Method $methodname is missing a required 'min_auth_level' option!");
|
||||
$docs = simplexml_load_string(OkapiServiceRunner::docs($methodname));
|
||||
$exploded = explode("/", $methodname);
|
||||
$result = array(
|
||||
'name' => $methodname,
|
||||
'short_name' => end($exploded),
|
||||
'ref_url' => $GLOBALS['absolute_server_URI']."okapi/$methodname.html",
|
||||
'auth_options' => array(
|
||||
'min_auth_level' => $options['min_auth_level'],
|
||||
'oauth_consumer' => $options['min_auth_level'] >= 2,
|
||||
'oauth_token' => $options['min_auth_level'] >= 3,
|
||||
)
|
||||
);
|
||||
if (!$docs->brief)
|
||||
throw new Exception("Missing <brief> element in the $methodname.xml file.");
|
||||
if ($docs->brief != self::get_inner_xml($docs->brief))
|
||||
throw new Exception("The <brief> element may not contain HTML markup ($methodname.xml).");
|
||||
if (strlen($docs->brief) > 80)
|
||||
throw new Exception("The <brief> description may not be longer than 80 characters ($methodname.xml).");
|
||||
if (strpos($docs->brief, "\n") !== false)
|
||||
throw new Exception("The <brief> element may not contain new-lines ($methodname.xml).");
|
||||
if (substr(trim($docs->brief), -1) == '.')
|
||||
throw new Exception("The <brief> element should not end with a dot ($methodname.xml).");
|
||||
$result['brief_description'] = self::get_inner_xml($docs->brief);
|
||||
if ($docs->{'issue-id'})
|
||||
$result['issue_id'] = (string)$docs->{'issue-id'};
|
||||
else
|
||||
$result['issue_id'] = null;
|
||||
if (!$docs->desc)
|
||||
throw new Exception("Missing <desc> element in the $methodname.xml file.");
|
||||
$result['description'] = self::get_inner_xml($docs->desc);
|
||||
$result['arguments'] = array();
|
||||
foreach ($docs->req as $arg) { $result['arguments'][] = self::arg_desc($arg); }
|
||||
foreach ($docs->opt as $arg) { $result['arguments'][] = self::arg_desc($arg); }
|
||||
foreach ($docs->{'import-params'} as $import_desc)
|
||||
{
|
||||
$attrs = $import_desc->attributes();
|
||||
$referenced_methodname = $attrs['method'];
|
||||
$referenced_method_info = OkapiServiceRunner::call('services/apiref/method',
|
||||
new OkapiInternalRequest(new OkapiInternalConsumer(), null, array('name' => $referenced_methodname)));
|
||||
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>";
|
||||
$arg['class'] = 'inherited';
|
||||
$result['arguments'][] = $arg;
|
||||
}
|
||||
}
|
||||
if ($docs->{'common-format-params'})
|
||||
{
|
||||
$result['arguments'][] = array(
|
||||
'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>"
|
||||
);
|
||||
$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>"
|
||||
);
|
||||
}
|
||||
if (!$docs->returns)
|
||||
throw new Exception("Missing <returns> element in the $methodname.xml file. ".
|
||||
"If your method does not return anything, you should document in nonetheless.");
|
||||
$result['returns'] = self::get_inner_xml($docs->returns);
|
||||
return Okapi::formatted_response($request, $result);
|
||||
}
|
||||
}
|
54
htdocs/okapi/services/apiref/method.xml
Normal file
54
htdocs/okapi/services/apiref/method.xml
Normal file
@ -0,0 +1,54 @@
|
||||
<xml>
|
||||
<brief>Get information on a given OKAPI service method</brief>
|
||||
<issue-id>13</issue-id>
|
||||
<desc>
|
||||
<p>This method allows you to <b>access API documentation</b> (the same which you
|
||||
are reading just now). Given a method name, it returns a complete method description.</p>
|
||||
</desc>
|
||||
<req name='name'>
|
||||
Name of a method (begins with "services/").
|
||||
</req>
|
||||
<common-format-params/>
|
||||
<returns>
|
||||
<p>A dictionary of the following structure:</p>
|
||||
<ul>
|
||||
<li><b>name</b> - name of the method,</li>
|
||||
<li><b>short_name</b> - name without a path,</li>
|
||||
<li><b>issue_id</b> - ID of the "general discussion" issue (in our project's
|
||||
homepage Issue Tracker) associated with this method <b>or null</b> if this
|
||||
method has associated issue,</li>
|
||||
<li><b>description</b> - HTML-formatted description of what the method does,</li>
|
||||
<li><b>brief_description</b> - brief (max 80 characters), single-line,
|
||||
plain-text description of what the method does,</li>
|
||||
<li><b>ref_url</b> - URL of the documentation page with method description,</li>
|
||||
<li>
|
||||
<b>auth_options</b> - a dictionary which describes authentication
|
||||
requirements for this method, it has a following structure:
|
||||
<ul>
|
||||
<li><b>min_auth_level</b> - integer, in range from 0 to 3,
|
||||
see Introduction page.</li>
|
||||
<li><b>oauth_consumer</b> - "required" when min_auth_level >= 2,
|
||||
"optional" otherwise,</li>
|
||||
<li><b>oauth_token</b> - "required" when min_auth_level == 3,
|
||||
"optional" otherwise.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><b>arguments</b> - list of dictionaries, describes method
|
||||
arguments. Each dictionary has a following structure:
|
||||
<ul>
|
||||
<li><b>name</b> - name of an argument,</li>
|
||||
<li><b>is_required</b> - boolean, true if the argument is required,</li>
|
||||
<li><b>description</b> - HTML-formatted description of an argument.</li>
|
||||
<li>
|
||||
<p><b>class</b> - one of the following values: <i>public</i>,
|
||||
<i>inherited</i> or <i>common-formatting</i> (other values might be
|
||||
introduced in future).</p>
|
||||
<p>Currently these values do not mean anything specific. They are
|
||||
used for different coloring/styling in the documentation pages.</p>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><b>returns</b> - HTML-formatted description method's return value.</li>
|
||||
</ul>
|
||||
</returns>
|
||||
</xml>
|
47
htdocs/okapi/services/apiref/method_index.php
Normal file
47
htdocs/okapi/services/apiref/method_index.php
Normal file
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace okapi\services\apiref\method_index;
|
||||
|
||||
use okapi\OkapiInternalRequest;
|
||||
|
||||
use Exception;
|
||||
use okapi\Okapi;
|
||||
use okapi\OkapiRequest;
|
||||
use okapi\ParamMissing;
|
||||
use okapi\InvalidParam;
|
||||
use okapi\OkapiServiceRunner;
|
||||
use okapi\Cache;
|
||||
use okapi\OkapiInternalConsumer;
|
||||
|
||||
class WebService
|
||||
{
|
||||
public static function options()
|
||||
{
|
||||
return array(
|
||||
'min_auth_level' => 0
|
||||
);
|
||||
}
|
||||
|
||||
public static function call(OkapiRequest $request)
|
||||
{
|
||||
$cache_key = "api_ref/method_index";
|
||||
$results = Cache::get($cache_key);
|
||||
if ($results == null)
|
||||
{
|
||||
$methodnames = OkapiServiceRunner::$all_names;
|
||||
sort($methodnames);
|
||||
$results = array();
|
||||
foreach ($methodnames as $methodname)
|
||||
{
|
||||
$info = OkapiServiceRunner::call('services/apiref/method', new OkapiInternalRequest(
|
||||
new OkapiInternalConsumer(), null, array('name' => $methodname)));
|
||||
$results[] = array(
|
||||
'name' => $info['name'],
|
||||
'brief_description' => $info['brief_description'],
|
||||
);
|
||||
}
|
||||
Cache::set($cache_key, $results, 3600);
|
||||
}
|
||||
return Okapi::formatted_response($request, $results);
|
||||
}
|
||||
}
|
17
htdocs/okapi/services/apiref/method_index.xml
Normal file
17
htdocs/okapi/services/apiref/method_index.xml
Normal file
@ -0,0 +1,17 @@
|
||||
<xml>
|
||||
<brief>Get a list of OKAPI methods with brief descriptions</brief>
|
||||
<issue-id>12</issue-id>
|
||||
<desc>
|
||||
<p>Get a list of OKAPI methods with brief descriptions.</p>
|
||||
</desc>
|
||||
<common-format-params/>
|
||||
<returns>
|
||||
A list of dictionaries, each of which contains one API
|
||||
method description in the following format:
|
||||
<ul>
|
||||
<li><b>name</b> - name of a method,</li>
|
||||
<li><b>brief_description</b> - brief (max 80 characters), single-line,
|
||||
plain-text description of what the method does.</li>
|
||||
</ul>
|
||||
</returns>
|
||||
</xml>
|
31
htdocs/okapi/services/apisrv/installation.php
Normal file
31
htdocs/okapi/services/apisrv/installation.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace okapi\services\apisrv\installation;
|
||||
|
||||
use Exception;
|
||||
use okapi\Okapi;
|
||||
use okapi\OkapiRequest;
|
||||
use okapi\ParamMissing;
|
||||
use okapi\InvalidParam;
|
||||
use okapi\OkapiServiceRunner;
|
||||
use okapi\OkapiInternalRequest;
|
||||
|
||||
class WebService
|
||||
{
|
||||
public static function options()
|
||||
{
|
||||
return array(
|
||||
'min_auth_level' => 0
|
||||
);
|
||||
}
|
||||
|
||||
public static function call(OkapiRequest $request)
|
||||
{
|
||||
$result = array();
|
||||
$result['site_url'] = $GLOBALS['absolute_server_URI'];
|
||||
$result['okapi_base_url'] = $result['site_url']."okapi/";
|
||||
$result['site_name'] = Okapi::get_normalized_site_name();
|
||||
$result['okapi_revision'] = Okapi::$revision;
|
||||
return Okapi::formatted_response($request, $result);
|
||||
}
|
||||
}
|
31
htdocs/okapi/services/apisrv/installation.xml
Normal file
31
htdocs/okapi/services/apisrv/installation.xml
Normal file
@ -0,0 +1,31 @@
|
||||
<xml>
|
||||
<brief>Get information on this OKAPI installation</brief>
|
||||
<issue-id>14</issue-id>
|
||||
<desc>
|
||||
Retrieve some basic information about this OKAPI installation.
|
||||
</desc>
|
||||
<common-format-params/>
|
||||
<returns>
|
||||
<p>A dictionary of the following structure:</p>
|
||||
<ul>
|
||||
<li>
|
||||
<b>site_url</b> - URL of the OpenCaching site which is running
|
||||
the OKAPI installation (usually this looks like
|
||||
"http://www.opencaching.<i>xx</i>/", where <b>xx</b> is a top
|
||||
level domain of a country).
|
||||
</li>
|
||||
<li>
|
||||
<b>okapi_base_url</b> - URL of the OKAPI installation (usually this is
|
||||
<b>site_url</b> with "okapi/" appended, but you should not assume
|
||||
that); this value is to be used as a prefix when constructing service
|
||||
method URLs,
|
||||
</li>
|
||||
<li>
|
||||
<b>site_name</b> - international name of the OpenCaching site,
|
||||
</li>
|
||||
<li><b>okapi_revision</b> - integer, an SVN revision of the OKAPI project
|
||||
installed on this OpenCaching site, <b>or null</b>, when could not
|
||||
determine revision number.</li>
|
||||
</ul>
|
||||
</returns>
|
||||
</xml>
|
117
htdocs/okapi/services/apisrv/installations.php
Normal file
117
htdocs/okapi/services/apisrv/installations.php
Normal file
@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
namespace okapi\services\apisrv\installations;
|
||||
|
||||
use Exception;
|
||||
use ErrorException;
|
||||
use okapi\Okapi;
|
||||
use okapi\Cache;
|
||||
use okapi\OkapiRequest;
|
||||
use okapi\ParamMissing;
|
||||
use okapi\InvalidParam;
|
||||
use okapi\OkapiServiceRunner;
|
||||
use okapi\OkapiInternalRequest;
|
||||
|
||||
class WebService
|
||||
{
|
||||
public static function options()
|
||||
{
|
||||
return array(
|
||||
'min_auth_level' => 0
|
||||
);
|
||||
}
|
||||
|
||||
public static function call(OkapiRequest $request)
|
||||
{
|
||||
# The list of installations is periodically refreshed by contacting OKAPI
|
||||
# repository. This method usually displays the cached version of it.
|
||||
|
||||
$cachekey = 'apisrv/installations';
|
||||
$backupkey = 'apisrv/installations-backup';
|
||||
$results = Cache::get($cachekey);
|
||||
if (!$results)
|
||||
{
|
||||
# Download the current list of OKAPI servers.
|
||||
|
||||
try
|
||||
{
|
||||
$opts = array(
|
||||
'http' => array(
|
||||
'method' => "GET",
|
||||
'timeout' => 5.0
|
||||
)
|
||||
);
|
||||
$context = stream_context_create($opts);
|
||||
$xml = file_get_contents("http://opencaching-api.googlecode.com/svn/trunk/etc/installations.xml",
|
||||
false, $context);
|
||||
}
|
||||
catch (ErrorException $e)
|
||||
{
|
||||
# Google failed on us. Try to respond with a backup list.
|
||||
|
||||
$results = Cache::get($backupkey);
|
||||
if ($results)
|
||||
{
|
||||
Cache::set($cachekey, $results, 12 * 3600); # so to retry no earlier than after 12 hours
|
||||
return Okapi::formatted_response($request, $results);
|
||||
}
|
||||
|
||||
# Backup has expired (or have never been cached). If we're on a development
|
||||
# server then probably it's okay. In production this SHOULD NOT happen.
|
||||
|
||||
$results = array(
|
||||
array(
|
||||
'site_url' => $GLOBALS['absolute_server_URI'],
|
||||
'site_name' => "Unable to retrieve!",
|
||||
'okapi_base_url' => $GLOBALS['absolute_server_URI']."okapi/",
|
||||
)
|
||||
);
|
||||
Cache::set($cachekey, $results, 12 * 3600); # so to retry no earlier than after 12 hours
|
||||
return Okapi::formatted_response($request, $results);
|
||||
}
|
||||
|
||||
$doc = simplexml_load_string($xml);
|
||||
$results = array();
|
||||
$i_was_included = false;
|
||||
foreach ($doc->installation as $inst)
|
||||
{
|
||||
$site_url = (string)$inst[0]['site_url'];
|
||||
if ($inst[0]['okapi_base_url'])
|
||||
$okapi_base_url = (string)$inst[0]['okapi_base_url'];
|
||||
else
|
||||
$okapi_base_url = $site_url."okapi/";
|
||||
if ($inst[0]['site_name'])
|
||||
$site_name = (string)$inst[0]['site_name'];
|
||||
else
|
||||
$site_name = Okapi::get_normalized_site_name($site_url);
|
||||
$results[] = array(
|
||||
'site_url' => $site_url,
|
||||
'site_name' => $site_name,
|
||||
'okapi_base_url' => $okapi_base_url,
|
||||
);
|
||||
if ($site_url == $GLOBALS['absolute_server_URI'])
|
||||
$i_was_included = true;
|
||||
}
|
||||
|
||||
# If running on a local development installation, then include the local
|
||||
# installation URL.
|
||||
|
||||
if (!$i_was_included)
|
||||
{
|
||||
$results[] = array(
|
||||
'site_url' => $GLOBALS['absolute_server_URI'],
|
||||
'site_name' => "DEVELSITE",
|
||||
'okapi_base_url' => $GLOBALS['absolute_server_URI']."okapi/",
|
||||
);
|
||||
# Contact OKAPI developers in order to get added to the official sites list!
|
||||
}
|
||||
|
||||
# Cache it for one day. Also, save a backup (valid for 30 days).
|
||||
|
||||
Cache::set($cachekey, $results, 86400);
|
||||
Cache::set($backupkey, $results, 86400*30);
|
||||
}
|
||||
|
||||
return Okapi::formatted_response($request, $results);
|
||||
}
|
||||
}
|
33
htdocs/okapi/services/apisrv/installations.xml
Normal file
33
htdocs/okapi/services/apisrv/installations.xml
Normal file
@ -0,0 +1,33 @@
|
||||
<xml>
|
||||
<brief>Get the list of all public OKAPI installations</brief>
|
||||
<issue-id>39</issue-id>
|
||||
<desc>
|
||||
Get the list of all public OKAPI installations. Keep in mind that
|
||||
OKAPI installations might differ slightly. If you plan on using
|
||||
multiple OKAPI installations in your application (which is a very
|
||||
good thing!) you should test it against the one with the <b>lowest</b>
|
||||
OKAPI revision number.
|
||||
</desc>
|
||||
<common-format-params/>
|
||||
<returns>
|
||||
<p>A dictionary of the following structure:</p>
|
||||
<ul>
|
||||
<li>
|
||||
<b>site_url</b> - URL of the OpenCaching site which is running
|
||||
the OKAPI installation (usually this looks like
|
||||
"http://www.opencaching.<i>xx</i>/", where <b>xx</b> is a top
|
||||
level domain of a country).
|
||||
</li>
|
||||
<li>
|
||||
<b>site_name</b> - universal name for this site (should be fine
|
||||
for all languages),
|
||||
</li>
|
||||
<li>
|
||||
<b>okapi_base_url</b> - URL of the OKAPI installation (usually this is
|
||||
<b>site_url</b> with "okapi/" appended, but you should not assume
|
||||
that); this value is to be used as a prefix when constructing service
|
||||
method URLs.
|
||||
</li>
|
||||
</ul>
|
||||
</returns>
|
||||
</xml>
|
62
htdocs/okapi/services/apisrv/stats.php
Normal file
62
htdocs/okapi/services/apisrv/stats.php
Normal file
@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace okapi\services\apisrv\stats;
|
||||
|
||||
use Exception;
|
||||
use okapi\Okapi;
|
||||
use okapi\Db;
|
||||
use okapi\Cache;
|
||||
use okapi\OkapiRequest;
|
||||
use okapi\ParamMissing;
|
||||
use okapi\InvalidParam;
|
||||
use okapi\OkapiServiceRunner;
|
||||
use okapi\OkapiInternalRequest;
|
||||
use okapi\Settings;
|
||||
|
||||
class WebService
|
||||
{
|
||||
public static function options()
|
||||
{
|
||||
return array(
|
||||
'min_auth_level' => 0
|
||||
);
|
||||
}
|
||||
|
||||
public static function call(OkapiRequest $request)
|
||||
{
|
||||
$cachekey = "apisrv/stats";
|
||||
$result = Cache::get($cachekey);
|
||||
if (!$result)
|
||||
{
|
||||
$result = array(
|
||||
'cache_count' => 0 + Db::select_value("
|
||||
select count(*) from caches where status in (1,2,3)
|
||||
"),
|
||||
'user_count' => 0 + Db::select_value("
|
||||
select count(*) from (
|
||||
select distinct user_id
|
||||
from cache_logs
|
||||
where
|
||||
type in (1,2)
|
||||
and ".((Settings::get('OC_BRANCH') == 'oc.pl') ? "deleted = 0" : "true")."
|
||||
UNION DISTINCT
|
||||
select distinct user_id
|
||||
from caches
|
||||
) as t;
|
||||
"),
|
||||
'apps_count' => 0 + Db::select_value("select count(*) from okapi_consumers;"),
|
||||
'apps_active' => 0 + Db::select_value("
|
||||
select count(distinct s.consumer_key)
|
||||
from
|
||||
okapi_stats_hourly s,
|
||||
okapi_consumers c
|
||||
where
|
||||
s.consumer_key = c.`key`
|
||||
and s.period_start > date_add(now(), interval -30 day)
|
||||
"),
|
||||
);
|
||||
Cache::set($cachekey, $result, 86400); # cache it for one day
|
||||
}
|
||||
return Okapi::formatted_response($request, $result);
|
||||
}
|
||||
}
|
20
htdocs/okapi/services/apisrv/stats.xml
Normal file
20
htdocs/okapi/services/apisrv/stats.xml
Normal file
@ -0,0 +1,20 @@
|
||||
<xml>
|
||||
<brief>Get some basic stats about the site</brief>
|
||||
<issue-id>43</issue-id>
|
||||
<desc>
|
||||
Retrieve some basic statistics about this OKAPI installation.
|
||||
If you want some more stats, post a comment!
|
||||
</desc>
|
||||
<common-format-params/>
|
||||
<returns>
|
||||
<p>A dictionary of the following structure:</p>
|
||||
<ul>
|
||||
<li><b>cache_count</b> - approximate total number of geocaches stored at this site,</li>
|
||||
<li><b>user_count</b> - approximate total number of active users of this site,</li>
|
||||
<li><b>apps_count</b> - approximate total number of all OKAPI applications (number of
|
||||
registered API Keys).</li>
|
||||
<li><b>apps_active</b> - approximate number of active OKAPI applications (the ones which issued
|
||||
at least one, non-anonymous OKAPI request during the last month).</li>
|
||||
</ul>
|
||||
</returns>
|
||||
</xml>
|
164
htdocs/okapi/services/caches/formatters/garmin.php
Normal file
164
htdocs/okapi/services/caches/formatters/garmin.php
Normal file
@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
namespace okapi\services\caches\formatters\garmin;
|
||||
|
||||
use okapi\Okapi;
|
||||
use okapi\OkapiRequest;
|
||||
use okapi\OkapiHttpResponse;
|
||||
use okapi\OkapiInternalRequest;
|
||||
use okapi\OkapiServiceRunner;
|
||||
use okapi\BadRequest;
|
||||
use okapi\ParamMissing;
|
||||
use okapi\InvalidParam;
|
||||
use okapi\OkapiAccessToken;
|
||||
use okapi\services\caches\search\SearchAssistant;
|
||||
|
||||
use \ZipArchive;
|
||||
|
||||
class WebService
|
||||
{
|
||||
private static $shutdown_function_registered = false;
|
||||
private static $files_to_unlink = array();
|
||||
|
||||
public static function options()
|
||||
{
|
||||
return array(
|
||||
'min_auth_level' => 1
|
||||
);
|
||||
}
|
||||
|
||||
public static function call(OkapiRequest $request)
|
||||
{
|
||||
$cache_codes = $request->get_parameter('cache_codes');
|
||||
if ($cache_codes === null) throw new ParamMissing('cache_codes');
|
||||
|
||||
# Issue 106 requires us to allow empty list of cache codes to be passed into this method.
|
||||
# All of the queries below have to be ready for $cache_codes to be empty!
|
||||
|
||||
$langpref = $request->get_parameter('langpref');
|
||||
if (!$langpref) $langpref = "en";
|
||||
$images = $request->get_parameter('images');
|
||||
if (!$images) $images = "all";
|
||||
if (!in_array($images, array("none", "all", "spoilers", "nonspoilers")))
|
||||
throw new InvalidParam('images');
|
||||
|
||||
# Start creating ZIP archive.
|
||||
|
||||
$tempfilename = sys_get_temp_dir()."/garmin".time().rand(100000,999999).".zip";
|
||||
$zip = new ZipArchive();
|
||||
if ($zip->open($tempfilename, ZIPARCHIVE::CREATE) !== true)
|
||||
throw new Exception("ZipArchive class could not create temp file $tempfilename. Check permissions!");
|
||||
|
||||
# Create basic structure
|
||||
|
||||
$zip->addEmptyDir("Garmin");
|
||||
$zip->addEmptyDir("Garmin/GPX");
|
||||
$zip->addEmptyDir("Garmin/GeocachePhotos");
|
||||
|
||||
# Include a GPX file compatible with Garmin devices. It should include all
|
||||
# Geocaching.com (groundspeak:) and Opencaching.com (ox:) extensions. It will
|
||||
# also include image references (actual images will be added as separate files later)
|
||||
# and personal data (if the method was invoked using Level 3 Authentication).
|
||||
|
||||
$zip->addFromString("Garmin/GPX/opencaching".time().rand(100000,999999).".gpx",
|
||||
OkapiServiceRunner::call('services/caches/formatters/gpx', new OkapiInternalRequest(
|
||||
$request->consumer, $request->token, array(
|
||||
'cache_codes' => $cache_codes,
|
||||
'langpref' => $langpref,
|
||||
'ns_ground' => 'true',
|
||||
'ns_ox' => 'true',
|
||||
'images' => 'ox:all',
|
||||
'attrs' => 'ox:tags',
|
||||
'trackables' => 'desc:count',
|
||||
'alt_wpts' => 'true',
|
||||
'recommendations' => 'desc:count',
|
||||
'latest_logs' => 'true',
|
||||
'lpc' => 'all',
|
||||
'my_notes' => ($request->token != null) ? "desc:text" : "none"
|
||||
)))->get_body());
|
||||
|
||||
# Then, include all the images.
|
||||
|
||||
$caches = OkapiServiceRunner::call('services/caches/geocaches', new OkapiInternalRequest(
|
||||
$request->consumer, $request->token, array('cache_codes' => $cache_codes,
|
||||
'langpref' => $langpref, 'fields' => "images")));
|
||||
if (count($caches) > 50)
|
||||
throw new InvalidParam('cache_codes', "The maximum number of caches allowed to be downloaded with this method is 50.");
|
||||
if ($images != 'none')
|
||||
{
|
||||
foreach ($caches as $cache_code => $dict)
|
||||
{
|
||||
$imgs = $dict['images'];
|
||||
if (count($imgs) == 0)
|
||||
continue;
|
||||
$dir = "Garmin/GeocachePhotos/".$cache_code[strlen($cache_code) - 1];
|
||||
$zip->addEmptyDir($dir); # fails silently if it already exists
|
||||
$dir .= "/".$cache_code[strlen($cache_code) - 2];
|
||||
$zip->addEmptyDir($dir);
|
||||
$dir .= "/".$cache_code;
|
||||
$zip->addEmptyDir($dir);
|
||||
foreach ($imgs as $no => $img)
|
||||
{
|
||||
if ($images == 'spoilers' && (!$img['is_spoiler']))
|
||||
continue;
|
||||
if ($images == 'nonspoilers' && $img['is_spoiler'])
|
||||
continue;
|
||||
if (strtolower(substr($img['url'], strlen($img['url']) - 4)) != ".jpg")
|
||||
continue;
|
||||
if ($img['is_spoiler']) {
|
||||
$zip->addEmptyDir($dir."/Spoilers");
|
||||
$zippath = $dir."/Spoilers/".$img['unique_caption'].".jpg";
|
||||
} else {
|
||||
$zippath = $dir."/".$img['unique_caption'].".jpg";
|
||||
}
|
||||
|
||||
# The safest way would be to use the URL, but that would be painfully slow!
|
||||
# That's why we're trying to access files directly (and fail silently on error).
|
||||
# This was tested on OCPL server only.
|
||||
|
||||
# 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";
|
||||
if (!file_exists($syspath))
|
||||
continue;
|
||||
$file = file_get_contents($syspath);
|
||||
if ($file)
|
||||
$zip->addFromString($zippath, $file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$zip->close();
|
||||
|
||||
# The result could be big. Bigger than our memory limit. We will
|
||||
# return an open file stream instead of a string. We also should
|
||||
# set a higher time limit, because downloading this response may
|
||||
# take some time over slow network connections (and I'm not sure
|
||||
# what is the PHP's default way of handling such scenario).
|
||||
|
||||
set_time_limit(600);
|
||||
$response = new OkapiHttpResponse();
|
||||
$response->content_type = "application/zip";
|
||||
$response->content_disposition = 'Content-Disposition: attachment; filename="results.zip"';
|
||||
$response->stream_length = filesize($tempfilename);
|
||||
$response->body = fopen($tempfilename, "rb");
|
||||
$response->allow_gzip = false;
|
||||
self::add_file_to_unlink($tempfilename);
|
||||
return $response;
|
||||
}
|
||||
|
||||
private static function add_file_to_unlink($filename)
|
||||
{
|
||||
if (!self::$shutdown_function_registered)
|
||||
register_shutdown_function(array("okapi\\services\\caches\\formatters\\garmin\\WebService", "unlink_temporary_files"));
|
||||
self::$files_to_unlink[] = $filename;
|
||||
}
|
||||
|
||||
public static function unlink_temporary_files()
|
||||
{
|
||||
foreach (self::$files_to_unlink as $filename)
|
||||
@unlink($filename);
|
||||
self::$files_to_unlink = array();
|
||||
}
|
||||
}
|
47
htdocs/okapi/services/caches/formatters/garmin.xml
Normal file
47
htdocs/okapi/services/caches/formatters/garmin.xml
Normal file
@ -0,0 +1,47 @@
|
||||
<xml>
|
||||
<brief>Retrieve ZIP file for Garmin devices</brief>
|
||||
<issue-id>99</issue-id>
|
||||
<desc>
|
||||
<p>Produce a ZIP file with content compatible with Geocaching-enabled Garmin
|
||||
GPS devices. The general idea is, <b>you should <u>extract</u> the contents</b> of the ZIP
|
||||
file directly <b>into the root directory</b> of your Garmin's internal memory storage
|
||||
(currently it doesn't work if you extract the data to the external MicroSD card). The device
|
||||
will then be filled with all the data we are currently able to fill it with.</p>
|
||||
<p>Currently, the ZIP file will contain GPX file and all JPEG images associated
|
||||
with the chosen geocaches. Options for the GPX file are fixed, use the
|
||||
services/caches/formatters/gpx method if
|
||||
you want a custom-tailored GPX file.</p>
|
||||
<p><b>Important note:</b> The contents of the returned ZIP archive may change in the
|
||||
future. You should not parse the contents, nor assume that they have any specific
|
||||
structure.</p>
|
||||
<p><b>Important note:</b> We might be increasing required authentication level for
|
||||
this method to level 2 or 3. You should be prepared for that. Use the highest authentication
|
||||
level you've got. Also, if you want the GPX file to include <b>personal data (like user's notes)</b>,
|
||||
you have to use Level 3 Authentication anyway.</p>
|
||||
<p><b>Note:</b> All non-JPEG images will be skipped. Currently OKAPI does not convert
|
||||
other types of images to JPEG.</p>
|
||||
</desc>
|
||||
<req name='cache_codes'>
|
||||
<p>Pipe-separated list of cache codes which you are interested in.
|
||||
No more than 50 codes are allowed. (This limit is smaller than usual, because
|
||||
we're afraid about the sizes of ZIP files produced.)
|
||||
This CAN be an empty string (it will still result in a valid ZIP file).</p>
|
||||
</req>
|
||||
<opt name='langpref' default='en'>
|
||||
<p>Pipe-separated list of ISO 639-1 language codes. This indicates the
|
||||
order of preference in which language will be chosen for GPX entities.</p>
|
||||
</opt>
|
||||
<opt name='images' default='all'>
|
||||
<p>One of the following values:</p>
|
||||
<ul>
|
||||
<li><b>none</b> - no images will be included in the result,</li>
|
||||
<li><b>all</b> - all images will be included in the result,</li>
|
||||
<li><b>spoilers</b> - only spoiler images will be included in the result,</li>
|
||||
<li><b>nonspoilers</b> - only non-spoiler images will be included in the result.</li>
|
||||
</ul>
|
||||
</opt>
|
||||
<returns>
|
||||
<p>ZIP file. You should extract it's contents directly into the root
|
||||
directory of your Garmin's internal memory storage.</p>
|
||||
</returns>
|
||||
</xml>
|
164
htdocs/okapi/services/caches/formatters/gpx.php
Normal file
164
htdocs/okapi/services/caches/formatters/gpx.php
Normal file
@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
namespace okapi\services\caches\formatters\gpx;
|
||||
|
||||
use okapi\Okapi;
|
||||
use okapi\OkapiRequest;
|
||||
use okapi\OkapiHttpResponse;
|
||||
use okapi\OkapiInternalRequest;
|
||||
use okapi\OkapiServiceRunner;
|
||||
use okapi\BadRequest;
|
||||
use okapi\ParamMissing;
|
||||
use okapi\OkapiAccessToken;
|
||||
use okapi\InvalidParam;
|
||||
use okapi\services\caches\search\SearchAssistant;
|
||||
use okapi\OkapiInternalConsumer;
|
||||
|
||||
class WebService
|
||||
{
|
||||
public static function options()
|
||||
{
|
||||
return array(
|
||||
'min_auth_level' => 1
|
||||
);
|
||||
}
|
||||
|
||||
/** Maps OKAPI cache type codes to Geocaching.com GPX cache types. */
|
||||
public static $cache_GPX_types = array(
|
||||
'Traditional' => 'Traditional Cache',
|
||||
'Multi' => 'Multi-Cache',
|
||||
'Quiz' => 'Unknown Cache',
|
||||
'Event' => 'Event Cache',
|
||||
'Virtual' => 'Virtual Cache',
|
||||
'Webcam' => 'Webcam Cache',
|
||||
'Moving' => 'Unknown Cache',
|
||||
'Own' => 'Unknown Cache',
|
||||
'Other' => 'Unknown Cache'
|
||||
);
|
||||
|
||||
/** Maps OpenCaching cache sizes Geocaching.com size codes. */
|
||||
public static $cache_GPX_sizes = array(
|
||||
1 => 'Micro',
|
||||
2 => 'Small',
|
||||
3 => 'Regular',
|
||||
4 => 'Large',
|
||||
5 => 'Large',
|
||||
null => 'Virtual'
|
||||
);
|
||||
/** Maps OpenCaching cache sizes opencaching.com (OX) size codes. */
|
||||
public static $cache_OX_sizes = array(
|
||||
1 => 2,
|
||||
2 => 3,
|
||||
3 => 4,
|
||||
4 => 5,
|
||||
5 => 5,
|
||||
null => null
|
||||
);
|
||||
|
||||
public static function call(OkapiRequest $request)
|
||||
{
|
||||
$vars = array();
|
||||
|
||||
# Validating arguments. We will also assign some of them to the
|
||||
# $vars variable which we will use later in the GPS template.
|
||||
|
||||
$cache_codes = $request->get_parameter('cache_codes');
|
||||
if ($cache_codes === null) throw new ParamMissing('cache_codes');
|
||||
|
||||
# Issue 106 requires us to allow empty list of cache codes to be passed into this method.
|
||||
# All of the queries below have to be ready for $cache_codes to be empty!
|
||||
|
||||
$langpref = $request->get_parameter('langpref');
|
||||
if (!$langpref) $langpref = "en";
|
||||
foreach (array('ns_ground', 'ns_gsak', 'ns_ox', 'latest_logs', 'alt_wpts', 'mark_found') as $param)
|
||||
{
|
||||
$val = $request->get_parameter($param);
|
||||
if (!$val) $val = "false";
|
||||
elseif (!in_array($val, array("true", "false")))
|
||||
throw new InvalidParam($param);
|
||||
$vars[$param] = ($val == "true");
|
||||
}
|
||||
if ($vars['latest_logs'] && (!$vars['ns_ground']))
|
||||
throw new BadRequest("In order for 'latest_logs' to work you have to also include 'ns_ground' extensions.");
|
||||
|
||||
$tmp = $request->get_parameter('my_notes');
|
||||
if (!$tmp) $tmp = "none";
|
||||
if (!in_array($tmp, array("none", "desc:text")))
|
||||
throw new InvalidParam("my_notes");
|
||||
if (($tmp != 'none') && ($request->token == null))
|
||||
throw new BadRequest("Level 3 Authentication is required to access my_notes data.");
|
||||
$vars['my_notes'] = $tmp;
|
||||
|
||||
$images = $request->get_parameter('images');
|
||||
if (!$images) $images = 'descrefs:nonspoilers';
|
||||
if (!in_array($images, array('none', 'descrefs:nonspoilers', 'descrefs:all', 'ox:all')))
|
||||
throw new InvalidParam('images', "'$images'");
|
||||
$vars['images'] = $images;
|
||||
|
||||
$tmp = $request->get_parameter('attrs');
|
||||
if (!$tmp) $tmp = 'desc:text';
|
||||
if (!in_array($tmp, array('none', 'desc:text', 'ox:tags')))
|
||||
throw new InvalidParam('attrs', "'$tmp'");
|
||||
$vars['attrs'] = $tmp;
|
||||
|
||||
$tmp = $request->get_parameter('trackables');
|
||||
if (!$tmp) $tmp = 'none';
|
||||
if (!in_array($tmp, array('none', 'desc:list', 'desc:count')))
|
||||
throw new InvalidParam('trackables', "'$tmp'");
|
||||
$vars['trackables'] = $tmp;
|
||||
|
||||
$tmp = $request->get_parameter('recommendations');
|
||||
if (!$tmp) $tmp = 'none';
|
||||
if (!in_array($tmp, array('none', 'desc:count')))
|
||||
throw new InvalidParam('recommendations', "'$tmp'");
|
||||
$vars['recommendations'] = $tmp;
|
||||
|
||||
$lpc = $request->get_parameter('lpc');
|
||||
if ($lpc === null) $lpc = 10; # will be checked in services/caches/geocaches call
|
||||
|
||||
$user_uuid = $request->get_parameter('user_uuid');
|
||||
|
||||
# We can get all the data we need from the services/caches/geocaches method.
|
||||
# We don't need to do any additional queries here.
|
||||
|
||||
$fields = 'code|name|location|date_created|url|type|status|size'.
|
||||
'|difficulty|terrain|description|hint|rating|owner|url|internal_id';
|
||||
if ($vars['images'] != 'none')
|
||||
$fields .= "|images";
|
||||
if ($vars['attrs'] != 'none')
|
||||
$fields .= "|attrnames";
|
||||
if ($vars['trackables'] == 'desc:list')
|
||||
$fields .= "|trackables";
|
||||
elseif ($vars['trackables'] == 'desc:count')
|
||||
$fields .= "|trackables_count";
|
||||
if ($vars['alt_wpts'] == 'true')
|
||||
$fields .= "|alt_wpts";
|
||||
if ($vars['recommendations'] != 'none')
|
||||
$fields .= "|recommendations|founds";
|
||||
if ($vars['my_notes'] != 'none')
|
||||
$fields .= "|my_notes";
|
||||
if ($vars['latest_logs'])
|
||||
$fields .= "|latest_logs";
|
||||
if ($vars['mark_found'])
|
||||
$fields .= "|is_found";
|
||||
|
||||
$vars['caches'] = OkapiServiceRunner::call('services/caches/geocaches', new OkapiInternalRequest(
|
||||
$request->consumer, $request->token, array('cache_codes' => $cache_codes,
|
||||
'langpref' => $langpref, 'fields' => $fields, 'lpc' => $lpc, 'user_uuid' => $user_uuid)));
|
||||
$vars['installation'] = OkapiServiceRunner::call('services/apisrv/installation', new OkapiInternalRequest(
|
||||
new OkapiInternalConsumer(), null, array()));
|
||||
$vars['cache_GPX_types'] = self::$cache_GPX_types;
|
||||
$vars['cache_GPX_sizes'] = self::$cache_GPX_sizes;
|
||||
$vars['cache_OX_sizes'] = self::$cache_OX_sizes;
|
||||
|
||||
$response = new OkapiHttpResponse();
|
||||
$response->content_type = "text/xml; charset=utf-8";
|
||||
$response->content_disposition = 'Content-Disposition: attachment; filename="results.gpx"';
|
||||
ob_start();
|
||||
Okapi::gettext_domain_init(explode("|", $langpref)); # Consumer gets properly localized GPX file.
|
||||
include 'gpxfile.tpl.php';
|
||||
Okapi::gettext_domain_restore();
|
||||
$response->body = ob_get_clean();
|
||||
return $response;
|
||||
}
|
||||
}
|
140
htdocs/okapi/services/caches/formatters/gpx.xml
Normal file
140
htdocs/okapi/services/caches/formatters/gpx.xml
Normal file
@ -0,0 +1,140 @@
|
||||
<xml>
|
||||
<brief>Retrieve geocaches in GPX format</brief>
|
||||
<issue-id>40</issue-id>
|
||||
<desc>
|
||||
<p>Produces a standards-compliant Geocaching GPX file.</p>
|
||||
<p>Unlike the services/caches/geocaches method responses, GPX files cannot
|
||||
contain names and descriptions in separate multiple languages. This method
|
||||
will attempt to choose the best language based on your preference
|
||||
(see <b>langpref</b> argument).</p>
|
||||
<p>Remember that a full-blown GPX file is much larger than a basic one.
|
||||
This method can produce many various types of GPX files. The simplest list of
|
||||
waypoints takes ~50 times less space than the same list with cache descriptions
|
||||
and log entries included.</p>
|
||||
</desc>
|
||||
<req name='cache_codes'>
|
||||
<p>Pipe-separated list of cache codes which you are interested in.
|
||||
No more than 500 codes are allowed. This CAN be an empty string (it will
|
||||
result in an empty, but valid, GPX file). All invalid cache codes will
|
||||
be skipped without any notice!</p>
|
||||
</req>
|
||||
<opt name='langpref' default='en'>
|
||||
<p>Pipe-separated list of ISO 639-1 language codes. This indicates the
|
||||
order of preference in which language will be chosen for GPX entities.</p>
|
||||
</opt>
|
||||
<opt name='ns_ground' default='false'>
|
||||
Boolean. If <b>true</b> then response will include
|
||||
<a href='http://www.groundspeak.com/cache/1/0/cache.xsd'>Groundspeak's
|
||||
GPX extension</a>. This namespace declares an extra <cache> element
|
||||
used by <a href='http://www.geocaching.com/'>geocaching.com</a> and <b>many others</b>.
|
||||
This namespace was initially associatied with geocaching.com, but now seems to
|
||||
be used in every geocaching GPX file. You probably want to have it.
|
||||
</opt>
|
||||
<opt name='ns_gsak' default='false'>
|
||||
<p>Boolean. If <b>true</b> then response will include
|
||||
<a href='http://www.gsak.net/xmlv1/5/gsak.xsd'>GSAK GPX extension</a>.
|
||||
This namespace declares an extra <wptExtension> element,
|
||||
which allows including "waypoint inheritance" information (parent-child relations).
|
||||
This in turn allows us to include "alternate waypoints" in the response.</p>
|
||||
<p>We know of <b>at least</b> <a href='http://www.gsak.net/'>one tool</a> that
|
||||
makes use of this.</p>
|
||||
</opt>
|
||||
<opt name='ns_ox' default='false'>
|
||||
Boolean. If <b>true</b> then response will include Garmin's
|
||||
<a href='http://www.opencaching.com/xmlschemas/opencaching/1/0/opencaching.xsd'>OpenCaching.com
|
||||
GPX extension</a>. This namespace declares an extra <opencaching> element
|
||||
used by <a href='http://www.opencaching.com/'>Garmin's Opencaching.com</a>.
|
||||
The element includes information on cache difficulty, ratings, tags and images.
|
||||
It is used within Garmin's Geocaching-enabled handheld GPS devices.
|
||||
</opt>
|
||||
<opt name='images' default='descrefs:nonspoilers'>
|
||||
<p>Which images to include (and how to include them). One of the following values:</p>
|
||||
<ul>
|
||||
<li><b>none</b> - no images will be included,</li>
|
||||
<li><b>descrefs:nonspoilers</b> - all non-spoiler images will be included
|
||||
as <img> references at the end of each cache description,</li>
|
||||
<li><b>descrefs:all</b> - all images will be included (including spoilers)
|
||||
as <img> references at the end of each cache description,</li>
|
||||
<li><b>ox:all</b> - all images will be included (including spoilers)
|
||||
as Garmin's <ox:image> references.</li>
|
||||
</ul>
|
||||
<p>Note: When using "descrefs:" mode, remember to set <b>ns_ground</b> to <b>true</b>.</p>
|
||||
<p>Note: When using "ox:" mode, remember to set <b>ns_ox</b> to <b>true</b>. You must also
|
||||
include JPEG files along the GPX for this to work properly - see
|
||||
services/caches/formatters/garmin for more information.</p>
|
||||
<p>In the future, more generic ways of including images in GPX files may emerge.</p>
|
||||
</opt>
|
||||
<opt name='attrs' default='desc:text'>
|
||||
<p>This argument controls wether cache attributes are included and how they are included.
|
||||
One of the following values:</p>
|
||||
<ul>
|
||||
<li><b>none</b> - no attributes will be included,</li>
|
||||
<li><b>desc:text</b> - attributes will be listed (in plain-text) within the cache description,</li>
|
||||
<li><b>ox:tags</b> - attributes will be converted to Garmin's ox:tag elements.</li>
|
||||
</ul>
|
||||
<p>Note: When using "desc:" mode, remember to set <b>ns_ground</b> to <b>true</b>.</p>
|
||||
<p>Note: When using "ox:" mode, remember to set <b>ns_ox</b> to <b>true</b>.</p>
|
||||
</opt>
|
||||
<opt name='trackables' default='none'>
|
||||
<p>This argument controls wether information on trackables is included and how it is included.
|
||||
One of the following values:</p>
|
||||
<ul>
|
||||
<li><b>none</b> - no trackables-data will be included,</li>
|
||||
<li><b>desc:list</b> - trackables will be listed (in plain-text) within the cache description,</li>
|
||||
<li><b>desc:count</b> - total number of trackables will be included (in plain-text) within the cache description.</li>
|
||||
</ul>
|
||||
<p>Note: When using "desc:" mode, remember to set <b>ns_ground</b> to <b>true</b>.</p>
|
||||
</opt>
|
||||
<opt name='recommendations' default='none'>
|
||||
<p>This argument controls wether information on recommendations is included and how it is included.
|
||||
One of the following values:</p>
|
||||
<ul>
|
||||
<li><b>none</b> - no recommendations-info will be included,</li>
|
||||
<li><b>desc:count</b> - number of recommendations (and founds) will be included
|
||||
(in plain-text) within the cache description.</li>
|
||||
</ul>
|
||||
<p>Note: When using "desc:" mode, remember to set <b>ns_ground</b> to <b>true</b>.</p>
|
||||
</opt>
|
||||
<opt name='my_notes' default='none'>
|
||||
<p>Allows you to include personal user's notes on each cache. One of the following values:</p>
|
||||
<ul>
|
||||
<li><b>none</b> - don't include personal notes,</li>
|
||||
<li><b>desc:text</b> - include personal notes as part of the cache description.</li>
|
||||
</ul>
|
||||
<p>Note: You need to use Level 3 Authentication in order to set it to anything else than "none".</p>
|
||||
</opt>
|
||||
<opt name='latest_logs' default='false'>
|
||||
<p>Boolean. If <b>true</b> then response will include a couple of
|
||||
latest log entries for this cache (see also the <b>lpc</b> argument).</p>
|
||||
<p>You <b>must</b> set <b>ns_ground</b> argument to <b>true</b> if you want to use this.</p>
|
||||
</opt>
|
||||
<opt name='lpc' default='10'>
|
||||
Log-entries per cache - the number of log entries included in each returned cache.
|
||||
This should be an integer or a special "all" value. Please note, that the <b>ns_ground</b>
|
||||
and <b>latest_logs</b> arguments must be set to <b>true</b> in order for the logs to be included.
|
||||
</opt>
|
||||
<opt name='alt_wpts' default='false'>
|
||||
<p>Boolean. If <b>true</b> then we will include additional (alternate) waypoints in the response.
|
||||
These are all places associated with the cache.</p>
|
||||
<p>Combine this with <b>ns_gsak</b> if you want the result to include additional references between
|
||||
waypoints and their geocaches. Please note that not many applications are currently able to
|
||||
properly understand GSAK metadata.</p>
|
||||
</opt>
|
||||
<opt name='mark_found' default='false'>
|
||||
<p>Boolean. If <b>true</b> then all caches which are already found, will be marked appropriately
|
||||
(i.e. with an "found cache" symbol).</p>
|
||||
<p>This field requires you to use the <b>user_uuid</b> parameter (or Level 3 Authentication).</p>
|
||||
</opt>
|
||||
<opt name="user_uuid">
|
||||
<p>User'd ID. Required to mark found caches using <b>Level 1</b> Authentication.</p>
|
||||
<p>If you use <b>Level 3</b> Authentication, you shouldn't use this parameter. Or, to be exact:</p>
|
||||
<ul>
|
||||
<li><b>user_uuid</b> will be extracted from the Access Token you use. You don't have to provide it.</li>
|
||||
<li>If you provide <b>user_uuid</b>, then it HAS TO match your Access Token. If it doesn't, OKAPI
|
||||
will respond with HTTP 400 error.</li>
|
||||
</ul>
|
||||
</opt>
|
||||
<returns>
|
||||
<p>GPX file. All invalid cache codes will be skipped without any notice!</p>
|
||||
</returns>
|
||||
</xml>
|
169
htdocs/okapi/services/caches/formatters/gpxfile.tpl.php
Normal file
169
htdocs/okapi/services/caches/formatters/gpxfile.tpl.php
Normal file
@ -0,0 +1,169 @@
|
||||
<?
|
||||
|
||||
echo '<?xml version="1.0" encoding="utf-8"?>'."\n";
|
||||
|
||||
?>
|
||||
<gpx xmlns="http://www.topografix.com/GPX/1/0" version="1.0" creator="OKAPI r<?= $vars['installation']['okapi_revision'] ?>"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="
|
||||
http://www.topografix.com/GPX/1/0 http://www.topografix.com/GPX/1/0/gpx.xsd
|
||||
http://www.opencaching.com/xmlschemas/opencaching/1/0 http://www.opencaching.com/xmlschemas/opencaching/1/0/opencaching.xsd
|
||||
http://www.groundspeak.com/cache/1/0 http://www.groundspeak.com/cache/1/0/cache.xsd
|
||||
http://geocaching.com.au/geocache/1 http://geocaching.com.au/geocache/1/geocache.xsd
|
||||
http://www.gsak.net/xmlv1/5 http://www.gsak.net/xmlv1/5/gsak.xsd
|
||||
">
|
||||
<name><?= $vars['installation']['site_name'] ?> Geocache Search Results</name>
|
||||
<desc><?= $vars['installation']['site_name'] ?> Geocache Search Results, downloaded via OKAPI - <?= $vars['installation']['okapi_base_url'] ?></desc>
|
||||
<author><?= $vars['installation']['site_name'] ?></author>
|
||||
<url><?= $vars['installation']['site_url'] ?></url>
|
||||
<urlname><?= $vars['installation']['site_name'] ?></urlname>
|
||||
<time><?= date('c') ?></time>
|
||||
<? foreach ($vars['caches'] as $c) { ?>
|
||||
<? if ($c == null) continue; /* This happens when there is an invalid code in cache_codes */ ?>
|
||||
<? list($lat, $lon) = explode("|", $c['location']); ?>
|
||||
<wpt lat="<?= $lat ?>" lon="<?= $lon ?>">
|
||||
<time><?= $c['date_created'] ?></time>
|
||||
<name><?= $c['code'] ?></name>
|
||||
<desc><?= htmlspecialchars($c['name'], ENT_COMPAT, 'UTF-8') ?> <?= _("hidden by") ?> <?= htmlspecialchars($c['owner']['username'], ENT_COMPAT, 'UTF-8') ?> :: <?= ucfirst($c['type']) ?> Cache (<?= $c['difficulty'] ?>/<?= $c['terrain'] ?><? if ($c['size'] !== null) { echo "/".$c['size']; } else { echo "/X"; } ?>/<?= $c['rating'] ?>)</desc>
|
||||
<url><?= $c['url'] ?></url>
|
||||
<urlname><?= htmlspecialchars($c['name'], ENT_COMPAT, 'UTF-8') ?></urlname>
|
||||
<sym><?= ($vars['mark_found'] && $c['is_found']) ? "Geocache Found" : "Geocache" ?></sym>
|
||||
<type>Geocache|<?= $vars['cache_GPX_types'][$c['type']] ?></type>
|
||||
<? if ($vars['ns_ground']) { /* Does user want us to include Groundspeak's <cache> element? */ ?>
|
||||
<groundspeak:cache archived="<?= ($c['status'] == 'Archived') ? "True" : "False" ?>" available="<?= ($c['status'] == 'Available') ? "True" : "False" ?>" id="<?= $c['internal_id'] ?>" xmlns:groundspeak="http://www.groundspeak.com/cache/1/0/1">
|
||||
<groundspeak:name><?= htmlspecialchars($c['name'], ENT_COMPAT, 'UTF-8') ?></groundspeak:name>
|
||||
<groundspeak:placed_by><?= htmlspecialchars($c['owner']['username'], ENT_COMPAT, 'UTF-8') ?></groundspeak:placed_by>
|
||||
<groundspeak:owner id="<?= $c['owner']['uuid'] ?>"><?= htmlspecialchars($c['owner']['username'], ENT_COMPAT, 'UTF-8') ?></groundspeak:owner>
|
||||
<groundspeak:type><?= $vars['cache_GPX_types'][$c['type']] ?></groundspeak:type>
|
||||
<groundspeak:container><?= $vars['cache_GPX_sizes'][$c['size']] ?></groundspeak:container>
|
||||
<groundspeak:difficulty><?= $c['difficulty'] ?></groundspeak:difficulty>
|
||||
<groundspeak:terrain><?= $c['terrain'] ?></groundspeak:terrain>
|
||||
<groundspeak:long_description html="True">
|
||||
<p>
|
||||
<a href="<?= $c['url'] ?>"><?= htmlspecialchars($c['name'], ENT_COMPAT, 'UTF-8') ?></a>
|
||||
<?= _("hidden by") ?> <a href='<?= $c['owner']['profile_url'] ?>'><?= htmlspecialchars($c['owner']['username'], ENT_COMPAT, 'UTF-8') ?></a><br/>
|
||||
<? if ($vars['recommendations'] == 'desc:count') { /* Does user want us to include recommendations count? */ ?>
|
||||
<?= sprintf(ngettext("%d recommendation", "%d recommendations", $c['recommendations']), $c['recommendations']) ?>
|
||||
(<?= sprintf(ngettext("found %d time", "found %d times", $c['founds']), $c['founds']) ?>).
|
||||
<? } ?>
|
||||
<? if ($vars['trackables'] == 'desc:count') { /* Does user want us to include trackables count? */ ?>
|
||||
<?= sprintf(ngettext("%d trackable", "%d trackables", $c['trackables_count']), $c['trackables_count']) ?>.
|
||||
<? } ?>
|
||||
</p>
|
||||
<? if (($vars['my_notes'] == 'desc:text') && ($c['my_notes'] != null)) { /* Does user want us to include personal notes? */ ?>
|
||||
<p><b><?= _("Personal notes") ?>:</b> <?= htmlspecialchars($c['my_notes'], ENT_COMPAT, 'UTF-8') ?></p>
|
||||
<? } ?>
|
||||
|
||||
<? if ($vars['attrs'] == 'desc:text' && count($c['attrnames']) > 0) { /* Does user want us to include attributes? */ ?>
|
||||
<p><?= _("Attributes") ?>:</p>
|
||||
<ul><li><?= implode("</li><li>", $c['attrnames']) ?></li></ul>
|
||||
<? } ?>
|
||||
<? if ($vars['trackables'] == 'desc:list' && count($c['trackables']) > 0) { /* Does user want us to include trackables list? */ ?>
|
||||
<p><?= _("Trackables") ?>:</p>
|
||||
<ul>
|
||||
<? foreach ($c['trackables'] as $t) { ?>
|
||||
<li><a href='<?= htmlspecialchars($t['url'], ENT_COMPAT, 'UTF-8') ?>'><?= htmlspecialchars($t['name'], ENT_COMPAT, 'UTF-8') ?></a> (<?= $t['code'] ?>)</li>
|
||||
<? } ?>
|
||||
</ul>
|
||||
<? } ?>
|
||||
<?= htmlspecialchars($c['description'], ENT_COMPAT, 'UTF-8') ?>
|
||||
<? if ((strpos($vars['images'], "descrefs:") === 0) && count($c['images']) > 0) { /* Does user want us to include <img> references in cache descriptions? */ ?>
|
||||
<?
|
||||
# We will split images into two subcategories: spoilers and nonspoilers.
|
||||
$spoilers = array();
|
||||
$nonspoilers = array();
|
||||
foreach ($c['images'] as $img)
|
||||
if ($img['is_spoiler']) $spoilers[] = $img;
|
||||
else $nonspoilers[] = $img;
|
||||
?>
|
||||
<? if (count($nonspoilers) > 0) { ?>
|
||||
<h2><?= _("Images") ?> (<?= count($nonspoilers) ?>)</h2>
|
||||
<? foreach ($nonspoilers as $img) { ?>
|
||||
<p><img src='<?= htmlspecialchars($img['url'], ENT_COMPAT, 'UTF-8') ?>'><br>
|
||||
<?= htmlspecialchars($img['caption'], ENT_COMPAT, 'UTF-8') ?></p>
|
||||
<? } ?>
|
||||
<? } ?>
|
||||
<? if (count($spoilers) > 0 && $vars['images'] == 'descrefs:all') { ?>
|
||||
<h2><?= _("Spoilers") ?> (<?= count($spoilers) ?>)</h2>
|
||||
<? foreach ($spoilers as $img) { ?>
|
||||
<p><img src='<?= htmlspecialchars($img['url'], ENT_COMPAT, 'UTF-8') ?>'><br>
|
||||
<?= htmlspecialchars($img['caption'], ENT_COMPAT, 'UTF-8') ?></p>
|
||||
<? } ?>
|
||||
<? } ?>
|
||||
<? } ?>
|
||||
<? if ((strpos($vars['images'], "ox:") === 0) && count($c['images']) > 0) { /* Include image descriptions (for ox:image numbers)? */ ?>
|
||||
<p><?= _("Image descriptions") ?>:</p>
|
||||
<ul>
|
||||
<? foreach ($c['images'] as $no => $img) { ?>
|
||||
<li><?= $img['unique_caption'] ?>. <?= htmlspecialchars($img['caption'], ENT_COMPAT, 'UTF-8') ?></li>
|
||||
<? } ?>
|
||||
</ul>
|
||||
<? } ?>
|
||||
</groundspeak:long_description>
|
||||
<groundspeak:encoded_hints><?= htmlspecialchars($c['hint'], ENT_COMPAT, 'UTF-8') ?></groundspeak:encoded_hints>
|
||||
<? if ($vars['latest_logs']) { /* Does user want us to include latest log entries? */ ?>
|
||||
<groundspeak:logs>
|
||||
<? foreach ($c['latest_logs'] as $log) { ?>
|
||||
<groundspeak:log id="<?= $log['uuid'] ?>">
|
||||
<groundspeak:date><?= $log['date'] ?></groundspeak:date>
|
||||
<groundspeak:type><?= $log['type'] ?></groundspeak:type>
|
||||
<groundspeak:finder id="<?= $log['user']['uuid'] ?>"><?= htmlspecialchars($log['user']['username'], ENT_COMPAT, 'UTF-8') ?></groundspeak:finder>
|
||||
<groundspeak:text encoded="False"><?= htmlspecialchars($log['comment'], ENT_COMPAT, 'UTF-8') ?></groundspeak:text>
|
||||
</groundspeak:log>
|
||||
<? } ?>
|
||||
</groundspeak:logs>
|
||||
<? } ?>
|
||||
<? /* groundspeak:travelbugs - does it actually DO anything? WRTODO */ ?>
|
||||
</groundspeak:cache>
|
||||
<? } ?>
|
||||
<? if ($vars['ns_ox']) { /* Does user want us to include Garmin's <opencaching> element? */ ?>
|
||||
<ox:opencaching xmlns:ox="http://www.opencaching.com/xmlschemas/opencaching/1/0">
|
||||
<ox:ratings>
|
||||
<? if ($c['rating'] !== null) { ?><ox:awesomeness><?= $c['rating'] ?></ox:awesomeness><? } ?>
|
||||
<ox:difficulty><?= $c['difficulty'] ?></ox:difficulty>
|
||||
<? if ($c['size'] !== null) { ?><ox:size><?= $vars['cache_OX_sizes'][$c['size']] ?></ox:size><? } ?>
|
||||
<ox:terrain><?= $c['terrain'] ?></ox:terrain>
|
||||
</ox:ratings>
|
||||
<? if ($vars['attrs'] == 'ox:tags' && count($c['attrnames']) > 0) { /* Does user want us to include ox:tags? */ ?>
|
||||
<ox:tags><ox:tag><?= implode("</ox:tag><ox:tag>", $c['attrnames']) ?></ox:tag></ox:tags>
|
||||
<? } ?>
|
||||
<? if ((strpos($vars['images'], "ox:") === 0) && count($c['images']) > 0) { /* Does user want us to include ox:image references? */ ?>
|
||||
<ox:images>
|
||||
<? foreach ($c['images'] as $no => $img) { ?>
|
||||
<ox:image>
|
||||
<ox:name><?= $img['unique_caption'] ?>.jpg</ox:name>
|
||||
<ox:size>0</ox:size>
|
||||
<ox:required>false</ox:required>
|
||||
<ox:spoiler><?= ($img['is_spoiler'] ? "true" : "false") ?></ox:spoiler>
|
||||
</ox:image>
|
||||
<? } ?>
|
||||
</ox:images>
|
||||
<? } ?>
|
||||
</ox:opencaching>
|
||||
<? } ?>
|
||||
</wpt>
|
||||
<? } ?>
|
||||
<? if ($vars['alt_wpts']) { ?>
|
||||
<? foreach ($vars['caches'] as $c) { ?>
|
||||
<? if ($c === null) continue; /* ignoring invalid cache codes */ ?>
|
||||
<? foreach ($c['alt_wpts'] as $wpt) { ?>
|
||||
<? list($lat, $lon) = explode("|", $wpt['location']); ?>
|
||||
<wpt lat="<?= $lat ?>" lon="<?= $lon ?>">
|
||||
<time><?= $c['date_created'] ?></time>
|
||||
<name><?= htmlspecialchars($wpt['name'], ENT_COMPAT, 'UTF-8') ?></name>
|
||||
<cmt><?= htmlspecialchars($wpt['description'], ENT_COMPAT, 'UTF-8') ?></cmt>
|
||||
<desc><?= htmlspecialchars($wpt['description'], ENT_COMPAT, 'UTF-8') ?></desc>
|
||||
<url><?= $c['url'] ?></url>
|
||||
<urlname><?= htmlspecialchars($c['name'], ENT_COMPAT, 'UTF-8') ?></urlname>
|
||||
<sym><?= $wpt['sym'] ?></sym>
|
||||
<type>Waypoint|<?= $wpt['sym'] ?></type>
|
||||
<? if ($vars['ns_gsak']) { ?>
|
||||
<wptExtension xmlns="http://www.gsak.net/xmlv1/5">
|
||||
<Parent><?= $c['code'] ?></Parent>
|
||||
</wptExtension>
|
||||
<? } ?>
|
||||
</wpt>
|
||||
<? } ?>
|
||||
<? } ?>
|
||||
<? } ?>
|
||||
</gpx>
|
55
htdocs/okapi/services/caches/geocache.php
Normal file
55
htdocs/okapi/services/caches/geocache.php
Normal file
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace okapi\services\caches\geocache;
|
||||
|
||||
use okapi\OkapiInternalRequest;
|
||||
use okapi\OkapiServiceRunner;
|
||||
use okapi\Okapi;
|
||||
use okapi\OkapiRequest;
|
||||
use okapi\ParamMissing;
|
||||
use okapi\InvalidParam;
|
||||
use okapi\services\caches\search\SearchAssistant;
|
||||
|
||||
class WebService
|
||||
{
|
||||
public static function options()
|
||||
{
|
||||
return array(
|
||||
'min_auth_level' => 1
|
||||
);
|
||||
}
|
||||
|
||||
public static function call(OkapiRequest $request)
|
||||
{
|
||||
$cache_code = $request->get_parameter('cache_code');
|
||||
if (!$cache_code) throw new ParamMissing('cache_code');
|
||||
$langpref = $request->get_parameter('langpref');
|
||||
if (!$langpref) $langpref = "en";
|
||||
$fields = $request->get_parameter('fields');
|
||||
if (!$fields) $fields = "code|name|location|type|status";
|
||||
$lpc = $request->get_parameter('lpc');
|
||||
if (!$lpc) $lpc = 10;
|
||||
$params = array(
|
||||
'cache_codes' => $cache_code,
|
||||
'langpref' => $langpref,
|
||||
'fields' => $fields,
|
||||
'lpc' => $lpc
|
||||
);
|
||||
$my_location = $request->get_parameter('my_location');
|
||||
if ($my_location)
|
||||
$params['my_location'] = $my_location;
|
||||
$user_uuid = $request->get_parameter('user_uuid');
|
||||
if ($user_uuid)
|
||||
$params['user_uuid'] = $user_uuid;
|
||||
|
||||
# There's no need to validate the fields/lpc parameters as the 'geocaches'
|
||||
# method does this (it will raise a proper exception on invalid values).
|
||||
|
||||
$results = OkapiServiceRunner::call('services/caches/geocaches', new OkapiInternalRequest(
|
||||
$request->consumer, $request->token, $params));
|
||||
$result = $results[$cache_code];
|
||||
if ($result == null)
|
||||
throw new InvalidParam('cache_code', "This cache does not exist.");
|
||||
return Okapi::formatted_response($request, $result);
|
||||
}
|
||||
}
|
232
htdocs/okapi/services/caches/geocache.xml
Normal file
232
htdocs/okapi/services/caches/geocache.xml
Normal file
@ -0,0 +1,232 @@
|
||||
<xml>
|
||||
<brief>Retrieve information on a single geocache</brief>
|
||||
<issue-id>19</issue-id>
|
||||
<desc>
|
||||
<p>Retrieve information on a single geocache.</p>
|
||||
</desc>
|
||||
<req name='cache_code'>Unique code of the geocache</req>
|
||||
<opt name='langpref' default='en'>
|
||||
<p>Pipe-separated list of ISO 639-1 language codes. This indicates the
|
||||
order of preference in which language will be chosen for fields like
|
||||
<b>name</b> and <b>description</b>.</p>
|
||||
<p>Please note, that you may also access caches descriptions in all
|
||||
available languages. If you want to do this, you should use fields
|
||||
<b>names</b>, <b>descriptions</b> (etc.) instead of <b>name</b> and
|
||||
<b>description</b> (etc.).</p>
|
||||
</opt>
|
||||
<opt name='fields' default='code|name|location|type|status'>
|
||||
<p>Pipe-separated list of field names which you are interested with.
|
||||
Selected fields will be included in the response.</p>
|
||||
<p>Currently available fields:</p>
|
||||
<ul>
|
||||
<li><b>code</b> - unique code of the geocache,</li>
|
||||
<li><b>name</b> - name of the geocache,</li>
|
||||
<li><b>names</b> - a dictionary (language code => plain-text string) of
|
||||
names of the geocache (at this time, there will be only one language,)
|
||||
but this may change in future),</li>
|
||||
<li><b>location</b> - location of the cache in the "lat|lon" format
|
||||
(<i>lat</i> and <i>lon</i> are in full degrees with a dot as a decimal point),</li>
|
||||
<li>
|
||||
<p><b>type</b> - cache type. This might be <b>pretty much everything</b>,
|
||||
but there are some predefined types that you might want to treat
|
||||
in a special way (separate icons etc.). Primary types include: </p>
|
||||
<ul>
|
||||
<li><b>Traditional</b>,</li>
|
||||
<li><b>Multi</b>,</li>
|
||||
<li><b>Quiz</b>,</li>
|
||||
<li><b>Virtual</b>.</li>
|
||||
<li><i>(any other value is valid too)</i></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p><b>status</b> - cache status. Valid cache status codes currently include:</p>
|
||||
<ul>
|
||||
<li><b>Available</b> - Cache is available and ready for search,</li>
|
||||
<li><b>Temporarily unavailable</b> - Cache is probably unavailable (i.e. in need of maintenance)</li>
|
||||
<li><b>Archived</b> - Cache is permanently unavailable (moved to the archives).</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><b>url</b> - URL of the cache's web page,</li>
|
||||
<li>
|
||||
<p><b>owner</b> - dictionary of:</p>
|
||||
<ul>
|
||||
<li><b>uuid</b> - geocache owner's user ID,</li>
|
||||
<li><b>username</b> - name of the user,</li>
|
||||
<li><b>profile_url</b> - URL of the user profile page,</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p><b>distance</b> - float, the distance to a cache, in meters.
|
||||
This requires <b>my_location</b> parameter to be provided.</p>
|
||||
<p>Please note, that sometimes it is faster to compute this yourself, on client-side, instead of quering OKAPI.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><b>bearing</b> - float, the absolute bearing to the cache, measured in degrees from north,
|
||||
<b>or null</b> if it cannot be calculated (i.e. when you are exactly at the target's location).
|
||||
This requires <b>my_location</b> parameter to be provided.</p>
|
||||
<p>Please note, that sometimes it is faster to compute this yourself, on client-side, instead of quering OKAPI.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><b>bearing2</b> - string, the absolute bearing to the cache, represented as a typical direction
|
||||
string of length of 1 or 2 characters (ex. "N", "NE", "E", "SE", etc.), or "n/a" if it cannot be calculated.
|
||||
This requires <b>my_location</b> parameter to be provided.</p>
|
||||
<p>Please note, that sometimes it is faster to compute this yourself, on client-side, instead of quering OKAPI.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><b>bearing3</b> - string, the absolute bearing to the cache, represented as a typical directon
|
||||
string of length of 1 or 2 or 3 characters (ex. "N", "NNE", "NE", "ENE", etc.), or "n/a" if it cannot be calculated.
|
||||
This requires <b>my_location</b> parameter to be provided.</p>
|
||||
<p>Please note, that sometimes it is faster to compute this yourself, on client-side, instead of quering OKAPI.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><b>is_found</b> - boolean, true if user already found this cache. See also <b>found_status</b> parameter
|
||||
of the services/caches/search/all method.</p>
|
||||
<p>This field requires you to use the <b>user_uuid</b> parameter (or Level 3 Authentication).</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><b>is_not_found</b> - boolean, true if user has submitted "Didn't find it" log entry for this cache.</p>
|
||||
<p>This field requires you to use the <b>user_uuid</b> parameter (or Level 3 Authentication).</p>
|
||||
</li>
|
||||
<li><b>founds</b> - number of times the geocache was successfully found,</li>
|
||||
<li><b>notfounds</b> - number of times the geocache was not found,</li>
|
||||
<li><b>size</b> - float (between 1 and 5), size rating of the container, or
|
||||
<b>null</b> if geocache does not have a container,</li>
|
||||
<li><b>difficulty</b> - float (between 1 and 5), difficulty rating of the cache,</li>
|
||||
<li><b>terrain</b> - float (between 1 and 5), terrain rating of the cache,</li>
|
||||
<li>
|
||||
<p><b>rating</b> - float (between 1 and 5), an overall rating of the cache,
|
||||
or <b>null</b> when the geocache does not have a sufficient amount of votes
|
||||
to have a rating.</p>
|
||||
<p>Please note: some OC installations do not use the rating/voting system. The <b>rating</b> will
|
||||
always be <b>null</b> on such installations.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><b>rating_votes</b> - number of votes submitted for the rating of this cache.</p>
|
||||
<p>Please note: some OC installations do not use the rating/voting system. The <b>rating_votes</b> will
|
||||
always be <b>0</b> on such installations.</p>
|
||||
</li>
|
||||
<li><b>recommendations</b> - number of recommendations for this cache,</li>
|
||||
<li><b>req_passwd</b> - boolean; states if this cache requires a password
|
||||
in order to submit a "Found it" log entry,</li>
|
||||
<li><b>description</b> - HTML string, description of the cache,</li>
|
||||
<li><b>descriptions</b> - a dictionary (language code => HTML string) of cache
|
||||
descriptions,</li>
|
||||
<li><b>hint</b> - plain-text string, cache hints/spoilers; in general, hints should not be displayed to the user
|
||||
unless user specifically asks for them,</li>
|
||||
<li><b>hints</b> - a dictionary (language code => plain-text string) of cache
|
||||
hints/spoilers,</li>
|
||||
<li>
|
||||
<p><b>images</b> - list of dictionaries, each dictionary represents one
|
||||
image saved along the cache description; each dictionary has the
|
||||
following structure:</p>
|
||||
<ul>
|
||||
<li><b>uuid</b> - UUID of the image,</li>
|
||||
<li><b>url</b> - an URL of the image,</li>
|
||||
<li><b>thumb_url</b> - an URL of a small (thumb) version of the image
|
||||
<b>or null</b> when no thumb is available (e.g. there are no thumbs
|
||||
for spoiler images),</li>
|
||||
<li><b>caption</b> - plain-text string, caption of the image,</li>
|
||||
<li><b>unique_caption</b> - plain-text string, to be used as a filename
|
||||
for Garmin's crappy images implementation (currently, they get image
|
||||
caption from the image's filename itself),</li>
|
||||
<li><b>is_spoiler</b> - boolean, if <b>true</b> then the image is
|
||||
a spoiler image and should not be displayed to the user unless
|
||||
the user explicitly asks for it.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p><b>attrnames</b> - list of names of attributes of the cache; the language will
|
||||
be selected based on the <b>langpref</b> argument.</p>
|
||||
<p>Note: currently each of the OC installations has its own set of attributes. That's
|
||||
why we don't reference them by their IDs - we only return them as text. This could change
|
||||
*if* OC installations would begin to use UUIDs for their attributes and synchronize them
|
||||
between all installations.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><b>latest_logs</b> - a couple of latest log entries in the cache.
|
||||
The format is the same as the one returned by the services/logs/logs method.</p>
|
||||
<p>Notice: The number of logs returned can be set with the <b>lpc</b> option.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><b>my_notes</b> - user's notes on the cache, in plain-text, or <b>null</b>
|
||||
if user hadn't defined any notes.</p>
|
||||
<p>This field requires you to use the <b>Level 3</b> Authentication.</p>
|
||||
</li>
|
||||
<li><b>trackables_count</b> - a total number of trackables hidden within the cache.</li>
|
||||
<li>
|
||||
<p><b>trackables</b> - list of dictionaries, each dictionary represents one
|
||||
trackable hidden within the cache container; each dictionary has the
|
||||
following structure:</p>
|
||||
<ul>
|
||||
<li><b>code</b> - code of the trackable,</li>
|
||||
<li><b>name</b> - plain text name of the trackable,</li>
|
||||
<li><b>url</b> - trackable's own webpage address <b>or null</b>, if OKAPI
|
||||
cannot automatically determine this address.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p><b>alt_wpts</b> - list of alternate/additional waypoints associated
|
||||
with this geocache. Each item is a dictionary of the following structure:</p>
|
||||
<ul>
|
||||
<li><b>name</b> - plain-text "codename" for the waypoint (not unique),</li>
|
||||
<li><b>location</b> - location of the waypoint in the "lat|lon" format
|
||||
(<i>lat</i> and <i>lon</i> are in full degrees with a dot as a decimal point),</li>
|
||||
<li><b>sym</b> - string, one of globally-recognized waypoint symbol
|
||||
names (such as "Flag, Green" or "Parking Area"),</li>
|
||||
<li><b>description</b> - plain-text longer description of the waypoint.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p><b>last_found</b> - date and time (ISO 8601) when the
|
||||
geocache was last found <b>or null</b> when it hasn't been yet found.</p>
|
||||
<p>Note, that some OpenCaching servers don't store the exact times along
|
||||
with the log entries.</p>
|
||||
</li>
|
||||
<li><b>last_modified</b> - date and time (ISO 8601) when the
|
||||
geocache was last modified (changed status, attributes, etc.),</li>
|
||||
<li><b>date_created</b> - date and time (ISO 8601) when the
|
||||
geocache was initially created,</li>
|
||||
<li><b>date_hidden</b> - date and time (ISO 8601) when the
|
||||
geocache was first hidden,</li>
|
||||
<!-- Note: I think cache uuids should not be ever revealed to the public.
|
||||
We have already one universally unique key - the cache code. It is uncommon
|
||||
to have multiple universally unique keys. -->
|
||||
<li><b>internal_id</b> - internal ID of the cache (<b>do not</b> use
|
||||
this, unless you know what you're doing; use the "code" as an identifier).</li>
|
||||
</ul>
|
||||
</opt>
|
||||
<opt name='lpc' default='10'>
|
||||
Log-entries per cache - the number of logs returned in the <b>latest_logs</b> field.
|
||||
This should be an integer or a special "all" value. Please note, that you must include
|
||||
the <b>latest_logs</b> field in <b>fields</b> in order to see the log entries.
|
||||
</opt>
|
||||
<opt name='my_location'>
|
||||
<p>The reference point for cache distance and bearing calculation (typically - the user's location),
|
||||
in the "lat|lon" format. This parameter is required when accessing <b>distance</b> and/or <b>bearing</b>
|
||||
fields.</p>
|
||||
<p>Use positive numbers for latitudes in the northern hemisphere and
|
||||
longitudes in the eastern hemisphere (and negative for southern and
|
||||
western hemispheres accordingly). These are full degrees with a dot
|
||||
as a decimal point (ex. "54.3|22.3").</p>
|
||||
</opt>
|
||||
<opt name="user_uuid">
|
||||
<p>User'd ID. Required to access fields like <b>is_found</b> using <b>Level 1</b> Authentication.</p>
|
||||
<p>Please note that if you want to access private fields (like <b>my_notes</b>), then
|
||||
this parameter won't help you. You have to use <b>Level 3</b> Authentication instead.</p>
|
||||
<p>If you use <b>Level 3</b> Authentication, you shouldn't use this parameter. Or, to be exact:</p>
|
||||
<ul>
|
||||
<li><b>user_uuid</b> will be extracted from the Access Token you use. You don't have to provide it.</li>
|
||||
<li>If you provide <b>user_uuid</b>, then it HAS TO match your Access Token. If it doesn't, OKAPI
|
||||
will respond with HTTP 400 error.</li>
|
||||
</ul>
|
||||
</opt>
|
||||
<common-format-params/>
|
||||
<returns>
|
||||
<p>A dictionary of fields you have selected.</p>
|
||||
<p>For example, for <i>geocache?cache_code=OP3D96&fields=type</i>
|
||||
query, the result might look something link this:</p>
|
||||
<pre>{"type": "Traditional"}</pre>
|
||||
<p>If given cache code does not exist, the method will
|
||||
respond with a HTTP 400 error.</p>
|
||||
</returns>
|
||||
</xml>
|
723
htdocs/okapi/services/caches/geocaches.php
Normal file
723
htdocs/okapi/services/caches/geocaches.php
Normal file
@ -0,0 +1,723 @@
|
||||
<?php
|
||||
|
||||
namespace okapi\services\caches\geocaches;
|
||||
|
||||
use Exception;
|
||||
use okapi\Okapi;
|
||||
use okapi\Db;
|
||||
use okapi\Settings;
|
||||
use okapi\OkapiRequest;
|
||||
use okapi\ParamMissing;
|
||||
use okapi\InvalidParam;
|
||||
use okapi\BadRequest;
|
||||
use okapi\OkapiInternalRequest;
|
||||
use okapi\OkapiServiceRunner;
|
||||
use okapi\OkapiAccessToken;
|
||||
use okapi\services\caches\search\SearchAssistant;
|
||||
|
||||
class WebService
|
||||
{
|
||||
public static function options()
|
||||
{
|
||||
return array(
|
||||
'min_auth_level' => 1
|
||||
);
|
||||
}
|
||||
|
||||
private static $valid_field_names = array('code', 'name', 'names', 'location', 'type',
|
||||
'status', 'url', 'owner', 'distance', 'bearing', 'bearing2', 'bearing3', 'is_found',
|
||||
'is_not_found', 'founds', 'notfounds', 'size', 'difficulty', 'terrain',
|
||||
'rating', 'rating_votes', 'recommendations', 'req_passwd', 'description',
|
||||
'descriptions', 'hint', 'hints', 'images', 'attrnames', 'latest_logs',
|
||||
'my_notes', 'trackables_count', 'trackables', 'alt_wpts', 'last_found',
|
||||
'last_modified', 'date_created', 'date_hidden', 'internal_id');
|
||||
|
||||
public static function call(OkapiRequest $request)
|
||||
{
|
||||
$cache_codes = $request->get_parameter('cache_codes');
|
||||
if ($cache_codes === null) throw new ParamMissing('cache_codes');
|
||||
if ($cache_codes === "")
|
||||
{
|
||||
# Issue 106 requires us to allow empty list of cache codes to be passed into this method.
|
||||
# All of the queries below have to be ready for $cache_codes to be empty!
|
||||
$cache_codes = array();
|
||||
}
|
||||
else
|
||||
$cache_codes = explode("|", $cache_codes);
|
||||
|
||||
if (count($cache_codes) > 500)
|
||||
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)))
|
||||
throw new InvalidParam('cache_codes', "Duplicate codes detected (make sure each cache is referenced only once).");
|
||||
|
||||
$langpref = $request->get_parameter('langpref');
|
||||
if (!$langpref) $langpref = "en";
|
||||
$langpref = explode("|", $langpref);
|
||||
|
||||
$fields = $request->get_parameter('fields');
|
||||
if (!$fields) $fields = "code|name|location|type|status";
|
||||
$fields = explode("|", $fields);
|
||||
foreach ($fields as $field)
|
||||
if (!in_array($field, self::$valid_field_names))
|
||||
throw new InvalidParam('fields', "'$field' is not a valid field code.");
|
||||
|
||||
$user_uuid = $request->get_parameter('user_uuid');
|
||||
if ($user_uuid != null)
|
||||
{
|
||||
$user_id = Db::select_value("select user_id from user where uuid='".mysql_real_escape_string($user_uuid)."'");
|
||||
if ($user_id == null)
|
||||
throw new InvalidParam('user_uuid', "User not found.");
|
||||
if (($request->token != null) && ($this->token->user_id != $user_id))
|
||||
throw new InvalidParam('user_uuid', "User does not match the Access Token used.");
|
||||
}
|
||||
elseif (($user_uuid == null) && ($request->token != null))
|
||||
$user_id = $request->token->user_id;
|
||||
else
|
||||
$user_id = null;
|
||||
|
||||
$lpc = $request->get_parameter('lpc');
|
||||
if ($lpc === null) $lpc = 10;
|
||||
if ($lpc == 'all')
|
||||
$lpc = null;
|
||||
else
|
||||
{
|
||||
if (!is_numeric($lpc))
|
||||
throw new InvalidParam('lpc', "Invalid number: '$lpc'");
|
||||
$lpc = intval($lpc);
|
||||
if ($lpc < 0)
|
||||
throw new InvalidParam('lpc', "Must be a positive value.");
|
||||
}
|
||||
|
||||
if (in_array('distance', $fields) || in_array('bearing', $fields) || in_array('bearing2', $fields)
|
||||
|| in_array('bearing3', $fields))
|
||||
{
|
||||
$tmp = $request->get_parameter('my_location');
|
||||
if (!$tmp)
|
||||
throw new BadRequest("When using 'distance' or 'bearing' fields, you have to supply 'my_location' parameter.");
|
||||
$parts = explode('|', $tmp);
|
||||
if (count($parts) != 2)
|
||||
throw new InvalidParam('my_location', "Expecting 2 pipe-separated parts, got ".count($parts).".");
|
||||
foreach ($parts as &$part_ref)
|
||||
{
|
||||
if (!preg_match("/^-?[0-9]+(\.?[0-9]*)$/", $part_ref))
|
||||
throw new InvalidParam('my_location', "'$part_ref' is not a valid float number.");
|
||||
$part_ref = floatval($part_ref);
|
||||
}
|
||||
list($center_lat, $center_lon) = $parts;
|
||||
if ($center_lat > 90 || $center_lat < -90)
|
||||
throw new InvalidParam('current_position', "Latitudes have to be within -90..90 range.");
|
||||
if ($center_lon > 180 || $center_lon < -180)
|
||||
throw new InvalidParam('current_position', "Longitudes have to be within -180..180 range.");
|
||||
}
|
||||
|
||||
if (Settings::get('OC_BRANCH') == 'oc.de')
|
||||
{
|
||||
# DE branch:
|
||||
# - Caches do not have ratings.
|
||||
# - Total numbers of founds and notfounds are kept in the "stat_caches" table.
|
||||
|
||||
$rs = Db::query("
|
||||
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,
|
||||
|
||||
ifnull(sc.toprating, 0) as topratings,
|
||||
ifnull(sc.found, 0) as founds,
|
||||
ifnull(sc.notfound, 0) as notfounds,
|
||||
sc.last_found,
|
||||
0 as votes, 0 as score
|
||||
-- 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))."')
|
||||
");
|
||||
}
|
||||
elseif (Settings::get('OC_BRANCH') == 'oc.pl')
|
||||
{
|
||||
# PL branch:
|
||||
# - Caches have ratings.
|
||||
# - Total numbers of found and notfounds are kept in the "caches" table.
|
||||
|
||||
$rs = Db::query("
|
||||
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.topratings,
|
||||
c.founds,
|
||||
c.notfounds,
|
||||
c.last_found,
|
||||
c.votes, c.score
|
||||
-- SEE ALSO OC.DE BRANCH ABOVE
|
||||
from
|
||||
caches c,
|
||||
user u
|
||||
where
|
||||
binary wp_oc in ('".implode("','", array_map('mysql_real_escape_string', $cache_codes))."')
|
||||
and c.user_id = u.user_id
|
||||
");
|
||||
}
|
||||
|
||||
$results = array();
|
||||
$cacheid2wptcode = array();
|
||||
while ($row = mysql_fetch_assoc($rs))
|
||||
{
|
||||
$entry = array();
|
||||
$cacheid2wptcode[$row['cache_id']] = $row['wp_oc'];
|
||||
foreach ($fields as $field)
|
||||
{
|
||||
switch ($field)
|
||||
{
|
||||
case 'code': $entry['code'] = $row['wp_oc']; break;
|
||||
case 'name': $entry['name'] = $row['name']; break;
|
||||
case 'names': $entry['names'] = array(Settings::get('SITELANG') => $row['name']); break; // for the future
|
||||
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 'owner':
|
||||
$entry['owner'] = array(
|
||||
'uuid' => $row['user_uuid'],
|
||||
'username' => $row['username'],
|
||||
'profile_url' => $GLOBALS['absolute_server_URI']."viewprofile.php?userid=".$row['user_id']
|
||||
);
|
||||
break;
|
||||
case 'distance':
|
||||
$entry['distance'] = (int)Okapi::get_distance($center_lat, $center_lon, $row['latitude'], $row['longitude']);
|
||||
break;
|
||||
case 'bearing':
|
||||
$tmp = Okapi::get_bearing($center_lat, $center_lon, $row['latitude'], $row['longitude']);
|
||||
$entry['bearing'] = ($tmp !== null) ? ((int)(10*$tmp)) / 10.0 : null;
|
||||
break;
|
||||
case 'bearing2':
|
||||
$tmp = Okapi::get_bearing($center_lat, $center_lon, $row['latitude'], $row['longitude']);
|
||||
$entry['bearing2'] = Okapi::bearing_as_two_letters($tmp);
|
||||
break;
|
||||
case 'bearing3':
|
||||
$tmp = Okapi::get_bearing($center_lat, $center_lon, $row['latitude'], $row['longitude']);
|
||||
$entry['bearing3'] = Okapi::bearing_as_three_letters($tmp);
|
||||
break;
|
||||
case 'is_found': /* handled separately */ break;
|
||||
case 'is_not_found': /* handled separately */ break;
|
||||
case 'founds': $entry['founds'] = $row['founds'] + 0; break;
|
||||
case 'notfounds': $entry['notfounds'] = $row['notfounds'] + 0; break;
|
||||
case 'size': $entry['size'] = ($row['size'] < 7) ? (float)($row['size'] - 1) : null; break;
|
||||
case 'difficulty': $entry['difficulty'] = round($row['difficulty'] / 2.0, 1); break;
|
||||
case 'terrain': $entry['terrain'] = round($row['terrain'] / 2.0, 1); break;
|
||||
case 'rating':
|
||||
if ($row['votes'] < 3) $entry['rating'] = null;
|
||||
elseif ($row['score'] >= 2.2) $entry['rating'] = 5.0;
|
||||
elseif ($row['score'] >= 1.4) $entry['rating'] = 4.0;
|
||||
elseif ($row['score'] >= 0.1) $entry['rating'] = 3.0;
|
||||
elseif ($row['score'] >= -1.0) $entry['rating'] = 2.0;
|
||||
else $entry['rating'] = 1.0;
|
||||
break;
|
||||
case 'rating_votes': $entry['rating_votes'] = $row['votes'] + 0; break;
|
||||
case 'recommendations': $entry['recommendations'] = $row['topratings'] + 0; break;
|
||||
case 'req_passwd': $entry['req_passwd'] = $row['logpw'] ? true : false; break;
|
||||
case 'description': /* handled separately */ break;
|
||||
case 'descriptions': /* handled separately */ break;
|
||||
case 'hint': /* handled separately */ break;
|
||||
case 'hints': /* handled separately */ break;
|
||||
case 'images': /* handled separately */ break;
|
||||
case 'attrnames': /* handled separately */ break;
|
||||
case 'latest_logs': /* handled separately */ break;
|
||||
case 'my_notes': /* handles separately */ break;
|
||||
case 'trackables_count': /* handled separately */ break;
|
||||
case 'trackables': /* handled separately */ break;
|
||||
case 'alt_wpts': /* handled separately */ break;
|
||||
case 'last_found': $entry['last_found'] = ($row['last_found'] > '1980') ? date('c', strtotime($row['last_found'])) : null; break;
|
||||
case 'last_modified': $entry['last_modified'] = date('c', strtotime($row['last_modified'])); break;
|
||||
case 'date_created': $entry['date_created'] = date('c', strtotime($row['date_created'])); break;
|
||||
case 'date_hidden': $entry['date_hidden'] = date('c', strtotime($row['date_hidden'])); break;
|
||||
case 'internal_id': $entry['internal_id'] = $row['cache_id']; break;
|
||||
default: throw new Exception("Missing field case: ".$field);
|
||||
}
|
||||
}
|
||||
$results[$row['wp_oc']] = $entry;
|
||||
}
|
||||
mysql_free_result($rs);
|
||||
|
||||
# is_found
|
||||
|
||||
if (in_array('is_found', $fields))
|
||||
{
|
||||
if ($user_id == null)
|
||||
throw new BadRequest("Either 'user_uuid' parameter OR Level 3 Authentication is required to access 'is_found' field.");
|
||||
$tmp = Db::select_column("
|
||||
select c.wp_oc
|
||||
from
|
||||
caches c,
|
||||
cache_logs cl
|
||||
where
|
||||
c.cache_id = cl.cache_id
|
||||
and cl.type = '".mysql_real_escape_string(Okapi::logtypename2id("Found it"))."'
|
||||
and cl.user_id = '".mysql_real_escape_string($user_id)."'
|
||||
");
|
||||
$tmp2 = array();
|
||||
foreach ($tmp as $cache_code)
|
||||
$tmp2[$cache_code] = true;
|
||||
foreach ($results as $cache_code => &$result_ref)
|
||||
$result_ref['is_found'] = isset($tmp2[$cache_code]);
|
||||
}
|
||||
|
||||
# is_not_found
|
||||
|
||||
if (in_array('is_not_found', $fields))
|
||||
{
|
||||
if ($user_id == null)
|
||||
throw new BadRequest("Either 'user_uuid' parameter OR Level 3 Authentication is required to access 'is_not_found' field.");
|
||||
$tmp = Db::select_column("
|
||||
select c.wp_oc
|
||||
from
|
||||
caches c,
|
||||
cache_logs cl
|
||||
where
|
||||
c.cache_id = cl.cache_id
|
||||
and cl.type = '".mysql_real_escape_string(Okapi::logtypename2id("Didn't find it"))."'
|
||||
and cl.user_id = '".mysql_real_escape_string($user_id)."'
|
||||
");
|
||||
$tmp2 = array();
|
||||
foreach ($tmp as $cache_code)
|
||||
$tmp2[$cache_code] = true;
|
||||
foreach ($results as $cache_code => &$result_ref)
|
||||
$result_ref['is_not_found'] = isset($tmp2[$cache_code]);
|
||||
}
|
||||
|
||||
# Descriptions and hints.
|
||||
|
||||
if (in_array('description', $fields) || in_array('descriptions', $fields)
|
||||
|| in_array('hint', $fields) || in_array('hints', $fields))
|
||||
{
|
||||
# At first, we will fill all those 4 fields, even if user requested just one
|
||||
# of them. We will chop off the remaining three at the end.
|
||||
|
||||
foreach ($results as &$result_ref)
|
||||
$result_ref['descriptions'] = array();
|
||||
foreach ($results as &$result_ref)
|
||||
$result_ref['hints'] = array();
|
||||
|
||||
# Get cache descriptions and hints.
|
||||
|
||||
$rs = Db::query("
|
||||
select cache_id, language, `desc`, hint
|
||||
from cache_desc
|
||||
where cache_id in ('".implode("','", array_map('mysql_real_escape_string', array_keys($cacheid2wptcode)))."')
|
||||
");
|
||||
while ($row = mysql_fetch_assoc($rs))
|
||||
{
|
||||
$cache_code = $cacheid2wptcode[$row['cache_id']];
|
||||
// strtolower - ISO 639-1 codes are lowercase
|
||||
if ($row['desc'])
|
||||
$results[$cache_code]['descriptions'][strtolower($row['language'])] = $row['desc'].
|
||||
"\n".self::get_cache_attribution_note($row['cache_id'], strtolower($row['language']));
|
||||
if ($row['hint'])
|
||||
$results[$cache_code]['hints'][strtolower($row['language'])] = $row['hint'];
|
||||
}
|
||||
foreach ($results as &$result_ref)
|
||||
{
|
||||
$result_ref['description'] = Okapi::pick_best_language($result_ref['descriptions'], $langpref);
|
||||
$result_ref['hint'] = Okapi::pick_best_language($result_ref['hints'], $langpref);
|
||||
}
|
||||
|
||||
# Remove unwanted fields.
|
||||
|
||||
foreach (array('description', 'descriptions', 'hint', 'hints') as $field)
|
||||
if (!in_array($field, $fields))
|
||||
foreach ($results as &$result_ref)
|
||||
unset($result_ref[$field]);
|
||||
}
|
||||
|
||||
# Images.
|
||||
|
||||
if (in_array('images', $fields))
|
||||
{
|
||||
foreach ($results as &$result_ref)
|
||||
$result_ref['images'] = array();
|
||||
$rs = Db::query("
|
||||
select object_id, uuid, url, thumb_url, title, spoiler
|
||||
from pictures
|
||||
where
|
||||
object_id in ('".implode("','", array_map('mysql_real_escape_string', array_keys($cacheid2wptcode)))."')
|
||||
and display = 1
|
||||
and object_type = 2
|
||||
and unknown_format = 0
|
||||
order by object_id, last_modified
|
||||
");
|
||||
$prev_cache_code = null;
|
||||
while ($row = mysql_fetch_assoc($rs))
|
||||
{
|
||||
$cache_code = $cacheid2wptcode[$row['object_id']];
|
||||
if ($cache_code != $prev_cache_code)
|
||||
{
|
||||
# Group images together. Images must have unique captions within one cache.
|
||||
self::reset_unique_captions();
|
||||
$prev_cache_code = $cache_code;
|
||||
}
|
||||
$results[$cache_code]['images'][] = array(
|
||||
'uuid' => $row['uuid'],
|
||||
'url' => $row['url'],
|
||||
'thumb_url' => $row['thumb_url'] ? $row['thumb_url'] : null,
|
||||
'caption' => $row['title'],
|
||||
'unique_caption' => self::get_unique_caption($row['title']),
|
||||
'is_spoiler' => ($row['spoiler'] ? true : false),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
# Attrnames
|
||||
|
||||
if (in_array('attrnames', $fields))
|
||||
{
|
||||
foreach ($results as &$result_ref)
|
||||
$result_ref['attrnames'] = array();
|
||||
|
||||
# ALL attribute names are loaded into memory here. Assuming there are
|
||||
# not so many of them, this will be fast enough. Possible optimalization:
|
||||
# Let mysql do the matching.
|
||||
|
||||
$dict = Okapi::get_all_atribute_names();
|
||||
$rs = Db::query("
|
||||
select cache_id, attrib_id
|
||||
from caches_attributes
|
||||
where cache_id in ('".implode("','", array_map('mysql_real_escape_string', array_keys($cacheid2wptcode)))."')
|
||||
");
|
||||
while ($row = mysql_fetch_assoc($rs))
|
||||
{
|
||||
$cache_code = $cacheid2wptcode[$row['cache_id']];
|
||||
if (isset($dict[$row['attrib_id']]))
|
||||
{
|
||||
# The "isset" condition was added because there were some attrib_ids in caches_attributes
|
||||
# which WERE NOT in cache_attrib. http://code.google.com/p/opencaching-api/issues/detail?id=77
|
||||
$results[$cache_code]['attrnames'][] = Okapi::pick_best_language($dict[$row['attrib_id']], $langpref);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Latest log entries.
|
||||
|
||||
if (in_array('latest_logs', $fields))
|
||||
{
|
||||
foreach ($results as &$result_ref)
|
||||
$result_ref['latest_logs'] = array();
|
||||
|
||||
# Get all log IDs with dates. Sort in groups. Filter out latest ones. This is the fastest
|
||||
# technique I could think of...
|
||||
|
||||
$rs = Db::query("
|
||||
select cache_id, id, date
|
||||
from cache_logs
|
||||
where
|
||||
cache_id in ('".implode("','", array_map('mysql_real_escape_string', array_keys($cacheid2wptcode)))."')
|
||||
and ".((Settings::get('OC_BRANCH') == 'oc.pl') ? "deleted = 0" : "true")."
|
||||
");
|
||||
$logids = array();
|
||||
if ($lpc !== null)
|
||||
{
|
||||
# User wants some of the latest logs.
|
||||
$tmp = array();
|
||||
while ($row = mysql_fetch_assoc($rs))
|
||||
$tmp[$row['cache_id']][] = $row;
|
||||
foreach ($tmp as $cache_key => &$rowslist_ref)
|
||||
{
|
||||
usort($rowslist_ref, function($rowa, $rowb) {
|
||||
# (reverse order by date)
|
||||
return ($rowa['date'] < $rowb['date']) ? 1 : (($rowa['date'] == $rowb['date']) ? 0 : -1);
|
||||
});
|
||||
for ($i = 0; $i < min(count($rowslist_ref), $lpc); $i++)
|
||||
{
|
||||
$logids[] = $rowslist_ref[$i]['id'];
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
# User wants ALL logs.
|
||||
while ($row = mysql_fetch_assoc($rs))
|
||||
$logids[] = $row['id'];
|
||||
}
|
||||
|
||||
# Now retrieve text and join.
|
||||
|
||||
$rs = Db::query("
|
||||
select cl.cache_id, cl.id, cl.uuid, cl.type, unix_timestamp(cl.date) as date, cl.text,
|
||||
u.uuid as user_uuid, u.username, u.user_id
|
||||
from cache_logs cl, user u
|
||||
where
|
||||
cl.id in ('".implode("','", array_map('mysql_real_escape_string', $logids))."')
|
||||
and ".((Settings::get('OC_BRANCH') == 'oc.pl') ? "cl.deleted = 0" : "true")."
|
||||
and cl.user_id = u.user_id
|
||||
order by cl.cache_id, cl.date desc
|
||||
");
|
||||
$cachelogs = array();
|
||||
while ($row = mysql_fetch_assoc($rs))
|
||||
{
|
||||
$results[$cacheid2wptcode[$row['cache_id']]]['latest_logs'][] = array(
|
||||
'uuid' => $row['uuid'],
|
||||
'date' => date('c', $row['date']),
|
||||
'user' => array(
|
||||
'uuid' => $row['user_uuid'],
|
||||
'username' => $row['username'],
|
||||
'profile_url' => $GLOBALS['absolute_server_URI']."viewprofile.php?userid=".$row['user_id'],
|
||||
),
|
||||
'type' => Okapi::logtypeid2name($row['type']),
|
||||
'comment' => $row['text']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
# My notes
|
||||
|
||||
if (in_array('my_notes', $fields))
|
||||
{
|
||||
if ($request->token == null)
|
||||
throw new BadRequest("Level 3 Authentication is required to access 'my_notes' field.");
|
||||
foreach ($results as &$result_ref)
|
||||
$result_ref['my_notes'] = null;
|
||||
if (Settings::get('OC_BRANCH') == 'oc.pl')
|
||||
{
|
||||
# OCPL uses cache_notes table to store notes.
|
||||
|
||||
$rs = Db::query("
|
||||
select cache_id, max(date) as date, group_concat(`desc`) as `desc`
|
||||
from cache_notes
|
||||
where
|
||||
cache_id in ('".implode("','", array_map('mysql_real_escape_string', array_keys($cacheid2wptcode)))."')
|
||||
and user_id = '".mysql_real_escape_string($request->token->user_id)."'
|
||||
group by cache_id
|
||||
");
|
||||
}
|
||||
else
|
||||
{
|
||||
# OCDE uses coordinates table (with type == 2) to store notes (this is somewhat weird).
|
||||
|
||||
$rs = Db::query("
|
||||
select cache_id, null as date, group_concat(description) as `desc`
|
||||
from coordinates
|
||||
where
|
||||
type = 2 -- personal note
|
||||
and cache_id in ('".implode("','", array_map('mysql_real_escape_string', array_keys($cacheid2wptcode)))."')
|
||||
and user_id = '".mysql_real_escape_string($request->token->user_id)."'
|
||||
group by cache_id
|
||||
");
|
||||
}
|
||||
while ($row = mysql_fetch_assoc($rs))
|
||||
{
|
||||
# This one is plain-text. We may add my_notes_html for those who want it in HTML.
|
||||
$results[$cacheid2wptcode[$row['cache_id']]]['my_notes'] = strip_tags($row['desc']);
|
||||
}
|
||||
}
|
||||
|
||||
if (in_array('trackables', $fields))
|
||||
{
|
||||
# Currently we support Geokrety only. But this interface should remain
|
||||
# compatible. In future, other trackables might be returned the same way.
|
||||
|
||||
$rs = Db::query("
|
||||
select
|
||||
gkiw.wp as cache_code,
|
||||
gki.id as gk_id,
|
||||
gki.name
|
||||
from
|
||||
gk_item_waypoint gkiw,
|
||||
gk_item gki
|
||||
where
|
||||
gkiw.id = gki.id
|
||||
and gkiw.wp in ('".implode("','", array_map('mysql_real_escape_string', $cache_codes))."')
|
||||
");
|
||||
$trs = array();
|
||||
while ($row = mysql_fetch_assoc($rs))
|
||||
$trs[$row['cache_code']][] = $row;
|
||||
foreach ($results as $cache_code => &$result_ref)
|
||||
{
|
||||
$result_ref['trackables'] = array();
|
||||
if (!isset($trs[$cache_code]))
|
||||
continue;
|
||||
foreach ($trs[$cache_code] as $t)
|
||||
{
|
||||
$result_ref['trackables'][] = array(
|
||||
'code' => 'GK'.str_pad(strtoupper(dechex($t['gk_id'])), 4, "0", STR_PAD_LEFT),
|
||||
'name' => $t['name'],
|
||||
'url' => 'http://geokrety.org/konkret.php?id='.$t['gk_id']
|
||||
);
|
||||
}
|
||||
}
|
||||
unset($trs);
|
||||
}
|
||||
if (in_array('trackables_count', $fields))
|
||||
{
|
||||
if (in_array('trackables', $fields))
|
||||
{
|
||||
# We already got all trackables data, no need to query database again.
|
||||
foreach ($results as $cache_code => &$result_ref)
|
||||
$result_ref['trackables_count'] = count($result_ref['trackables']);
|
||||
}
|
||||
else
|
||||
{
|
||||
$rs = Db::query("
|
||||
select wp as cache_code, count(*) as count
|
||||
from gk_item_waypoint
|
||||
where wp in ('".implode("','", array_map('mysql_real_escape_string', $cache_codes))."')
|
||||
group by wp
|
||||
");
|
||||
$tr_counts = array();
|
||||
while ($row = mysql_fetch_assoc($rs))
|
||||
$tr_counts[$row['cache_code']] = $row['count'];
|
||||
foreach ($results as $cache_code => &$result_ref)
|
||||
{
|
||||
if (isset($tr_counts[$cache_code]))
|
||||
$result_ref['trackables_count'] = $tr_counts[$cache_code] + 0;
|
||||
else
|
||||
$result_ref['trackables_count'] = 0;
|
||||
}
|
||||
unset($tr_counts);
|
||||
}
|
||||
}
|
||||
|
||||
# Alternate/Additional waypoints.
|
||||
|
||||
if (in_array('alt_wpts', $fields))
|
||||
{
|
||||
foreach ($results as &$result_ref)
|
||||
$result_ref['alt_wpts'] = array();
|
||||
if (Settings::get('OC_BRANCH') == 'oc.pl')
|
||||
{
|
||||
# OCPL uses 'waypoints' table to store additional waypoints. OCPL also have
|
||||
# a special 'status' field to denote a hidden waypoint (i.e. final location
|
||||
# of a multicache). Such hidden waypoints are not exposed by OKAPI. A stage
|
||||
# fields is used for ordering and naming.
|
||||
|
||||
$rs = Db::query("
|
||||
select
|
||||
cache_id, stage, latitude, longitude, `desc`,
|
||||
case type
|
||||
when 3 then 'Flag, Red'
|
||||
when 4 then 'Circle with X'
|
||||
when 5 then 'Parking Area'
|
||||
else 'Flag, Green'
|
||||
end as sym
|
||||
from waypoints
|
||||
where
|
||||
cache_id in ('".implode("','", array_map('mysql_real_escape_string', array_keys($cacheid2wptcode)))."')
|
||||
and status = 1
|
||||
order by cache_id, stage, `desc`
|
||||
");
|
||||
}
|
||||
else
|
||||
{
|
||||
# OCDE uses 'coordinates' table (with type=1) to store additional waypoints.
|
||||
# All waypoints are are public.
|
||||
|
||||
$rs = Db::query("
|
||||
select
|
||||
cache_id,
|
||||
null as stage,
|
||||
latitude, longitude,
|
||||
description as `desc`,
|
||||
case subtype
|
||||
when 1 then 'Parking Area'
|
||||
else 'Flag, Green'
|
||||
end as sym
|
||||
from coordinates
|
||||
where
|
||||
type = 1
|
||||
and cache_id in ('".implode("','", array_map('mysql_real_escape_string', array_keys($cacheid2wptcode)))."')
|
||||
order by cache_id, `desc`
|
||||
");
|
||||
}
|
||||
while ($row = mysql_fetch_assoc($rs))
|
||||
{
|
||||
$results[$cacheid2wptcode[$row['cache_id']]]['alt_wpts'][] = array(
|
||||
'name' => $cacheid2wptcode[$row['cache_id']]."-".($row['stage'] ? $row['stage'] : "wpt"),
|
||||
'location' => round($row['latitude'], 6)."|".round($row['longitude'], 6),
|
||||
'sym' => $row['sym'],
|
||||
'description' => ($row['stage'] ? _("Stage")." ".$row['stage'].": " : "").$row['desc'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
# Check which cache codes were not found and mark them with null.
|
||||
foreach ($cache_codes as $cache_code)
|
||||
if (!isset($results[$cache_code]))
|
||||
$results[$cache_code] = null;
|
||||
|
||||
# Order the results in the same order as the input codes were given.
|
||||
# This might come in handy for languages which support ordered dictionaries
|
||||
# (especially with conjunction with the search_and_retrieve method).
|
||||
# See issue#97. PHP dictionaries (assoc arrays) are ordered structures,
|
||||
# so we just have to rewrite it (sequentially).
|
||||
|
||||
$ordered_results = array();
|
||||
foreach ($cache_codes as $cache_code)
|
||||
$ordered_results[$cache_code] = $results[$cache_code];
|
||||
|
||||
return Okapi::formatted_response($request, $ordered_results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create unique caption, safe to be used as a file name for images
|
||||
* uploaded into Garmin's GPS devices. Use reset_unique_captions to reset
|
||||
* unique counter.
|
||||
*/
|
||||
private static function get_unique_caption($caption)
|
||||
{
|
||||
# Garmins keep hanging on long file names. We don't have any specification from
|
||||
# Garmin and cannot determine WHY. That's why we won't use captions until we
|
||||
# know more.
|
||||
|
||||
$caption = self::$caption_no."";
|
||||
self::$caption_no++;
|
||||
return $caption;
|
||||
|
||||
/* This code is harmful for Garmins!
|
||||
$caption = preg_replace('#[^\\pL\d ]+#u', '-', $caption);
|
||||
$caption = trim($caption, '-');
|
||||
if (function_exists('iconv'))
|
||||
{
|
||||
$new = iconv("utf-8", "ASCII//TRANSLIT", $caption);
|
||||
if (!$new)
|
||||
$new = iconv("UTF-8", "ASCII//IGNORE", $caption);
|
||||
} else {
|
||||
$new = $caption;
|
||||
}
|
||||
$new = str_replace(array('/', '\\', '?', '%', '*', ':', '|', '"', '<', '>', '.'), '', $new);
|
||||
$new = trim($new);
|
||||
if ($new == "")
|
||||
$new = "(no caption)";
|
||||
if (strlen($new) > 240)
|
||||
$new = substr($new, 0, 240);
|
||||
$new = self::$caption_no." - ".$new;
|
||||
self::$caption_no++;
|
||||
return $new;*/
|
||||
}
|
||||
private static $caption_no = 1;
|
||||
private static function reset_unique_captions()
|
||||
{
|
||||
self::$caption_no = 1;
|
||||
}
|
||||
|
||||
public static function get_cache_attribution_note($cache_id, $lang)
|
||||
{
|
||||
$site_url = $GLOBALS['absolute_server_URI'];
|
||||
$site_name = Okapi::get_normalized_site_name();
|
||||
$cache_url = $site_url."viewcache.php?cacheid=$cache_id";
|
||||
|
||||
# This list if to be extended (opencaching.de, etc.). (_)
|
||||
|
||||
switch ($lang)
|
||||
{
|
||||
case 'pl':
|
||||
return "<p>Opis <a href='$cache_url'>skrzynki</a> pochodzi z serwisu <a href='$site_url'>$site_name</a>.</p>";
|
||||
break;
|
||||
default:
|
||||
return "<p>This <a href='$cache_url'>geocache</a> description comes from the <a href='$site_url'>$site_name</a> site.</p>";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
45
htdocs/okapi/services/caches/geocaches.xml
Normal file
45
htdocs/okapi/services/caches/geocaches.xml
Normal file
@ -0,0 +1,45 @@
|
||||
<xml>
|
||||
<brief>Retrieve information on multiple geocaches</brief>
|
||||
<issue-id>20</issue-id>
|
||||
<desc>
|
||||
<p>This method works like the services/caches/geocache method, but works
|
||||
with multiple geocaches (instead of only one).</p>
|
||||
</desc>
|
||||
<req name='cache_codes'>
|
||||
<p>Pipe-separated list of cache cache codes. These represent the
|
||||
geocaches you are interested in. No more than 500 codes are allowed.
|
||||
Unlike in the "geocache" method, this CAN be an empty string (it will
|
||||
result in an empty, but valid, response).</p>
|
||||
</req>
|
||||
<opt name='langpref' default='en'>
|
||||
<p>Pipe-separated list of ISO 639-1 language codes. This indicates the
|
||||
order of preference in which language will be chosen for fields like
|
||||
<b>name</b> and <b>description</b>.</p>
|
||||
<p>Please note, that you may also access caches descriptions in all
|
||||
available languages. If you want to do this, you should use fields
|
||||
<b>names</b>, <b>descriptions</b> (etc.) instead of <b>name</b> and
|
||||
<b>description</b> (etc.).</p>
|
||||
</opt>
|
||||
<opt name='fields' default='code|name|location|type|status'>
|
||||
<p>Same as in the services/caches/geocache method. Pipe-separated list
|
||||
of field names which you are interested with.
|
||||
See services/caches/geocache method for a list available values.</p>
|
||||
</opt>
|
||||
<opt name='lpc' default='10'>
|
||||
Same as in the services/caches/geocache method.
|
||||
</opt>
|
||||
<opt name='user_uuid'>
|
||||
Same as in the services/caches/geocache method.
|
||||
</opt>
|
||||
<common-format-params/>
|
||||
<returns>
|
||||
<p>A dictionary. Cache codes you provide will be mapped to dictionary keys,
|
||||
and each value will be a dictionary of fields you have selected.</p>
|
||||
<p>For example, for <i>geocaches?cache_codes=OP3D96|OC124&fields=type</i>
|
||||
query, the result might look something link this:</p>
|
||||
<pre>{"OP3D96": {"type": "Traditional"}, "OC124": null}</pre>
|
||||
<p>Value of <b>null</b> means that the given cache haven't been found.
|
||||
(This behavior is different than in the services/caches/geocache method, which
|
||||
responds with a HTTP 400 error in such case.)</p>
|
||||
</returns>
|
||||
</xml>
|
36
htdocs/okapi/services/caches/search/all.php
Normal file
36
htdocs/okapi/services/caches/search/all.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
# This method is the simplest of all. It just returns all cashes, in any order.
|
||||
# Results might be limited only with the "standard filtering arguments",
|
||||
# implemented in the OkapiSearchAssistant::get_common_search_params.
|
||||
#
|
||||
# Its existance is intentional - though a bit inpractical, it serves as a
|
||||
# reference base for every other search method which might use "standard
|
||||
# filters" (those defined in OkapiSearchAssistant::get_common_search_params).
|
||||
|
||||
namespace okapi\services\caches\search\all;
|
||||
|
||||
use okapi\Okapi;
|
||||
use okapi\OkapiRequest;
|
||||
use okapi\ParamMissing;
|
||||
use okapi\InvalidParam;
|
||||
use okapi\services\caches\search\SearchAssistant;
|
||||
|
||||
require_once 'searching.inc.php';
|
||||
|
||||
class WebService
|
||||
{
|
||||
public static function options()
|
||||
{
|
||||
return array(
|
||||
'min_auth_level' => 1
|
||||
);
|
||||
}
|
||||
|
||||
public static function call(OkapiRequest $request)
|
||||
{
|
||||
$search_params = SearchAssistant::get_common_search_params($request);
|
||||
$result = SearchAssistant::get_common_search_result($search_params);
|
||||
return Okapi::formatted_response($request, $result);
|
||||
}
|
||||
}
|
162
htdocs/okapi/services/caches/search/all.xml
Normal file
162
htdocs/okapi/services/caches/search/all.xml
Normal file
@ -0,0 +1,162 @@
|
||||
<xml>
|
||||
<brief>Search for geocaches</brief>
|
||||
<issue-id>15</issue-id>
|
||||
<desc>
|
||||
<p>Search for geocaches using some simple filters.
|
||||
All of them are optional, but you surely want to use at least some of them.</p>
|
||||
<ul>
|
||||
<li>If you're looking for a way to keep your geocaching database in sync with ours,
|
||||
have a peek at the <b>replicate</b> module.</li>
|
||||
<li>Usually you will want to use search_and_retrieve method instead of this one.
|
||||
This way, you can get much more data in <b>one request</b>.</li>
|
||||
</ul>
|
||||
</desc>
|
||||
<opt name='type'>
|
||||
<p>Pipe-separated list of cache type codes. If given, the results will be limited
|
||||
to the given cache types. For a list of cache type codes, see services/caches/geocache
|
||||
method.</p>
|
||||
<p><b>Notice:</b> If you want to include all cache types <b>except</b>
|
||||
given ones, prepend your list with the "-" sign.</p>
|
||||
</opt>
|
||||
<opt name='status' default='Available'>
|
||||
<p>Pipe-separated list of status codes. Only caches matching any of the given
|
||||
statuses will included in the response. For a list of cache status codes, see
|
||||
services/caches/geocache method.</p>
|
||||
</opt>
|
||||
<opt name='owner_uuid'>
|
||||
<p>Pipe-separated list of user IDs. If given, the list of returned caches
|
||||
will be limited to those owned by the given users.</p>
|
||||
<p><b>Notice:</b> User ID and Username are two different things!</p>
|
||||
<p><b>Notice:</b> If you want to include all caches <b>except</b> the ones
|
||||
owned by the given users, prepend your list with the "-" sign.</p>
|
||||
</opt>
|
||||
<opt name='name'>
|
||||
<p>UTF-8 encoded string - the name (or part of the name) of the cache.</p>
|
||||
<p>Allowed wildcard characters:</p>
|
||||
<ul>
|
||||
<li>asterisk ("*") will match any string of characters,</li>
|
||||
<li>underscore ("_") will match any single character.</li>
|
||||
</ul>
|
||||
<p>Name matching is case-insensitive. Maximum length for the <b>name</b> parameter
|
||||
is 100 characters.</p>
|
||||
<p><b>Examples:</b></p>
|
||||
<ul>
|
||||
<li>"name=the *" returns caches which name starts with "the " (or "The ", etc.),</li>
|
||||
<li>"name=water tower" returns caches which have <b>exactly</b> that name (case insensitive),</li>
|
||||
<li>"name=*water tower*" will match caches like "Beautiful Water Tower II",</li>
|
||||
<li>"name=*tower_" will match "Water Towers" but will <b>not</b> match "Water Tower".</li>
|
||||
</ul>
|
||||
<p><b>Notice:</b> Cache code and name are two different things - if you want to
|
||||
find a cache by its code, use the services/caches/geocache method.</p>
|
||||
</opt>
|
||||
<opt name='terrain' default='1-5'>
|
||||
<p>A string "X-Y", where X and Y are integers between 1 and 5, and X <= Y.
|
||||
Only caches with terrain rating between these numbers (inclusive) will be returned.</p>
|
||||
</opt>
|
||||
<opt name='difficulty' default='1-5'>
|
||||
<p>A string "X-Y", where X and Y are integers between 1 and 5, and X <= Y.
|
||||
Only caches with difficulty rating between these numbers (inclusive) will be returned.</p>
|
||||
</opt>
|
||||
<opt name='size'>
|
||||
<p>A string "X-Y", where X and Y are integers between 1 and 5, and X <= Y.
|
||||
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>
|
||||
</opt>
|
||||
<opt name='rating'>
|
||||
<p>A string "X-Y", where X and Y are integers between 1 and 5, and X <= 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>
|
||||
<p><b>Notice:</b> Some OC installations do not provide ratings for their geocaches.
|
||||
On such installation, this parameter will be ignored.</p>
|
||||
</opt>
|
||||
<opt name='min_rcmds'>
|
||||
<p>There are two possible value-types for this argument:</p>
|
||||
<ul>
|
||||
<li>Integer N. If <i>N</i> given, the result will contain only those caches which have
|
||||
at least <i>N</i> recommendations.</li>
|
||||
<li>Integer N with a percent sign appended (e.g. "20%"). The result will contain
|
||||
only those caches which have at last <i>N%</i> ratio of recommendations/founds.
|
||||
Please note, that this is useful only with conjunction with <b>min_founds</b>
|
||||
parameter.</li>
|
||||
</ul>
|
||||
</opt>
|
||||
<opt name='min_founds'>
|
||||
<p>Integer N. If <i>N</i> given, the result will contain only those caches which have
|
||||
been found at least <i>N</i> times. Useful if you want to skip "unverified" caches.</p>
|
||||
</opt>
|
||||
<opt name='max_founds'>
|
||||
<p>Integer N. If <i>N</i> given, the result will contain only those caches which have
|
||||
been found at most <i>N</i> times. Useful for FTF hunters.</p>
|
||||
</opt>
|
||||
<opt name='modified_since'>
|
||||
<p>A date and time string. This should be in ISO 8601 format (although currently
|
||||
any format acceptable by PHP's <a href='http://pl2.php.net/strtotime'>strtotime</a>
|
||||
function also will do).</p>
|
||||
<p>If given, the list of returned geocaches will be limited to those which were
|
||||
created or modified since that date. (Note: currently caches do not get
|
||||
"modified" when user makes a log entry.)</p>
|
||||
</opt>
|
||||
<opt name='found_status' default='either'>
|
||||
<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>Should be one of the following:</p>
|
||||
<ul>
|
||||
<li><b>found_only</b> - only caches found by the user will be returned,</li>
|
||||
<li><b>notfound_only</b> - only caches <b>not</b> found by the user will be returned,</li>
|
||||
<li><b>either</b> - all caches will be returned.</li>
|
||||
</ul>
|
||||
</opt>
|
||||
<opt name='found_by'>
|
||||
<p>User UUID. If given, the response will only include geocaches found by
|
||||
the given user.</p>
|
||||
</opt>
|
||||
<opt name='not_found_by'>
|
||||
<p>User UUID. If given, the response will only include geocaches not found by
|
||||
the given user.</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).
|
||||
See <b>owner_uuid</b> parameter if you don't want to use OAuth.</p>
|
||||
<p>Boolean. If set to <b>true</b>, caches which the user is an owner of will
|
||||
not be included in the result.</p>
|
||||
</opt>
|
||||
<opt name='limit' default='100'>
|
||||
<p>Integer in range 1..500. Maximum number of cache codes returned.</p>
|
||||
</opt>
|
||||
<opt name='offset' default='0'>
|
||||
<p>An offset, the amount of items to skip at the beginning of the result. Combined with limit
|
||||
allows pagination of the result set. Please note, that the sum of offset and limit still cannot
|
||||
exceed 500. You MAY NOT use this method to list all the caches, it is for searching only.
|
||||
Have a peek at the <b>replicate</b> module if you need all the caches.</p>
|
||||
</opt>
|
||||
<opt name='order_by'>
|
||||
<p>Pipe separated list of fields to order the results by. Prefix the field name with
|
||||
a '-' sign to indicate a descending order.</p>
|
||||
<p>Currently, fields which you can order by include: <b>code</b>, <b>name</b>,
|
||||
<b>founds</b>, <b>rcmds</b>, <b>rcmds%</b> (tell us if you want more).</p>
|
||||
<p><b>Examples:</b></p>
|
||||
<ul>
|
||||
<li>to order by cache name use "order_by=name" or "order_by=+name",</li>
|
||||
<li>to have the largest caches in front, use "order_by=-size",</li>
|
||||
<li>multicolumn sorting is also allowed, ex. "order_by=-founds|-size|distance"</li>
|
||||
</ul>
|
||||
<p><b>Note:</b> Try to avoid executing separate OKAPI request every time you
|
||||
want to sort the data which you <b>already have</b>. Consider caching and
|
||||
sorting on client side. This will speed up things for both sides.</p>
|
||||
</opt>
|
||||
<common-format-params/>
|
||||
<returns>
|
||||
<p>A dictionary of the following structure:</p>
|
||||
<ul>
|
||||
<li><b>results</b> - a list of cache codes,</li>
|
||||
<li><b>more</b> - boolean, <b>true</b> means that there were more
|
||||
results for your query, but they were not returned because of the
|
||||
<b>limit</b> parameter.</li>
|
||||
</ul>
|
||||
</returns>
|
||||
</xml>
|
87
htdocs/okapi/services/caches/search/bbox.php
Normal file
87
htdocs/okapi/services/caches/search/bbox.php
Normal file
@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace okapi\services\caches\search\bbox;
|
||||
|
||||
require_once 'searching.inc.php';
|
||||
|
||||
use okapi\Okapi;
|
||||
use okapi\OkapiRequest;
|
||||
use okapi\ParamMissing;
|
||||
use okapi\InvalidParam;
|
||||
use okapi\services\caches\search\SearchAssistant;
|
||||
|
||||
class WebService
|
||||
{
|
||||
public static function options()
|
||||
{
|
||||
return array(
|
||||
'min_auth_level' => 1
|
||||
);
|
||||
}
|
||||
|
||||
public static function call(OkapiRequest $request)
|
||||
{
|
||||
# You may wonder, why there are no parameters like "bbox" or "center" in the
|
||||
# "search/all" method. This is *intentional* and should be kept this way.
|
||||
# Such parameters would fall in conflict with each other and - in result -
|
||||
# make the documentation very fuzzy. That's why they were intentionally
|
||||
# left out of the "search/all" method, and put in separate (individual) ones.
|
||||
# It's much easier to grasp their meaning this way.
|
||||
|
||||
$tmp = $request->get_parameter('bbox');
|
||||
if (!$tmp)
|
||||
throw new ParamMissing('bbox');
|
||||
$parts = explode('|', $tmp);
|
||||
if (count($parts) != 4)
|
||||
throw new InvalidParam('bbox', "Expecting 4 pipe-separated parts, got ".count($parts).".");
|
||||
foreach ($parts as &$part_ref)
|
||||
{
|
||||
if (!preg_match("/^-?[0-9]+(\.?[0-9]*)$/", $part_ref))
|
||||
throw new InvalidParam('bbox', "'$part_ref' is not a valid float number.");
|
||||
$part_ref = floatval($part_ref);
|
||||
}
|
||||
list($bbsouth, $bbwest, $bbnorth, $bbeast) = $parts;
|
||||
if ($bbnorth <= $bbsouth)
|
||||
throw new InvalidParam('bbox', "Northern edge must be situated to the north of the southern edge.");
|
||||
if ($bbeast == $bbwest)
|
||||
throw new InvalidParam('bbox', "Eastern edge longitude is the same as the western one.");
|
||||
if ($bbnorth > 90 || $bbnorth < -90 || $bbsouth > 90 || $bbsouth < -90)
|
||||
throw new InvalidParam('bbox', "Latitudes have to be within -90..90 range.");
|
||||
if ($bbeast > 180 || $bbeast < -180 || $bbwest > 180 || $bbwest < -180)
|
||||
throw new InvalidParam('bbox', "Longitudes have to be within -180..180 range.");
|
||||
|
||||
# Construct SQL conditions for the specified bounding box.
|
||||
|
||||
$where_conds = array();
|
||||
$where_conds[] = "caches.latitude between '".mysql_real_escape_string($bbsouth)."' and '".mysql_real_escape_string($bbnorth)."'";
|
||||
if ($bbeast > $bbwest)
|
||||
{
|
||||
# Easy one.
|
||||
$where_conds[] = "caches.longitude between '".mysql_real_escape_string($bbwest)."' and '".mysql_real_escape_string($bbeast)."'";
|
||||
}
|
||||
else
|
||||
{
|
||||
# We'll have to assume that this box goes through the 180-degree meridian.
|
||||
# For example, $bbwest = 179 and $bbeast = -179.
|
||||
$where_conds[] = "(caches.longitude > '".mysql_real_escape_string($bbwest)."' or caches.longitude < '".mysql_real_escape_string($bbeast)."')";
|
||||
}
|
||||
|
||||
#
|
||||
# In the method description, we promised to return caches ordered by the *rough*
|
||||
# distance from the center of the bounding box. We'll use ORDER BY with a simplified
|
||||
# distance formula and combine it with the LIMIT clause to get the best results.
|
||||
#
|
||||
|
||||
$center_lat = ($bbsouth + $bbnorth) / 2.0;
|
||||
$center_lon = ($bbwest + $bbeast) / 2.0;
|
||||
|
||||
$search_params = SearchAssistant::get_common_search_params($request);
|
||||
$search_params['where_conds'] = array_merge($where_conds, $search_params['where_conds']);
|
||||
$search_params['order_by'][] = Okapi::get_distance_sql($center_lat, $center_lon,
|
||||
"caches.latitude", "caches.longitude"); # not replaced; added to the end!
|
||||
|
||||
$result = SearchAssistant::get_common_search_result($search_params);
|
||||
|
||||
return Okapi::formatted_response($request, $result);
|
||||
}
|
||||
}
|
34
htdocs/okapi/services/caches/search/bbox.xml
Normal file
34
htdocs/okapi/services/caches/search/bbox.xml
Normal file
@ -0,0 +1,34 @@
|
||||
<xml>
|
||||
<brief>Search for caches within specified bounding box</brief>
|
||||
<issue-id>16</issue-id>
|
||||
<desc>
|
||||
<p>This method is similar to the search/all method, but the results
|
||||
are restricted to the caches situated within a given bounding box
|
||||
(a rectangle on the map).</p>
|
||||
<p>Unless overriden, results are ordered by the distance from the center of the bounding
|
||||
box. This means, that if you hit the limit of geocaches
|
||||
returned, you will receive the ones that are in the middle of your
|
||||
box, and miss the ones on the edges.</p>
|
||||
<p>Usually you will want to use search_and_retrieve method instead of this one.
|
||||
This way, you can get much more data in <b>one request</b>.</p>
|
||||
</desc>
|
||||
<req name='bbox'>
|
||||
<p>The bounding box within to search for caches. The box is defined
|
||||
by a string in "<b>S|W|N|E</b>" format, where:</p>
|
||||
<ul>
|
||||
<li><b>S</b> stands for southern edge latitude of the box,</li>
|
||||
<li><b>W</b> stands for western edge longitude of the box,</li>
|
||||
<li><b>N</b> stands for northern edge latitude of the box,</li>
|
||||
<li><b>E</b> stands for eastern edge longitude of the box.</li>
|
||||
</ul>
|
||||
<p>Use positive numbers for latitudes in the northern hemisphere and
|
||||
longitudes in the eastern hemisphere (and negative for southern and
|
||||
western hemispheres accordingly). These are full degrees with a dot
|
||||
as a decimal point (ex. "48.7|15.8|54|24.9").</p>
|
||||
</req>
|
||||
<import-params method='services/caches/search/all'/>
|
||||
<common-format-params/>
|
||||
<returns>
|
||||
<p>Same format as in the search/all method.</p>
|
||||
</returns>
|
||||
</xml>
|
165
htdocs/okapi/services/caches/search/by_urls.php
Normal file
165
htdocs/okapi/services/caches/search/by_urls.php
Normal file
@ -0,0 +1,165 @@
|
||||
<?php
|
||||
|
||||
namespace okapi\services\caches\search\by_urls;
|
||||
|
||||
require_once 'searching.inc.php';
|
||||
|
||||
use okapi\Okapi;
|
||||
use okapi\OkapiRequest;
|
||||
use okapi\ParamMissing;
|
||||
use okapi\InvalidParam;
|
||||
use okapi\Db;
|
||||
|
||||
class WebService
|
||||
{
|
||||
public static function options()
|
||||
{
|
||||
return array(
|
||||
'min_auth_level' => 1
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns one of: array('cache_code', 'OPXXXX'), array('internal_id', '12345'),
|
||||
* array('uuid', 'A408C3...') or null.
|
||||
*/
|
||||
private static function get_cache_key($url)
|
||||
{
|
||||
# Determine our own domain.
|
||||
|
||||
static $host = null;
|
||||
static $length = null;
|
||||
if ($host == null)
|
||||
{
|
||||
$host = parse_url($GLOBALS['absolute_server_URI'], PHP_URL_HOST);
|
||||
if (strpos($host, "www.") === 0)
|
||||
$host = substr($host, 4);
|
||||
$length = strlen($host);
|
||||
}
|
||||
|
||||
# Parse the URL
|
||||
|
||||
$uri = parse_url($url);
|
||||
if ($uri == false)
|
||||
return null;
|
||||
if ((!isset($uri['scheme'])) || (!in_array($uri['scheme'], array('http', 'https'))))
|
||||
return null;
|
||||
if ((!isset($uri['host'])) || (substr($uri['host'], -$length) != $host))
|
||||
return null;
|
||||
$parts = array();
|
||||
if (isset($uri['query']))
|
||||
$parts = array_merge($parts, explode('&', $uri['query']));
|
||||
if (isset($uri['fragment']))
|
||||
$parts = array_merge($parts, explode('&', $uri['fragment']));
|
||||
foreach ($parts as $param)
|
||||
{
|
||||
$item = explode('=', $param, 2);
|
||||
if (count($item) != 2)
|
||||
continue;
|
||||
$key = $item[0];
|
||||
$value = $item[1];
|
||||
if ($key == 'wp')
|
||||
return array('cache_code', $value);
|
||||
if ($key == 'cacheid')
|
||||
return array('internal_id', $value);
|
||||
if ($key == 'uuid')
|
||||
return array('uuid', $value);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function call(OkapiRequest $request)
|
||||
{
|
||||
# Retrieve the list of URLs to check.
|
||||
|
||||
$tmp = $request->get_parameter('urls');
|
||||
if (!$tmp)
|
||||
throw new ParamMissing('urls');
|
||||
$urls = explode('|', $tmp);
|
||||
$as_dict = $request->get_parameter('as_dict');
|
||||
if (!$as_dict) $as_dict = 'false';
|
||||
if (!in_array($as_dict, array('true', 'false')))
|
||||
throw new InvalidParam('as_dict');
|
||||
$as_dict = ($as_dict == 'true');
|
||||
|
||||
# Generate the lists of keys.
|
||||
|
||||
$results = array();
|
||||
$urls_with = array(
|
||||
'cache_code' => array(),
|
||||
'internal_id' => array(),
|
||||
'uuid' => array()
|
||||
);
|
||||
foreach ($urls as &$url_ref)
|
||||
{
|
||||
$key = self::get_cache_key($url_ref);
|
||||
if ($key != null)
|
||||
$urls_with[$key[0]][$url_ref] = $key[1];
|
||||
else
|
||||
$results[$url_ref] = null;
|
||||
}
|
||||
|
||||
# Include 'cache_code' references.
|
||||
|
||||
foreach ($urls_with['cache_code'] as $url => $cache_code)
|
||||
$results[$url] = $cache_code;
|
||||
|
||||
# Include 'internal_id' references.
|
||||
|
||||
$internal_ids = array_values($urls_with['internal_id']);
|
||||
if (count($internal_ids) > 0)
|
||||
{
|
||||
$rs = Db::query("
|
||||
select cache_id, wp_oc
|
||||
from caches
|
||||
where cache_id in ('".implode("','", array_map('mysql_real_escape_string', $internal_ids))."')
|
||||
");
|
||||
$dict = array();
|
||||
while ($row = mysql_fetch_assoc($rs))
|
||||
$dict[$row['cache_id']] = $row['wp_oc'];
|
||||
foreach ($urls_with['internal_id'] as $url => $internal_id)
|
||||
{
|
||||
if (isset($dict[$internal_id]))
|
||||
$results[$url] = $dict[$internal_id];
|
||||
else
|
||||
$results[$url] = null;
|
||||
}
|
||||
}
|
||||
|
||||
# Include 'uuid' references.
|
||||
|
||||
$uuids = array_values($urls_with['uuid']);
|
||||
if (count($uuids) > 0)
|
||||
{
|
||||
$rs = Db::query("
|
||||
select uuid, wp_oc
|
||||
from caches
|
||||
where uuid in ('".implode("','", array_map('mysql_real_escape_string', $uuids))."')
|
||||
");
|
||||
$dict = array();
|
||||
while ($row = mysql_fetch_assoc($rs))
|
||||
$dict[$row['uuid']] = $row['wp_oc'];
|
||||
foreach ($urls_with['uuid'] as $url => $uuid)
|
||||
{
|
||||
if (isset($dict[$uuid]))
|
||||
$results[$url] = $dict[$uuid];
|
||||
else
|
||||
$results[$url] = null;
|
||||
}
|
||||
}
|
||||
|
||||
# Format the results according to the 'as_dict' parameter.
|
||||
|
||||
if ($as_dict)
|
||||
return Okapi::formatted_response($request, $results);
|
||||
else
|
||||
{
|
||||
$cache_codes = array();
|
||||
foreach ($results as $url => $cache_code)
|
||||
if ($cache_code != null)
|
||||
$cache_codes[$cache_code] = true;
|
||||
$flattened = array('results' => array_keys($cache_codes));
|
||||
return Okapi::formatted_response($request, $flattened);
|
||||
}
|
||||
}
|
||||
}
|
32
htdocs/okapi/services/caches/search/by_urls.xml
Normal file
32
htdocs/okapi/services/caches/search/by_urls.xml
Normal file
@ -0,0 +1,32 @@
|
||||
<xml>
|
||||
<brief>Resolve cache references from given URLs</brief>
|
||||
<issue-id>116</issue-id>
|
||||
<desc>
|
||||
<p>Given a set of URLs, determine codes of geocaches referenced within them.</p>
|
||||
<p>This might be useful if you have a link to an OpenCaching geocache page, but you're
|
||||
not able to extract cache code from such link (and you need the cache code to operate
|
||||
on a cache using OKAPI). Geocache pages may be referenced using various types of URLs,
|
||||
only some of them contain the cache code.</p>
|
||||
</desc>
|
||||
<req name='urls'>
|
||||
<p>Pipe-separated list of URLs. No more than 500.</p>
|
||||
</req>
|
||||
<opt name='as_dict' default='false'>
|
||||
<p>If <b>false</b>, then the result of this method will be compatible with
|
||||
search/all method (and therefore can be used with search_and_retrieve).</p>
|
||||
<p>If <b>true</b>, then the result will be a dictionary. Your URLs will be
|
||||
mapped to the keys of this dictionary. Each value will be either a string
|
||||
(cache code) or null (if no cache code was found for the given URL).</p>
|
||||
</opt>
|
||||
<common-format-params/>
|
||||
<returns>
|
||||
<p>As described in the <b>as_dict</b> parameter.</p>
|
||||
<p>Examples:</p>
|
||||
<p>For <i>by_urls?urls=url1|url2|url3</i>
|
||||
query, the result might look something link this:</p>
|
||||
<pre>{"results": ["OP28F9"]}</pre>
|
||||
<p>For <i>by_urls?urls=url1|url2|url3&as_dict=true</i>
|
||||
query, the result might look something link this:</p>
|
||||
<pre>{"url1": "OP28F9", "url2": null, "url3": "OP28F9"}</pre>
|
||||
</returns>
|
||||
</xml>
|
86
htdocs/okapi/services/caches/search/nearest.php
Normal file
86
htdocs/okapi/services/caches/search/nearest.php
Normal file
@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace okapi\services\caches\search\nearest;
|
||||
|
||||
require_once 'searching.inc.php';
|
||||
|
||||
use okapi\Okapi;
|
||||
use okapi\OkapiRequest;
|
||||
use okapi\ParamMissing;
|
||||
use okapi\InvalidParam;
|
||||
use okapi\services\caches\search\SearchAssistant;
|
||||
|
||||
class WebService
|
||||
{
|
||||
public static function options()
|
||||
{
|
||||
return array(
|
||||
'min_auth_level' => 1
|
||||
);
|
||||
}
|
||||
|
||||
public static function call(OkapiRequest $request)
|
||||
{
|
||||
# You may wonder, why there are no parameters like "bbox" or "center" in the
|
||||
# "search/all" method. This is *intentional* and should be kept this way.
|
||||
# Such parameters would fall in conflict with each other and - in result -
|
||||
# make the documentation very fuzzy. That's why they were intentionally
|
||||
# left out of the "search/all" method, and put in separate (individual) ones.
|
||||
# It's much easier to grasp their meaning this way.
|
||||
|
||||
$tmp = $request->get_parameter('center');
|
||||
if (!$tmp)
|
||||
throw new ParamMissing('center');
|
||||
$parts = explode('|', $tmp);
|
||||
if (count($parts) != 2)
|
||||
throw new InvalidParam('center', "Expecting 2 pipe-separated parts, got ".count($parts).".");
|
||||
foreach ($parts as &$part_ref)
|
||||
{
|
||||
if (!preg_match("/^-?[0-9]+(\.?[0-9]*)$/", $part_ref))
|
||||
throw new InvalidParam('center', "'$part_ref' is not a valid float number.");
|
||||
$part_ref = floatval($part_ref);
|
||||
}
|
||||
list($center_lat, $center_lon) = $parts;
|
||||
if ($center_lat > 90 || $center_lat < -90)
|
||||
throw new InvalidParam('center', "Latitudes have to be within -90..90 range.");
|
||||
if ($center_lon > 180 || $center_lon < -180)
|
||||
throw new InvalidParam('center', "Longitudes have to be within -180..180 range.");
|
||||
|
||||
#
|
||||
# In the method description, we promised to return caches ordered by the *rough*
|
||||
# distance from the center point. We'll use ORDER BY with a simplified distance
|
||||
# formula and combine it with the LIMIT clause to get the best results.
|
||||
#
|
||||
|
||||
$distance_formula = Okapi::get_distance_sql($center_lat, $center_lon, "caches.latitude", "caches.longitude");
|
||||
|
||||
# 'radius' parameter is optional. If not given, we'll have to calculate the
|
||||
# distance for every cache in the database.
|
||||
|
||||
$where_conds = array();
|
||||
$radius = null;
|
||||
if ($tmp = $request->get_parameter('radius'))
|
||||
{
|
||||
if (!preg_match("/^-?[0-9]+(\.?[0-9]*)$/", $tmp))
|
||||
throw new InvalidParam('radius', "'$tmp' is not a valid float number.");
|
||||
$radius = floatval($tmp);
|
||||
if ($radius <= 0)
|
||||
throw new InvalidParam('radius', "Has to be a positive number.");
|
||||
$radius *= 1000; # this one is given in kilemeters, converting to meters!
|
||||
$where_conds[] = "$distance_formula <= '".mysql_real_escape_string($radius)."'";
|
||||
}
|
||||
|
||||
$search_params = SearchAssistant::get_common_search_params($request);
|
||||
$search_params['where_conds'] = array_merge($where_conds, $search_params['where_conds']);
|
||||
$search_params['order_by'][] = $distance_formula; # not replaced; added to the end!
|
||||
|
||||
$result = SearchAssistant::get_common_search_result($search_params);
|
||||
if ($radius == null)
|
||||
{
|
||||
# 'more' is meaningless in this case, we'll remove it.
|
||||
unset($result['more']);
|
||||
}
|
||||
|
||||
return Okapi::formatted_response($request, $result);
|
||||
}
|
||||
}
|
30
htdocs/okapi/services/caches/search/nearest.xml
Normal file
30
htdocs/okapi/services/caches/search/nearest.xml
Normal file
@ -0,0 +1,30 @@
|
||||
<xml>
|
||||
<brief>Search for nearest geocaches</brief>
|
||||
<issue-id>17</issue-id>
|
||||
<desc>
|
||||
<p>Find the nearest geocaches. Unless overriden, results are ordered by the distance from
|
||||
the given center point.</p>
|
||||
<p>Usually you will want to use search_and_retrieve method instead of this one.
|
||||
This way, you can get much more data in <b>one request</b>.</p>
|
||||
</desc>
|
||||
<req name='center'>
|
||||
<p>The center point (typically - the user's location), in the
|
||||
"lat|lon" format.</p>
|
||||
<p>Use positive numbers for latitudes in the northern hemisphere and
|
||||
longitudes in the eastern hemisphere (and negative for southern and
|
||||
western hemispheres accordingly). These are full degrees with a dot
|
||||
as a decimal point (ex. "54.3|22.3").</p>
|
||||
</req>
|
||||
<opt name='radius'>
|
||||
<p>Maximal distance (from the center point) for the cache to
|
||||
be included in the results. Unlike in most other places, this
|
||||
distance is given <b>in kilometers</b> instead of meters
|
||||
(it can contain a floating point though).</p>
|
||||
</opt>
|
||||
<import-params method='services/caches/search/all'/>
|
||||
<common-format-params/>
|
||||
<returns>
|
||||
<p>Same format as in the search/all method. The <b>more</b> key will
|
||||
be skipped when no <b>radius</b> argument is given.</p>
|
||||
</returns>
|
||||
</xml>
|
462
htdocs/okapi/services/caches/search/searching.inc.php
Normal file
462
htdocs/okapi/services/caches/search/searching.inc.php
Normal file
@ -0,0 +1,462 @@
|
||||
<?php
|
||||
|
||||
namespace okapi\services\caches\search;
|
||||
|
||||
use okapi\Okapi;
|
||||
use okapi\Db;
|
||||
use okapi\OkapiInternalRequest;
|
||||
use okapi\OkapiServiceRunner;
|
||||
use okapi\OkapiRequest;
|
||||
use okapi\InvalidParam;
|
||||
use okapi\BadRequest;
|
||||
use okapi\Settings;
|
||||
use Exception;
|
||||
|
||||
class SearchAssistant
|
||||
{
|
||||
/**
|
||||
* Load, parse and check common geocache search parameters from the
|
||||
* given OKAPI request. Most cache search methods share a common set
|
||||
* of filtering parameters recognized by this method. It returns
|
||||
* a dictionary of the following structure:
|
||||
*
|
||||
* - "where_conds" - list of additional WHERE conditions to be ANDed
|
||||
* to the rest of your SQL query,
|
||||
* - "offset" - value of the offset parameter to be used in the LIMIT clause,
|
||||
* - "limit" - value of the limit parameter to be used in the LIMIT clause,
|
||||
* - "order_by" - list of order by clauses to be included in the "order by"
|
||||
* SQL clause,
|
||||
* - "extra_tables" - extra tables to be included in the FROM clause.
|
||||
*/
|
||||
public static function get_common_search_params(OkapiRequest $request)
|
||||
{
|
||||
$where_conds = array('true');
|
||||
$extra_tables = array();
|
||||
|
||||
# At the beginning we have to set up some "magic e$Xpressions".
|
||||
# We will use them to make our query run on both OCPL and OCDE databases.
|
||||
|
||||
if (Settings::get('OC_BRANCH') == 'oc.pl')
|
||||
{
|
||||
# OCPL's 'caches' table contains some fields which OCDE's does not
|
||||
# (topratings, founds, notfounds, last_found, votes, score). If
|
||||
# we're being run on OCPL installation, we will simply use them.
|
||||
|
||||
$X_TOPRATINGS = 'caches.topratings';
|
||||
$X_FOUNDS = 'caches.founds';
|
||||
$X_NOTFOUNDS = 'caches.notfounds';
|
||||
$X_LAST_FOUND = 'caches.last_found';
|
||||
$X_VOTES = 'caches.votes';
|
||||
$X_SCORE = 'caches.score';
|
||||
}
|
||||
else
|
||||
{
|
||||
# OCDE holds this data in a separate table. Additionally, OCDE
|
||||
# does not provide a rating system (votes and score fields).
|
||||
# If we're being run on OCDE database, we will include this
|
||||
# additional table in our query (along with a proper WHERE
|
||||
# condition) and we will map the field expressions to
|
||||
# approriate places.
|
||||
|
||||
$extra_tables[] = 'stat_caches';
|
||||
$where_conds[] = 'stat_caches.cache_id = caches.cache_id';
|
||||
|
||||
$X_TOPRATINGS = 'stat_caches.toprating';
|
||||
$X_FOUNDS = 'stat_caches.found';
|
||||
$X_NOTFOUNDS = 'stat_caches.notfound';
|
||||
$X_LAST_FOUND = 'stat_caches.last_found';
|
||||
$X_VOTES = '0'; // no support for ratings
|
||||
$X_SCORE = '0'; // no support for ratings
|
||||
}
|
||||
|
||||
#
|
||||
# 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.");
|
||||
}
|
||||
}
|
||||
$where_conds[] = "caches.type $operator ('".implode("','", array_map('mysql_real_escape_string', $types))."')";
|
||||
}
|
||||
|
||||
#
|
||||
# status - filter by status codes
|
||||
#
|
||||
|
||||
$tmp = $request->get_parameter('status');
|
||||
if ($tmp == null) $tmp = "Available";
|
||||
$codes = array();
|
||||
foreach (explode("|", $tmp) as $name)
|
||||
{
|
||||
try
|
||||
{
|
||||
$codes[] = Okapi::cache_status_name2id($name);
|
||||
}
|
||||
catch (Exception $e)
|
||||
{
|
||||
throw new InvalidParam('status', "'$name' is not a valid cache status.");
|
||||
}
|
||||
}
|
||||
$where_conds[] = "caches.status in ('".implode("','", array_map('mysql_real_escape_string', $codes))."')";
|
||||
|
||||
#
|
||||
# owner_uuid
|
||||
#
|
||||
|
||||
if ($tmp = $request->get_parameter('owner_uuid'))
|
||||
{
|
||||
$operator = "in";
|
||||
if ($tmp[0] == '-')
|
||||
{
|
||||
$tmp = substr($tmp, 1);
|
||||
$operator = "not in";
|
||||
}
|
||||
try
|
||||
{
|
||||
$users = OkapiServiceRunner::call("services/users/users", new OkapiInternalRequest(
|
||||
$request->consumer, null, array('user_uuids' => $tmp, 'fields' => 'internal_id')));
|
||||
}
|
||||
catch (InvalidParam $e) # invalid uuid
|
||||
{
|
||||
throw new InvalidParam('owner_uuid', $e->whats_wrong_about_it);
|
||||
}
|
||||
$user_ids = array();
|
||||
foreach ($users as $user)
|
||||
$user_ids[] = $user['internal_id'];
|
||||
$where_conds[] = "caches.user_id $operator ('".implode("','", array_map('mysql_real_escape_string', $user_ids))."')";
|
||||
}
|
||||
|
||||
#
|
||||
# terrain, difficulty, size, rating - these are similar, we'll do them in a loop
|
||||
#
|
||||
|
||||
foreach (array('terrain', 'difficulty', 'size', 'rating') as $param_name)
|
||||
{
|
||||
if ($tmp = $request->get_parameter($param_name))
|
||||
{
|
||||
if (!preg_match("/^[1-5]-[1-5]$/", $tmp))
|
||||
throw new InvalidParam($param_name, "'$tmp'");
|
||||
list($min, $max) = explode("-", $tmp);
|
||||
if ($min > $max)
|
||||
throw new InvalidParam($param_name, "'$tmp'");
|
||||
switch ($param_name)
|
||||
{
|
||||
case 'terrain':
|
||||
$where_conds[] = "caches.terrain between 2*$min and 2*$max";
|
||||
break;
|
||||
case 'difficulty':
|
||||
$where_conds[] = "caches.difficulty between 2*$min and 2*$max";
|
||||
break;
|
||||
case 'size':
|
||||
$where_conds[] = "caches.size between $min+1 and $max+1";
|
||||
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";
|
||||
}
|
||||
else
|
||||
{
|
||||
# OCDE does not support rating. We will ignore this parameter.
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#
|
||||
# min_rcmds
|
||||
#
|
||||
|
||||
if ($tmp = $request->get_parameter('min_rcmds'))
|
||||
{
|
||||
if ($tmp[strlen($tmp) - 1] == '%')
|
||||
{
|
||||
$tmp = substr($tmp, 0, strlen($tmp) - 1);
|
||||
if (!is_numeric($tmp))
|
||||
throw new InvalidParam('min_rcmds', "'$tmp'");
|
||||
$tmp = intval($tmp);
|
||||
if ($tmp > 100 || $tmp < 0)
|
||||
throw new InvalidParam('min_rcmds', "'$tmp'");
|
||||
$tmp = floatval($tmp) / 100.0;
|
||||
$where_conds[] = "$X_TOPRATINGS >= $X_FOUNDS * '".mysql_real_escape_string($tmp)."'";
|
||||
$where_conds[] = "$X_FOUNDS > 0";
|
||||
}
|
||||
if (!is_numeric($tmp))
|
||||
throw new InvalidParam('min_rcmds', "'$tmp'");
|
||||
$where_conds[] = "$X_TOPRATINGS >= '".mysql_real_escape_string($tmp)."'";
|
||||
}
|
||||
|
||||
#
|
||||
# min_founds
|
||||
#
|
||||
|
||||
if ($tmp = $request->get_parameter('min_founds'))
|
||||
{
|
||||
if (!is_numeric($tmp))
|
||||
throw new InvalidParam('min_founds', "'$tmp'");
|
||||
$where_conds[] = "$X_FOUNDS >= '".mysql_real_escape_string($tmp)."'";
|
||||
}
|
||||
|
||||
#
|
||||
# max_founds
|
||||
#
|
||||
|
||||
if ($tmp = $request->get_parameter('max_founds'))
|
||||
{
|
||||
if (!is_numeric($tmp))
|
||||
throw new InvalidParam('max_founds', "'$tmp'");
|
||||
$where_conds[] = "$X_FOUNDS <= '".mysql_real_escape_string($tmp)."'";
|
||||
}
|
||||
|
||||
#
|
||||
# modified_since
|
||||
#
|
||||
|
||||
if ($tmp = $request->get_parameter('modified_since'))
|
||||
{
|
||||
$timestamp = strtotime($tmp);
|
||||
if ($timestamp)
|
||||
$where_conds[] = "unix_timestamp(caches.last_modified) > '".mysql_real_escape_string($timestamp)."'";
|
||||
else
|
||||
throw new InvalidParam('modified_since', "'$tmp' is not in a valid format or is not a valid date.");
|
||||
}
|
||||
|
||||
#
|
||||
# found_status
|
||||
#
|
||||
|
||||
if ($tmp = $request->get_parameter('found_status'))
|
||||
{
|
||||
if ($request->token == null)
|
||||
throw new InvalidParam('found_status', "Might be used only for requests signed with an Access Token.");
|
||||
if (!in_array($tmp, array('found_only', 'notfound_only', 'either')))
|
||||
throw new InvalidParam('found_status', "'$tmp'");
|
||||
if ($tmp != 'either')
|
||||
{
|
||||
$found_cache_ids = self::get_found_cache_ids($request->token->user_id);
|
||||
$operator = ($tmp == 'found_only') ? "in" : "not in";
|
||||
$where_conds[] = "caches.cache_id $operator ('".implode("','", array_map('mysql_real_escape_string', $found_cache_ids))."')";
|
||||
}
|
||||
}
|
||||
|
||||
#
|
||||
# found_by
|
||||
#
|
||||
|
||||
if ($tmp = $request->get_parameter('found_by'))
|
||||
{
|
||||
try {
|
||||
$user = OkapiServiceRunner::call("services/users/user", new OkapiInternalRequest(
|
||||
$request->consumer, null, array('user_uuid' => $tmp, 'fields' => 'internal_id')));
|
||||
} catch (InvalidParam $e) { # invalid uuid
|
||||
throw new InvalidParam('found_by', $e->whats_wrong_about_it);
|
||||
}
|
||||
$found_cache_ids = self::get_found_cache_ids($user['internal_id']);
|
||||
$where_conds[] = "caches.cache_id in ('".implode("','", array_map('mysql_real_escape_string', $found_cache_ids))."')";
|
||||
}
|
||||
|
||||
#
|
||||
# not_found_by
|
||||
#
|
||||
|
||||
if ($tmp = $request->get_parameter('not_found_by'))
|
||||
{
|
||||
try {
|
||||
$user = OkapiServiceRunner::call("services/users/user", new OkapiInternalRequest(
|
||||
$request->consumer, null, array('user_uuid' => $tmp, 'fields' => 'internal_id')));
|
||||
} catch (InvalidParam $e) { # invalid uuid
|
||||
throw new InvalidParam('not_found_by', $e->whats_wrong_about_it);
|
||||
}
|
||||
$found_cache_ids = self::get_found_cache_ids($user['internal_id']);
|
||||
$where_conds[] = "caches.cache_id not in ('".implode("','", array_map('mysql_real_escape_string', $found_cache_ids))."')";
|
||||
}
|
||||
|
||||
#
|
||||
# exclude_my_own
|
||||
#
|
||||
|
||||
if ($tmp = $request->get_parameter('exclude_my_own'))
|
||||
{
|
||||
if ($request->token == null)
|
||||
throw new InvalidParam('exclude_my_own', "Might be used only for requests signed with an Access Token.");
|
||||
if (!in_array($tmp, array('true', 'false')))
|
||||
throw new InvalidParam('exclude_my_own', "'$tmp'");
|
||||
if ($tmp == 'true')
|
||||
$where_conds[] = "caches.user_id != '".mysql_real_escape_string($request->token->user_id)."'";
|
||||
}
|
||||
|
||||
#
|
||||
# name
|
||||
#
|
||||
|
||||
if ($tmp = $request->get_parameter('name'))
|
||||
{
|
||||
# WRTODO: Make this more user-friendly. See:
|
||||
# http://code.google.com/p/opencaching-api/issues/detail?id=121
|
||||
|
||||
if (strlen($tmp) > 100)
|
||||
throw new InvalidParam('name', "Maximum length of 'name' parameter is 100 characters");
|
||||
$tmp = str_replace("*", "%", str_replace("%", "%%", $tmp));
|
||||
$where_conds[] = "caches.name LIKE '".mysql_real_escape_string($tmp)."'";
|
||||
}
|
||||
|
||||
#
|
||||
# limit
|
||||
#
|
||||
|
||||
$limit = $request->get_parameter('limit');
|
||||
if ($limit == null) $limit = "100";
|
||||
if (!is_numeric($limit))
|
||||
throw new InvalidParam('limit', "'$limit'");
|
||||
if ($limit < 1 || $limit > 500)
|
||||
throw new InvalidParam('limit', "Has to be between 1 and 500.");
|
||||
|
||||
#
|
||||
# offset
|
||||
#
|
||||
|
||||
$offset = $request->get_parameter('offset');
|
||||
if ($offset == null) $offset = "0";
|
||||
if (!is_numeric($offset))
|
||||
throw new InvalidParam('offset', "'$offset'");
|
||||
if ($offset + $limit > 500)
|
||||
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.");
|
||||
|
||||
#
|
||||
# order_by
|
||||
#
|
||||
|
||||
$order_clauses = array();
|
||||
$order_by = $request->get_parameter('order_by');
|
||||
if ($order_by != null)
|
||||
{
|
||||
$order_by = explode('|', $order_by);
|
||||
foreach ($order_by as $field)
|
||||
{
|
||||
$dir = 'asc';
|
||||
if ($field[0] == '-')
|
||||
{
|
||||
$dir = 'desc';
|
||||
$field = substr($field, 1);
|
||||
}
|
||||
elseif ($field[0] == '+')
|
||||
$field = substr($field, 1); # ignore leading "+"
|
||||
switch ($field)
|
||||
{
|
||||
case 'code': $cl = "caches.wp_oc"; break;
|
||||
case 'name': $cl = "caches.name"; break;
|
||||
case 'founds': $cl = "$X_FOUNDS"; break;
|
||||
case 'rcmds': $cl = "$X_TOPRATINGS"; break;
|
||||
case 'rcmds%':
|
||||
$cl = "$X_TOPRATINGS / if($X_FOUNDS = 0, 1, $X_FOUNDS)";
|
||||
break;
|
||||
default:
|
||||
throw new InvalidParam('order_by', "Unsupported field '$field'");
|
||||
}
|
||||
$order_clauses[] = "($cl) $dir";
|
||||
}
|
||||
}
|
||||
|
||||
$ret_array = array(
|
||||
'where_conds' => $where_conds,
|
||||
'offset' => (int)$offset,
|
||||
'limit' => (int)$limit,
|
||||
'order_by' => $order_clauses,
|
||||
'extra_tables' => $extra_tables,
|
||||
);
|
||||
|
||||
return $ret_array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for caches using given conditions and options. Return
|
||||
* an array in a "standard" format of array('results' => list of
|
||||
* cache codes, 'more' => boolean). This method takes care of the
|
||||
* 'more' variable in an appropriate way.
|
||||
*
|
||||
* The $options parameter include:
|
||||
* - where_conds - list of additional WHERE conditions to be ANDed
|
||||
* to the rest of your SQL query,
|
||||
* - extra_tables - list of additional tables to be joined within
|
||||
* the query,
|
||||
* - order_by - list or SQL clauses to be used with ORDER BY,
|
||||
* - limit - maximum number of cache codes to be returned.
|
||||
*/
|
||||
public static function get_common_search_result($options)
|
||||
{
|
||||
$tables = array_merge(
|
||||
array('caches'),
|
||||
$options['extra_tables']
|
||||
);
|
||||
$where_conds = array_merge(
|
||||
array('caches.wp_oc is not null'),
|
||||
$options['where_conds']
|
||||
);
|
||||
|
||||
# We need to pull limit+1 items, in order to properly determine the
|
||||
# value of "more" variable.
|
||||
|
||||
$cache_codes = Db::select_column("
|
||||
select caches.wp_oc
|
||||
from ".implode(", ", $tables)."
|
||||
where ".implode(" and ", $where_conds)."
|
||||
".((count($options['order_by']) > 0) ? "order by ".implode(", ", $options['order_by']) : "")."
|
||||
limit ".($options['offset']).", ".($options['limit'] + 1).";
|
||||
");
|
||||
|
||||
if (count($cache_codes) > $options['limit'])
|
||||
{
|
||||
$more = true;
|
||||
array_pop($cache_codes); # get rid of the one above the limit
|
||||
} else {
|
||||
$more = false;
|
||||
}
|
||||
|
||||
$result = array(
|
||||
'results' => $cache_codes,
|
||||
'more' => $more,
|
||||
);
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of cache IDs which were found by given user.
|
||||
* Parameter needs to be *internal* user id, not uuid.
|
||||
*/
|
||||
private static function get_found_cache_ids($internal_user_id)
|
||||
{
|
||||
return Db::select_column("
|
||||
select cache_id
|
||||
from cache_logs
|
||||
where
|
||||
user_id = '".mysql_real_escape_string($internal_user_id)."'
|
||||
and type = 1
|
||||
and ".((Settings::get('OC_BRANCH') == 'oc.pl') ? "deleted = 0" : "true")."
|
||||
");
|
||||
}
|
||||
}
|
105
htdocs/okapi/services/caches/shortcuts/search_and_retrieve.php
Normal file
105
htdocs/okapi/services/caches/shortcuts/search_and_retrieve.php
Normal file
@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace okapi\services\caches\shortcuts\search_and_retrieve;
|
||||
|
||||
use okapi\Okapi;
|
||||
use okapi\OkapiHttpResponse;
|
||||
use okapi\OkapiInternalRequest;
|
||||
use okapi\OkapiServiceRunner;
|
||||
use okapi\OkapiRequest;
|
||||
use okapi\ParamMissing;
|
||||
use okapi\InvalidParam;
|
||||
|
||||
class WebService
|
||||
{
|
||||
public static function options()
|
||||
{
|
||||
return array(
|
||||
'min_auth_level' => 1
|
||||
);
|
||||
}
|
||||
|
||||
public static function call(OkapiRequest $request)
|
||||
{
|
||||
# Check search method
|
||||
$search_method = $request->get_parameter('search_method');
|
||||
if (!$search_method)
|
||||
throw new ParamMissing('search_method');
|
||||
if (strpos($search_method, "services/caches/search/") !== 0)
|
||||
throw new InvalidParam('search_method', "Should begin with 'services/caches/search/'.");
|
||||
if (!OkapiServiceRunner::exists($search_method))
|
||||
throw new InvalidParam('search_method', "Method does not exist: '$search_method'");
|
||||
$search_params = $request->get_parameter('search_params');
|
||||
if (!$search_params)
|
||||
throw new ParamMissing('search_params');
|
||||
$search_params = json_decode($search_params, true);
|
||||
if (!is_array($search_params))
|
||||
throw new InvalidParam('search_params', "Should be a JSON-encoded dictionary");
|
||||
|
||||
# Check retrieval method
|
||||
$retr_method = $request->get_parameter('retr_method');
|
||||
if (!$retr_method)
|
||||
throw new ParamMissing('retr_method');
|
||||
if (!OkapiServiceRunner::exists($retr_method))
|
||||
throw new InvalidParam('retr_method', "Method does not exist: '$retr_method'");
|
||||
$retr_params = $request->get_parameter('retr_params');
|
||||
if (!$retr_params)
|
||||
throw new ParamMissing('retr_params');
|
||||
$retr_params = json_decode($retr_params, true);
|
||||
if (!is_array($retr_params))
|
||||
throw new InvalidParam('retr_params', "Should be a JSON-encoded dictionary");
|
||||
|
||||
# Wrapped?
|
||||
$wrap = $request->get_parameter('wrap');
|
||||
if ($wrap == null) throw new ParamMissing('wrap');
|
||||
if (!in_array($wrap, array('true', 'false')))
|
||||
throw new InvalidParam('wrap');
|
||||
$wrap = ($wrap == 'true');
|
||||
|
||||
# Run search method
|
||||
try
|
||||
{
|
||||
$search_result = OkapiServiceRunner::call($search_method, new OkapiInternalRequest(
|
||||
$request->consumer, $request->token, $search_params));
|
||||
}
|
||||
catch (BadRequest $e)
|
||||
{
|
||||
throw new InvalidParam('search_params', "Search method responded with the ".
|
||||
"following error message: ".$e->getMessage());
|
||||
}
|
||||
|
||||
# Run retrieval method
|
||||
try
|
||||
{
|
||||
$retr_result = OkapiServiceRunner::call($retr_method, new OkapiInternalRequest(
|
||||
$request->consumer, $request->token, array_merge($retr_params,
|
||||
array('cache_codes' => implode("|", $search_result['results'])))));
|
||||
}
|
||||
catch (BadRequest $e)
|
||||
{
|
||||
throw new InvalidParam('retr_params', "Retrieval method responded with the ".
|
||||
"following error message: ".$e->getMessage());
|
||||
}
|
||||
|
||||
if ($wrap)
|
||||
{
|
||||
# $retr_result might be a PHP object, but also might be a binary response
|
||||
# (e.g. a GPX file).
|
||||
if ($retr_result instanceof OkapiHttpResponse)
|
||||
$result = array('results' => $retr_result->get_body());
|
||||
else
|
||||
$result = array('results' => $retr_result);
|
||||
foreach ($search_result as $key => &$value_ref)
|
||||
if ($key != 'results')
|
||||
$result[$key] = $value_ref;
|
||||
return Okapi::formatted_response($request, $result);
|
||||
}
|
||||
else
|
||||
{
|
||||
if ($retr_result instanceof OkapiHttpResponse)
|
||||
return $retr_result;
|
||||
else
|
||||
return Okapi::formatted_response($request, $retr_result);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
<xml>
|
||||
<brief>Search for caches and retrieve formatted results</brief>
|
||||
<issue-id>18</issue-id>
|
||||
<desc>
|
||||
<p>We think that searching and retrieving data are two different things
|
||||
and that's why they were cleanly separated in the documentation (put in
|
||||
separate methods). This was done primarily to keep the docs clean.
|
||||
This method allows you to do a quick search+retrieve task in one request.</p>
|
||||
<p>All services/caches/search/* methods respond with a list of cache codes.
|
||||
Then you have to use other method (like services/caches/geocaches) to
|
||||
retrieve the names and locations, based on those cache codes. The
|
||||
<b>search_and_retrieve</b> method allows you to do these two steps in one
|
||||
method call.</p>
|
||||
<p>First, you have to choose both methods and their parameters - one method
|
||||
which returns the cache codes, and the other one, that responds
|
||||
with additional data for the given caches.</p>
|
||||
</desc>
|
||||
<req name='search_method'>
|
||||
<p>Name of the search method (begin with "services/").</p>
|
||||
<p>E.g. <i>services/caches/search/nearest</i>.</p>
|
||||
</req>
|
||||
<req name='search_params'>
|
||||
<p>JSON-formatted dictionary of parameters to be passed on
|
||||
to the search method.</p>
|
||||
<p>E.g. <i>{"center": "49|19", "status": "Available"}</i>.</p>
|
||||
</req>
|
||||
<req name='retr_method'>
|
||||
<p>Name of the retrieval method (begin with "services/").</p>
|
||||
<p>E.g. <i>services/caches/geocaches</i>.</p>
|
||||
</req>
|
||||
<req name='retr_params'>
|
||||
<p>JSON-formatted dictionary of parameters to be passed on
|
||||
to the retrieval method.</p>
|
||||
<p>E.g. <i>{"fields": "name|location|type"}</i></p>
|
||||
<p>The method will be called with one additional parameter - <b>cache_codes</b>.
|
||||
These will be the cache codes collected from the results of the search method.</p>
|
||||
</req>
|
||||
<req name='wrap'>
|
||||
<p>Boolean.</p>
|
||||
<ul>
|
||||
<li>If <b>true</b>, then results will be wrapped in an additional
|
||||
object, in order to include any additional data received along with the
|
||||
search_method response (i.e. the <b>more</b> value).</li>
|
||||
<li>If <b>false</b>, then this method will return exactly what the
|
||||
<b>retr_method</b> will respond with.</li>
|
||||
</ul>
|
||||
</req>
|
||||
<common-format-params/>
|
||||
<returns>
|
||||
<p>If <b>wrap</b> is <b>true</b>, then the method will return a
|
||||
dictionary of the following structure:</p>
|
||||
<ul>
|
||||
<li><b>results</b> - anything the retrival method
|
||||
responds with (as long as it's not an error).</li>
|
||||
<li>any extra keys and values received as a response of
|
||||
the search_method (i.e. the <b>more</b> variable).</li>
|
||||
</ul>
|
||||
<p>If <b>wrap</b> is <b>false</b>, then the method will return
|
||||
anything the retrieval methods responds with (along with its HTTP headers).</p>
|
||||
<p><b>Example:</b></p>
|
||||
<pre>search_and_retrieve<br/>?search_method=services/caches/search/bbox<br/>&search_params={"bbox":"49|19|50|20","limit":"1"}<br/>&retr_method=services/caches/geocaches<br/>&retr_params={"fields":"location"}<br/>&wrap=false</pre>
|
||||
<p>Possible output:<br/><code>{"OP205A": {"location": "49.572417|19.525867"}}</code></p>
|
||||
<p>The same example with <i>wrap=true</i> would return:<br/><code>{"results": {"OP205A": {"location": "49.572417|19.525867"}}, "more": true}</code></p>
|
||||
</returns>
|
||||
</xml>
|
99
htdocs/okapi/services/logs/entries.php
Normal file
99
htdocs/okapi/services/logs/entries.php
Normal file
@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace okapi\services\logs\entries;
|
||||
|
||||
use Exception;
|
||||
use okapi\Okapi;
|
||||
use okapi\Db;
|
||||
use okapi\OkapiRequest;
|
||||
use okapi\ParamMissing;
|
||||
use okapi\InvalidParam;
|
||||
use okapi\OkapiInternalRequest;
|
||||
use okapi\OkapiServiceRunner;
|
||||
use okapi\Settings;
|
||||
use okapi\services\caches\search\SearchAssistant;
|
||||
|
||||
class WebService
|
||||
{
|
||||
public static function options()
|
||||
{
|
||||
return array(
|
||||
'min_auth_level' => 1
|
||||
);
|
||||
}
|
||||
|
||||
private static $valid_field_names = array('uuid', 'cache_code', 'date', 'user', 'type', 'comment');
|
||||
|
||||
public static function call(OkapiRequest $request)
|
||||
{
|
||||
$log_uuids = $request->get_parameter('log_uuids');
|
||||
if ($log_uuids === null) throw new ParamMissing('log_uuids');
|
||||
if ($log_uuids === "")
|
||||
{
|
||||
$log_uuids = array();
|
||||
}
|
||||
else
|
||||
$log_uuids = explode("|", $log_uuids);
|
||||
|
||||
if (count($log_uuids) > 500)
|
||||
throw new InvalidParam('log_uuids', "Maximum allowed number of referenced ".
|
||||
"log entries is 500. You provided ".count($log_uuids)." UUIDs.");
|
||||
if (count($log_uuids) != count(array_unique($log_uuids)))
|
||||
throw new InvalidParam('log_uuids', "Duplicate UUIDs detected (make sure each UUID is referenced only once).");
|
||||
$fields = $request->get_parameter('fields');
|
||||
if (!$fields) $fields = "date|user|type|comment";
|
||||
$fields = explode("|", $fields);
|
||||
foreach ($fields as $field)
|
||||
if (!in_array($field, self::$valid_field_names))
|
||||
throw new InvalidParam('fields', "'$field' is not a valid field code.");
|
||||
|
||||
$rs = Db::query("
|
||||
select cl.id, c.wp_oc as cache_code, cl.uuid, cl.type, unix_timestamp(cl.date) as date, cl.text,
|
||||
u.uuid as user_uuid, u.username, u.user_id
|
||||
from cache_logs cl, user u, caches c
|
||||
where
|
||||
cl.uuid in ('".implode("','", array_map('mysql_real_escape_string', $log_uuids))."')
|
||||
and ".((Settings::get('OC_BRANCH') == 'oc.pl') ? "cl.deleted = 0" : "true")."
|
||||
and cl.user_id = u.user_id
|
||||
and c.cache_id = cl.cache_id
|
||||
");
|
||||
$results = array();
|
||||
while ($row = mysql_fetch_assoc($rs))
|
||||
{
|
||||
$results[$row['uuid']] = array(
|
||||
'uuid' => $row['uuid'],
|
||||
'cache_code' => $row['cache_code'],
|
||||
'date' => date('c', $row['date']),
|
||||
'user' => array(
|
||||
'uuid' => $row['user_uuid'],
|
||||
'username' => $row['username'],
|
||||
'profile_url' => $GLOBALS['absolute_server_URI']."viewprofile.php?userid=".$row['user_id'],
|
||||
),
|
||||
'type' => Okapi::logtypeid2name($row['type']),
|
||||
'comment' => $row['text']
|
||||
);
|
||||
}
|
||||
mysql_free_result($rs);
|
||||
|
||||
# Check which UUIDs were not found and mark them with null.
|
||||
|
||||
foreach ($log_uuids as $log_uuid)
|
||||
if (!isset($results[$log_uuid]))
|
||||
$results[$log_uuid] = null;
|
||||
|
||||
# Remove unwanted fields.
|
||||
|
||||
foreach (self::$valid_field_names as $field)
|
||||
if (!in_array($field, $fields))
|
||||
foreach ($results as &$result_ref)
|
||||
unset($result_ref[$field]);
|
||||
|
||||
# Order the results in the same order as the input codes were given.
|
||||
|
||||
$ordered_results = array();
|
||||
foreach ($log_uuids as $log_uuid)
|
||||
$ordered_results[$log_uuid] = $results[$log_uuid];
|
||||
|
||||
return Okapi::formatted_response($request, $ordered_results);
|
||||
}
|
||||
}
|
27
htdocs/okapi/services/logs/entries.xml
Normal file
27
htdocs/okapi/services/logs/entries.xml
Normal file
@ -0,0 +1,27 @@
|
||||
<xml>
|
||||
<brief>Retrieve information on multiple log entries</brief>
|
||||
<issue-id>107</issue-id>
|
||||
<desc>
|
||||
<p>This method works like the services/logs/entry method, but works
|
||||
with multiple log entries (instead of only one).</p>
|
||||
</desc>
|
||||
<req name='log_uuids'>
|
||||
<p>Pipe-separated list of UUIDs. These represent the log entries you
|
||||
are interested in. No more than 500 codes are allowed.
|
||||
Unlike in the "entry" method, this CAN be an empty string (it will
|
||||
result in an empty, but valid, response).</p>
|
||||
</req>
|
||||
<opt name='fields' default='date|user|type|comment'>
|
||||
<p>Same as in the services/logs/entry method. Pipe-separated list
|
||||
of field names which you are interested with.
|
||||
See services/logs/entry method for a list of available values.</p>
|
||||
</opt>
|
||||
<common-format-params/>
|
||||
<returns>
|
||||
<p>A dictionary. UUIDs you provide will be mapped to dictionary keys,
|
||||
and each value will be a dictionary of fields you have selected.</p>
|
||||
<p>Value of <b>null</b> means that the given UUID haven't been found.
|
||||
(This behavior is different than in the services/logs/entry method, which
|
||||
responds with a HTTP 400 error in such case.)</p>
|
||||
</returns>
|
||||
</xml>
|
36
htdocs/okapi/services/logs/entry.php
Normal file
36
htdocs/okapi/services/logs/entry.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace okapi\services\logs\entry;
|
||||
|
||||
use okapi\OkapiInternalRequest;
|
||||
use okapi\OkapiServiceRunner;
|
||||
use okapi\Okapi;
|
||||
use okapi\OkapiRequest;
|
||||
use okapi\ParamMissing;
|
||||
use okapi\InvalidParam;
|
||||
|
||||
class WebService
|
||||
{
|
||||
public static function options()
|
||||
{
|
||||
return array(
|
||||
'min_auth_level' => 1
|
||||
);
|
||||
}
|
||||
|
||||
public static function call(OkapiRequest $request)
|
||||
{
|
||||
$log_uuid = $request->get_parameter('log_uuid');
|
||||
if (!$log_uuid) throw new ParamMissing('log_uuid');
|
||||
$fields = $request->get_parameter('fields');
|
||||
if (!$fields) $fields = "date|user|type|comment";
|
||||
|
||||
$results = OkapiServiceRunner::call('services/logs/entries', new OkapiInternalRequest(
|
||||
$request->consumer, $request->token, array('log_uuids' => $log_uuid,
|
||||
'fields' => $fields)));
|
||||
$result = $results[$log_uuid];
|
||||
if ($result == null)
|
||||
throw new InvalidParam('log_uuid', "This log entry does not exist.");
|
||||
return Okapi::formatted_response($request, $result);
|
||||
}
|
||||
}
|
42
htdocs/okapi/services/logs/entry.xml
Normal file
42
htdocs/okapi/services/logs/entry.xml
Normal file
@ -0,0 +1,42 @@
|
||||
<xml>
|
||||
<brief>Retrieve information on a single log entry</brief>
|
||||
<issue-id>108</issue-id>
|
||||
<desc>
|
||||
<p>Retrieve information on a single log entry.</p>
|
||||
</desc>
|
||||
<req name='log_uuid'>UUID of the log entry</req>
|
||||
<opt name='fields' default='date|user|type|comment'>
|
||||
<p>Pipe-separated list of field names which you are interested with.
|
||||
Selected fields will be included in the response.</p>
|
||||
<p>Currently available fields:</p>
|
||||
<ul>
|
||||
<li><b>uuid</b> - unique ID of the log entry,</li>
|
||||
<li><b>cache_code</b> - code of the cache which the log entry refers to,</li>
|
||||
<li>
|
||||
<p><b>date</b> - date and time (ISO 8601) when the log entry was submitted.</p>
|
||||
<p>Note, that some OpenCaching servers don't store the exact times along
|
||||
with the log entries.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><b>user</b> - a dictionary:</p>
|
||||
<ul>
|
||||
<li><b>uuid</b> - ID of the user (author of the log entry),</li>
|
||||
<li><b>username</b> - name of the user (who submitted the log entry),</li>
|
||||
<li><b>profile_url</b> - URL of the profile page of the user,</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p><b>type</b> - string; log type. This could be <b>pretty much everything</b>, but
|
||||
there are three primary types: "Found it", "Didn't find it" or "Comment".
|
||||
You may treat every other string as "Comment".</p>
|
||||
</li>
|
||||
<li><b>comment</b> - HTML string, text entered with the log entry.</li>
|
||||
</ul>
|
||||
</opt>
|
||||
<common-format-params/>
|
||||
<returns>
|
||||
<p>A dictionary of fields you have selected.</p>
|
||||
<p>If given log entry does not exist, the method will
|
||||
respond with a HTTP 400 error.</p>
|
||||
</returns>
|
||||
</xml>
|
70
htdocs/okapi/services/logs/logs.php
Normal file
70
htdocs/okapi/services/logs/logs.php
Normal file
@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace okapi\services\logs\logs;
|
||||
|
||||
use Exception;
|
||||
use okapi\Okapi;
|
||||
use okapi\Db;
|
||||
use okapi\OkapiRequest;
|
||||
use okapi\ParamMissing;
|
||||
use okapi\InvalidParam;
|
||||
use okapi\OkapiInternalRequest;
|
||||
use okapi\OkapiServiceRunner;
|
||||
use okapi\Settings;
|
||||
use okapi\services\caches\search\SearchAssistant;
|
||||
|
||||
class WebService
|
||||
{
|
||||
public static function options()
|
||||
{
|
||||
return array(
|
||||
'min_auth_level' => 1
|
||||
);
|
||||
}
|
||||
public static function call(OkapiRequest $request)
|
||||
{
|
||||
$cache_code = $request->get_parameter('cache_code');
|
||||
if (!$cache_code) throw new ParamMissing('cache_code');
|
||||
$fields = $request->get_parameter('fields');
|
||||
if (!$fields) $fields = "uuid|date|user|type|comment";
|
||||
|
||||
$offset = $request->get_parameter('offset');
|
||||
if (!$offset) $offset = "0";
|
||||
if ((((int)$offset) != $offset) || ((int)$offset) < 0)
|
||||
throw new InvalidParam('offset', "Expecting non-negative integer.");
|
||||
$limit = $request->get_parameter('limit');
|
||||
if (!$limit) $limit = "none";
|
||||
if ($limit == "none") $limit = "999999999";
|
||||
if ((((int)$limit) != $limit) || ((int)$limit) < 0)
|
||||
throw new InvalidParam('limit', "Expecting non-negative integer or 'none'.");
|
||||
|
||||
# Check if code exists and retrieve cache ID (this will throw
|
||||
# a proper exception on invalid code).
|
||||
|
||||
$cache = OkapiServiceRunner::call('services/caches/geocache', new OkapiInternalRequest(
|
||||
$request->consumer, null, array('cache_code' => $cache_code, 'fields' => 'internal_id')));
|
||||
|
||||
# Cache exists. Getting the uuids of its logs.
|
||||
|
||||
$log_uuids = Db::select_column("
|
||||
select uuid
|
||||
from cache_logs
|
||||
where
|
||||
cache_id = '".mysql_real_escape_string($cache['internal_id'])."'
|
||||
and ".((Settings::get('OC_BRANCH') == 'oc.pl') ? "deleted = 0" : "true")."
|
||||
order by date desc
|
||||
limit $offset, $limit
|
||||
");
|
||||
|
||||
# Getting the logs themselves. Formatting as an ordered list.
|
||||
|
||||
$logs = OkapiServiceRunner::call('services/logs/entries', new OkapiInternalRequest(
|
||||
$request->consumer, $request->token, array('log_uuids' => implode("|", $log_uuids),
|
||||
'fields' => 'uuid|date|user|type|comment')));
|
||||
$results = array();
|
||||
foreach ($log_uuids as $log_uuid)
|
||||
$results[] = $logs[$log_uuid];
|
||||
|
||||
return Okapi::formatted_response($request, $results);
|
||||
}
|
||||
}
|
31
htdocs/okapi/services/logs/logs.xml
Normal file
31
htdocs/okapi/services/logs/logs.xml
Normal file
@ -0,0 +1,31 @@
|
||||
<xml>
|
||||
<brief>Retrieve all log entries for the specified geocache</brief>
|
||||
<issue-id>41</issue-id>
|
||||
<desc>
|
||||
<p>Retrieve the log entries for the specified geocache. Use the offset and
|
||||
limit parameters for pagination. If you want only the latest entries, you
|
||||
may also use the <b>latest_logs</b> field in the services/caches/geocache method.</p>
|
||||
<p>Log entries are ordered by a descending date of the entry.</p>
|
||||
</desc>
|
||||
<req name='cache_code'>
|
||||
<p>Code of the geocache.</p>
|
||||
</req>
|
||||
<opt name='fields' default='uuid|date|user|type|comment'>
|
||||
<p>Same as in the services/logs/entry method. Pipe-separated list
|
||||
of field names which you are interested with.
|
||||
See services/logs/entry method for a list of available values.</p>
|
||||
</opt>
|
||||
<opt name='offset' default='0'>
|
||||
<p>Number of entries to skip at the beginning. Use this along the <b>limit</b> parameter
|
||||
for pagination.</p>
|
||||
</opt>
|
||||
<opt name='limit' default='none'>
|
||||
<p>Maximum number of entries to return or <b>none</b>
|
||||
if you want all the entries.</p>
|
||||
</opt>
|
||||
<common-format-params/>
|
||||
<returns>
|
||||
<p>A list of log entries, ordered by date. Each log entry is a dictionary of a format
|
||||
described in the "entry" method.</p>
|
||||
</returns>
|
||||
</xml>
|
535
htdocs/okapi/services/logs/submit.php
Normal file
535
htdocs/okapi/services/logs/submit.php
Normal file
@ -0,0 +1,535 @@
|
||||
<?php
|
||||
|
||||
namespace okapi\services\logs\submit;
|
||||
|
||||
use Exception;
|
||||
use okapi\Okapi;
|
||||
use okapi\Db;
|
||||
use okapi\OkapiRequest;
|
||||
use okapi\ParamMissing;
|
||||
use okapi\InvalidParam;
|
||||
use okapi\OkapiInternalRequest;
|
||||
use okapi\OkapiServiceRunner;
|
||||
use okapi\OkapiAccessToken;
|
||||
use okapi\Settings;
|
||||
use okapi\services\caches\search\SearchAssistant;
|
||||
use okapi\BadRequest;
|
||||
|
||||
|
||||
/**
|
||||
* This exception is thrown by WebService::_call method, when error is detected in
|
||||
* user-supplied data. It is not a BadRequest exception - it does not imply that
|
||||
* the Consumer did anything wrong (it's the user who did). This exception shouldn't
|
||||
* be used outside of this file.
|
||||
*/
|
||||
class CannotPublishException extends Exception {}
|
||||
|
||||
class WebService
|
||||
{
|
||||
public static function options()
|
||||
{
|
||||
return array(
|
||||
'min_auth_level' => 3
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish a new log entry and return log entry uuid. Throws
|
||||
* CannotPublishException or BadRequest on errors.
|
||||
*/
|
||||
private static function _call(OkapiRequest $request)
|
||||
{
|
||||
# Developers! Please notice the fundamental difference between throwing
|
||||
# CannotPublishException and standard BadRequest/InvalidParam exceptions!
|
||||
# Notice, that this is "_call" method, not the usual "call" (see below
|
||||
# for "call").
|
||||
|
||||
$cache_code = $request->get_parameter('cache_code');
|
||||
if (!$cache_code) throw new ParamMissing('cache_code');
|
||||
$logtype = $request->get_parameter('logtype');
|
||||
if (!$logtype) throw new ParamMissing('logtype');
|
||||
if (!in_array($logtype, array('Found it', "Didn't find it", 'Comment')))
|
||||
throw new InvalidParam('logtype', "'$logtype' in not a valid logtype code.");
|
||||
$comment = $request->get_parameter('comment');
|
||||
if (!$comment) $comment = "";
|
||||
$tmp = $request->get_parameter('when');
|
||||
if ($tmp)
|
||||
{
|
||||
$when = strtotime($tmp);
|
||||
if (!$when)
|
||||
throw new InvalidParam('when', "'$tmp' is not in a valid format or is not a valid date.");
|
||||
if ($when > time() + 5*60)
|
||||
throw new CannotPublishException(_("You are trying to publish a log entry with a date in future. ".
|
||||
"Cache log entries are allowed to be published in the past, but NOT in the future."));
|
||||
}
|
||||
else
|
||||
$when = time();
|
||||
$on_duplicate = $request->get_parameter('on_duplicate');
|
||||
if (!$on_duplicate) $on_duplicate = "silent_success";
|
||||
if (!in_array($on_duplicate, array('silent_success', 'user_error', 'continue')))
|
||||
throw new InvalidParam('on_duplicate', "Unknown option: '$on_duplicate'.");
|
||||
$rating = $request->get_parameter('rating');
|
||||
if ($rating !== null && (!in_array($rating, array(1,2,3,4,5))))
|
||||
throw new InvalidParam('rating', "If present, it must be an integer in the 1..5 scale.");
|
||||
if ($rating && $logtype != 'Found it')
|
||||
throw new BadRequest("Rating is allowed only for 'Found it' logtypes.");
|
||||
if ($rating !== null && (Settings::get('OC_BRANCH') == 'oc.de'))
|
||||
{
|
||||
# We will remove the rating request and change the success message
|
||||
# (which will be returned IF the rest of the query will meet all the
|
||||
# requirements).
|
||||
|
||||
self::$success_message .= " ".sprintf(_("However, your cache rating was ignored, because %s does not have a rating system."),
|
||||
Okapi::get_normalized_site_name());
|
||||
$rating = null;
|
||||
}
|
||||
$recommend = $request->get_parameter('recommend');
|
||||
if (!$recommend) $recommend = 'false';
|
||||
if (!in_array($recommend, array('true', 'false')))
|
||||
throw new InvalidParam('recommend', "Unknown option: '$recommend'.");
|
||||
$recommend = ($recommend == 'true');
|
||||
if ($recommend && $logtype != 'Found it')
|
||||
throw new BadRequest(_("Recommending is allowed only for 'Found it' logtypes."));
|
||||
$needs_maintenance = $request->get_parameter('needs_maintenance');
|
||||
if (!$needs_maintenance) $needs_maintenance = 'false';
|
||||
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 not supported, just ignore it.
|
||||
self::$success_message .= " ".sprintf(_("However, your \"needs maintenance\" flag was ignored, because %s does not support this feature."),
|
||||
Okapi::get_normalized_site_name());
|
||||
$needs_maintenance = false;
|
||||
}
|
||||
|
||||
# Check if cache exists and retrieve cache internal ID (this will throw
|
||||
# a proper exception on invalid cache_code). Also, get the user object.
|
||||
|
||||
$cache = OkapiServiceRunner::call('services/caches/geocache', new OkapiInternalRequest(
|
||||
$request->consumer, null, array('cache_code' => $cache_code,
|
||||
'fields' => 'internal_id|status|owner|type|req_passwd')));
|
||||
$user = OkapiServiceRunner::call('services/users/by_internal_id', new OkapiInternalRequest(
|
||||
$request->consumer, $request->token, array('internal_id' => $request->token->user_id,
|
||||
'fields' => 'is_admin|uuid|internal_id|caches_found|rcmds_given')));
|
||||
|
||||
# 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)
|
||||
throw new CannotPublishException(_("Your have to supply some text for your comment."));
|
||||
|
||||
# Password check.
|
||||
|
||||
if ($logtype == 'Found it' && $cache['req_passwd'])
|
||||
{
|
||||
$valid_password = Db::select_value("
|
||||
select logpw
|
||||
from caches
|
||||
where cache_id = '".mysql_real_escape_string($cache['internal_id'])."'
|
||||
");
|
||||
$supplied_password = $request->get_parameter('password');
|
||||
if (!$supplied_password)
|
||||
throw new CannotPublishException(_("This cache requires a password. You didn't provide one!"));
|
||||
if (strtolower($supplied_password) != strtolower($valid_password))
|
||||
throw new CannotPublishException(_("Invalid password!"));
|
||||
}
|
||||
|
||||
# Very weird part (as usual). OC stores HTML-lized comments in the database
|
||||
# (it doesn't really matter if 'text_html' field is set to FALSE). OKAPI must
|
||||
# save it in HTML either way. However, escaping plain-text doesn't work.
|
||||
# If we put "<b>" in, it still gets converted to "<b>" before display!
|
||||
# NONE of this process is documented. There doesn't seem to be ANY way to force
|
||||
# OCPL to treat a string as either plain-text nor HTML. It's always something
|
||||
# in between! In other words, we cannot guarantee how it gets displayed in
|
||||
# the end. Since text_html=0 doesn't add <br/>s on its own, we can only add
|
||||
# proper <br/>s and hope it's okay. We will remove the original $comment
|
||||
# variable from our namespace and act on the "pseudoencoded" one.
|
||||
|
||||
$PSEUDOENCODED_comment = htmlspecialchars($comment, ENT_QUOTES);
|
||||
$PSEUDOENCODED_comment = nl2br($PSEUDOENCODED_comment);
|
||||
unset($comment);
|
||||
|
||||
# Duplicate detection.
|
||||
|
||||
if ($on_duplicate != 'continue')
|
||||
{
|
||||
# Attempt to find a log entry made by the same user, for the same cache, with
|
||||
# the same date, type, comment, etc. Note, that these are not ALL the fields
|
||||
# we could check, but should work ok in most cases. Also note, that we
|
||||
# DO NOT guarantee that duplicate detection will succeed. If it doesn't,
|
||||
# nothing bad happens (user will just post two similar log entries).
|
||||
# Keep this simple!
|
||||
|
||||
$duplicate_uuid = Db::select_value("
|
||||
select uuid
|
||||
from cache_logs
|
||||
where
|
||||
user_id = '".mysql_real_escape_string($request->token->user_id)."'
|
||||
and cache_id = '".mysql_real_escape_string($cache['internal_id'])."'
|
||||
and type = '".mysql_real_escape_string(Okapi::logtypename2id($logtype))."'
|
||||
and date = from_unixtime('".mysql_real_escape_string($when)."')
|
||||
and text = '".mysql_real_escape_string($PSEUDOENCODED_comment)."'
|
||||
limit 1
|
||||
");
|
||||
if ($duplicate_uuid != null)
|
||||
{
|
||||
if ($on_duplicate == 'silent_success')
|
||||
{
|
||||
# Act as if the log has been submitted successfully.
|
||||
return $duplicate_uuid;
|
||||
}
|
||||
elseif ($on_duplicate == 'user_error')
|
||||
{
|
||||
throw new CannotPublishException(_("You have already submitted a log entry with exactly the same contents."));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Check if already found it (and make sure the user is not the owner).
|
||||
|
||||
if (($logtype == 'Found it') || ($logtype == "Didn't find it"))
|
||||
{
|
||||
$has_already_found_it = Db::select_value("
|
||||
select 1
|
||||
from cache_logs
|
||||
where
|
||||
user_id = '".mysql_real_escape_string($user['internal_id'])."'
|
||||
and cache_id = '".mysql_real_escape_string($cache['internal_id'])."'
|
||||
and type = '".mysql_real_escape_string(Okapi::logtypename2id("Found it"))."'
|
||||
and ".((Settings::get('OC_BRANCH') == 'oc.pl') ? "deleted = 0" : "true")."
|
||||
");
|
||||
if ($has_already_found_it)
|
||||
throw new CannotPublishException(_("You have already submitted a \"Found it\" log entry once. Now you may submit \"Comments\" only!"));
|
||||
if ($user['uuid'] == $cache['owner']['uuid'])
|
||||
throw new CannotPublishException(_("You are the owner of this cache. You may submit \"Comments\" only!"));
|
||||
}
|
||||
|
||||
# Check if the user has already rated the cache. BTW: I don't get this one.
|
||||
# If we already know, that the cache was NOT found yet, then HOW could the
|
||||
# user submit a rating for it? Anyway, I will stick to the procedure
|
||||
# found in log.php. On the bright side, it's fail-safe.
|
||||
|
||||
if ($rating)
|
||||
{
|
||||
$has_already_rated = Db::select_value("
|
||||
select 1
|
||||
from scores
|
||||
where
|
||||
user_id = '".mysql_real_escape_string($user['internal_id'])."'
|
||||
and cache_id = '".mysql_real_escape_string($cache['internal_id'])."'
|
||||
");
|
||||
if ($has_already_rated)
|
||||
throw new CannotPublishException(_("You have already rated this cache once. Your rating cannot be changed."));
|
||||
}
|
||||
|
||||
# If user wants to recommend...
|
||||
|
||||
if ($recommend)
|
||||
{
|
||||
# Do the same "fail-safety" check as we did for the rating.
|
||||
|
||||
$already_recommended = Db::select_value("
|
||||
select 1
|
||||
from cache_rating
|
||||
where
|
||||
user_id = '".mysql_real_escape_string($user['internal_id'])."'
|
||||
and cache_id = '".mysql_real_escape_string($cache['internal_id'])."'
|
||||
");
|
||||
if ($already_recommended)
|
||||
throw new CannotPublishException(_("You have already recommended this cache once."));
|
||||
|
||||
# Check the number of recommendations.
|
||||
|
||||
$founds = $user['caches_found'] + 1; // +1, because he'll find THIS ONE in a moment, right?
|
||||
$rcmds_left = floor($founds / 10.0) - $user['rcmds_given'];
|
||||
if ($rcmds_left <= 0)
|
||||
throw new CannotPublishException(_("You don't have any recommendations to give. Find more caches first!"));
|
||||
}
|
||||
|
||||
# If user checked the "needs_maintenance" flag, we will shuffle things a little...
|
||||
|
||||
if ($needs_maintenance)
|
||||
{
|
||||
# If we're here, then we also know that the "Needs maintenance" log type is supported
|
||||
# by this OC site. However, it's a separate log type, so we might have to submit
|
||||
# two log types together:
|
||||
|
||||
if ($logtype == 'Comment')
|
||||
{
|
||||
# If user submits a "Comment", we'll just change its type to "Needs maintenance".
|
||||
# Only one log entry will be issued.
|
||||
|
||||
$logtype = 'Needs maintenance';
|
||||
$second_logtype = null;
|
||||
$second_PSEUDOENCODED_comment = null;
|
||||
}
|
||||
elseif ($logtype == 'Found it')
|
||||
{
|
||||
# If "Found it", then we'll issue two log entries: one "Found it" with the
|
||||
# original comment, and second one "Needs maintenance" with empty comment.
|
||||
|
||||
$second_logtype = 'Needs maintenance';
|
||||
$second_PSEUDOENCODED_comment = "";
|
||||
}
|
||||
elseif ($logtype == "Didn't find it")
|
||||
{
|
||||
# If "Didn't find it", then we'll issue two log entries, but this time
|
||||
# we'll do this the other way around. The first "Didn't find it" entry
|
||||
# will have an empty comment. We will move the comment to the second
|
||||
# "Needs maintenance" log entry. (It's okay for this behavior to change
|
||||
# in the future, but it seems natural to me.)
|
||||
|
||||
$second_logtype = 'Needs maintenance';
|
||||
$second_PSEUDOENCODED_comment = $PSEUDOENCODED_comment;
|
||||
$PSEUDOENCODED_comment = "";
|
||||
}
|
||||
else
|
||||
throw new Exception();
|
||||
}
|
||||
else
|
||||
{
|
||||
# User didn't check the "Needs maintenance" flag OR "Needs maintenance" log type
|
||||
# isn't supported by this server.
|
||||
|
||||
$second_logtype = null;
|
||||
$second_PSEUDOENCODED_comment = null;
|
||||
}
|
||||
|
||||
# Finally! Insert the rows into the log entries table. Update
|
||||
# cache stats and user stats.
|
||||
|
||||
$log_uuid = self::insert_log_row($request->consumer->key, $cache['internal_id'], $user['internal_id'], $logtype, $when, $PSEUDOENCODED_comment);
|
||||
self::increment_cache_stats($cache['internal_id'], $when, $logtype);
|
||||
self::increment_user_stats($user['internal_id'], $logtype);
|
||||
if ($second_logtype != null)
|
||||
{
|
||||
# Reminder: This will never be called while SUPPORTS_LOGTYPE_NEEDS_MAINTENANCE is off.
|
||||
|
||||
self::insert_log_row($request->consumer->key, $cache['internal_id'], $user['internal_id'], $second_logtype, $when + 1, $second_PSEUDOENCODED_comment);
|
||||
self::increment_cache_stats($cache['internal_id'], $when + 1, $second_logtype);
|
||||
self::increment_user_stats($user['internal_id'], $second_logtype);
|
||||
}
|
||||
|
||||
# Save the rating.
|
||||
|
||||
if ($rating)
|
||||
{
|
||||
# This code will be called for OCPL branch only. Earlier, we made sure,
|
||||
# to set $rating to null, if we're running on OCDE.
|
||||
|
||||
# OCPL has a little strange way of storing cumulative rating. Instead
|
||||
# of storing the sum of all ratings, OCPL stores the computed average
|
||||
# and update it using multiple floating-point operations. Moreover,
|
||||
# the "score" field in the database is on the -3..3 scale (NOT 1..5),
|
||||
# and the translation made at retrieval time is DIFFERENT than the
|
||||
# one made here (both of them are non-linear). Also, once submitted,
|
||||
# the rating can never be changed. It surely feels quite inconsistent,
|
||||
# but presumably has some deep logic into it. See also here (Polish):
|
||||
# http://wiki.opencaching.pl/index.php/Oceny_skrzynek
|
||||
|
||||
switch ($rating)
|
||||
{
|
||||
case 1: $db_score = -2.0; break;
|
||||
case 2: $db_score = -0.5; break;
|
||||
case 3: $db_score = 0.7; break;
|
||||
case 4: $db_score = 1.7; break;
|
||||
case 5: $db_score = 3.0; break;
|
||||
default: throw new Exception();
|
||||
}
|
||||
Db::execute("
|
||||
update caches
|
||||
set
|
||||
score = (score*votes + '".mysql_real_escape_string($db_score)."')/(votes + 1),
|
||||
votes = votes + 1
|
||||
where cache_id = '".mysql_real_escape_string($cache['internal_id'])."'
|
||||
");
|
||||
Db::execute("
|
||||
insert into scores (user_id, cache_id, score)
|
||||
values (
|
||||
'".mysql_real_escape_string($user['internal_id'])."',
|
||||
'".mysql_real_escape_string($cache['internal_id'])."',
|
||||
'".mysql_real_escape_string($db_score)."'
|
||||
);
|
||||
");
|
||||
}
|
||||
|
||||
# Save recommendation.
|
||||
|
||||
if ($recommend)
|
||||
{
|
||||
# Both OCPL and OCDE don't update any stats regarding the number of recommendations
|
||||
# (or - if they do - they do so using triggers). In other words, this is the only
|
||||
# query we have to execute here, regarding the recommendation.
|
||||
|
||||
Db::execute("
|
||||
insert into cache_rating (user_id, cache_id)
|
||||
values (
|
||||
'".mysql_real_escape_string($user['internal_id'])."',
|
||||
'".mysql_real_escape_string($cache['internal_id'])."'
|
||||
);
|
||||
");
|
||||
}
|
||||
|
||||
# Call OC's event handler.
|
||||
|
||||
require_once($GLOBALS['rootpath'].'lib/eventhandler.inc.php');
|
||||
event_new_log($cache['internal_id'], $user['internal_id']);
|
||||
|
||||
# Success. Return the uuid.
|
||||
|
||||
return $log_uuid;
|
||||
}
|
||||
|
||||
private static $success_message = null;
|
||||
public static function call(OkapiRequest $request)
|
||||
{
|
||||
# This is the "real" entry point. A wrapper for the _call method.
|
||||
|
||||
$langpref = $request->get_parameter('langpref');
|
||||
if (!$langpref) $langpref = "en";
|
||||
|
||||
# Error messages thrown via CannotPublishException exceptions should be localized.
|
||||
# They will be delivered for end user to display in his language.
|
||||
|
||||
Okapi::gettext_domain_init(explode("|", $langpref));
|
||||
try
|
||||
{
|
||||
# If appropriate, $success_message might be changed inside the _call.
|
||||
self::$success_message = _("Your cache log entry was posted successfully.");
|
||||
$log_uuid = self::_call($request);
|
||||
$result = array(
|
||||
'success' => true,
|
||||
'message' => self::$success_message,
|
||||
'log_uuid' => $log_uuid
|
||||
);
|
||||
Okapi::gettext_domain_restore();
|
||||
}
|
||||
catch (CannotPublishException $e)
|
||||
{
|
||||
Okapi::gettext_domain_restore();
|
||||
$result = array(
|
||||
'success' => false,
|
||||
'message' => $e->getMessage(),
|
||||
'log_uuid' => null
|
||||
);
|
||||
}
|
||||
|
||||
return Okapi::formatted_response($request, $result);
|
||||
}
|
||||
|
||||
private static function increment_cache_stats($cache_internal_id, $when, $logtype)
|
||||
{
|
||||
if (Settings::get('OC_BRANCH') == 'oc.de')
|
||||
{
|
||||
# OCDE handles cache stats updates using triggers. So, they are already
|
||||
# incremented properly.
|
||||
}
|
||||
else
|
||||
{
|
||||
# OCPL doesn't use triggers for this. We need to update manually.
|
||||
|
||||
if ($logtype == 'Found it')
|
||||
{
|
||||
Db::execute("
|
||||
update caches
|
||||
set
|
||||
founds = founds + 1,
|
||||
last_found = greatest(last_found, from_unixtime('".mysql_real_escape_string($when)."'))
|
||||
where cache_id = '".mysql_real_escape_string($cache_internal_id)."'
|
||||
");
|
||||
}
|
||||
elseif ($logtype == "Didn't find it")
|
||||
{
|
||||
Db::execute("
|
||||
update caches
|
||||
set notfounds = notfounds + 1
|
||||
where cache_id = '".mysql_real_escape_string($cache_internal_id)."'
|
||||
");
|
||||
}
|
||||
elseif ($logtype == 'Comment')
|
||||
{
|
||||
Db::execute("
|
||||
update caches
|
||||
set notes = notes + 1
|
||||
where cache_id = '".mysql_real_escape_string($cache_internal_id)."'
|
||||
");
|
||||
}
|
||||
else
|
||||
{
|
||||
# This log type is not represented in cache stats.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static function increment_user_stats($user_internal_id, $logtype)
|
||||
{
|
||||
if (Settings::get('OC_BRANCH') == 'oc.de')
|
||||
{
|
||||
# OCDE handles cache stats updates using triggers. So, they are already
|
||||
# incremented properly.
|
||||
}
|
||||
else
|
||||
{
|
||||
# OCPL doesn't have triggers for this. We need to update manually.
|
||||
|
||||
switch ($logtype)
|
||||
{
|
||||
case 'Found it': $field_to_increment = 'founds_count'; break;
|
||||
case "Didn't find it": $field_to_increment = 'notfounds_count'; break;
|
||||
case 'Comment': $field_to_increment = 'log_notes_count'; break;
|
||||
default:
|
||||
# This log type is not represented in user stats.
|
||||
return;
|
||||
}
|
||||
Db::execute("
|
||||
update user
|
||||
set $field_to_increment = $field_to_increment + 1
|
||||
where user_id = '".mysql_real_escape_string($user_internal_id)."'
|
||||
");
|
||||
}
|
||||
}
|
||||
|
||||
private static function insert_log_row($consumer_key, $cache_internal_id, $user_internal_id, $logtype, $when, $PSEUDOENCODED_comment)
|
||||
{
|
||||
$log_uuid = create_uuid();
|
||||
Db::execute("
|
||||
insert into cache_logs (uuid, cache_id, user_id, type, date, text, last_modified, date_created, node)
|
||||
values (
|
||||
'".mysql_real_escape_string($log_uuid)."',
|
||||
'".mysql_real_escape_string($cache_internal_id)."',
|
||||
'".mysql_real_escape_string($user_internal_id)."',
|
||||
'".mysql_real_escape_string(Okapi::logtypename2id($logtype))."',
|
||||
from_unixtime('".mysql_real_escape_string($when)."'),
|
||||
'".mysql_real_escape_string($PSEUDOENCODED_comment)."',
|
||||
now(),
|
||||
now(),
|
||||
'".mysql_real_escape_string($GLOBALS['oc_nodeid'])."'
|
||||
);
|
||||
");
|
||||
$log_internal_id = Db::last_insert_id();
|
||||
|
||||
# Store additional information on consumer_key which have created this log entry.
|
||||
# (Maybe we'll want to display this somewhere later.)
|
||||
|
||||
Db::execute("
|
||||
insert into okapi_cache_logs (log_id, consumer_key)
|
||||
values (
|
||||
'".mysql_real_escape_string($log_internal_id)."',
|
||||
'".mysql_real_escape_string($consumer_key)."'
|
||||
);
|
||||
");
|
||||
|
||||
return $log_uuid;
|
||||
}
|
||||
}
|
90
htdocs/okapi/services/logs/submit.xml
Normal file
90
htdocs/okapi/services/logs/submit.xml
Normal file
@ -0,0 +1,90 @@
|
||||
<xml>
|
||||
<brief>Submit a log entry</brief>
|
||||
<issue-id>42</issue-id>
|
||||
<desc>
|
||||
<p>Submit a log entry for the geocache. Please note, that you won't be
|
||||
able to use this method until you learn to handle OAuth.</p>
|
||||
</desc>
|
||||
<req name='cache_code'>
|
||||
<p>Code of the geocache.</p>
|
||||
</req>
|
||||
<req name='logtype'>
|
||||
<p>Type of an entry. This should be one of: <i>Found it</i>, <i>Didn't find it</i>
|
||||
or <i>Comment</i>.</p>
|
||||
</req>
|
||||
<opt name='comment'>
|
||||
<p>Text to be submitted with the log entry. Plain-text (no HTML).</p>
|
||||
<p><b>Note:</b> Due to <a href='http://code.google.com/p/opencaching-api/issues/detail?id=124'>some issues</a>
|
||||
(which we cannot currently fix), you MAY be allowed to use some basic HTML tags (on some OC installations).
|
||||
However, you should not (this may stop working at any time).</p>
|
||||
</opt>
|
||||
<opt name='when'>
|
||||
<p>A date and time string. This should be in ISO 8601 format (although currently
|
||||
any format acceptable by PHP's <a href='http://pl2.php.net/strtotime'>strtotime</a>
|
||||
function also will do).</p>
|
||||
<p>Indicates when the cache was found. If given, the log will be published
|
||||
with the given date and time. If not, log will be published using the current
|
||||
date and time.</p>
|
||||
</opt>
|
||||
<opt name='password'>
|
||||
<p>Some caches require a password in order to submit a "Found it" log entry.
|
||||
You may check if this cache requires password with <b>req_passwd</b> field
|
||||
of the services/caches/geocache method.</p>
|
||||
</opt>
|
||||
<opt name='langpref' default='en'>
|
||||
<p>Pipe-separated list of ISO 639-1 language codes. This indicates the
|
||||
order of preference in which language will be chosen for error messages.</p>
|
||||
</opt>
|
||||
<opt name='on_duplicate' default='silent_success'>
|
||||
<p>How should OKAPI react when you are trying to submit a duplicate entry?
|
||||
One of the following values:</p>
|
||||
<ul>
|
||||
<li><b>silent_success</b> - try to respond with success=true, but don't
|
||||
add a new log entry (existing log_uuid will be returned),</li>
|
||||
<li><b>user_error</b> - respond with success=false and a proper user message,</li>
|
||||
<li><b>continue</b> - don't detect duplicates (note, that this will still
|
||||
fail in some cases, i.e. when you're trying to submit a "Found it" entry for an
|
||||
already found cache).</li>
|
||||
</ul>
|
||||
<p>Note, that duplicate detection may take the <b>when</b> parameter into account.
|
||||
When you don't supply it, "when" is generated for you. This means that you may
|
||||
have to supply the "when" parameter if you want duplicate detection to work.</p>
|
||||
</opt>
|
||||
<opt name='rating'>
|
||||
<p>An integer in range between 1 and 5 - user's optional rating of a found cache.</p>
|
||||
<p>Important: <b>logtype</b> has to be "Found it" in order to use this argument.</p>
|
||||
<p>Note: You should allow your user to <b>not</b> rate a found cache.</p>
|
||||
<p>Note: Currently, some OC installations do not support cache ratings. On such installations
|
||||
user's rating will be <b>ignored</b> (if you include the rating, log entry will be posted
|
||||
successfully, but rating will be ignored).</p>
|
||||
</opt>
|
||||
<opt name='recommend' default='false'>
|
||||
<p>Set to <b>true</b> if the user wants to recommend this cache.</p>
|
||||
<p>Important: <b>logtype</b> has to be "Found it" in order to use this argument.</p>
|
||||
<p>Recommending may only succeed when the user meets certain criteria
|
||||
set on him by the OC site. If criteria are not met, the request will
|
||||
end with user error (HTTP 200, success=false).</p>
|
||||
</opt>
|
||||
<!--
|
||||
<opt name='needs_maintenance' default='false'>
|
||||
<p>Set to <b>true</b> if the user thinks that the cache needs some special attension
|
||||
of its owner. Users should describe the reason for maintenance in their comments.</p>
|
||||
<p>Note: Depending on OC installation and user's log entry type, OKAPI may actually
|
||||
publish two separate log entries when you check this flag (one of them with empty
|
||||
comment). Even then, you will still receive only one UUID reference (pointing to
|
||||
only one of those entries). Moreover, on some OC servers this flag might be
|
||||
<b>completely ignored</b> (not all OC servers support this feature).</p>
|
||||
</opt>
|
||||
-->
|
||||
<common-format-params/>
|
||||
<returns>
|
||||
<p>A dictionary of the following structure:</p>
|
||||
<ul>
|
||||
<li><b>success</b> - true, if the log entry was submitted successfully,</li>
|
||||
<li><b>message</b> - plain-text string, a message for the user, which acknowledges success
|
||||
or describes an error (usually you want to display this only when success is false),</li>
|
||||
<li><b>log_uuid</b> - ID of the newly created log entry, <b>or null</b>
|
||||
in case of an error.</li>
|
||||
</ul>
|
||||
</returns>
|
||||
</xml>
|
76
htdocs/okapi/services/logs/userlogs.php
Normal file
76
htdocs/okapi/services/logs/userlogs.php
Normal file
@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace okapi\services\logs\userlogs;
|
||||
|
||||
use Exception;
|
||||
use okapi\Okapi;
|
||||
use okapi\Db;
|
||||
use okapi\OkapiRequest;
|
||||
use okapi\ParamMissing;
|
||||
use okapi\InvalidParam;
|
||||
use okapi\OkapiInternalRequest;
|
||||
use okapi\OkapiServiceRunner;
|
||||
use okapi\Settings;
|
||||
use okapi\services\caches\search\SearchAssistant;
|
||||
|
||||
class WebService
|
||||
{
|
||||
public static function options()
|
||||
{
|
||||
return array(
|
||||
'min_auth_level' => 1
|
||||
);
|
||||
}
|
||||
public static function call(OkapiRequest $request)
|
||||
{
|
||||
$user_uuid = $request->get_parameter('user_uuid');
|
||||
if (!$user_uuid) throw new ParamMissing('user_uuid');
|
||||
$limit = $request->get_parameter('limit');
|
||||
if (!$limit) $limit = "20";
|
||||
if (!is_numeric($limit))
|
||||
throw new InvalidParam('limit', "'$limit'");
|
||||
$limit = intval($limit);
|
||||
if (($limit < 1) || ($limit > 1000))
|
||||
throw new InvalidParam('limit', "Has to be in range 1..1000.");
|
||||
$offset = $request->get_parameter('offset');
|
||||
if (!$offset) $offset = "0";
|
||||
if (!is_numeric($offset))
|
||||
throw new InvalidParam('offset', "'$offset'");
|
||||
$offset = intval($offset);
|
||||
if ($offset < 0)
|
||||
throw new InvalidParam('offset', "'$offset'");
|
||||
|
||||
# Check if user exists and retrieve user's ID (this will throw
|
||||
# a proper exception on invalid UUID).
|
||||
$user = OkapiServiceRunner::call('services/users/user', new OkapiInternalRequest(
|
||||
$request->consumer, null, array('user_uuid' => $user_uuid, 'fields' => 'internal_id')));
|
||||
|
||||
# User exists. Retrieving logs.
|
||||
|
||||
$rs = Db::query("
|
||||
select cl.id, cl.uuid, cl.type, unix_timestamp(cl.date) as date, cl.text,
|
||||
c.wp_oc as cache_code
|
||||
from cache_logs cl, caches c
|
||||
where
|
||||
cl.user_id = '".mysql_real_escape_string($user['internal_id'])."'
|
||||
and ".((Settings::get('OC_BRANCH') == 'oc.pl') ? "cl.deleted = 0" : "true")."
|
||||
and c.status not in (4,5,6)
|
||||
and cl.cache_id = c.cache_id
|
||||
order by cl.date desc
|
||||
limit $offset, $limit
|
||||
");
|
||||
$results = array();
|
||||
while ($row = mysql_fetch_assoc($rs))
|
||||
{
|
||||
$results[] = array(
|
||||
'uuid' => $row['uuid'],
|
||||
'date' => date('c', $row['date']),
|
||||
'cache_code' => $row['cache_code'],
|
||||
'type' => Okapi::logtypeid2name($row['type']),
|
||||
'comment' => $row['text']
|
||||
);
|
||||
}
|
||||
|
||||
return Okapi::formatted_response($request, $results);
|
||||
}
|
||||
}
|
38
htdocs/okapi/services/logs/userlogs.xml
Normal file
38
htdocs/okapi/services/logs/userlogs.xml
Normal file
@ -0,0 +1,38 @@
|
||||
<xml>
|
||||
<brief>Retrieve log entries of a specified user</brief>
|
||||
<issue-id>80</issue-id>
|
||||
<desc>
|
||||
<p>Retrieve log entries of a specified user.</p>
|
||||
</desc>
|
||||
<req name='user_uuid'>
|
||||
<p>ID of the user. (Use services/users/by_username to get it.)</p>
|
||||
</req>
|
||||
<opt name='limit' default='20'>
|
||||
<p>Integer N. If given, no more than N logs will be returned (the most recent ones).
|
||||
Maximum allowed value is 1000.</p>
|
||||
<p>Note: Some users have thousands of log entries!</p>
|
||||
</opt>
|
||||
<opt name='offset' default='0'>
|
||||
<p>Combined with the <b>limit</b> argument, this gives you an abbility to get
|
||||
<b>all</b> log entries of the user (with multiple requests).</p>
|
||||
</opt>
|
||||
<common-format-params/>
|
||||
<returns>
|
||||
<p>Log entries. A dictionary of the following format:</p>
|
||||
<ul>
|
||||
<li><b>uuid</b> - ID of the log entry,</li>
|
||||
<li>
|
||||
<p><b>date</b> - date and time (ISO 8601) when the log entry was submitted.</p>
|
||||
<p>Note, that some OpenCaching servers don't store the exact times along
|
||||
with the log entries.</p>
|
||||
</li>
|
||||
<li><b>cache_code</b> - code of the geocache,</li>
|
||||
<li>
|
||||
<p><b>type</b> - string; log type. This could be <b>pretty much everything</b>, but
|
||||
there are three primary types: "Found it", "Didn't find it" or "Comment".
|
||||
You may treat every other string as "Comment".</p>
|
||||
</li>
|
||||
<li><b>comment</b> - HTML string, text entered with the log entry.</li>
|
||||
</ul>
|
||||
</returns>
|
||||
</xml>
|
37
htdocs/okapi/services/oauth/access_token.php
Normal file
37
htdocs/okapi/services/oauth/access_token.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace okapi\services\oauth\access_token;
|
||||
|
||||
use okapi\Okapi;
|
||||
use okapi\OkapiHttpResponse;
|
||||
use okapi\OkapiRequest;
|
||||
use okapi\ParamMissing;
|
||||
use okapi\InvalidParam;
|
||||
|
||||
class WebService
|
||||
{
|
||||
public static function options()
|
||||
{
|
||||
return array(
|
||||
'min_auth_level' => 3,
|
||||
'token_type' => 'request'
|
||||
);
|
||||
}
|
||||
|
||||
public static function call(OkapiRequest $request)
|
||||
{
|
||||
$verifier = $request->get_parameter('oauth_verifier');
|
||||
if (!$verifier)
|
||||
{
|
||||
# We require the 1.0a flow (throw an error when there is no oauth_verifier).
|
||||
throw new ParamMissing("oauth_verifier");
|
||||
}
|
||||
|
||||
$new_token = Okapi::$data_store->new_access_token($request->token, $request->consumer, $verifier);
|
||||
|
||||
$response = new OkapiHttpResponse();
|
||||
$response->content_type = "text/plain; charset=utf-8";
|
||||
$response->body = $new_token;
|
||||
return $response;
|
||||
}
|
||||
}
|
20
htdocs/okapi/services/oauth/access_token.xml
Normal file
20
htdocs/okapi/services/oauth/access_token.xml
Normal file
@ -0,0 +1,20 @@
|
||||
<xml>
|
||||
<brief>Exchange authorized Request Token for an Access Token</brief>
|
||||
<issue-id>21</issue-id>
|
||||
<desc>
|
||||
<p>Exchange authorized Request Token for an Access Token.
|
||||
Access Token can be used to access resources of a user who
|
||||
authorized the Request Token.</p>
|
||||
<p>Standard OAuth "Consumer & Request Token" Authorization arguments
|
||||
are required.</p>
|
||||
</desc>
|
||||
<req name='oauth_verifier'>
|
||||
Consult <a href='http://oauth.net/documentation/spec/'>OAuth documentation</a> for details.
|
||||
</req>
|
||||
<returns>
|
||||
<p>Standard OAuth 1.0a Token response - a string in a form-encoded format:</p>
|
||||
<pre>oauth_token=...&oauth_token_secret=...</pre>
|
||||
<p>You <b>must</b> be prepared that there might be more parameters returned
|
||||
in the future (you should ignore them gracefully).</p>
|
||||
</returns>
|
||||
</xml>
|
40
htdocs/okapi/services/oauth/authorize.php
Normal file
40
htdocs/okapi/services/oauth/authorize.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace okapi\services\oauth\authorize;
|
||||
|
||||
use okapi\Okapi;
|
||||
use okapi\OkapiRedirectResponse;
|
||||
use okapi\OkapiRequest;
|
||||
use okapi\ParamMissing;
|
||||
use okapi\InvalidParam;
|
||||
|
||||
class WebService
|
||||
{
|
||||
public static function options()
|
||||
{
|
||||
return array(
|
||||
'min_auth_level' => 0
|
||||
);
|
||||
}
|
||||
|
||||
public static function call(OkapiRequest $request)
|
||||
{
|
||||
$token_key = $request->get_parameter('oauth_token');
|
||||
if (!$token_key)
|
||||
throw new ParamMissing("oauth_token");
|
||||
$langpref = $request->get_parameter('langpref');
|
||||
$interactivity = $request->get_parameter('interactivity');
|
||||
if (!$interactivity) $interactivity = 'minimal';
|
||||
if (!in_array($interactivity, array('minimal', 'confirm_user')))
|
||||
throw new InvalidParam('interactivity', $interactivity);
|
||||
|
||||
# Redirect to the "apps" folder. This is done there (not here)
|
||||
# because: 1) we don't want any cookie or session-handling
|
||||
# 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".
|
||||
"?oauth_token=".$token_key.(($langpref != null) ? "&langpref=".$langpref : "").
|
||||
"&interactivity=".$interactivity);
|
||||
}
|
||||
}
|
66
htdocs/okapi/services/oauth/authorize.xml
Normal file
66
htdocs/okapi/services/oauth/authorize.xml
Normal file
@ -0,0 +1,66 @@
|
||||
<xml>
|
||||
<brief>Authorize the Request Token</brief>
|
||||
<issue-id>22</issue-id>
|
||||
<desc>
|
||||
<p>Unlike other methods, the <b>authorize</b> method is to be executed
|
||||
inside the User's browser. Consumer's role is to <b>redirect</b> the User to
|
||||
this URL, then wait if he ever comes back with a callback request.</p>
|
||||
<p>Once the User is redirected to this URL, several things will happen:</p>
|
||||
<ul>
|
||||
<li>If he's not already logged in, he will be asked to do so.</li>
|
||||
<li>OKAPI will check if the User haven't previously granted your
|
||||
application access to his OpenCaching account.</li>
|
||||
<li>If User did not previously authorize your application, OKAPI
|
||||
will display an "Authorization Request" form to the User. User
|
||||
will be presented with a choice to allow or not to allow your
|
||||
application access to his account.</li>
|
||||
<li>If the User clicks one of these two options ("allow" or "don't allow"),
|
||||
he's browser will be redirected to the <b>callback_url</b> you defined
|
||||
while getting your Request Token.
|
||||
If you did not provide a callback (in other word, provided "oob"),
|
||||
user will be redirected to a default "authorized" page, where he
|
||||
will be presented with an oauth_verifier (user will know it
|
||||
by name of a <b>PIN code</b>) and asked to type it into your application.</li>
|
||||
</ul>
|
||||
</desc>
|
||||
<req name='oauth_token'>
|
||||
Consult <a href='http://oauth.net/documentation/spec/'>OAuth documentation</a> for details.
|
||||
</req>
|
||||
<opt name='interactivity' default='minimal'>
|
||||
<p>Currently, one of the following values:</p>
|
||||
<ul>
|
||||
<li><b>minimal</b> - OKAPI will use as little interactivity as it can.
|
||||
It will assume that currently logged in user is the user which you
|
||||
want to authorize. If the user has already authorized your application,
|
||||
he will not be asked to do this again.</li>
|
||||
<li><b>confirm_user</b> - even if a user is logged in, OKAPI will NOT
|
||||
assume that this is the user who wants to be authorized. OKAPI will
|
||||
offer to authorize a different user (e.g. by automatically logging out
|
||||
the user who is currently logged in).</li>
|
||||
</ul>
|
||||
</opt>
|
||||
<opt name='langpref'>
|
||||
<p>Pipe-separated list of ISO 639-1 language codes. This indicates the
|
||||
order of preference in which the language will be chosen for the authorization page.</p>
|
||||
<p>By default, OKAPI will display the page in the primary native language of local
|
||||
OpenCaching installation.</p>
|
||||
</opt>
|
||||
<returns>
|
||||
<p>Technically, a HTTP 302 Redirect - it will direct user's browser to the OKAPI apps
|
||||
authorization page.</p>
|
||||
<p>Whether with callback_url or with a manual user entry - you will get
|
||||
your <b>oauth_verifier</b>, which allows you to continue the 3-legged
|
||||
authentication dance.</p>
|
||||
<p>If you used <b>callback_url</b>, you should wait for a HTTP GET request,
|
||||
with one additional GET parameter appended:</p>
|
||||
<ul>
|
||||
<li><b>oauth_token</b> - the Request Token that has been just authorized,</li>
|
||||
<li><b>oauth_verifier</b> - the PIN code required to get an Access Token.</li>
|
||||
</ul>
|
||||
<p>OR, in case when user denied the request:</p>
|
||||
<ul>
|
||||
<li><b>oauth_token</b> - the Request Token,</li>
|
||||
<li><b>error</b> - codename of an error - <b>access_denied</b>.</li>
|
||||
</ul>
|
||||
</returns>
|
||||
</xml>
|
36
htdocs/okapi/services/oauth/request_token.php
Normal file
36
htdocs/okapi/services/oauth/request_token.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?
|
||||
|
||||
namespace okapi\services\oauth\request_token;
|
||||
|
||||
use okapi\Okapi;
|
||||
use okapi\OkapiHttpResponse;
|
||||
use okapi\OkapiRequest;
|
||||
use okapi\ParamMissing;
|
||||
use okapi\InvalidParam;
|
||||
|
||||
class WebService
|
||||
{
|
||||
public static function options()
|
||||
{
|
||||
return array(
|
||||
'min_auth_level' => 2
|
||||
);
|
||||
}
|
||||
|
||||
public static function call(OkapiRequest $request)
|
||||
{
|
||||
$callback = $request->get_parameter('oauth_callback');
|
||||
if (!$callback)
|
||||
{
|
||||
# We require the 1.0a flow (throw an error when there is no oauth_callback).
|
||||
throw new ParamMissing("oauth_callback");
|
||||
}
|
||||
|
||||
$new_token = Okapi::$data_store->new_request_token($request->consumer, $callback);
|
||||
|
||||
$response = new OkapiHttpResponse();
|
||||
$response->content_type = "text/plain; charset=utf-8";
|
||||
$response->body = $new_token."&oauth_callback_confirmed=true";
|
||||
return $response;
|
||||
}
|
||||
}
|
24
htdocs/okapi/services/oauth/request_token.xml
Normal file
24
htdocs/okapi/services/oauth/request_token.xml
Normal file
@ -0,0 +1,24 @@
|
||||
<xml>
|
||||
<brief>Get a new unauthorized OAuth Request Token</brief>
|
||||
<issue-id>23</issue-id>
|
||||
<desc>
|
||||
<p>Get a new unauthorized OAuth Request Token. This token is bound to
|
||||
the Consumer it was issued to. It has a short expiration date and
|
||||
can be used in one way only - first it has to get authorized by
|
||||
the user, then it has to be exchanged for an Access Token.</p>
|
||||
</desc>
|
||||
<req name='oauth_callback'>
|
||||
<p>An URL which you want a User to be redirected to after a
|
||||
successful Request Token Authorization (see "authorize" method).
|
||||
If the client is unable to receive callbacks, the parameter
|
||||
must be set to "oob", OKAPI will provide a user with a
|
||||
PIN code (oauth_verifier) in this case.</p>
|
||||
Consult <a href='http://oauth.net/documentation/spec/'>OAuth documentation</a> for details.
|
||||
</req>
|
||||
<returns>
|
||||
<p>Standard OAuth 1.0a Token response - a string in a form-encoded format:</p>
|
||||
<pre>oauth_token=...&oauth_token_secret=...&oauth_callback_confirmed=true</pre>
|
||||
<p>You <b>must</b> be prepared that there might be more parameters returned
|
||||
in the future (you should ignore them gracefully).</p>
|
||||
</returns>
|
||||
</xml>
|
53
htdocs/okapi/services/replicate/changelog.php
Normal file
53
htdocs/okapi/services/replicate/changelog.php
Normal file
@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace okapi\services\replicate\changelog;
|
||||
|
||||
use Exception;
|
||||
use okapi\Okapi;
|
||||
use okapi\Db;
|
||||
use okapi\OkapiRequest;
|
||||
use okapi\ParamMissing;
|
||||
use okapi\InvalidParam;
|
||||
use okapi\BadRequest;
|
||||
use okapi\DoesNotExist;
|
||||
use okapi\OkapiInternalRequest;
|
||||
use okapi\OkapiInternalConsumer;
|
||||
use okapi\OkapiServiceRunner;
|
||||
use okapi\services\replicate\ReplicateCommon;
|
||||
|
||||
class WebService
|
||||
{
|
||||
public static function options()
|
||||
{
|
||||
return array(
|
||||
'min_auth_level' => 1
|
||||
);
|
||||
}
|
||||
|
||||
public static function call(OkapiRequest $request)
|
||||
{
|
||||
require_once 'replicate_common.inc.php';
|
||||
|
||||
$since = $request->get_parameter('since');
|
||||
if ($since === null) throw new ParamMissing('since');
|
||||
if ((int)$since != $since) throw new InvalidParam('since');
|
||||
|
||||
# Let's check the $since parameter.
|
||||
|
||||
if (!ReplicateCommon::check_since_param($since))
|
||||
throw new BadRequest("The 'since' parameter is too old. You must update your database more frequently.");
|
||||
|
||||
# Select a best chunk for the given $since, get the chunk from the database (or cache).
|
||||
|
||||
list($from, $to) = ReplicateCommon::select_best_chunk($since);
|
||||
$clog_entries = ReplicateCommon::get_chunk($from, $to);
|
||||
|
||||
$result = array(
|
||||
'changelog' => &$clog_entries,
|
||||
'revision' => $to + 0,
|
||||
'more' => $to < ReplicateCommon::get_revision(),
|
||||
);
|
||||
|
||||
return Okapi::formatted_response($request, $result);
|
||||
}
|
||||
}
|
134
htdocs/okapi/services/replicate/changelog.xml
Normal file
134
htdocs/okapi/services/replicate/changelog.xml
Normal file
@ -0,0 +1,134 @@
|
||||
<xml>
|
||||
<brief>Get the list of changes for your database</brief>
|
||||
<issue-id>109</issue-id>
|
||||
<desc>
|
||||
<p><b>Beta status.</b> Get the list of changes to be replayed on your own copy
|
||||
of OpenCaching database. Use this method periodically (ex. once per hour) to
|
||||
keep your database in sync with ours.</p>
|
||||
|
||||
<p>For some applications it might be desireable to have a quick access to the entire
|
||||
OpenCaching database (instead of quering for specific portions of it). You may use
|
||||
OKAPI's <b>replicate</b> module to achive this effect. The <b>changelog</b> method
|
||||
is the primary replication service which you will use. However, to correctly set up
|
||||
your first database copy, you will need to use the <b>fulldump</b> method.</p>
|
||||
|
||||
<p>A couple of things for you to remember:</p>
|
||||
<ul>
|
||||
<li>You <b>must</b> update your database frequently for this method to work.
|
||||
We don't keep the changelog indefinitelly. You must update at least once every week.</li>
|
||||
<li>You <b>should not</b> update your database more frequently than once every
|
||||
5 minutes. This won't do you any good, since we update the changelog only once
|
||||
every 5 minutes anyway.</li>
|
||||
</ul>
|
||||
|
||||
<p>Let's assume that you already have a copy of OKAPI database, but it's
|
||||
already 2 days old. You want to use the <b>changelog</b> method to update
|
||||
your copy to the most current state.</p>
|
||||
|
||||
<p>Changelog is the list of all changes which appeared in the OKAPI database
|
||||
since the last time you downloaded it. What you have to do is to download this
|
||||
list of changes and to replay them on your copy of our database. After you do
|
||||
this, your database is up-to-date.</p>
|
||||
|
||||
<p>We use <b>revision</b> numbers to keep track of all the versions of the
|
||||
database. Every time you update a database, you receive the <b>revision</b>
|
||||
number along with it. You must keep this number carefully, because you need
|
||||
it in order for us to generate a proper changelog for you next time you ask
|
||||
for it.</p>
|
||||
|
||||
<p>Example. This is a valid list of requests you should issue and their
|
||||
responses:</p>
|
||||
<ul>
|
||||
<li><b>fulldump</b> - you receive a fulldump of our database with the
|
||||
revision number 238004. <b>You will call this method only once, never again.</b></li>
|
||||
<li><b>changelog</b>?since=238004 - OKAPI checks if there were any changes
|
||||
recorded since revision 238004. It responds with the list of changes and the
|
||||
new revision number 238017. You replay the changes on your database.</li>
|
||||
<li>You wait for some time between requesting the changelog again.</li>
|
||||
<li><b>changelog</b>?since=238017 - etc.</li>
|
||||
</ul>
|
||||
|
||||
|
||||
</desc>
|
||||
<req name='since'>
|
||||
<p>Current revision of your database. This should be the same as the value
|
||||
of <b>revision</b> attribute, which you received with your previous update.</p>
|
||||
<p>Old revisions are deleted, the <b>since</b> argument MUST referer to a revision no older
|
||||
than 10 days. You will have to download a fulldump if you have an older copy
|
||||
(shame on you!).</p>
|
||||
</req>
|
||||
<common-format-params/>
|
||||
<returns>
|
||||
<p>A dictionary of the following structure:</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p><b>changelog</b> - a <b>list</b> of dictionaries. Each dictionary has the following structure:</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p><b>object_type</b> - string, object type to which the change refers to. One of the following values:</p>
|
||||
<ul>
|
||||
<li><b>geocache</b> - this change refers to a geocache object,</li>
|
||||
<li><b>log_entry</b> - this change refers to a log entry.</li>
|
||||
</ul>
|
||||
<p>More object types will come in the future. You should ignore all changelog
|
||||
entries with an unknown object_type.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><b>object_key</b> - a dictionary of fields which compose the primary key for the object.
|
||||
This will be the <b>code</b> field for the <b>geocache</b> object, and <b>uuid</b> field
|
||||
for the <b>log_entry</b> object.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><b>change_type</b> - string, the type of the change. One of the following values:</p>
|
||||
<ul>
|
||||
<li><b>replace</b> - the object was inserted or updated. You should check if you
|
||||
already have the object in your database. If you have it, you should update its
|
||||
fields accordingly. If you don't, you should create it.</li>
|
||||
<li><b>delete</b> - the object was deleted. You should check if you already have
|
||||
the object in your database. If you do, you should delete it.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p><b>fields</b> - a dictionary of fields associated with the object (present only
|
||||
if <b>change_type</b> equals <b>replace</b>).</p>
|
||||
<ul>
|
||||
<li>For <b>geocache</b> objects, this might be any subset of fields described
|
||||
in the services/caches/geocache method. Note that not all of these fields will
|
||||
be included here (i.e. latest_logs will not).</li>
|
||||
<li>For <b>log_entry</b> objects, this might be any subset of fields described
|
||||
in the services/logs/logs method, <b>plus</b> additional <b>cache_code</b> field,
|
||||
the code of the geocache to which the log entry refers to.</li>
|
||||
</ul>
|
||||
<p>In theory this dictionary should contain only these fields which actually changed.
|
||||
In truth, it MAY contain all the other fields too. This behavior may change in future.</p>
|
||||
</li>
|
||||
</ul>
|
||||
<p>You should iterate through this changelog sequentially. All the changes should be applied
|
||||
in the same order as they were listed in the changelog. Single object may appear multiple times
|
||||
inside the changelog. Changelog will contain all the changes which occured since the time you
|
||||
specified in the <b>since</b> parameter and it MAY contain some more, which were submitted
|
||||
<b>before</b> this date (see below). The changes which are unnecessary MAY be skipped (ex. when
|
||||
cache description changes multiple times, we may want to include only the last change).</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><b>revision</b> - this is the revision number which you should use in the <b>since</b> parameter
|
||||
when you call this method next time.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><b>more</b> - boolean. If <b>false</b>, then it means that the entire changelog had been
|
||||
pulled. If <b>true</b>, then there are more items waiting to be pulled - you should rerun this
|
||||
method again (with the value of <b>revision</b> inserted in the <b>since</b> parameter).</p>
|
||||
<p>The changelog is usually tiny, but it also might be huge at times. It may even
|
||||
contain all the caches in the database (i.e. if we decide to do some changes on all
|
||||
caches in a bulk). This is the reason why we introduced the <b>more</b> attribute. It makes it
|
||||
easier to parse the changelog sequentially.</p>
|
||||
</li>
|
||||
</ul>
|
||||
<p>The response MAY contain changes recorded before the revision which you stated in your <b>since</b>
|
||||
parameter. This is due to caching. If we already have a response ready, that was produced for a
|
||||
"since" parameter which was a little more in the past, we may want to return this cached copy,
|
||||
instead of producing a new one. It will be faster for us, and you shouldn't even notice. We will
|
||||
still make sure to set the <b>revision</b> and <b>more</b> attributes in a correct way, which
|
||||
in turn will make you query us again, if you need to.</p>
|
||||
</returns>
|
||||
</xml>
|
80
htdocs/okapi/services/replicate/fulldump.php
Normal file
80
htdocs/okapi/services/replicate/fulldump.php
Normal file
@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace okapi\services\replicate\fulldump;
|
||||
|
||||
use Exception;
|
||||
use okapi\Okapi;
|
||||
use okapi\Db;
|
||||
use okapi\Cache;
|
||||
use okapi\OkapiHttpResponse;
|
||||
use okapi\OkapiRequest;
|
||||
use okapi\ParamMissing;
|
||||
use okapi\InvalidParam;
|
||||
use okapi\BadRequest;
|
||||
|
||||
class WebService
|
||||
{
|
||||
public static function options()
|
||||
{
|
||||
return array(
|
||||
'min_auth_level' => 1
|
||||
);
|
||||
}
|
||||
|
||||
private static function count_calls($consumer_key, $days)
|
||||
{
|
||||
return (
|
||||
Db::select_value("
|
||||
select count(*)
|
||||
from okapi_stats_temp
|
||||
where
|
||||
consumer_key = '".mysql_real_escape_string($consumer_key)."'
|
||||
and service_name='services/replicate/fulldump'
|
||||
")
|
||||
+
|
||||
Db::select_value("
|
||||
select sum(total_calls)
|
||||
from okapi_stats_hourly
|
||||
where
|
||||
consumer_key = '".mysql_real_escape_string($consumer_key)."'
|
||||
and service_name='services/replicate/fulldump'
|
||||
and period_start > date_add(now(), interval -$days day)
|
||||
limit 1
|
||||
")
|
||||
);
|
||||
}
|
||||
|
||||
public static function call(OkapiRequest $request)
|
||||
{
|
||||
require_once 'replicate_common.inc.php';
|
||||
|
||||
$data = Cache::get("last_fulldump");
|
||||
if ($data == null)
|
||||
throw new BadRequest("No fulldump found. Try again later. If this doesn't help ".
|
||||
"contact site administrator and/or OKAPI developers.");
|
||||
|
||||
# Check consumer's quota
|
||||
|
||||
$please = $request->get_parameter('pleeaase');
|
||||
if ($please != 'true')
|
||||
{
|
||||
$not_good = 3 < self::count_calls($request->consumer->key, 30);
|
||||
if ($not_good)
|
||||
throw new BadRequest("Consumer's monthly quota exceeded. Try later or call with '&pleeaase=true'.");
|
||||
}
|
||||
else
|
||||
{
|
||||
$not_good = 5 < self::count_calls($request->consumer->key, 1);
|
||||
if ($not_good)
|
||||
throw new BadRequest("No more please. Seriously, dude...");
|
||||
}
|
||||
|
||||
$response = new OkapiHttpResponse();
|
||||
$response->content_type = $data['meta']['content_type'];
|
||||
$response->content_disposition = '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;
|
||||
return $response;
|
||||
}
|
||||
}
|
52
htdocs/okapi/services/replicate/fulldump.xml
Normal file
52
htdocs/okapi/services/replicate/fulldump.xml
Normal file
@ -0,0 +1,52 @@
|
||||
<xml>
|
||||
<brief>Download OKAPI database snapshot</brief>
|
||||
<issue-id>110</issue-id>
|
||||
<desc>
|
||||
<p><b>Beta status.</b> Download the latest snapshot of OKAPI database. You should call this method
|
||||
only once in your lifetime.</p>
|
||||
|
||||
<p>For some applications it might be desireable to have a quick access to the entire
|
||||
OpenCaching database (instead of quering for specific portions of it). You may use
|
||||
OKAPI's <b>replicate</b> module to achive this effect. The <b>changelog</b> method
|
||||
is the primary replication service which you will use. However, to correctly set up
|
||||
your first database copy, you will need to use the <b>fulldump</b> method.</p>
|
||||
|
||||
<p><b>Important:</b> This method MAY change substantially or it might even get removed.
|
||||
We don't plan on doing this, but we may be forced to (i.e. to prevent abuse).</p>
|
||||
|
||||
<p>A couple of things for you to remember:</p>
|
||||
|
||||
<ul>
|
||||
<li>Currently, this functionality is available <b>for developers only</b>,
|
||||
NOT for the individual users. We don't want users to download a fresh snapshot of
|
||||
the entire database every time they want, this could kill our servers.
|
||||
If you want to enable such features for your users, you can do it, but you
|
||||
must use <b>your own server</b> for data traffic (especially, fulldump requests).</li>
|
||||
<li>Fulldump is a copy of the entire database. We generate such copy once every couple of
|
||||
days. This copy if intended for you to start only, later you must use the changelog to
|
||||
keep it up-to-date.</li>
|
||||
<li>Every time our database is extended (new fields or new object types), and you want to
|
||||
make use of these new fields, you are of course allowed to download a fulldump copy again.</li>
|
||||
<li>There is no XMLMAP version of this file.</li>
|
||||
</ul>
|
||||
</desc>
|
||||
<returns>
|
||||
<p>Archive with JSON-encoded files. File named <b>index.json</b> will contain a dictionary
|
||||
of the following structure:</p>
|
||||
<ul>
|
||||
<li><b>revision</b> - revision of the database snapshot contained in the archive,</li>
|
||||
<li><b>data_files</b> - list of filenames which contain the changelog entries for
|
||||
you to parse. Each file contains a JSON-encoded list of changelog entries, in format
|
||||
described in the <b>changelog</b> method ("replace" only).</li>
|
||||
<li><b>meta</b> - a dictionary of other meta data, not important.</li>
|
||||
</ul>
|
||||
<p>Note: We use TGZ or TBZ2 format to encode this archive:</p>
|
||||
<ul>
|
||||
<li><b>TGZ</b> archive (more commonly known as <b>.tar.gz</b> archive) is a TAR
|
||||
archive compressed with GZIP.</li>
|
||||
<li><b>TBZ2</b> archive (more commonly known as <b>.tar.bz2</b> archive) is a TAR
|
||||
archive compressed with BZIP2.</li>
|
||||
</ul>
|
||||
<p>There are many tools available for handling these archives. In Linux, try "tar -xf filename".</p>
|
||||
</returns>
|
||||
</xml>
|
52
htdocs/okapi/services/replicate/info.php
Normal file
52
htdocs/okapi/services/replicate/info.php
Normal file
@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace okapi\services\replicate\info;
|
||||
|
||||
use Exception;
|
||||
use okapi\Okapi;
|
||||
use okapi\Db;
|
||||
use okapi\Cache;
|
||||
use okapi\OkapiRequest;
|
||||
use okapi\ParamMissing;
|
||||
use okapi\InvalidParam;
|
||||
use okapi\BadRequest;
|
||||
use okapi\DoesNotExist;
|
||||
use okapi\OkapiInternalRequest;
|
||||
use okapi\OkapiInternalConsumer;
|
||||
use okapi\OkapiServiceRunner;
|
||||
use okapi\services\replicate\ReplicateCommon;
|
||||
|
||||
class WebService
|
||||
{
|
||||
public static function options()
|
||||
{
|
||||
return array(
|
||||
'min_auth_level' => 1
|
||||
);
|
||||
}
|
||||
|
||||
public static function call(OkapiRequest $request)
|
||||
{
|
||||
require_once 'replicate_common.inc.php';
|
||||
|
||||
$result = array();
|
||||
$result['changelog'] = array(
|
||||
'min_since' => ReplicateCommon::get_min_since(),
|
||||
'revision' => ReplicateCommon::get_revision(),
|
||||
);
|
||||
$dump = Cache::get("last_fulldump");
|
||||
if ($dump)
|
||||
{
|
||||
$result['latest_fulldump'] = array(
|
||||
'revision' => $dump['revision'],
|
||||
'generated_at' => $dump['meta']['generated_at'],
|
||||
'size' => $dump['meta']['compressed_size'],
|
||||
'size_uncompressed' => $dump['meta']['uncompressed_size'],
|
||||
);
|
||||
} else {
|
||||
$result['latest_fulldump'] = null;
|
||||
}
|
||||
|
||||
return Okapi::formatted_response($request, $result);
|
||||
}
|
||||
}
|
31
htdocs/okapi/services/replicate/info.xml
Normal file
31
htdocs/okapi/services/replicate/info.xml
Normal file
@ -0,0 +1,31 @@
|
||||
<xml>
|
||||
<brief>Get information on current changelog and fulldump state</brief>
|
||||
<issue-id>111</issue-id>
|
||||
<desc>
|
||||
<p><b>Beta status.</b> Get information on current changelog and fulldump state.</p>
|
||||
</desc>
|
||||
<common-format-params/>
|
||||
<returns>
|
||||
<p>A dictionary of the following structure:</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p><b>changelog</b> - a dictionary of the following structure:</p>
|
||||
<ul>
|
||||
<li><b>min_since</b> - the lowest allowed value for the <b>since</b> attribute
|
||||
(the ID of the last changelog entry was already removed from our database),</li>
|
||||
<li><b>revision</b> - the current revision of the database (the ID of the
|
||||
newest changelog entry kept in our database),</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p><b>latest_fulldump</b> - a dictionary of the following structure:</p>
|
||||
<ul>
|
||||
<li><b>revision</b> - revision of the database saved in the latest fulldump,</li>
|
||||
<li><b>generated_at</b> - date and time (ISO 8601) when the fulldump was generated,</li>
|
||||
<li><b>size</b> - size of the file, in bytes,</li>
|
||||
<li><b>size_uncompressed</b> - approximate size of the uncompressed contents, in bytes.</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</returns>
|
||||
</xml>
|
455
htdocs/okapi/services/replicate/replicate_common.inc.php
Normal file
455
htdocs/okapi/services/replicate/replicate_common.inc.php
Normal file
@ -0,0 +1,455 @@
|
||||
<?php
|
||||
|
||||
namespace okapi\services\replicate;
|
||||
|
||||
use Exception;
|
||||
use okapi\Okapi;
|
||||
use okapi\Db;
|
||||
use okapi\OkapiRequest;
|
||||
use okapi\ParamMissing;
|
||||
use okapi\InvalidParam;
|
||||
use okapi\BadRequest;
|
||||
use okapi\DoesNotExist;
|
||||
use okapi\OkapiInternalRequest;
|
||||
use okapi\OkapiInternalConsumer;
|
||||
use okapi\OkapiServiceRunner;
|
||||
use okapi\Cache;
|
||||
use okapi\Settings;
|
||||
|
||||
class ReplicateCommon
|
||||
{
|
||||
private static $chunk_size = 200;
|
||||
private static $logged_cache_fields = 'code|names|location|type|status|url|owner|founds|notfounds|size|difficulty|terrain|rating|rating_votes|recommendations|req_passwd|descriptions|hints|images|trackables_count|trackables|alt_wpts|last_found|last_modified|date_created|date_hidden';
|
||||
|
||||
private static $logged_log_entry_fields = 'uuid|cache_code|date|user|type|comment';
|
||||
|
||||
/** Return current (greatest) changelog revision number. */
|
||||
public static function get_revision()
|
||||
{
|
||||
return Okapi::get_var('clog_revision', 0) + 0;
|
||||
}
|
||||
|
||||
/** Return the number of the oldest changelog revision kept in database. */
|
||||
public static function get_min_since()
|
||||
{
|
||||
static $cache = null;
|
||||
if ($cache == null)
|
||||
{
|
||||
$cache = Db::select_value("select min(id) from okapi_clog");
|
||||
if ($cache === null)
|
||||
$cache = 1;
|
||||
$cache -= 1;
|
||||
}
|
||||
return $cache;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Compare two dictionaries. Return the $new dictionary with all unchanged
|
||||
* keys removed. Only the changed ones will remain.
|
||||
*/
|
||||
private static function get_diff($old, $new)
|
||||
{
|
||||
if ($old === null)
|
||||
return $new;
|
||||
$changed_keys = array();
|
||||
foreach ($new as $key => $value)
|
||||
{
|
||||
if (!array_key_exists($key, $old))
|
||||
$changed_keys[] = $key;
|
||||
elseif ($old[$key] != $new[$key])
|
||||
$changed_keys[] = $key;
|
||||
}
|
||||
$changed = array();
|
||||
foreach ($changed_keys as $key)
|
||||
$changed[$key] = $new[$key];
|
||||
return $changed;
|
||||
}
|
||||
|
||||
/** Check for modifications in the database and update the changelog table accordingly. */
|
||||
public static function update_clog_table()
|
||||
{
|
||||
$now = Db::select_value("select now()");
|
||||
$last_update = Okapi::get_var('last_clog_update');
|
||||
if ($last_update === null)
|
||||
$last_update = Db::select_value("select date_add(now(), interval -1 day)");
|
||||
|
||||
# Usually this will be fast. But, for example, if admin changes ALL the
|
||||
# caches, this will take forever. But we still want it to finish properly
|
||||
# without interruption.
|
||||
|
||||
set_time_limit(0);
|
||||
ignore_user_abort(true);
|
||||
|
||||
# Get the list of modified cache codes. Split it into groups of N cache codes.
|
||||
|
||||
$cache_codes = Db::select_column("
|
||||
select wp_oc
|
||||
from caches
|
||||
where okapi_syncbase > '".mysql_real_escape_string($last_update)."';
|
||||
");
|
||||
$cache_code_groups = Okapi::make_groups($cache_codes, 50);
|
||||
unset($cache_codes);
|
||||
|
||||
# For each group, update the changelog table accordingly.
|
||||
|
||||
foreach ($cache_code_groups as $cache_codes)
|
||||
{
|
||||
self::generate_changelog_entries('services/caches/geocaches', 'geocache', 'cache_codes',
|
||||
'code', $cache_codes, self::$logged_cache_fields, false, true, 30*86400);
|
||||
}
|
||||
|
||||
# Same as above, for log entries.
|
||||
|
||||
$offset = 0;
|
||||
while (true)
|
||||
{
|
||||
$log_uuids = Db::select_column("
|
||||
select uuid
|
||||
from cache_logs
|
||||
where last_modified > '".mysql_real_escape_string($last_update)."'
|
||||
limit $offset, 10000;
|
||||
");
|
||||
if (count($log_uuids) == 0)
|
||||
break;
|
||||
$offset += 10000;
|
||||
$log_uuid_groups = Okapi::make_groups($log_uuids, 100);
|
||||
unset($log_uuids);
|
||||
foreach ($log_uuid_groups as $log_uuids)
|
||||
{
|
||||
self::generate_changelog_entries('services/logs/entries', 'log', 'log_uuids',
|
||||
'uuid', $log_uuids, self::$logged_log_entry_fields, false, true, 3600);
|
||||
}
|
||||
}
|
||||
if (Settings::get('OC_BRANCH') == 'oc.de')
|
||||
{
|
||||
# On OCDE branch, deleted log entries are MOVED to another table.
|
||||
# So the above queries won't detect them. We need to run one more.
|
||||
# We will assume there are not so many of them and we don't have to
|
||||
# split them in groups as we did above.
|
||||
|
||||
$DELETED_uuids = Db::select_column("
|
||||
select uuid
|
||||
from cache_logs_archived
|
||||
where last_modified > '".mysql_real_escape_string($last_update)."'
|
||||
");
|
||||
self::generate_changelog_entries('services/logs/entries', 'log', 'log_uuids',
|
||||
'uuid', $DELETED_uuids, self::$logged_log_entry_fields, false, true, 3600);
|
||||
}
|
||||
|
||||
# Update state variables and release DB lock.
|
||||
|
||||
Okapi::set_var("last_clog_update", $now);
|
||||
$revision = Db::select_value("select max(id) from okapi_clog");
|
||||
Okapi::set_var("clog_revision", $revision);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate OKAPI changelog entries. This method will call $feeder_method OKAPI
|
||||
* service with the following parameters: array($feeder_keys_param => implode('|', $key_values),
|
||||
* 'fields' => $fields). Then it will generate the changelog, based on the result.
|
||||
* This looks pretty much the same for various object types, that's why it's here.
|
||||
* If $use_cache is true, then all the dictionaries from $feeder_method will be also
|
||||
* kept in OKAPI cache, for future comparison.
|
||||
*/
|
||||
private static function generate_changelog_entries($feeder_method, $object_type, $feeder_keys_param,
|
||||
$key_name, $key_values, $fields, $fulldump_mode, $use_cache, $cache_timeout = 86400)
|
||||
{
|
||||
# Retrieve the previous versions of all objects from OKAPI cache.
|
||||
|
||||
if ($use_cache)
|
||||
{
|
||||
$cache_keys = array();
|
||||
foreach ($key_values as $key)
|
||||
$cache_keys[] = 'clog#'.$object_type.'#'.$key;
|
||||
$cached_values = Cache::get_many($cache_keys);
|
||||
Cache::delete_many($cache_keys);
|
||||
unset($cache_keys);
|
||||
}
|
||||
|
||||
# Get the current values for objects. Compare them with their previous versions
|
||||
# and generate changelog entries.
|
||||
|
||||
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)));
|
||||
$entries = array();
|
||||
foreach ($current_values as $key => $object)
|
||||
{
|
||||
if ($object !== null)
|
||||
{
|
||||
if ($use_cache)
|
||||
{
|
||||
$diff = self::get_diff($cached_values['clog#'.$object_type.'#'.$key], $object);
|
||||
if (count($diff) == 0)
|
||||
continue;
|
||||
}
|
||||
$entries[] = array(
|
||||
'object_type' => $object_type,
|
||||
'object_key' => array($key_name => $key),
|
||||
'change_type' => 'replace',
|
||||
'data' => ($use_cache ? $diff : $object),
|
||||
);
|
||||
if ($use_cache)
|
||||
$cached_values['clog#'.$object_type.'#'.$key] = $object;
|
||||
}
|
||||
else
|
||||
{
|
||||
$entries[] = array(
|
||||
'object_type' => $object_type,
|
||||
'object_key' => array($key_name => $key),
|
||||
'change_type' => 'delete',
|
||||
);
|
||||
if ($use_cache)
|
||||
$cached_values['clog#'.$object_type.'#'.$key] = null;
|
||||
}
|
||||
}
|
||||
|
||||
if ($fulldump_mode)
|
||||
{
|
||||
return $entries;
|
||||
}
|
||||
else
|
||||
{
|
||||
# Save the entries to the clog table.
|
||||
|
||||
if (count($entries) > 0)
|
||||
{
|
||||
$data_values = array();
|
||||
foreach ($entries as $entry)
|
||||
$data_values[] = gzdeflate(serialize($entry));
|
||||
Db::execute("
|
||||
insert into okapi_clog (data)
|
||||
values ('".implode("'),('", array_map('mysql_real_escape_string', $data_values))."');
|
||||
");
|
||||
}
|
||||
|
||||
# Update the values kept in OKAPI cache.
|
||||
|
||||
if ($use_cache)
|
||||
Cache::set_many($cached_values, $cache_timeout);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the 'since' parameter is up-do-date. If it is not, then it means
|
||||
* that the user waited too long and he has to download the fulldump again.
|
||||
*/
|
||||
public static function check_since_param($since)
|
||||
{
|
||||
$first_id = Db::select_value("
|
||||
select id from okapi_clog where id > '".mysql_real_escape_string($since)."' limit 1
|
||||
");
|
||||
if ($first_id === null)
|
||||
return true; # okay, since points to the newest revision
|
||||
if ($first_id == $since + 1)
|
||||
return true; # okay, revision $since + 1 is present
|
||||
|
||||
# If we're here, then this means that $first_id > $since + 1.
|
||||
# Revision $since + 1 is already deleted, $since must be too old!
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select best chunk for a given $since parameter. This function will try to select
|
||||
* one chunk for different values of $since parameter, this is done in order to
|
||||
* allow more optimal caching. Returns: list($from, $to). NOTICE: If $since is
|
||||
* at the newest revision, then this will return list($since + 1, $since) - an
|
||||
* empty chunk.
|
||||
*/
|
||||
public static function select_best_chunk($since)
|
||||
{
|
||||
$current_revision = self::get_revision();
|
||||
$last_chunk_cut = $current_revision - ($current_revision % self::$chunk_size);
|
||||
if ($since >= $last_chunk_cut)
|
||||
{
|
||||
# If, for example, we have a choice to give user 50 items he wants, or 80 items
|
||||
# which we probably already have in cache (and this includes the 50 which the
|
||||
# user wants), then we'll give him 80. If user wants less than half of what we
|
||||
# have (ex. 30), then we'll give him only his 30.
|
||||
|
||||
if ($current_revision - $since > $since - $last_chunk_cut)
|
||||
return array($last_chunk_cut + 1, $current_revision);
|
||||
else
|
||||
return array($since + 1, $current_revision);
|
||||
}
|
||||
$prev_chunk_cut = $since - ($since % self::$chunk_size);
|
||||
return array($prev_chunk_cut + 1, $prev_chunk_cut + self::$chunk_size);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return changelog chunk, starting at $from, ending as $to.
|
||||
*/
|
||||
public static function get_chunk($from, $to)
|
||||
{
|
||||
if ($to < $from)
|
||||
return array();
|
||||
if ($to - $from > self::$chunk_size)
|
||||
throw new Exception("You should not get chunksize bigger than ".self::$chunk_size." entries at one time.");
|
||||
|
||||
# Check if we already have this chunk in cache.
|
||||
|
||||
$cache_key = 'clog_chunk#'.$from.'-'.$to;
|
||||
$chunk = Cache::get($cache_key);
|
||||
if ($chunk === null)
|
||||
{
|
||||
$rs = Db::query("
|
||||
select id, data
|
||||
from okapi_clog
|
||||
where id between '".mysql_real_escape_string($from)."' and '".mysql_real_escape_string($to)."'
|
||||
order by id
|
||||
");
|
||||
$chunk = array();
|
||||
while ($row = mysql_fetch_assoc($rs))
|
||||
{
|
||||
$chunk[] = unserialize(gzinflate($row['data']));
|
||||
}
|
||||
|
||||
# Cache timeout depends on the chunk starting and ending point. Chunks
|
||||
# which start and end on the boundries of chunk_size should be cached
|
||||
# longer (they can be accessed even after 10 days). Other chunks won't
|
||||
# be ever accessed after the next revision appears, so there is not point
|
||||
# in storing them that long.
|
||||
|
||||
if (($from % self::$chunk_size === 0) && ($to % self::$chunk_size === 0))
|
||||
$timeout = 10 * 86400;
|
||||
else
|
||||
$timeout = 86400;
|
||||
Cache::set($cache_key, $chunk, $timeout);
|
||||
}
|
||||
|
||||
return $chunk;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new fulldump file and put it into the OKAPI cache table.
|
||||
* Return the cache key.
|
||||
*/
|
||||
public static function generate_fulldump()
|
||||
{
|
||||
# First we will create temporary files, then compress them in the end.
|
||||
|
||||
$revision = self::get_revision();
|
||||
$generated_at = date('c', time());
|
||||
$dir = Okapi::get_var_dir()."/okapi-db-dump";
|
||||
$i = 1;
|
||||
$json_files = array();
|
||||
|
||||
# Cleanup (from a previous, possibly unsuccessful, execution)
|
||||
|
||||
shell_exec("rm -f $dir/*");
|
||||
shell_exec("rmdir $dir");
|
||||
shell_exec("mkdir $dir");
|
||||
shell_exec("chmod 777 $dir");
|
||||
|
||||
# Geocaches
|
||||
|
||||
$cache_codes = Db::select_column("select wp_oc from caches");
|
||||
$cache_code_groups = Okapi::make_groups($cache_codes, 200);
|
||||
unset($cache_codes);
|
||||
foreach ($cache_code_groups as $cache_codes)
|
||||
{
|
||||
$basename = "part".str_pad($i, 5, "0", STR_PAD_LEFT);
|
||||
$json_files[] = $basename.".json";
|
||||
$entries = self::generate_changelog_entries('services/caches/geocaches', 'geocache', 'cache_codes',
|
||||
'code', $cache_codes, self::$logged_cache_fields, true, false);
|
||||
$filtered = array();
|
||||
foreach ($entries as $entry)
|
||||
if ($entry['change_type'] == 'replace')
|
||||
$filtered[] = $entry;
|
||||
unset($entries);
|
||||
$fp = fopen("$dir/$basename.json", "wb");
|
||||
fwrite($fp, json_encode($filtered));
|
||||
fclose($fp);
|
||||
unset($filtered);
|
||||
$i++;
|
||||
}
|
||||
unset($cache_code_groups);
|
||||
|
||||
# Log entries. We cannot load all the uuids at one time, this would take
|
||||
# too much memory. Hence the offset/limit loop.
|
||||
|
||||
$offset = 0;
|
||||
while (true)
|
||||
{
|
||||
$log_uuids = Db::select_column("
|
||||
select uuid
|
||||
from cache_logs
|
||||
where ".((Settings::get('OC_BRANCH') == 'oc.pl') ? "deleted = 0" : "true")."
|
||||
order by uuid
|
||||
limit $offset, 10000
|
||||
");
|
||||
if (count($log_uuids) == 0)
|
||||
break;
|
||||
$offset += 10000;
|
||||
$log_uuid_groups = Okapi::make_groups($log_uuids, 500);
|
||||
unset($log_uuids);
|
||||
foreach ($log_uuid_groups as $log_uuids)
|
||||
{
|
||||
$basename = "part".str_pad($i, 5, "0", STR_PAD_LEFT);
|
||||
$json_files[] = $basename.".json";
|
||||
$entries = self::generate_changelog_entries('services/logs/entries', 'log', 'log_uuids',
|
||||
'uuid', $log_uuids, self::$logged_log_entry_fields, true, false);
|
||||
$filtered = array();
|
||||
foreach ($entries as $entry)
|
||||
if ($entry['change_type'] == 'replace')
|
||||
$filtered[] = $entry;
|
||||
unset($entries);
|
||||
$fp = fopen("$dir/$basename.json", "wb");
|
||||
fwrite($fp, json_encode($filtered));
|
||||
fclose($fp);
|
||||
unset($filtered);
|
||||
$i++;
|
||||
}
|
||||
}
|
||||
|
||||
# Package data.
|
||||
|
||||
$metadata = array(
|
||||
'revision' => $revision,
|
||||
'data_files' => $json_files,
|
||||
'meta' => array(
|
||||
'site_name' => Okapi::get_normalized_site_name(),
|
||||
'okapi_revision' => Okapi::$revision,
|
||||
'generated_at' => $generated_at,
|
||||
),
|
||||
);
|
||||
$fp = fopen("$dir/index.json", "wb");
|
||||
fwrite($fp, json_encode($metadata));
|
||||
fclose($fp);
|
||||
|
||||
# Compute uncompressed size.
|
||||
|
||||
$size = filesize("$dir/index.json");
|
||||
foreach ($json_files as $filename)
|
||||
$size += filesize("$dir/$filename");
|
||||
|
||||
# Create JSON archive. We use tar options: -j for bzip2, -z for gzip
|
||||
# (bzip2 is MUCH slower).
|
||||
|
||||
$use_bzip2 = true;
|
||||
$dumpfilename = "okapi-dump.tar.".($use_bzip2 ? "bz2" : "gz");
|
||||
shell_exec("tar --directory $dir -c".($use_bzip2 ? "j" : "z")."f $dir/$dumpfilename index.json ".implode(" ", $json_files). " 2>&1");
|
||||
|
||||
# Delete temporary files.
|
||||
|
||||
shell_exec("rm -f $dir/*.json");
|
||||
|
||||
# Move the archive one directory upwards, replacing the previous one.
|
||||
# Remove the temporary directory.
|
||||
|
||||
shell_exec("mv -f $dir/$dumpfilename ".Okapi::get_var_dir());
|
||||
shell_exec("rmdir $dir");
|
||||
|
||||
# Update the database info.
|
||||
|
||||
$metadata['meta']['filepath'] = Okapi::get_var_dir().'/'.$dumpfilename;
|
||||
$metadata['meta']['content_type'] = ($use_bzip2 ? "application/octet-stream" : "application/x-gzip");
|
||||
$metadata['meta']['public_filename'] = 'okapi-dump-r'.$metadata['revision'].'.tar.'.($use_bzip2 ? "bz2" : "gz");
|
||||
$metadata['meta']['uncompressed_size'] = $size;
|
||||
$metadata['meta']['compressed_size'] = filesize($metadata['meta']['filepath']);
|
||||
Cache::set("last_fulldump", $metadata, 10 * 86400);
|
||||
}
|
||||
}
|
39
htdocs/okapi/services/users/by_internal_id.php
Normal file
39
htdocs/okapi/services/users/by_internal_id.php
Normal file
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace okapi\services\users\by_internal_id;
|
||||
|
||||
use okapi\Okapi;
|
||||
use okapi\OkapiRequest;
|
||||
use okapi\ParamMissing;
|
||||
use okapi\InvalidParam;
|
||||
use okapi\BadRequest;
|
||||
use okapi\OkapiInternalRequest;
|
||||
use okapi\OkapiServiceRunner;
|
||||
use okapi\services\caches\search\SearchAssistant;
|
||||
|
||||
class WebService
|
||||
{
|
||||
public static function options()
|
||||
{
|
||||
return array(
|
||||
'min_auth_level' => 1
|
||||
);
|
||||
}
|
||||
|
||||
public static function call(OkapiRequest $request)
|
||||
{
|
||||
$internal_id = $request->get_parameter('internal_id');
|
||||
if (!$internal_id) throw new ParamMissing('internal_id');
|
||||
$fields = $request->get_parameter('fields');
|
||||
|
||||
# There's no need to validate the fields parameter.
|
||||
|
||||
$results = OkapiServiceRunner::call('services/users/by_internal_ids', new OkapiInternalRequest(
|
||||
$request->consumer, $request->token, array('internal_ids' => $internal_id,
|
||||
'fields' => $fields)));
|
||||
$result = $results[$internal_id];
|
||||
if ($result == null)
|
||||
throw new InvalidParam('internal_id', "There is no user by this internal_id.");
|
||||
return Okapi::formatted_response($request, $result);
|
||||
}
|
||||
}
|
18
htdocs/okapi/services/users/by_internal_id.xml
Normal file
18
htdocs/okapi/services/users/by_internal_id.xml
Normal file
@ -0,0 +1,18 @@
|
||||
<xml>
|
||||
<brief>Find single user, by his internal_id</brief>
|
||||
<issue-id>44</issue-id>
|
||||
<desc>
|
||||
<p>Retrieve information on a single user, referencing him by his <b>internal</b> ID.</p>
|
||||
<p>In general, you should not use this method. Reference your users by their
|
||||
normal ID - <b>user_uuid</b>. Also, internal IDs are not universally unique and are
|
||||
a poor choice for a key.</p>
|
||||
</desc>
|
||||
<req name='internal_id'>Internal ID</req>
|
||||
<req name='fields'>
|
||||
<p>See services/users/user method.</p>
|
||||
</req>
|
||||
<common-format-params/>
|
||||
<returns>
|
||||
<p>See services/users/user method.</p>
|
||||
</returns>
|
||||
</xml>
|
68
htdocs/okapi/services/users/by_internal_ids.php
Normal file
68
htdocs/okapi/services/users/by_internal_ids.php
Normal file
@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace okapi\services\users\by_internal_ids;
|
||||
|
||||
use okapi\Okapi;
|
||||
use okapi\Db;
|
||||
use okapi\OkapiInternalRequest;
|
||||
use okapi\OkapiServiceRunner;
|
||||
use okapi\OkapiRequest;
|
||||
use okapi\ParamMissing;
|
||||
use okapi\InvalidParam;
|
||||
use okapi\services\caches\search\SearchAssistant;
|
||||
|
||||
class WebService
|
||||
{
|
||||
public static function options()
|
||||
{
|
||||
return array(
|
||||
'min_auth_level' => 1
|
||||
);
|
||||
}
|
||||
|
||||
public static function call(OkapiRequest $request)
|
||||
{
|
||||
$internal_ids = $request->get_parameter('internal_ids');
|
||||
if (!$internal_ids) throw new ParamMissing('internal_ids');
|
||||
$internal_ids = explode("|", $internal_ids);
|
||||
if (count($internal_ids) > 500)
|
||||
throw new InvalidParam('internal_ids', "Maximum allowed number of referenced users ".
|
||||
"is 500. You provided ".count($internal_ids)." references.");
|
||||
$fields = $request->get_parameter('fields');
|
||||
if (!$fields)
|
||||
throw new ParamMissing('fields');
|
||||
|
||||
# There's no need to validate the fields parameter as the 'users'
|
||||
# method does this (it will raise a proper exception on invalid values).
|
||||
|
||||
$rs = Db::query("
|
||||
select user_id, uuid
|
||||
from user
|
||||
where user_id in ('".implode("','", array_map('mysql_real_escape_string', $internal_ids))."')
|
||||
");
|
||||
$internalid2useruuid = array();
|
||||
while ($row = mysql_fetch_assoc($rs))
|
||||
{
|
||||
$internalid2useruuid[$row['user_id']] = $row['uuid'];
|
||||
}
|
||||
mysql_free_result($rs);
|
||||
|
||||
# Retrieve data on given user_uuids.
|
||||
$id_results = OkapiServiceRunner::call('services/users/users', new OkapiInternalRequest(
|
||||
$request->consumer, $request->token, array('user_uuids' => implode("|", array_values($internalid2useruuid)),
|
||||
'fields' => $fields)));
|
||||
|
||||
# Map user_uuids to internal_ids. Also check which internal_ids were not found
|
||||
# and mark them with null.
|
||||
$results = array();
|
||||
foreach ($internal_ids as $internal_id)
|
||||
{
|
||||
if (!isset($internalid2useruuid[$internal_id]))
|
||||
$results[$internal_id] = null;
|
||||
else
|
||||
$results[$internal_id] = $id_results[$internalid2useruuid[$internal_id]];
|
||||
}
|
||||
|
||||
return Okapi::formatted_response($request, $results);
|
||||
}
|
||||
}
|
20
htdocs/okapi/services/users/by_internal_ids.xml
Normal file
20
htdocs/okapi/services/users/by_internal_ids.xml
Normal file
@ -0,0 +1,20 @@
|
||||
<xml>
|
||||
<brief>Find multiple users, by their internal IDs</brief>
|
||||
<issue-id>45</issue-id>
|
||||
<desc>
|
||||
<p>This method works like the services/users/by_internal_id method, but works
|
||||
with multiple users (instead of only one).</p>
|
||||
</desc>
|
||||
<req name='internal_ids'>
|
||||
<p>Pipe-separated list of internal user IDs. No more than 500 are allowed.</p>
|
||||
</req>
|
||||
<req name='fields'>
|
||||
<p>See services/users/user method.</p>
|
||||
</req>
|
||||
<common-format-params/>
|
||||
<returns>
|
||||
<p>A dictionary. Internal IDs you provide will be mapped to dictionary keys,
|
||||
and each value will be a dictionary of fields you have selected.</p>
|
||||
<p>Value of <b>null</b> means that the given internal ID haven't been found.</p>
|
||||
</returns>
|
||||
</xml>
|
42
htdocs/okapi/services/users/by_username.php
Normal file
42
htdocs/okapi/services/users/by_username.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace okapi\services\users\by_username;
|
||||
|
||||
use okapi\BadRequest;
|
||||
|
||||
use okapi\OkapiInternalRequest;
|
||||
|
||||
use okapi\OkapiServiceRunner;
|
||||
|
||||
use okapi\Okapi;
|
||||
use okapi\OkapiRequest;
|
||||
use okapi\ParamMissing;
|
||||
use okapi\InvalidParam;
|
||||
use okapi\services\caches\search\SearchAssistant;
|
||||
|
||||
class WebService
|
||||
{
|
||||
public static function options()
|
||||
{
|
||||
return array(
|
||||
'min_auth_level' => 1
|
||||
);
|
||||
}
|
||||
|
||||
public static function call(OkapiRequest $request)
|
||||
{
|
||||
$username = $request->get_parameter('username');
|
||||
if (!$username) throw new ParamMissing('username');
|
||||
$fields = $request->get_parameter('fields');
|
||||
|
||||
# There's no need to validate the fields parameter.
|
||||
|
||||
$results = OkapiServiceRunner::call('services/users/by_usernames', new OkapiInternalRequest(
|
||||
$request->consumer, $request->token, array('usernames' => $username,
|
||||
'fields' => $fields)));
|
||||
$result = $results[$username];
|
||||
if ($result == null)
|
||||
throw new InvalidParam('username', "There is no user by this username.");
|
||||
return Okapi::formatted_response($request, $result);
|
||||
}
|
||||
}
|
22
htdocs/okapi/services/users/by_username.xml
Normal file
22
htdocs/okapi/services/users/by_username.xml
Normal file
@ -0,0 +1,22 @@
|
||||
<xml>
|
||||
<brief>Find single user, by his/her username</brief>
|
||||
<issue-id>24</issue-id>
|
||||
<desc>
|
||||
<p>Retrieve information on a single user, referencing him by his username.</p>
|
||||
<p><b>IMPORTANT:</b> user IDs don't change, but usernames <b>may</b> change.</p>
|
||||
<p>Usually, you'll want to run this method only once, in order to get the IDs
|
||||
of referenced users. You should not store usernames in your database, it is
|
||||
much safer to reference them by their IDs.</p>
|
||||
</desc>
|
||||
<req name='username'>Name of the user.</req>
|
||||
<req name='fields'>
|
||||
<p>Same as in the services/users/user method. Pipe-separated list
|
||||
of field names which you are interested with.
|
||||
See services/users/user method for a list available values.</p>
|
||||
</req>
|
||||
<common-format-params/>
|
||||
<returns>
|
||||
<p>A dictionary of fields you have selected.</p>
|
||||
<p>If given user does not exist, the method will respond with a HTTP 400 error.</p>
|
||||
</returns>
|
||||
</xml>
|
68
htdocs/okapi/services/users/by_usernames.php
Normal file
68
htdocs/okapi/services/users/by_usernames.php
Normal file
@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace okapi\services\users\by_usernames;
|
||||
|
||||
use okapi\Okapi;
|
||||
use okapi\Db;
|
||||
use okapi\OkapiRequest;
|
||||
use okapi\ParamMissing;
|
||||
use okapi\InvalidParam;
|
||||
use okapi\services\caches\search\SearchAssistant;
|
||||
use okapi\OkapiServiceRunner;
|
||||
use okapi\OkapiInternalRequest;
|
||||
|
||||
class WebService
|
||||
{
|
||||
public static function options()
|
||||
{
|
||||
return array(
|
||||
'min_auth_level' => 1
|
||||
);
|
||||
}
|
||||
|
||||
public static function call(OkapiRequest $request)
|
||||
{
|
||||
$usernames = $request->get_parameter('usernames');
|
||||
if (!$usernames) throw new ParamMissing('usernames');
|
||||
$usernames = explode("|", $usernames);
|
||||
if (count($usernames) > 500)
|
||||
throw new InvalidParam('usernames', "Maximum allowed number of referenced users ".
|
||||
"is 500. You provided ".count($usernames)." usernames.");
|
||||
$fields = $request->get_parameter('fields');
|
||||
if (!$fields)
|
||||
throw new ParamMissing('fields');
|
||||
|
||||
# There's no need to validate the fields parameter as the 'users'
|
||||
# method does this (it will raise a proper exception on invalid values).
|
||||
|
||||
$rs = Db::query("
|
||||
select username, uuid
|
||||
from user
|
||||
where username in ('".implode("','", array_map('mysql_real_escape_string', $usernames))."')
|
||||
");
|
||||
$username2useruuid = array();
|
||||
while ($row = mysql_fetch_assoc($rs))
|
||||
{
|
||||
$username2useruuid[$row['username']] = $row['uuid'];
|
||||
}
|
||||
mysql_free_result($rs);
|
||||
|
||||
# Retrieve data on given user_uuids.
|
||||
$id_results = OkapiServiceRunner::call('services/users/users', new OkapiInternalRequest(
|
||||
$request->consumer, $request->token, array('user_uuids' => implode("|", array_values($username2useruuid)),
|
||||
'fields' => $fields)));
|
||||
|
||||
# Map user_uuids to usernames. Also check which usernames were not found
|
||||
# and mark them with null.
|
||||
$results = array();
|
||||
foreach ($usernames as $username)
|
||||
{
|
||||
if (!isset($username2useruuid[$username]))
|
||||
$results[$username] = null;
|
||||
else
|
||||
$results[$username] = $id_results[$username2useruuid[$username]];
|
||||
}
|
||||
|
||||
return Okapi::formatted_response($request, $results);
|
||||
}
|
||||
}
|
24
htdocs/okapi/services/users/by_usernames.xml
Normal file
24
htdocs/okapi/services/users/by_usernames.xml
Normal file
@ -0,0 +1,24 @@
|
||||
<xml>
|
||||
<brief>Find multiple users, by their usernames</brief>
|
||||
<issue-id>25</issue-id>
|
||||
<desc>
|
||||
<p>This method works like the services/users/by_username method, but works
|
||||
with multiple users (instead of only one).</p>
|
||||
</desc>
|
||||
<req name='usernames'>
|
||||
<p>Pipe-separated list of usernames. No more than 500 are allowed.</p>
|
||||
</req>
|
||||
<req name='fields'>
|
||||
<p>Same as in the services/users/user method. Pipe-separated list
|
||||
of field names which you are interested with.
|
||||
See services/users/user method for a list available values.</p>
|
||||
</req>
|
||||
<common-format-params/>
|
||||
<returns>
|
||||
<p>A dictionary. Usernames you provide will be mapped to dictionary keys,
|
||||
and each value will be a dictionary of fields you have selected.</p>
|
||||
<p>Value of <b>null</b> means that the given user haven't been found.
|
||||
(This behavior is different than in the services/users/by_username method, which
|
||||
responds with a HTTP 400 error in such case.)</p>
|
||||
</returns>
|
||||
</xml>
|
56
htdocs/okapi/services/users/user.php
Normal file
56
htdocs/okapi/services/users/user.php
Normal file
@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace okapi\services\users\user;
|
||||
|
||||
use okapi\BadRequest;
|
||||
|
||||
use okapi\OkapiInternalRequest;
|
||||
|
||||
use okapi\OkapiServiceRunner;
|
||||
|
||||
use okapi\Okapi;
|
||||
use okapi\OkapiRequest;
|
||||
use okapi\ParamMissing;
|
||||
use okapi\InvalidParam;
|
||||
use okapi\services\caches\search\SearchAssistant;
|
||||
|
||||
class WebService
|
||||
{
|
||||
public static function options()
|
||||
{
|
||||
return array(
|
||||
'min_auth_level' => 1
|
||||
);
|
||||
}
|
||||
|
||||
public static function call(OkapiRequest $request)
|
||||
{
|
||||
$user_uuid = $request->get_parameter('user_uuid');
|
||||
if (!$user_uuid)
|
||||
{
|
||||
if ($request->token)
|
||||
{
|
||||
$tmp = OkapiServiceRunner::call('services/users/by_internal_id', new OkapiInternalRequest(
|
||||
$request->consumer, null, array('internal_id' => $request->token->user_id, 'fields' => 'uuid')));
|
||||
$user_uuid = $tmp['uuid'];
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new BadRequest("You must either: 1. supply the user_uuid argument, or "
|
||||
."2. sign your request with an Access Token.");
|
||||
}
|
||||
}
|
||||
$fields = $request->get_parameter('fields');
|
||||
|
||||
# There's no need to validate the fields parameter as the 'users'
|
||||
# method does this (it will raise a proper exception on invalid values).
|
||||
|
||||
$results = OkapiServiceRunner::call('services/users/users', new OkapiInternalRequest(
|
||||
$request->consumer, $request->token, array('user_uuids' => $user_uuid,
|
||||
'fields' => $fields)));
|
||||
$result = $results[$user_uuid];
|
||||
if ($result == null)
|
||||
throw new InvalidParam('user_uuid', "There is no user by this ID.");
|
||||
return Okapi::formatted_response($request, $result);
|
||||
}
|
||||
}
|
43
htdocs/okapi/services/users/user.xml
Normal file
43
htdocs/okapi/services/users/user.xml
Normal file
@ -0,0 +1,43 @@
|
||||
<xml>
|
||||
<brief>Retrieve information on a single user</brief>
|
||||
<issue-id>26</issue-id>
|
||||
<desc>
|
||||
Retrieve information on a single user. This method might be also used
|
||||
to retrieve data of the user in who's name the Access Token have been
|
||||
issued. To do this, include the Access Token in your request, and <b>don't</b>
|
||||
include the user_uuid argument.
|
||||
</desc>
|
||||
<req name='fields'>
|
||||
<p>Pipe-separated list of field names which you are interested with.
|
||||
Selected fields will be included in the response.</p>
|
||||
<p>Currently available fields:</p>
|
||||
<ul>
|
||||
<li><b>uuid</b> - ID of the user,</li>
|
||||
<li><b>username</b> - username (login) of the user,</li>
|
||||
<li><b>profile_url</b> - URL of the user's OpenCaching profile page,</li>
|
||||
<li>
|
||||
<p><b>is_admin</b> - boolean; true is user has admin privileges.</p>
|
||||
<p>This value can be accessed only with <b>Level 3</b> Authentication
|
||||
and only for the user of your Access Token. For all other reads, is_admin
|
||||
will equal <b>null</b>.</p>
|
||||
</li>
|
||||
<li><b>internal_id</b> - internal ID of the user (<b>DO NOT</b> use this!
|
||||
use the <b>uuid</b> as the user identifier),</li>
|
||||
<li><b>caches_found</b> - number of "Found it" log entries,</li>
|
||||
<li><b>caches_notfound</b> - number of "Didn't find it" log entries,</li>
|
||||
<li><b>caches_hidden</b> - number of caches owned,</li>
|
||||
<li><b>rcmds_given</b> - number of recommendations given.</li>
|
||||
</ul>
|
||||
</req>
|
||||
<opt name='user_uuid'>
|
||||
<p>ID of the user.</p>
|
||||
<p>This parameter is optional only when you sign your
|
||||
request with an Access Token (Level 3 Authentication). Otherwise,
|
||||
it is <b>required</b>.</p>
|
||||
</opt>
|
||||
<common-format-params/>
|
||||
<returns>
|
||||
<p>A dictionary of fields you have selected.</p>
|
||||
<p>If given user does not exist, the method will respond with a HTTP 400 error.</p>
|
||||
</returns>
|
||||
</xml>
|
163
htdocs/okapi/services/users/users.php
Normal file
163
htdocs/okapi/services/users/users.php
Normal file
@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
namespace okapi\services\users\users;
|
||||
|
||||
use Exception;
|
||||
use okapi\Okapi;
|
||||
use okapi\Db;
|
||||
use okapi\OkapiRequest;
|
||||
use okapi\ParamMissing;
|
||||
use okapi\InvalidParam;
|
||||
use okapi\Settings;
|
||||
use okapi\services\caches\search\SearchAssistant;
|
||||
|
||||
class WebService
|
||||
{
|
||||
public static function options()
|
||||
{
|
||||
return array(
|
||||
'min_auth_level' => 1
|
||||
);
|
||||
}
|
||||
|
||||
private static $valid_field_names = array('uuid', 'username', 'profile_url', 'internal_id', 'is_admin',
|
||||
'caches_found', 'caches_notfound', 'caches_hidden', 'rcmds_given');
|
||||
|
||||
public static function call(OkapiRequest $request)
|
||||
{
|
||||
$user_uuids = $request->get_parameter('user_uuids');
|
||||
if (!$user_uuids) throw new ParamMissing('user_uuids');
|
||||
$user_uuids = explode("|", $user_uuids);
|
||||
if (count($user_uuids) > 500)
|
||||
throw new InvalidParam('user_uuids', "Maximum allowed number of referenced users ".
|
||||
"is 500. You provided ".count($user_uuids)." user IDs.");
|
||||
$fields = $request->get_parameter('fields');
|
||||
if (!$fields)
|
||||
throw new ParamMissing('fields');
|
||||
$fields = explode("|", $fields);
|
||||
foreach ($fields as $field)
|
||||
if (!in_array($field, self::$valid_field_names))
|
||||
throw new InvalidParam('fields', "'$field' is not a valid field code.");
|
||||
$rs = Db::query("
|
||||
select user_id, uuid, username, admin
|
||||
from user
|
||||
where uuid in ('".implode("','", array_map('mysql_real_escape_string', $user_uuids))."')
|
||||
");
|
||||
$results = array();
|
||||
$id2uuid = array();
|
||||
$uuid2id = array();
|
||||
while ($row = mysql_fetch_assoc($rs))
|
||||
{
|
||||
$id2uuid[$row['user_id']] = $row['uuid'];
|
||||
$uuid2id[$row['uuid']] = $row['user_id'];
|
||||
$entry = array();
|
||||
foreach ($fields as $field)
|
||||
{
|
||||
switch ($field)
|
||||
{
|
||||
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 'is_admin':
|
||||
if (!$request->token) {
|
||||
$entry['is_admin'] = null;
|
||||
} elseif ($request->token->user_id != $row['user_id']) {
|
||||
$entry['is_admin'] = null;
|
||||
} else {
|
||||
$entry['is_admin'] = $row['admin'] ? true : false;
|
||||
}
|
||||
break;
|
||||
case 'internal_id': $entry['internal_id'] = $row['user_id']; break;
|
||||
case 'caches_found': /* handled separately */ break;
|
||||
case 'caches_notfound': /* handled separately */ break;
|
||||
case 'caches_hidden': /* handled separately */ break;
|
||||
case 'rcmds_given': /* handled separately */ break;
|
||||
default: throw new Exception("Missing field case: ".$field);
|
||||
}
|
||||
}
|
||||
$results[$row['uuid']] = $entry;
|
||||
}
|
||||
mysql_free_result($rs);
|
||||
|
||||
# caches_found, caches_notfound, caches_hidden
|
||||
|
||||
if (in_array('caches_found', $fields) || in_array('caches_notfound', $fields) || in_array('caches_hidden', $fields)
|
||||
|| in_array('rcmds_given', $fields))
|
||||
{
|
||||
# We will load all these stats together. Then we may remove these which
|
||||
# the user doesn't need.
|
||||
|
||||
$extras = array();
|
||||
|
||||
if (Settings::get('OC_BRANCH') == 'oc.pl')
|
||||
{
|
||||
# OCPL stores user stats in 'user' table.
|
||||
|
||||
$rs = Db::query("
|
||||
select user_id, founds_count, notfounds_count, hidden_count
|
||||
from user
|
||||
where user_id in ('".implode("','", array_map('mysql_real_escape_string', array_keys($id2uuid)))."')
|
||||
");
|
||||
}
|
||||
else
|
||||
{
|
||||
# OCDE stores user stats in 'stat_user' table.
|
||||
|
||||
$rs = Db::query("
|
||||
select
|
||||
user_id,
|
||||
found as founds_count,
|
||||
notfound as notfounds_count,
|
||||
hidden as hidden_count
|
||||
from stat_user
|
||||
where user_id in ('".implode("','", array_map('mysql_real_escape_string', array_keys($id2uuid)))."')
|
||||
");
|
||||
}
|
||||
|
||||
while ($row = mysql_fetch_assoc($rs))
|
||||
{
|
||||
$extras[$row['user_id']] = array();;
|
||||
$extra_ref = &$extras[$row['user_id']];
|
||||
$extra_ref['caches_found'] = 0 + $row['founds_count'];
|
||||
$extra_ref['caches_notfound'] = 0 + $row['notfounds_count'];
|
||||
$extra_ref['caches_hidden'] = 0 + $row['hidden_count'];
|
||||
}
|
||||
mysql_free_result($rs);
|
||||
|
||||
if (in_array('rcmds_given', $fields))
|
||||
{
|
||||
$rs = Db::query("
|
||||
select user_id, count(*) as rcmds_given
|
||||
from cache_rating
|
||||
where user_id in ('".implode("','", array_map('mysql_real_escape_string', array_keys($id2uuid)))."')
|
||||
group by user_id
|
||||
");
|
||||
$rcmds_counts = array();
|
||||
while ($row = mysql_fetch_assoc($rs))
|
||||
$rcmds_counts[$row['user_id']] = $row['rcmds_given'];
|
||||
foreach ($extras as $user_id => &$extra_ref)
|
||||
{
|
||||
$extra_ref['rcmds_given'] = isset($rcmds_counts[$user_id]) ? 0 + $rcmds_counts[$user_id] : 0;
|
||||
}
|
||||
}
|
||||
|
||||
# "Apply" only those fields which the consumer wanted.
|
||||
|
||||
foreach (array('caches_found', 'caches_notfound', 'caches_hidden', 'rcmds_given') as $field)
|
||||
{
|
||||
if (!in_array($field, $fields))
|
||||
continue;
|
||||
foreach ($results as $uuid => &$result_ref)
|
||||
$result_ref[$field] = $extras[$uuid2id[$uuid]][$field];
|
||||
}
|
||||
}
|
||||
|
||||
# Check which user IDs were not found and mark them with null.
|
||||
|
||||
foreach ($user_uuids as $user_uuid)
|
||||
if (!isset($results[$user_uuid]))
|
||||
$results[$user_uuid] = null;
|
||||
|
||||
return Okapi::formatted_response($request, $results);
|
||||
}
|
||||
}
|
24
htdocs/okapi/services/users/users.xml
Normal file
24
htdocs/okapi/services/users/users.xml
Normal file
@ -0,0 +1,24 @@
|
||||
<xml>
|
||||
<brief>Retrieve information on multiple users</brief>
|
||||
<issue-id>27</issue-id>
|
||||
<desc>
|
||||
<p>This method works like the services/users/user method, but works
|
||||
with multiple users (instead of only one).</p>
|
||||
</desc>
|
||||
<req name='user_uuids'>
|
||||
<p>Pipe-separated list of user IDs. No more than 500 IDs are allowed.</p>
|
||||
</req>
|
||||
<req name='fields'>
|
||||
<p>Same as in the services/users/user method. Pipe-separated list
|
||||
of field names which you are interested with.
|
||||
See services/users/user method for a list available values.</p>
|
||||
</req>
|
||||
<common-format-params/>
|
||||
<returns>
|
||||
<p>A dictionary. User IDs you provide will be mapped to dictionary keys,
|
||||
and each value will be a dictionary of fields you have selected.</p>
|
||||
<p>Value of <b>null</b> means that the given user haven't been found.
|
||||
(This behavior is different than in the services/users/user method, which
|
||||
responds with a HTTP 400 error in such case.)</p>
|
||||
</returns>
|
||||
</xml>
|
194
htdocs/okapi/settings.php
Normal file
194
htdocs/okapi/settings.php
Normal file
@ -0,0 +1,194 @@
|
||||
<?
|
||||
|
||||
namespace okapi;
|
||||
|
||||
use Exception;
|
||||
use okapi\Locales;
|
||||
|
||||
# DO NOT MODIFY THIS FILE. This file should always look like the original here:
|
||||
# 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');
|
||||
#
|
||||
# This file provides documentation and DEFAULT values for those settings.
|
||||
#
|
||||
# Please note: These settings WILL mutate. Some of them might get deprecated,
|
||||
# others might change their meaning and/or possible values.
|
||||
|
||||
final class Settings
|
||||
{
|
||||
/** Default values for setting keys. */
|
||||
private static $DEFAULT_SETTINGS = array(
|
||||
|
||||
/**
|
||||
* Currently there are two mainstream branches of OpenCaching code.
|
||||
* Which branch is you installation using?
|
||||
*
|
||||
* Possible values: "oc.pl" or "oc.de". (As far as we know, oc.us and
|
||||
* oc.org.uk use "oc.pl" branch, the rest uses "oc.de" branch.)
|
||||
*/
|
||||
'OC_BRANCH' => "oc.pl",
|
||||
|
||||
/**
|
||||
* Each OpenCaching site has a default language. I.e. the language in
|
||||
* which all the names of caches are entered. What is the ISO 639-1 code
|
||||
* of this language? Note: ISO 639-1 codes are always lowercase.
|
||||
*
|
||||
* E.g. "pl", "en", "de".
|
||||
*/
|
||||
'SITELANG' => "en",
|
||||
|
||||
/**
|
||||
* All OKAPI documentation pages should remain English-only, but some
|
||||
* other pages (and results) might be translated to their localized
|
||||
* versions. We try to catch up to all OKAPI instances and
|
||||
* fill our default translation tables with all the languages of all
|
||||
* OKAPI installations. But we also give you an option to use your own
|
||||
* translation table if you really need to. Use this variable to pass your
|
||||
* own gettext initialization function/method. See default_gettext_init
|
||||
* function below for details.
|
||||
*/
|
||||
'GETTEXT_INIT' => array('\okapi\Settings', 'default_gettext_init'),
|
||||
|
||||
/**
|
||||
* By default, OKAPI uses "okapi_messages" domain file for translations.
|
||||
* Use this variable when you want it to use your own domain.
|
||||
*/
|
||||
'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
|
||||
* if you also move all the files which were inside.
|
||||
*/
|
||||
'VAR_DIR' => 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"
|
||||
* log entries, leave it at false.
|
||||
*/
|
||||
'SUPPORTS_LOGTYPE_NEEDS_MAINTENANCE' => false,
|
||||
|
||||
/**
|
||||
* Set to true, to prevent OKAPI from sending email messages. ALLOWED ONLY ON
|
||||
* DEVELOPMENT ENVIRONMENT! Sending emails is vital for OKAPI administration and
|
||||
* usage! (I.e. users need this to receive their tokens upon registration.)
|
||||
*/
|
||||
'DEBUG_PREVENT_EMAILS' => false,
|
||||
|
||||
/**
|
||||
* Set to true, to prevent OKAPI from using sem_get family of functions.
|
||||
* ALLOWED ONLY ON DEVELOPMENT ENVIRONMENT! Semaphores are vital for OKAPI's
|
||||
* performance and data integrity!
|
||||
*/
|
||||
'DEBUG_PREVENT_SEMAPHORES' => false,
|
||||
);
|
||||
|
||||
/**
|
||||
* Final values for settings keys (defaults + local overrides).
|
||||
* (Loaded upon first access.)
|
||||
*/
|
||||
private static $SETTINGS = null;
|
||||
|
||||
/**
|
||||
* Initialize self::$SETTINGS.
|
||||
*/
|
||||
private static function load_settings()
|
||||
{
|
||||
# Check the settings.inc.php for overrides.
|
||||
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Throw an exception, if given $value is invalid for the given $key. */
|
||||
private static function verify($key, $value)
|
||||
{
|
||||
$debug = (isset($GLOBALS['debug_page']) && $GLOBALS['debug_page']);
|
||||
if (($key == 'OC_BRANCH') && (!in_array($value, 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.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value for the $key setting.
|
||||
*/
|
||||
public static function get($key)
|
||||
{
|
||||
if (self::$SETTINGS == null)
|
||||
self::load_settings();
|
||||
|
||||
if (!array_key_exists($key, self::$SETTINGS))
|
||||
throw new Exception("Tried to access an invalid settings key: '$key'");
|
||||
|
||||
return self::$SETTINGS[$key];
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind "okapi_messages" with our local i18n database. Set proper locale
|
||||
* based on the language codes passed and return the locale code.
|
||||
* $langprefs is a list of language codes in order of preference.
|
||||
*
|
||||
* Please note, that OKAPI consumers may ask OKAPI to return contents
|
||||
* in a specified language. (For example, consumers from Germany may ask
|
||||
* Polish OKAPI server to return GPX file in German.) If you insist on using
|
||||
* your own translation tables, you should still fallback to the default
|
||||
* OKAPI translations table in case of other languages!
|
||||
*/
|
||||
public static function default_gettext_init($langprefs)
|
||||
{
|
||||
require_once 'locale/locales.php';
|
||||
$locale = Locales::get_best_locale($langprefs);
|
||||
putenv("LC_ALL=$locale");
|
||||
setlocale(LC_ALL, $locale);
|
||||
setlocale(LC_NUMERIC, "POSIX"); # We don't want *this one* to get out of control.
|
||||
bindtextdomain("okapi_messages", $GLOBALS['rootpath'].'okapi/locale');
|
||||
return $locale;
|
||||
}
|
||||
|
||||
public static function describe_settings()
|
||||
{
|
||||
return print_r(self::$SETTINGS, true);
|
||||
}
|
||||
}
|
BIN
htdocs/okapi/static/bottom.gif
Normal file
BIN
htdocs/okapi/static/bottom.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
71
htdocs/okapi/static/common.css
Normal file
71
htdocs/okapi/static/common.css
Normal file
@ -0,0 +1,71 @@
|
||||
* { padding: 0; margin: 0; border: 0; }
|
||||
html { overflow-y: scroll; }
|
||||
html, body { height: 100%; font-family: "lucida grande", "Segoe UI", tahoma, arial, sans-serif; color: #444; }
|
||||
input, select { font-family: "lucida grande", "Segoe UI", tahoma, arial, sans-serif; color: #444; }
|
||||
a { cursor: pointer; }
|
||||
span.note { color: #888; font-size: 70%; font-weight: normal; }
|
||||
|
||||
body.api { background: #f5f1eb; }
|
||||
.okd_mid { width: 1025px; margin: 0 auto; background: url(middle.gif) #f5f1eb; background-position: top center; background-repeat: repeat-y; }
|
||||
.okd_top { width: 1025px; margin: 0 auto; background: url(top.jpg); background-position: top center; background-repeat: no-repeat; }
|
||||
.okd_top_pad { height: 230px; width: 1025px; }
|
||||
.okd_bottom { width: 1025px; height: 64px; margin: 0 auto; background: url(bottom.gif); background-position: top center; background-repeat: no-repeat; }
|
||||
.okd_switcher { text-align: right; padding: 30px 30px 0 0; font-size: 11px; color: #888; }
|
||||
.okd_switcher select { border: 1px solid #bbb; padding: 2px; font-size: 12px; }
|
||||
|
||||
.signup { border: 1px solid #bbb; }
|
||||
.signup td { background: #eee9e9; padding: 8px 16px; }
|
||||
.signup input.text { border: 1px solid #aaa; padding: 3px 4px 2px 4px; width: 300px }
|
||||
.signup .button { margin-top: 10px; padding: 3px 12px; background: #977; border: 2px solid #744; cursor: pointer; color: #fff; font-weight: bold; }
|
||||
|
||||
.apimenu { vertical-align: top; width: 225px; font-size: 15px; padding: 0px 20px 0 32px; }
|
||||
.apimenu .revision { float: right; font-size: 11px; color: #888; position: relative; top: -30px; }
|
||||
.apimenu a { color: #804937; text-decoration: none; }
|
||||
.apimenu a:hover { text-decoration: underline; }
|
||||
.apimenu .module { margin: 10px 0 2px 0; font-weight: bold; }
|
||||
.apimenu .methods { padding-left: 20px; }
|
||||
.apimenu .selected { font-weight: bold; }
|
||||
|
||||
.article { padding: 10px 48px 40px 31px; vertical-align: top; font-size: 15px; line-height: 1.4em; }
|
||||
.article a { color: #3e48a8; text-decoration: underline; }
|
||||
.article h1 { padding: 0 0 30px 0; font-weight: bold; font-style: italic; font-size: 22px; }
|
||||
.article h1 .subh1 { font-size: 18px; font-weight: normal; color: #888; }
|
||||
.article h1 .subh1 b { font-weight: normal; color: #944; }
|
||||
.article h2 { clear: both; margin: 50px 0 10px 0; padding-top: 20px; font-weight: bold; font-style: italic; font-size: 22px; border-top: 2px dotted #aaf; }
|
||||
.article p { margin-bottom: 15px; }
|
||||
.article pre { margin-bottom: 15px; }
|
||||
.article ul { margin-bottom: 15px; margin-left: 35px; }
|
||||
.article ol { margin-bottom: 15px; margin-left: 35px; }
|
||||
.article li { margin: 6px 0; }
|
||||
.article li p { margin: 6px 0; }
|
||||
.article .issue-comments { font-size: 20px; font-weight: bold; }
|
||||
.article .issue-comments .notice { font-size: 11px; font-weight: normal; color: #888; padding-left: 10px; }
|
||||
.article .deprecated { color: #888; }
|
||||
.article .deprecated a { color: #636898; }
|
||||
.method { background: #ccc; margin-bottom: 20px; }
|
||||
.method td { background: #fff; padding: 5px 8px 10px 8px; vertical-align: baseline; }
|
||||
.method .public td { background: #fdf9f5; }
|
||||
.method .public td.argname { border-left: 5px solid #edd; }
|
||||
.method .inherited td { background: #ffffd8; }
|
||||
.method .inherited td.argname { border-left: 5px solid #eeb; }
|
||||
.method .common-formatting td { background: #f5f5ff; }
|
||||
.method .common-formatting td.argname { border-left: 5px solid #ddf; }
|
||||
.method td.argname { font-weight: bold; text-align: center; font-style: italic; font-size: 16px; padding: 5px 20px; }
|
||||
.method td.required { font-weight: bold; text-align: center; padding: 5px 20px; }
|
||||
.method td.optional { font-style: italic; text-align: center; padding: 5px 20px; }
|
||||
.method td.argdesc { }
|
||||
.method td.argdesc p:last-child { margin-bottom: 0; }
|
||||
.method td.oauth_args { background: #fdf9f5; font-size: 12px; color: #666; line-height: 1.2em; }
|
||||
.method td.caption { text-align: left; padding: 10px; background: #eae2d5; }
|
||||
.method td.description { text-align: left; padding: 10px; background: #fff; }
|
||||
.method td.description p:last-child { margin-bottom: 0; }
|
||||
.method td.returns { text-align: left; padding: 10px; background: #fff; }
|
||||
.method td.returns p:last-child { margin-bottom: 0; }
|
||||
.method td.precaption { padding: 4px 2px; background: #fdf2e2; }
|
||||
.method td.precaption table { margin: 0 auto 0 0; }
|
||||
.method td.precaption table td { background: transparent; padding: 0 5px; font-size: 12px; color: #777; line-height: 1.1em; }
|
||||
.method td.precaption .level { font-weight: bold; }
|
||||
.method td.precaption .level0 { color: #333; }
|
||||
.method td.precaption .level1 { color: #333; }
|
||||
.method td.precaption .level2 { color: #c44; }
|
||||
.method td.precaption .level3 { color: #c44; }
|
30
htdocs/okapi/static/common.js
Normal file
30
htdocs/okapi/static/common.js
Normal file
@ -0,0 +1,30 @@
|
||||
$(function() {
|
||||
$('.issue-comments').each(function() {
|
||||
var div = $(this);
|
||||
var issue_id = div.attr('issue_id');
|
||||
$.ajax({
|
||||
type: 'GET',
|
||||
dataType: 'json',
|
||||
url: okapi_base_url + 'services/apiref/issue',
|
||||
data: {
|
||||
'issue_id': issue_id
|
||||
},
|
||||
success: function(issue)
|
||||
{
|
||||
var comments = (issue.comment_count == 1) ? "comment" : "comments";
|
||||
var link = $("<a>" + issue.comment_count + " " + comments + "</a>");
|
||||
link.attr('href', issue.url);
|
||||
div.append(link);
|
||||
var notice = $("<span class='notice'>Notice: comments are shared between all OKAPI installations.</span>");
|
||||
div.append(notice);
|
||||
}
|
||||
});
|
||||
});
|
||||
$('#switcher').change(function() {
|
||||
var current_base_url = $('#switcher option[current]').attr('value');
|
||||
var new_base_url = $('#switcher option:selected').attr('value');
|
||||
if (current_base_url != new_base_url)
|
||||
window.location.href = window.location.href.replace(current_base_url, new_base_url);
|
||||
});
|
||||
$('#switcher option[current]').attr('selected', true);
|
||||
});
|
83
htdocs/okapi/static/examples/javascript_nearest.html
Normal file
83
htdocs/okapi/static/examples/javascript_nearest.html
Normal file
@ -0,0 +1,83 @@
|
||||
<!doctype html>
|
||||
<html lang='en'>
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
|
||||
<title>OKAPI JavaScript Example</title>
|
||||
<script src='https://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js'></script>
|
||||
<script>
|
||||
$(function() {
|
||||
|
||||
// Fill the OKAPI installations select-box with dynamic installation list.
|
||||
|
||||
$.ajax({
|
||||
url: 'http://opencaching.pl/okapi/services/apisrv/installations',
|
||||
dataType: 'json',
|
||||
success: function(installations) {
|
||||
for (var i in installations) {
|
||||
var inst = installations[i];
|
||||
$('#installations').append(
|
||||
$("<option></option>")
|
||||
.attr('value', inst.okapi_base_url)
|
||||
.text(inst.okapi_base_url)
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$('#get_nearest').click(function() {
|
||||
$('#get_nearest').hide();
|
||||
$('#results').text("Attempting to access your location...");
|
||||
if (navigator.geolocation)
|
||||
{
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
function(position) {
|
||||
$('#results').text("Location acquired! Search for geocaches...");
|
||||
$.ajax({
|
||||
url: $('#installations option:selected').attr('value') + 'services/caches/shortcuts/search_and_retrieve',
|
||||
dataType: 'json',
|
||||
data: {
|
||||
'search_method': 'services/caches/search/nearest',
|
||||
'search_params': '{"center": "' + position.coords.latitude + "|" +
|
||||
position.coords.longitude + '", "limit": 5}',
|
||||
'retr_method': 'services/caches/geocaches',
|
||||
'retr_params': '{"fields": "code|name|url"}',
|
||||
'wrap': 'false',
|
||||
'consumer_key': $('#consumer_key').attr('value')
|
||||
},
|
||||
success: function(response) {
|
||||
var ul = $("<ul></ul>");
|
||||
for (var cache_code in response) {
|
||||
var cache = response[cache_code];
|
||||
var li = $("<li><a></a></li>");
|
||||
li.find('a').attr('href', cache.url).text(cache.name);
|
||||
ul.append(li);
|
||||
}
|
||||
$('#results').html("<p>Nearest geocaches:</p>");
|
||||
$('#results').append(ul);
|
||||
},
|
||||
error: function() {
|
||||
$('#results').text("Error :( Have you entered a valid Consumer Key?");
|
||||
}
|
||||
});
|
||||
},
|
||||
function (error) {
|
||||
$('#results').text("Your browser refused to give me your location.");
|
||||
}
|
||||
);
|
||||
} else {
|
||||
$('#results').text("Your browser does not support geolocation.");
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<style>
|
||||
a { color: #008; text-decoration: underline; cursor: pointer; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<p>Select OKAPI installation: <select id='installations'></select></p>
|
||||
<p>Enter your Consumer Key: <input type='text' id='consumer_key'></p>
|
||||
<p><a id='get_nearest'>Click here to view the nearest caches</a></p>
|
||||
<div id='results'></div>
|
||||
</body>
|
||||
</html>
|
BIN
htdocs/okapi/static/logo-xsmall.gif
Normal file
BIN
htdocs/okapi/static/logo-xsmall.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.4 KiB |
BIN
htdocs/okapi/static/middle.gif
Normal file
BIN
htdocs/okapi/static/middle.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.0 KiB |
BIN
htdocs/okapi/static/oc_logo.png
Normal file
BIN
htdocs/okapi/static/oc_logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.9 KiB |
185
htdocs/okapi/static/syntax_highlighter/SyntaxHighlighter.css
Normal file
185
htdocs/okapi/static/syntax_highlighter/SyntaxHighlighter.css
Normal file
@ -0,0 +1,185 @@
|
||||
.dp-highlighter
|
||||
{
|
||||
font-family: "Consolas", "Courier New", Courier, mono, serif;
|
||||
font-size: 12px;
|
||||
background-color: #E7E5DC;
|
||||
width: 99%;
|
||||
overflow: auto;
|
||||
margin: 18px 0 18px 0 !important;
|
||||
padding-top: 1px; /* adds a little border on top when controls are hidden */
|
||||
}
|
||||
|
||||
/* clear styles */
|
||||
.dp-highlighter ol,
|
||||
.dp-highlighter ol li,
|
||||
.dp-highlighter ol li span
|
||||
{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.dp-highlighter a,
|
||||
.dp-highlighter a:hover
|
||||
{
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dp-highlighter .bar
|
||||
{
|
||||
padding-left: 45px;
|
||||
}
|
||||
|
||||
.dp-highlighter.collapsed .bar,
|
||||
.dp-highlighter.nogutter .bar
|
||||
{
|
||||
padding-left: 0px;
|
||||
}
|
||||
|
||||
.dp-highlighter ol
|
||||
{
|
||||
list-style: decimal; /* for ie */
|
||||
background-color: #fff;
|
||||
margin: 0px 0px 1px 45px !important; /* 1px bottom margin seems to fix occasional Firefox scrolling */
|
||||
padding: 0px;
|
||||
color: #5C5C5C;
|
||||
}
|
||||
|
||||
.dp-highlighter.nogutter ol,
|
||||
.dp-highlighter.nogutter ol li
|
||||
{
|
||||
list-style: none !important;
|
||||
margin-left: 0px !important;
|
||||
}
|
||||
|
||||
.dp-highlighter ol li,
|
||||
.dp-highlighter .columns div
|
||||
{
|
||||
list-style: decimal-leading-zero; /* better look for others, override cascade from OL */
|
||||
list-style-position: outside !important;
|
||||
border-left: 3px solid #6CE26C;
|
||||
background-color: #F8F8F8;
|
||||
color: #5C5C5C;
|
||||
padding: 0 3px 0 10px !important;
|
||||
margin: 0 !important;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
.dp-highlighter.nogutter ol li,
|
||||
.dp-highlighter.nogutter .columns div
|
||||
{
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.dp-highlighter .columns
|
||||
{
|
||||
background-color: #F8F8F8;
|
||||
color: gray;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dp-highlighter .columns div
|
||||
{
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.dp-highlighter ol li.alt
|
||||
{
|
||||
/*background-color: #FFF;*/
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.dp-highlighter ol li span
|
||||
{
|
||||
color: black;
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
/* Adjust some properties when collapsed */
|
||||
|
||||
.dp-highlighter.collapsed ol
|
||||
{
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.dp-highlighter.collapsed ol li
|
||||
{
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Additional modifications when in print-view */
|
||||
|
||||
.dp-highlighter.printing
|
||||
{
|
||||
border: none;
|
||||
}
|
||||
|
||||
.dp-highlighter.printing .tools
|
||||
{
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.dp-highlighter.printing li
|
||||
{
|
||||
display: list-item !important;
|
||||
}
|
||||
|
||||
/* Styles for the tools */
|
||||
|
||||
.dp-highlighter .tools
|
||||
{
|
||||
padding: 3px 8px 3px 10px;
|
||||
font: 9px Verdana, Geneva, Arial, Helvetica, sans-serif;
|
||||
color: silver;
|
||||
background-color: #f8f8f8;
|
||||
padding-bottom: 10px;
|
||||
border-left: 3px solid #6CE26C;
|
||||
}
|
||||
|
||||
.dp-highlighter.nogutter .tools
|
||||
{
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.dp-highlighter.collapsed .tools
|
||||
{
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.dp-highlighter .tools a
|
||||
{
|
||||
font-size: 9px;
|
||||
color: #a0a0a0;
|
||||
background-color: inherit;
|
||||
text-decoration: none;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.dp-highlighter .tools a:hover
|
||||
{
|
||||
color: red;
|
||||
background-color: inherit;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* About dialog styles */
|
||||
|
||||
.dp-about { background-color: #fff; color: #333; margin: 0px; padding: 0px; }
|
||||
.dp-about table { width: 100%; height: 100%; font-size: 11px; font-family: Tahoma, Verdana, Arial, sans-serif !important; }
|
||||
.dp-about td { padding: 10px; vertical-align: top; }
|
||||
.dp-about .copy { border-bottom: 1px solid #ACA899; height: 95%; }
|
||||
.dp-about .title { color: red; background-color: inherit; font-weight: bold; }
|
||||
.dp-about .para { margin: 0 0 4px 0; }
|
||||
.dp-about .footer { background-color: #ECEADB; color: #333; border-top: 1px solid #fff; text-align: right; }
|
||||
.dp-about .close { font-size: 11px; font-family: Tahoma, Verdana, Arial, sans-serif !important; background-color: #ECEADB; color: #333; width: 60px; height: 22px; }
|
||||
|
||||
/* Language specific styles */
|
||||
|
||||
.dp-highlighter .comment, .dp-highlighter .comments { color: #008200; background-color: inherit; }
|
||||
.dp-highlighter .string { color: blue; background-color: inherit; }
|
||||
.dp-highlighter .keyword { color: #069; font-weight: bold; background-color: inherit; }
|
||||
.dp-highlighter .preprocessor { color: gray; background-color: inherit; }
|
BIN
htdocs/okapi/static/syntax_highlighter/clipboard.swf
Normal file
BIN
htdocs/okapi/static/syntax_highlighter/clipboard.swf
Normal file
Binary file not shown.
10
htdocs/okapi/static/syntax_highlighter/shBrushCSharp.js
vendored
Normal file
10
htdocs/okapi/static/syntax_highlighter/shBrushCSharp.js
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
/*
|
||||
* JsMin
|
||||
* Javascript Compressor
|
||||
* http://www.crockford.com/
|
||||
* http://www.smallsharptools.com/
|
||||
*/
|
||||
|
||||
dp.sh.Brushes.CSharp=function()
|
||||
{var keywords='abstract as base bool break byte case catch char checked class const '+'continue decimal default delegate do double else enum event explicit '+'extern false finally fixed float for foreach get goto if implicit in int '+'interface internal is lock long namespace new null object operator out '+'override params private protected public readonly ref return sbyte sealed set '+'short sizeof stackalloc static string struct switch this throw true try '+'typeof uint ulong unchecked unsafe ushort using virtual void while';this.regexList=[{regex:dp.sh.RegexLib.SingleLineCComments,css:'comment'},{regex:dp.sh.RegexLib.MultiLineCComments,css:'comment'},{regex:dp.sh.RegexLib.DoubleQuotedString,css:'string'},{regex:dp.sh.RegexLib.SingleQuotedString,css:'string'},{regex:new RegExp('^\\s*#.*','gm'),css:'preprocessor'},{regex:new RegExp(this.GetKeywords(keywords),'gm'),css:'keyword'}];this.CssClass='dp-c';this.Style='.dp-c .vars { color: #d00; }';}
|
||||
dp.sh.Brushes.CSharp.prototype=new dp.sh.Highlighter();dp.sh.Brushes.CSharp.Aliases=['c#','c-sharp','csharp'];
|
10
htdocs/okapi/static/syntax_highlighter/shBrushCpp.js
vendored
Normal file
10
htdocs/okapi/static/syntax_highlighter/shBrushCpp.js
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
/*
|
||||
* JsMin
|
||||
* Javascript Compressor
|
||||
* http://www.crockford.com/
|
||||
* http://www.smallsharptools.com/
|
||||
*/
|
||||
|
||||
dp.sh.Brushes.Cpp=function()
|
||||
{var datatypes='ATOM BOOL BOOLEAN BYTE CHAR COLORREF DWORD DWORDLONG DWORD_PTR '+'DWORD32 DWORD64 FLOAT HACCEL HALF_PTR HANDLE HBITMAP HBRUSH '+'HCOLORSPACE HCONV HCONVLIST HCURSOR HDC HDDEDATA HDESK HDROP HDWP '+'HENHMETAFILE HFILE HFONT HGDIOBJ HGLOBAL HHOOK HICON HINSTANCE HKEY '+'HKL HLOCAL HMENU HMETAFILE HMODULE HMONITOR HPALETTE HPEN HRESULT '+'HRGN HRSRC HSZ HWINSTA HWND INT INT_PTR INT32 INT64 LANGID LCID LCTYPE '+'LGRPID LONG LONGLONG LONG_PTR LONG32 LONG64 LPARAM LPBOOL LPBYTE LPCOLORREF '+'LPCSTR LPCTSTR LPCVOID LPCWSTR LPDWORD LPHANDLE LPINT LPLONG LPSTR LPTSTR '+'LPVOID LPWORD LPWSTR LRESULT PBOOL PBOOLEAN PBYTE PCHAR PCSTR PCTSTR PCWSTR '+'PDWORDLONG PDWORD_PTR PDWORD32 PDWORD64 PFLOAT PHALF_PTR PHANDLE PHKEY PINT '+'PINT_PTR PINT32 PINT64 PLCID PLONG PLONGLONG PLONG_PTR PLONG32 PLONG64 POINTER_32 '+'POINTER_64 PSHORT PSIZE_T PSSIZE_T PSTR PTBYTE PTCHAR PTSTR PUCHAR PUHALF_PTR '+'PUINT PUINT_PTR PUINT32 PUINT64 PULONG PULONGLONG PULONG_PTR PULONG32 PULONG64 '+'PUSHORT PVOID PWCHAR PWORD PWSTR SC_HANDLE SC_LOCK SERVICE_STATUS_HANDLE SHORT '+'SIZE_T SSIZE_T TBYTE TCHAR UCHAR UHALF_PTR UINT UINT_PTR UINT32 UINT64 ULONG '+'ULONGLONG ULONG_PTR ULONG32 ULONG64 USHORT USN VOID WCHAR WORD WPARAM WPARAM WPARAM '+'char bool short int __int32 __int64 __int8 __int16 long float double __wchar_t '+'clock_t _complex _dev_t _diskfree_t div_t ldiv_t _exception _EXCEPTION_POINTERS '+'FILE _finddata_t _finddatai64_t _wfinddata_t _wfinddatai64_t __finddata64_t '+'__wfinddata64_t _FPIEEE_RECORD fpos_t _HEAPINFO _HFILE lconv intptr_t '+'jmp_buf mbstate_t _off_t _onexit_t _PNH ptrdiff_t _purecall_handler '+'sig_atomic_t size_t _stat __stat64 _stati64 terminate_function '+'time_t __time64_t _timeb __timeb64 tm uintptr_t _utimbuf '+'va_list wchar_t wctrans_t wctype_t wint_t signed';var keywords='break case catch class const __finally __exception __try '+'const_cast continue private public protected __declspec '+'default delete deprecated dllexport dllimport do dynamic_cast '+'else enum explicit extern if for friend goto inline '+'mutable naked namespace new noinline noreturn nothrow '+'register reinterpret_cast return selectany '+'sizeof static static_cast struct switch template this '+'thread throw true false try typedef typeid typename union '+'using uuid virtual void volatile whcar_t while';this.regexList=[{regex:dp.sh.RegexLib.SingleLineCComments,css:'comment'},{regex:dp.sh.RegexLib.MultiLineCComments,css:'comment'},{regex:dp.sh.RegexLib.DoubleQuotedString,css:'string'},{regex:dp.sh.RegexLib.SingleQuotedString,css:'string'},{regex:new RegExp('^ *#.*','gm'),css:'preprocessor'},{regex:new RegExp(this.GetKeywords(datatypes),'gm'),css:'datatypes'},{regex:new RegExp(this.GetKeywords(keywords),'gm'),css:'keyword'}];this.CssClass='dp-cpp';this.Style='.dp-cpp .datatypes { color: #2E8B57; font-weight: bold; }';}
|
||||
dp.sh.Brushes.Cpp.prototype=new dp.sh.Highlighter();dp.sh.Brushes.Cpp.Aliases=['cpp','c','c++'];
|
14
htdocs/okapi/static/syntax_highlighter/shBrushCss.js
vendored
Normal file
14
htdocs/okapi/static/syntax_highlighter/shBrushCss.js
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
/*
|
||||
* JsMin
|
||||
* Javascript Compressor
|
||||
* http://www.crockford.com/
|
||||
* http://www.smallsharptools.com/
|
||||
*/
|
||||
|
||||
dp.sh.Brushes.CSS=function()
|
||||
{var keywords='ascent azimuth background-attachment background-color background-image background-position '+'background-repeat background baseline bbox border-collapse border-color border-spacing border-style border-top '+'border-right border-bottom border-left border-top-color border-right-color border-bottom-color border-left-color '+'border-top-style border-right-style border-bottom-style border-left-style border-top-width border-right-width '+'border-bottom-width border-left-width border-width border cap-height caption-side centerline clear clip color '+'content counter-increment counter-reset cue-after cue-before cue cursor definition-src descent direction display '+'elevation empty-cells float font-size-adjust font-family font-size font-stretch font-style font-variant font-weight font '+'height letter-spacing line-height list-style-image list-style-position list-style-type list-style margin-top '+'margin-right margin-bottom margin-left margin marker-offset marks mathline max-height max-width min-height min-width orphans '+'outline-color outline-style outline-width outline overflow padding-top padding-right padding-bottom padding-left padding page '+'page-break-after page-break-before page-break-inside pause pause-after pause-before pitch pitch-range play-during position '+'quotes richness size slope src speak-header speak-numeral speak-punctuation speak speech-rate stemh stemv stress '+'table-layout text-align text-decoration text-indent text-shadow text-transform unicode-bidi unicode-range units-per-em '+'vertical-align visibility voice-family volume white-space widows width widths word-spacing x-height z-index';var values='above absolute all always aqua armenian attr aural auto avoid baseline behind below bidi-override black blink block blue bold bolder '+'both bottom braille capitalize caption center center-left center-right circle close-quote code collapse compact condensed '+'continuous counter counters crop cross crosshair cursive dashed decimal decimal-leading-zero default digits disc dotted double '+'embed embossed e-resize expanded extra-condensed extra-expanded fantasy far-left far-right fast faster fixed format fuchsia '+'gray green groove handheld hebrew help hidden hide high higher icon inline-table inline inset inside invert italic '+'justify landscape large larger left-side left leftwards level lighter lime line-through list-item local loud lower-alpha '+'lowercase lower-greek lower-latin lower-roman lower low ltr marker maroon medium message-box middle mix move narrower '+'navy ne-resize no-close-quote none no-open-quote no-repeat normal nowrap n-resize nw-resize oblique olive once open-quote outset '+'outside overline pointer portrait pre print projection purple red relative repeat repeat-x repeat-y rgb ridge right right-side '+'rightwards rtl run-in screen scroll semi-condensed semi-expanded separate se-resize show silent silver slower slow '+'small small-caps small-caption smaller soft solid speech spell-out square s-resize static status-bar sub super sw-resize '+'table-caption table-cell table-column table-column-group table-footer-group table-header-group table-row table-row-group teal '+'text-bottom text-top thick thin top transparent tty tv ultra-condensed ultra-expanded underline upper-alpha uppercase upper-latin '+'upper-roman url visible wait white wider w-resize x-fast x-high x-large x-loud x-low x-slow x-small x-soft xx-large xx-small yellow';var fonts='[mM]onospace [tT]ahoma [vV]erdana [aA]rial [hH]elvetica [sS]ans-serif [sS]erif';this.regexList=[{regex:dp.sh.RegexLib.MultiLineCComments,css:'comment'},{regex:dp.sh.RegexLib.DoubleQuotedString,css:'string'},{regex:dp.sh.RegexLib.SingleQuotedString,css:'string'},{regex:new RegExp('\\#[a-zA-Z0-9]{3,6}','g'),css:'value'},{regex:new RegExp('(-?\\d+)(\.\\d+)?(px|em|pt|\:|\%|)','g'),css:'value'},{regex:new RegExp('!important','g'),css:'important'},{regex:new RegExp(this.GetKeywordsCSS(keywords),'gm'),css:'keyword'},{regex:new RegExp(this.GetValuesCSS(values),'g'),css:'value'},{regex:new RegExp(this.GetValuesCSS(fonts),'g'),css:'value'}];this.CssClass='dp-css';this.Style='.dp-css .value { color: black; }'+'.dp-css .important { color: red; }';}
|
||||
dp.sh.Highlighter.prototype.GetKeywordsCSS=function(str)
|
||||
{return'\\b([a-z_]|)'+str.replace(/ /g,'(?=:)\\b|\\b([a-z_\\*]|\\*|)')+'(?=:)\\b';}
|
||||
dp.sh.Highlighter.prototype.GetValuesCSS=function(str)
|
||||
{return'\\b'+str.replace(/ /g,'(?!-)(?!:)\\b|\\b()')+'\:\\b';}
|
||||
dp.sh.Brushes.CSS.prototype=new dp.sh.Highlighter();dp.sh.Brushes.CSS.Aliases=['css'];
|
10
htdocs/okapi/static/syntax_highlighter/shBrushDelphi.js
vendored
Normal file
10
htdocs/okapi/static/syntax_highlighter/shBrushDelphi.js
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
/*
|
||||
* JsMin
|
||||
* Javascript Compressor
|
||||
* http://www.crockford.com/
|
||||
* http://www.smallsharptools.com/
|
||||
*/
|
||||
|
||||
dp.sh.Brushes.Delphi=function()
|
||||
{var keywords='abs addr and ansichar ansistring array as asm begin boolean byte cardinal '+'case char class comp const constructor currency destructor div do double '+'downto else end except exports extended false file finalization finally '+'for function goto if implementation in inherited int64 initialization '+'integer interface is label library longint longword mod nil not object '+'of on or packed pansichar pansistring pchar pcurrency pdatetime pextended '+'pint64 pointer private procedure program property pshortstring pstring '+'pvariant pwidechar pwidestring protected public published raise real real48 '+'record repeat set shl shortint shortstring shr single smallint string then '+'threadvar to true try type unit until uses val var varirnt while widechar '+'widestring with word write writeln xor';this.regexList=[{regex:new RegExp('\\(\\*[\\s\\S]*?\\*\\)','gm'),css:'comment'},{regex:new RegExp('{(?!\\$)[\\s\\S]*?}','gm'),css:'comment'},{regex:dp.sh.RegexLib.SingleLineCComments,css:'comment'},{regex:dp.sh.RegexLib.SingleQuotedString,css:'string'},{regex:new RegExp('\\{\\$[a-zA-Z]+ .+\\}','g'),css:'directive'},{regex:new RegExp('\\b[\\d\\.]+\\b','g'),css:'number'},{regex:new RegExp('\\$[a-zA-Z0-9]+\\b','g'),css:'number'},{regex:new RegExp(this.GetKeywords(keywords),'gm'),css:'keyword'}];this.CssClass='dp-delphi';this.Style='.dp-delphi .number { color: blue; }'+'.dp-delphi .directive { color: #008284; }'+'.dp-delphi .vars { color: #000; }';}
|
||||
dp.sh.Brushes.Delphi.prototype=new dp.sh.Highlighter();dp.sh.Brushes.Delphi.Aliases=['delphi','pascal'];
|
10
htdocs/okapi/static/syntax_highlighter/shBrushJScript.js
vendored
Normal file
10
htdocs/okapi/static/syntax_highlighter/shBrushJScript.js
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
/*
|
||||
* JsMin
|
||||
* Javascript Compressor
|
||||
* http://www.crockford.com/
|
||||
* http://www.smallsharptools.com/
|
||||
*/
|
||||
|
||||
dp.sh.Brushes.JScript=function()
|
||||
{var keywords='abstract boolean break byte case catch char class const continue debugger '+'default delete do double else enum export extends false final finally float '+'for function goto if implements import in instanceof int interface long native '+'new null package private protected public return short static super switch '+'synchronized this throw throws transient true try typeof var void volatile while with';this.regexList=[{regex:dp.sh.RegexLib.SingleLineCComments,css:'comment'},{regex:dp.sh.RegexLib.MultiLineCComments,css:'comment'},{regex:dp.sh.RegexLib.DoubleQuotedString,css:'string'},{regex:dp.sh.RegexLib.SingleQuotedString,css:'string'},{regex:new RegExp('^\\s*#.*','gm'),css:'preprocessor'},{regex:new RegExp(this.GetKeywords(keywords),'gm'),css:'keyword'}];this.CssClass='dp-c';}
|
||||
dp.sh.Brushes.JScript.prototype=new dp.sh.Highlighter();dp.sh.Brushes.JScript.Aliases=['js','jscript','javascript'];
|
10
htdocs/okapi/static/syntax_highlighter/shBrushJava.js
vendored
Normal file
10
htdocs/okapi/static/syntax_highlighter/shBrushJava.js
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
/*
|
||||
* JsMin
|
||||
* Javascript Compressor
|
||||
* http://www.crockford.com/
|
||||
* http://www.smallsharptools.com/
|
||||
*/
|
||||
|
||||
dp.sh.Brushes.Java=function()
|
||||
{var keywords='abstract assert boolean break byte case catch char class const '+'continue default do double else enum extends '+'false final finally float for goto if implements import '+'instanceof int interface long native new null '+'package private protected public return '+'short static strictfp super switch synchronized this throw throws true '+'transient try void volatile while';this.regexList=[{regex:dp.sh.RegexLib.SingleLineCComments,css:'comment'},{regex:dp.sh.RegexLib.MultiLineCComments,css:'comment'},{regex:dp.sh.RegexLib.DoubleQuotedString,css:'string'},{regex:dp.sh.RegexLib.SingleQuotedString,css:'string'},{regex:new RegExp('\\b([\\d]+(\\.[\\d]+)?|0x[a-f0-9]+)\\b','gi'),css:'number'},{regex:new RegExp('(?!\\@interface\\b)\\@[\\$\\w]+\\b','g'),css:'annotation'},{regex:new RegExp('\\@interface\\b','g'),css:'keyword'},{regex:new RegExp(this.GetKeywords(keywords),'gm'),css:'keyword'}];this.CssClass='dp-j';this.Style='.dp-j .annotation { color: #646464; }'+'.dp-j .number { color: #C00000; }';}
|
||||
dp.sh.Brushes.Java.prototype=new dp.sh.Highlighter();dp.sh.Brushes.Java.Aliases=['java'];
|
10
htdocs/okapi/static/syntax_highlighter/shBrushPhp.js
vendored
Normal file
10
htdocs/okapi/static/syntax_highlighter/shBrushPhp.js
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
/*
|
||||
* JsMin
|
||||
* Javascript Compressor
|
||||
* http://www.crockford.com/
|
||||
* http://www.smallsharptools.com/
|
||||
*/
|
||||
|
||||
dp.sh.Brushes.Php=function()
|
||||
{var funcs='abs acos acosh addcslashes addslashes '+'array_change_key_case array_chunk array_combine array_count_values array_diff '+'array_diff_assoc array_diff_key array_diff_uassoc array_diff_ukey array_fill '+'array_filter array_flip array_intersect array_intersect_assoc array_intersect_key '+'array_intersect_uassoc array_intersect_ukey array_key_exists array_keys array_map '+'array_merge array_merge_recursive array_multisort array_pad array_pop array_product '+'array_push array_rand array_reduce array_reverse array_search array_shift '+'array_slice array_splice array_sum array_udiff array_udiff_assoc '+'array_udiff_uassoc array_uintersect array_uintersect_assoc '+'array_uintersect_uassoc array_unique array_unshift array_values array_walk '+'array_walk_recursive atan atan2 atanh base64_decode base64_encode base_convert '+'basename bcadd bccomp bcdiv bcmod bcmul bindec bindtextdomain bzclose bzcompress '+'bzdecompress bzerrno bzerror bzerrstr bzflush bzopen bzread bzwrite ceil chdir '+'checkdate checkdnsrr chgrp chmod chop chown chr chroot chunk_split class_exists '+'closedir closelog copy cos cosh count count_chars date decbin dechex decoct '+'deg2rad delete ebcdic2ascii echo empty end ereg ereg_replace eregi eregi_replace error_log '+'error_reporting escapeshellarg escapeshellcmd eval exec exit exp explode extension_loaded '+'feof fflush fgetc fgetcsv fgets fgetss file_exists file_get_contents file_put_contents '+'fileatime filectime filegroup fileinode filemtime fileowner fileperms filesize filetype '+'floatval flock floor flush fmod fnmatch fopen fpassthru fprintf fputcsv fputs fread fscanf '+'fseek fsockopen fstat ftell ftok getallheaders getcwd getdate getenv gethostbyaddr gethostbyname '+'gethostbynamel getimagesize getlastmod getmxrr getmygid getmyinode getmypid getmyuid getopt '+'getprotobyname getprotobynumber getrandmax getrusage getservbyname getservbyport gettext '+'gettimeofday gettype glob gmdate gmmktime ini_alter ini_get ini_get_all ini_restore ini_set '+'interface_exists intval ip2long is_a is_array is_bool is_callable is_dir is_double '+'is_executable is_file is_finite is_float is_infinite is_int is_integer is_link is_long '+'is_nan is_null is_numeric is_object is_readable is_real is_resource is_scalar is_soap_fault '+'is_string is_subclass_of is_uploaded_file is_writable is_writeable mkdir mktime nl2br '+'parse_ini_file parse_str parse_url passthru pathinfo readlink realpath rewind rewinddir rmdir '+'round str_ireplace str_pad str_repeat str_replace str_rot13 str_shuffle str_split '+'str_word_count strcasecmp strchr strcmp strcoll strcspn strftime strip_tags stripcslashes '+'stripos stripslashes stristr strlen strnatcasecmp strnatcmp strncasecmp strncmp strpbrk '+'strpos strptime strrchr strrev strripos strrpos strspn strstr strtok strtolower strtotime '+'strtoupper strtr strval substr substr_compare';var keywords='and or xor __FILE__ __LINE__ array as break case '+'cfunction class const continue declare default die do else '+'elseif empty enddeclare endfor endforeach endif endswitch endwhile '+'extends for foreach function include include_once global if '+'new old_function return static switch use require require_once '+'var while __FUNCTION__ __CLASS__ '+'__METHOD__ abstract interface public implements extends private protected throw';this.regexList=[{regex:dp.sh.RegexLib.SingleLineCComments,css:'comment'},{regex:dp.sh.RegexLib.MultiLineCComments,css:'comment'},{regex:dp.sh.RegexLib.DoubleQuotedString,css:'string'},{regex:dp.sh.RegexLib.SingleQuotedString,css:'string'},{regex:new RegExp('\\$\\w+','g'),css:'vars'},{regex:new RegExp(this.GetKeywords(funcs),'gmi'),css:'func'},{regex:new RegExp(this.GetKeywords(keywords),'gm'),css:'keyword'}];this.CssClass='dp-c';}
|
||||
dp.sh.Brushes.Php.prototype=new dp.sh.Highlighter();dp.sh.Brushes.Php.Aliases=['php'];
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user