OKAPI Project update (r1031)

This commit is contained in:
Wojciech Rygielski
2014-10-08 10:06:07 +02:00
parent 2becadb611
commit ccf5cf33a3
135 changed files with 19432 additions and 18479 deletions

View File

@@ -23,75 +23,75 @@ require_once($GLOBALS['rootpath'].'okapi/urls.php');
if (ob_list_handlers() == array('default output handler')) if (ob_list_handlers() == array('default output handler'))
{ {
# We will assume that this one comes from "output_buffering" being turned on # 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 # 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. # pages. But we don't need it in OKAPI. We will just turn this off.
ob_end_clean(); ob_end_clean();
} }
class OkapiScriptEntryPointController class OkapiScriptEntryPointController
{ {
public static function dispatch_request($uri) public static function dispatch_request($uri)
{ {
# Chop off the ?args=... part. # Chop off the ?args=... part.
if (strpos($uri, '?') !== false) if (strpos($uri, '?') !== false)
$uri = substr($uri, 0, strpos($uri, '?')); $uri = substr($uri, 0, strpos($uri, '?'));
# Chop off everything before "/okapi/". This should work for okay for most "weird" # 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: # server configurations. It will also address a more subtle issue described here:
# http://stackoverflow.com/questions/8040461/request-uri-unexpectedly-contains-fqdn # http://stackoverflow.com/questions/8040461/request-uri-unexpectedly-contains-fqdn
if (strpos($uri, "/okapi/") !== false) if (strpos($uri, "/okapi/") !== false)
$uri = substr($uri, strpos($uri, "/okapi/")); $uri = substr($uri, strpos($uri, "/okapi/"));
# Make sure we're in the right directory (.htaccess should make sure of that). # Make sure we're in the right directory (.htaccess should make sure of that).
if (strpos($uri, "/okapi/") !== 0) if (strpos($uri, "/okapi/") !== 0)
throw new Exception("'$uri' is outside of the /okapi/ path."); throw new Exception("'$uri' is outside of the /okapi/ path.");
$uri = substr($uri, 7); $uri = substr($uri, 7);
# Initializing internals and running pre-request cronjobs (we don't want # Initializing internals and running pre-request cronjobs (we don't want
# cronjobs to be run before "okapi/update", for example before database # cronjobs to be run before "okapi/update", for example before database
# was installed). # was installed).
$allow_cronjobs = ($uri != "update"); $allow_cronjobs = ($uri != "update");
Okapi::init_internals($allow_cronjobs); Okapi::init_internals($allow_cronjobs);
# Checking for allowed patterns... # Checking for allowed patterns...
try try
{ {
foreach (OkapiUrls::$mapping as $pattern => $namespace) foreach (OkapiUrls::$mapping as $pattern => $namespace)
{ {
$matches = null; $matches = null;
if (preg_match("#$pattern#", $uri, $matches)) if (preg_match("#$pattern#", $uri, $matches))
{ {
# Pattern matched! Moving on to the proper View... # Pattern matched! Moving on to the proper View...
array_shift($matches); array_shift($matches);
require_once($GLOBALS['rootpath']."okapi/views/$namespace.php"); require_once($GLOBALS['rootpath']."okapi/views/$namespace.php");
$response = call_user_func_array(array('\\okapi\\views\\'. $response = call_user_func_array(array('\\okapi\\views\\'.
str_replace('/', '\\', $namespace).'\\View', 'call'), $matches); str_replace('/', '\\', $namespace).'\\View', 'call'), $matches);
if ($response) if ($response)
$response->display(); $response->display();
return; return;
} }
} }
} }
catch (Http404 $e) catch (Http404 $e)
{ {
/* pass */ /* pass */
} }
# None of the patterns matched OR method threw the Http404 exception. # None of the patterns matched OR method threw the Http404 exception.
require_once($GLOBALS['rootpath']."okapi/views/http404.php"); require_once($GLOBALS['rootpath']."okapi/views/http404.php");
$response = \okapi\views\http404\View::call(); $response = \okapi\views\http404\View::call();
$response->display(); $response->display();
} }
} }
Okapi::gettext_domain_init(); Okapi::gettext_domain_init();

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -6,180 +6,180 @@ use OAuthDataStore;
class OkapiDataStore extends OAuthDataStore class OkapiDataStore extends OAuthDataStore
{ {
public function lookup_consumer($consumer_key) public function lookup_consumer($consumer_key)
{ {
$row = Db::select_row(" $row = Db::select_row("
select `key`, secret, name, url, email, admin select `key`, secret, name, url, email, admin
from okapi_consumers from okapi_consumers
where `key` = '".mysql_real_escape_string($consumer_key)."' where `key` = '".mysql_real_escape_string($consumer_key)."'
"); ");
if (!$row) if (!$row)
return null; return null;
return new OkapiConsumer($row['key'], $row['secret'], $row['name'], return new OkapiConsumer($row['key'], $row['secret'], $row['name'],
$row['url'], $row['email'], $row['admin'] ? true : false); $row['url'], $row['email'], $row['admin'] ? true : false);
} }
public function lookup_token($consumer, $token_type, $token) public function lookup_token($consumer, $token_type, $token)
{ {
$row = Db::select_row(" $row = Db::select_row("
select `key`, consumer_key, secret, token_type, user_id, verifier, callback select `key`, consumer_key, secret, token_type, user_id, verifier, callback
from okapi_tokens from okapi_tokens
where where
consumer_key = '".mysql_real_escape_string($consumer->key)."' consumer_key = '".mysql_real_escape_string($consumer->key)."'
and token_type = '".mysql_real_escape_string($token_type)."' and token_type = '".mysql_real_escape_string($token_type)."'
and `key` = '".mysql_real_escape_string($token)."' and `key` = '".mysql_real_escape_string($token)."'
"); ");
if (!$row) if (!$row)
return null; return null;
switch ($row['token_type']) switch ($row['token_type'])
{ {
case 'request': case 'request':
return new OkapiRequestToken($row['key'], $row['secret'], return new OkapiRequestToken($row['key'], $row['secret'],
$row['consumer_key'], $row['callback'], $row['user_id'], $row['consumer_key'], $row['callback'], $row['user_id'],
$row['verifier']); $row['verifier']);
case 'access': case 'access':
return new OkapiAccessToken($row['key'], $row['secret'], return new OkapiAccessToken($row['key'], $row['secret'],
$row['consumer_key'], $row['user_id']); $row['consumer_key'], $row['user_id']);
default: default:
throw new Exception(); throw new Exception();
} }
} }
public function lookup_nonce($consumer, $token, $nonce, $timestamp) public function lookup_nonce($consumer, $token, $nonce, $timestamp)
{ {
# Since it's not important for us to save the actual token and nonce # Since it's not important for us to save the actual token and nonce
# value, we will save a hash only. We could also include the consumer # value, we will save a hash only. We could also include the consumer
# key in this hash and drop the column, but we will leave it be for # key in this hash and drop the column, but we will leave it be for
# now (for a couple of less important reasons). # now (for a couple of less important reasons).
$nonce_hash = md5(serialize(array( $nonce_hash = md5(serialize(array(
$token ? $token->key : null, $token ? $token->key : null,
$timestamp, $timestamp,
$nonce $nonce
))); )));
try try
{ {
# Time timestamp is saved separately, because we are periodically # Time timestamp is saved separately, because we are periodically
# removing older nonces from the database (see cronjobs). # removing older nonces from the database (see cronjobs).
Db::execute(" Db::execute("
insert into okapi_nonces (consumer_key, nonce_hash, timestamp) insert into okapi_nonces (consumer_key, nonce_hash, timestamp)
values ( values (
'".mysql_real_escape_string($consumer->key)."', '".mysql_real_escape_string($consumer->key)."',
'".mysql_real_escape_string($nonce_hash)."', '".mysql_real_escape_string($nonce_hash)."',
'".mysql_real_escape_string($timestamp)."' '".mysql_real_escape_string($timestamp)."'
); );
"); ");
return null; return null;
} }
catch (\Exception $e) catch (\Exception $e)
{ {
# INSERT failed. This nonce was already used. # INSERT failed. This nonce was already used.
return $nonce; return $nonce;
} }
} }
public function new_request_token($consumer, $callback = null) public function new_request_token($consumer, $callback = null)
{ {
if ((preg_match("#^[a-z][a-z0-9_.-]*://#", $callback) > 0) || if ((preg_match("#^[a-z][a-z0-9_.-]*://#", $callback) > 0) ||
$callback == "oob") $callback == "oob")
{ /* ok */ } { /* ok */ }
else { throw new BadRequest("oauth_callback should begin with <scheme>://, or should equal 'oob'."); } else { throw new BadRequest("oauth_callback should begin with lower case <scheme>://, or should equal 'oob'."); }
$token = new OkapiRequestToken(Okapi::generate_key(20), Okapi::generate_key(40), $token = new OkapiRequestToken(Okapi::generate_key(20), Okapi::generate_key(40),
$consumer->key, $callback, null, Okapi::generate_key(8, true)); $consumer->key, $callback, null, Okapi::generate_key(8, true));
Db::execute(" Db::execute("
insert into okapi_tokens insert into okapi_tokens
(`key`, secret, token_type, timestamp, (`key`, secret, token_type, timestamp,
user_id, consumer_key, verifier, callback) user_id, consumer_key, verifier, callback)
values ( values (
'".mysql_real_escape_string($token->key)."', '".mysql_real_escape_string($token->key)."',
'".mysql_real_escape_string($token->secret)."', '".mysql_real_escape_string($token->secret)."',
'request', 'request',
unix_timestamp(), unix_timestamp(),
null, null,
'".mysql_real_escape_string($consumer->key)."', '".mysql_real_escape_string($consumer->key)."',
'".mysql_real_escape_string($token->verifier)."', '".mysql_real_escape_string($token->verifier)."',
".(($token->callback_url == 'oob') ".(($token->callback_url == 'oob')
? "null" ? "null"
: "'".mysql_real_escape_string($token->callback_url)."'" : "'".mysql_real_escape_string($token->callback_url)."'"
)." )."
); );
"); ");
return $token; return $token;
} }
public function new_access_token($token, $consumer, $verifier = null) public function new_access_token($token, $consumer, $verifier = null)
{ {
if ($token->consumer_key != $consumer->key) if ($token->consumer_key != $consumer->key)
throw new BadRequest("Request Token given is not associated with the Consumer who signed the request."); throw new BadRequest("Request Token given is not associated with the Consumer who signed the request.");
if (!$token->authorized_by_user_id) if (!$token->authorized_by_user_id)
throw new BadRequest("Request Token given has not been authorized."); throw new BadRequest("Request Token given has not been authorized.");
if ($token->verifier != $verifier) if ($token->verifier != $verifier)
throw new BadRequest("Invalid verifier."); throw new BadRequest("Invalid verifier.");
# Invalidate the Request Token. # Invalidate the Request Token.
Db::execute(" Db::execute("
delete from okapi_tokens delete from okapi_tokens
where `key` = '".mysql_real_escape_string($token->key)."' where `key` = '".mysql_real_escape_string($token->key)."'
"); ");
# In OKAPI, all Access Tokens are long lived. Therefore, we don't want # 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 # 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) # if there is already an Access Token generated for this (Consumer, User)
# pair and return it if there is. # pair and return it if there is.
$row = Db::select_row(" $row = Db::select_row("
select `key`, secret select `key`, secret
from okapi_tokens from okapi_tokens
where where
token_type = 'access' token_type = 'access'
and user_id = '".mysql_real_escape_string($token->authorized_by_user_id)."' and user_id = '".mysql_real_escape_string($token->authorized_by_user_id)."'
and consumer_key = '".mysql_real_escape_string($consumer->key)."' and consumer_key = '".mysql_real_escape_string($consumer->key)."'
"); ");
if ($row) if ($row)
{ {
# Use existing Access Token # Use existing Access Token
$access_token = new OkapiAccessToken($row['key'], $row['secret'], $access_token = new OkapiAccessToken($row['key'], $row['secret'],
$consumer->key, $token->authorized_by_user_id); $consumer->key, $token->authorized_by_user_id);
} }
else else
{ {
# Generate a new Access Token. # Generate a new Access Token.
$access_token = new OkapiAccessToken(Okapi::generate_key(20), Okapi::generate_key(40), $access_token = new OkapiAccessToken(Okapi::generate_key(20), Okapi::generate_key(40),
$consumer->key, $token->authorized_by_user_id); $consumer->key, $token->authorized_by_user_id);
Db::execute(" Db::execute("
insert into okapi_tokens insert into okapi_tokens
(`key`, secret, token_type, timestamp, user_id, consumer_key) (`key`, secret, token_type, timestamp, user_id, consumer_key)
values ( values (
'".mysql_real_escape_string($access_token->key)."', '".mysql_real_escape_string($access_token->key)."',
'".mysql_real_escape_string($access_token->secret)."', '".mysql_real_escape_string($access_token->secret)."',
'access', 'access',
unix_timestamp(), unix_timestamp(),
'".mysql_real_escape_string($access_token->user_id)."', '".mysql_real_escape_string($access_token->user_id)."',
'".mysql_real_escape_string($consumer->key)."' '".mysql_real_escape_string($consumer->key)."'
); );
"); ");
} }
return $access_token; return $access_token;
} }
public function cleanup() public function cleanup()
{ {
Db::execute(" Db::execute("
delete from okapi_nonces delete from okapi_nonces
where where
timestamp < unix_timestamp(date_add(now(), interval -6 minute)) timestamp < unix_timestamp(date_add(now(), interval -6 minute))
or timestamp > unix_timestamp(date_add(now(), interval 6 minute)) or timestamp > unix_timestamp(date_add(now(), interval 6 minute))
"); ");
Db::execute(" Db::execute("
delete from okapi_tokens delete from okapi_tokens
where where
token_type = 'request' token_type = 'request'
and timestamp < unix_timestamp(date_add(now(), interval -2 hour)) and timestamp < unix_timestamp(date_add(now(), interval -2 hour))
"); ");
} }
} }

View File

@@ -34,6 +34,7 @@ use okapi\OkapiServiceRunner;
use okapi\OkapiInternalRequest; use okapi\OkapiInternalRequest;
use okapi\OkapiFacadeConsumer; use okapi\OkapiFacadeConsumer;
use okapi\OkapiFacadeAccessToken; use okapi\OkapiFacadeAccessToken;
use okapi\Cache;
require_once($GLOBALS['rootpath']."okapi/core.php"); require_once($GLOBALS['rootpath']."okapi/core.php");
OkapiErrorHandler::$treat_notices_as_errors = true; OkapiErrorHandler::$treat_notices_as_errors = true;
@@ -45,131 +46,214 @@ Okapi::init_internals();
*/ */
class Facade class Facade
{ {
/** /**
* Perform OKAPI service call, signed by internal 'facade' consumer key, and return the result * 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 * (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 * 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). * 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) public static function service_call($service_name, $user_id_or_null, $parameters)
{ {
$request = new OkapiInternalRequest( $request = new OkapiInternalRequest(
new OkapiFacadeConsumer(), new OkapiFacadeConsumer(),
($user_id_or_null !== null) ? new OkapiFacadeAccessToken($user_id_or_null) : null, ($user_id_or_null !== null) ? new OkapiFacadeAccessToken($user_id_or_null) : null,
$parameters $parameters
); );
$request->perceive_as_http_request = true; $request->perceive_as_http_request = true;
return OkapiServiceRunner::call($service_name, $request); return OkapiServiceRunner::call($service_name, $request);
} }
/** /**
* This works like service_call with two exceptions: 1. It passes all your * This works like service_call with two exceptions: 1. It passes all your
* current HTTP request headers to OKAPI (which can make use of them in * current HTTP request headers to OKAPI (which can make use of them in
* terms of caching), 2. It outputs the service response directly, instead * terms of caching), 2. It outputs the service response directly, instead
* of returning it. * of returning it.
*/ */
public static function service_display($service_name, $user_id_or_null, $parameters) public static function service_display($service_name, $user_id_or_null, $parameters)
{ {
$request = new OkapiInternalRequest( $request = new OkapiInternalRequest(
new OkapiFacadeConsumer(), new OkapiFacadeConsumer(),
($user_id_or_null !== null) ? new OkapiFacadeAccessToken($user_id_or_null) : null, ($user_id_or_null !== null) ? new OkapiFacadeAccessToken($user_id_or_null) : null,
$parameters $parameters
); );
$request->perceive_as_http_request = true; $request->perceive_as_http_request = true;
if (isset($_SERVER['HTTP_IF_NONE_MATCH'])) if (isset($_SERVER['HTTP_IF_NONE_MATCH']))
$request->etag = $_SERVER['HTTP_IF_NONE_MATCH']; $request->etag = $_SERVER['HTTP_IF_NONE_MATCH'];
$response = OkapiServiceRunner::call($service_name, $request); $response = OkapiServiceRunner::call($service_name, $request);
$response->display(); $response->display();
} }
/** /**
* Create a search set from a temporary table. This is very similar to * Create a search set from a temporary table. This is very similar to
* the "services/caches/search/save" method, but allows OC server to * the "services/caches/search/save" method, but allows OC server to
* include its own result instead of using OKAPI's search options. The * include its own result instead of using OKAPI's search options. The
* $temp_table should be a valid name of a temporary table with the * $temp_table should be a valid name of a temporary table with the
* following (or similar) structure: * following (or similar) structure:
* *
* create temporary table temp_12345 ( * create temporary table temp_12345 (
* cache_id integer primary key * cache_id integer primary key
* ) engine=memory; * ) engine=memory;
*/ */
public static function import_search_set($temp_table, $min_store, $max_ref_age) public static function import_search_set($temp_table, $min_store, $max_ref_age)
{ {
require_once 'services/caches/search/save.php'; require_once($GLOBALS['rootpath'].'okapi/services/caches/search/save.php');
$tables = array('caches', $temp_table); $tables = array('caches', $temp_table);
$where_conds = array( $where_conds = array(
$temp_table.".cache_id = caches.cache_id", $temp_table.".cache_id = caches.cache_id",
'caches.status in (1,2,3)', 'caches.status in (1,2,3)',
); );
return \okapi\services\caches\search\save\WebService::get_set( return \okapi\services\caches\search\save\WebService::get_set(
$tables, $where_conds, $min_store, $max_ref_age $tables, $where_conds, $min_store, $max_ref_age
); );
} }
/** /**
* Mark the specified caches as *possibly* modified. The replicate module * Mark the specified caches as *possibly* modified. The replicate module
* will scan for changes within these caches on the next changelog update. * will scan for changes within these caches on the next changelog update.
* This is useful in some cases, when OKAPI cannot detect the modification * This is useful in some cases, when OKAPI cannot detect the modification
* for itself (grep OCPL code for examples). See issue #179. * for itself (grep OCPL code for examples). See issue #179.
* *
* $cache_codes - a single cache code OR an array of cache codes. * $cache_codes - a single cache code OR an array of cache codes.
*/ */
public static function schedule_geocache_check($cache_codes) public static function schedule_geocache_check($cache_codes)
{ {
if (!is_array($cache_codes)) if (!is_array($cache_codes))
$cache_codes = array($cache_codes); $cache_codes = array($cache_codes);
Db::execute(" Db::execute("
update caches update caches
set okapi_syncbase = now() set okapi_syncbase = now()
where wp_oc in ('".implode("','", array_map('mysql_real_escape_string', $cache_codes))."') where wp_oc in ('".implode("','", array_map('mysql_real_escape_string', $cache_codes))."')
"); ");
} }
/** /**
* Find all log entries of the specified user for the specified cache and * Find all log entries of the specified user for the specified cache and
* mark them as *possibly* modified. See issue #265. * mark them as *possibly* modified. See issue #265.
* *
* $cache_id - internal ID of the geocache, * $cache_id - internal ID of the geocache,
* $user_id - internal ID of the user. * $user_id - internal ID of the user.
*/ */
public static function schedule_user_entries_check($cache_id, $user_id) public static function schedule_user_entries_check($cache_id, $user_id)
{ {
Db::execute(" Db::execute("
update cache_logs update cache_logs
set okapi_syncbase = now() set okapi_syncbase = now()
where where
cache_id = '".mysql_real_escape_string($cache_id)."' cache_id = '".mysql_real_escape_string($cache_id)."'
and user_id = '".mysql_real_escape_string($user_id)."' and user_id = '".mysql_real_escape_string($user_id)."'
"); ");
} }
/** /**
* Run OKAPI database update. * Run OKAPI database update.
* Will output messages to stdout. * Will output messages to stdout.
*/ */
public static function database_update() public static function database_update()
{ {
require_once($GLOBALS['rootpath']."okapi/views/update.php"); require_once($GLOBALS['rootpath']."okapi/views/update.php");
$update = new views\update\View; $update = new views\update\View;
$update->call(); $update->call();
} }
/** /**
* You will probably want to call that with FALSE when using Facade * You will probably want to call that with FALSE when using Facade
* in buggy, legacy OC code. This will disable OKAPI's default behavior * in buggy, legacy OC code. This will disable OKAPI's default behavior
* of treating NOTICEs as errors. * of treating NOTICEs as errors.
*/ */
public static function disable_error_handling() public static function disable_error_handling()
{ {
OkapiErrorHandler::disable(); OkapiErrorHandler::disable();
} }
/** /**
* If you disabled OKAPI's error handling with disable_error_handling, * If you disabled OKAPI's error handling with disable_error_handling,
* you may reenable it with this method. * you may reenable it with this method.
*/ */
public static function reenable_error_handling() public static function reenable_error_handling()
{ {
OkapiErrorHandler::reenable(); OkapiErrorHandler::reenable();
} }
/**
* Store the object $value in OKAPI's cache, under the name of $key.
*
* Parameters:
*
* $key -- must be a string of max 57 characters in length (you can use
* md5(...) to shorten your keys). Use the same $key to retrieve your
* value later.
*
* $value -- can be any serializable PHP object. Currently there's no
* strict size limit, but try to keep it below 1 MB (for future
* compatibility with memcached).
*
* $timeout -- *maximum* time allowed to store the value, given in seconds
* (however, the value *can* be removed sooner than that, see the note
* below). Timeout can be also set to null, but you should avoid this,
* because such objects may clutter the cache unnecessarilly. (You must
* remember to remove them yourself!)
*
* Please note, that this cache is not guaranteed to be persistent.
* Usually it is, but it can be emptied in case of emergency (low disk
* space), or if we decide to switch the underlying cache engine in the
* future (e.g. to memcached). Most of your values should be safe though.
*/
public static function cache_set($key, $value, $timeout)
{
Cache::set("facade#".$key, $value, $timeout);
}
/** Same as `cache_set`, but works on many key->value pair at once. */
public static function cache_set_many($dict, $timeout)
{
$prefixed_dict = array();
foreach ($dict as $key => &$value_ref) {
$prefixed_dict["facade#".$key] = &$value_ref;
}
Cache::set_many($prefixed_dict, $timeout);
}
/**
* Retrieve object stored in cache under the name of $key. If object does
* not exist or its timeout has expired, return null.
*/
public static function cache_get($key)
{
return Cache::get("facade#".$key);
}
/** Same as `cache_get`, but it works on multiple keys at once. */
public static function get_many($keys)
{
$prefixed_keys = array();
foreach ($keys as $key) {
$prefixed_keys[] = "facade#".$key;
}
$prefixed_result = Cache::get_many($prefixed_keys);
$result = array();
foreach ($prefixed_result as $prefixed_key => &$value_ref) {
$result[substr($prefixed_key, 7)] = &$value_ref;
}
return $result;
}
/**
* Delete the entry named $key from the cache.
*/
public static function cache_delete($key)
{
Cache::delete("facade#".$key);
}
/** Same as `cache_delete`, but works on many keys at once. */
public static function cache_delete_many($keys)
{
$prefixed_keys = array();
foreach ($keys as $key) {
$prefixed_keys[] = "facade#".$key;
}
Cache::delete_many($prefixed_keys);
}
} }
# (This comment is added here simply to debug OKAPI deployment.....)

View File

@@ -8,23 +8,23 @@ namespace okapi;
*/ */
class OCSession class OCSession
{ {
/** Return ID of currently logged in user or NULL if no user is logged in. */ /** Return ID of currently logged in user or NULL if no user is logged in. */
public static function get_user_id() public static function get_user_id()
{ {
static $cached_result = false; static $cached_result = false;
if ($cached_result !== false) if ($cached_result !== false)
return $cached_result; return $cached_result;
$cookie_name = Settings::get('OC_COOKIE_NAME'); $cookie_name = Settings::get('OC_COOKIE_NAME');
if (!isset($_COOKIE[$cookie_name])) if (!isset($_COOKIE[$cookie_name]))
return null; return null;
$OC_data = unserialize(base64_decode($_COOKIE[$cookie_name])); $OC_data = unserialize(base64_decode($_COOKIE[$cookie_name]));
if (!isset($OC_data['sessionid'])) if (!isset($OC_data['sessionid']))
return null; return null;
$OC_sessionid = $OC_data['sessionid']; $OC_sessionid = $OC_data['sessionid'];
if (!$OC_sessionid) if (!$OC_sessionid)
return null; return null;
return Db::select_value("select user_id from sys_sessions where uuid='".mysql_real_escape_string($OC_sessionid)."'"); return Db::select_value("select user_id from sys_sessions where uuid='".mysql_real_escape_string($OC_sessionid)."'");
} }
} }

View File

@@ -2,11 +2,11 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: OKAPI\n" "Project-Id-Version: OKAPI\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2013-07-10 12:30+0100\n" "POT-Creation-Date: 2014-01-23 15:51+0100\n"
"PO-Revision-Date: 2013-07-10 12:31+0100\n" "PO-Revision-Date: 2014-01-23 15:52+0100\n"
"Last-Translator: following <following@online.de>\n" "Last-Translator: Wojciech Rygielski <rygielski@mimuw.edu.pl>\n"
"Language-Team: following <following@online.de>\n" "Language-Team: following <following@online.de>\n"
"Language: German\n" "Language: de\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
@@ -14,20 +14,29 @@ msgstr ""
"X-Poedit-Basepath: .\n" "X-Poedit-Basepath: .\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Poedit-SourceCharset: UTF-8\n" "X-Poedit-SourceCharset: UTF-8\n"
"X-Generator: Poedit 1.5.5\n" "X-Generator: Poedit 1.6.3\n"
"X-Poedit-SearchPath-0: c:\\source\\oc\\server-3.0\\htdocs\\okapi\n" "X-Poedit-SearchPath-0: c:\\source\\oc\\server-3.0\\htdocs\\okapi\n"
"X-Poedit-SearchPath-1: D:\\PRIV\\Projekty\\EclipseWorkspace\\opencaching-api" "X-Poedit-SearchPath-1: D:\\PRIV\\Projekty\\EclipseWorkspace\\opencaching-api"
"\\okapi\n" "\\okapi\n"
#: c:\source\oc\server-3.0\htdocs\okapi/services/caches/geocaches.php:956 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/caches/geocaches.php:957
msgid "Stage" msgid "Stage"
msgstr "Station" msgstr "Station"
#: c:\source\oc\server-3.0\htdocs\okapi/services/caches/geocaches.php:1108 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/caches/geocaches.php:986
msgid "User location"
msgstr ""
#: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/caches/geocaches.php:989
#, php-format
msgid "Your own custom coordinates for the %s geocache"
msgstr ""
#: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/caches/geocaches.php:1148
msgid "National Park / Landscape" msgid "National Park / Landscape"
msgstr "Nationalpark / Landschaft" msgstr "Nationalpark / Landschaft"
#: c:\source\oc\server-3.0\htdocs\okapi/services/caches/geocaches.php:1260 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/caches/geocaches.php:1300
#, php-format #, php-format
msgid "" msgid ""
"This <a href='%s'>geocache</a> description comes from the <a href='%s'>%s</" "This <a href='%s'>geocache</a> description comes from the <a href='%s'>%s</"
@@ -35,7 +44,7 @@ msgid ""
msgstr "" msgstr ""
"Diese <a href='%s'>Cache</a>-Beschreibung stammt von <a href='%s'>%s</a>." "Diese <a href='%s'>Cache</a>-Beschreibung stammt von <a href='%s'>%s</a>."
#: c:\source\oc\server-3.0\htdocs\okapi/services/caches/geocaches.php:1272 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/caches/geocaches.php:1312
#, php-format #, php-format
msgid "" msgid ""
"&copy; <a href='%s'>%s</a>, <a href='%s'>%s</a>, <a href='http://" "&copy; <a href='%s'>%s</a>, <a href='%s'>%s</a>, <a href='http://"
@@ -46,7 +55,7 @@ msgstr ""
"creativecommons.org/licenses/by-nc-nd/3.0/de/'>CC-BY-NC-ND</a>, Stand: %s; " "creativecommons.org/licenses/by-nc-nd/3.0/de/'>CC-BY-NC-ND</a>, Stand: %s; "
"alle Logeinträge &copy; jeweiliger Autor" "alle Logeinträge &copy; jeweiliger Autor"
#: c:\source\oc\server-3.0\htdocs\okapi/services/caches/geocaches.php:1283 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/caches/geocaches.php:1323
#, php-format #, php-format
msgid "" msgid ""
"&copy; <a href='%s'>%s</a>, <a href='%s'>%s</a>, <a href='http://" "&copy; <a href='%s'>%s</a>, <a href='%s'>%s</a>, <a href='http://"
@@ -57,63 +66,84 @@ msgstr ""
"creativecommons.org/licenses/by-nc-nd/3.0/de/'>CC-BY-NC-ND</a>; alle " "creativecommons.org/licenses/by-nc-nd/3.0/de/'>CC-BY-NC-ND</a>; alle "
"Logeinträge &copy; jeweiliger Autor" "Logeinträge &copy; jeweiliger Autor"
#: c:\source\oc\server-3.0\htdocs\okapi/services/caches/formatters/gpxfile.tpl.php:31 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/caches/formatters/gpx.php:360
#: c:\source\oc\server-3.0\htdocs\okapi/services/caches/formatters/gpxfile.tpl.php:60 msgid ""
"<b>Geocache coordinates have been changed.</b> They have been replaced with "
"your own custom coordinates which you have provided for this geocache."
msgstr ""
#: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/caches/formatters/gpx.php:366
msgid ""
"<b>Geocache coordinates have been changed.</b> Currently they point to one "
"of the alternate waypoints originally described as:"
msgstr ""
#: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/caches/formatters/gpx.php:379
msgid "Original geocache location"
msgstr ""
#: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/caches/formatters/gpx.php:381
#, php-format
msgid "Original (owner-supplied) location of the %s geocache"
msgstr ""
#: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/caches/formatters/gpxfile.tpl.php:30
#: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/caches/formatters/gpxfile.tpl.php:62
msgid "hidden by" msgid "hidden by"
msgstr "versteckt von" msgstr "versteckt von"
#: c:\source\oc\server-3.0\htdocs\okapi/services/caches/formatters/gpxfile.tpl.php:62 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/caches/formatters/gpxfile.tpl.php:64
#, php-format #, php-format
msgid "%d recommendation" msgid "%d recommendation"
msgid_plural "%d recommendations" msgid_plural "%d recommendations"
msgstr[0] "%d Empfehlung" msgstr[0] "%d Empfehlung"
msgstr[1] "%d Empfehlungen" msgstr[1] "%d Empfehlungen"
#: c:\source\oc\server-3.0\htdocs\okapi/services/caches/formatters/gpxfile.tpl.php:63 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/caches/formatters/gpxfile.tpl.php:65
#, php-format #, php-format
msgid "found %d time" msgid "found %d time"
msgid_plural "found %d times" msgid_plural "found %d times"
msgstr[0] "%d mal gefunden" msgstr[0] "%d mal gefunden"
msgstr[1] "%d mal gefunden" msgstr[1] "%d mal gefunden"
#: c:\source\oc\server-3.0\htdocs\okapi/services/caches/formatters/gpxfile.tpl.php:66 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/caches/formatters/gpxfile.tpl.php:68
#, php-format #, php-format
msgid "%d trackable" msgid "%d trackable"
msgid_plural "%d trackables" msgid_plural "%d trackables"
msgstr[0] "%d Geokret" msgstr[0] "%d Geokret"
msgstr[1] "%d Geokrets" msgstr[1] "%d Geokrets"
#: c:\source\oc\server-3.0\htdocs\okapi/services/caches/formatters/gpxfile.tpl.php:70 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/caches/formatters/gpxfile.tpl.php:72
msgid "Personal notes" msgid "Personal notes"
msgstr "Persönliche Notizen" msgstr "Persönliche Notizen"
#: c:\source\oc\server-3.0\htdocs\okapi/services/caches/formatters/gpxfile.tpl.php:74 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/caches/formatters/gpxfile.tpl.php:76
msgid "Attributes" msgid "Attributes"
msgstr "Attribute" msgstr "Attribute"
#: c:\source\oc\server-3.0\htdocs\okapi/services/caches/formatters/gpxfile.tpl.php:78 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/caches/formatters/gpxfile.tpl.php:80
msgid "Trackables" msgid "Trackables"
msgstr "Geokrets" msgstr "Geokrets"
#: c:\source\oc\server-3.0\htdocs\okapi/services/caches/formatters/gpxfile.tpl.php:88 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/caches/formatters/gpxfile.tpl.php:90
#: c:\source\oc\server-3.0\htdocs\okapi/services/caches/formatters/gpxfile.tpl.php:104 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/caches/formatters/gpxfile.tpl.php:106
msgid "Images" msgid "Images"
msgstr "Bilder" msgstr "Bilder"
#: c:\source\oc\server-3.0\htdocs\okapi/services/caches/formatters/gpxfile.tpl.php:111 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/caches/formatters/gpxfile.tpl.php:113
msgid "Spoilers" msgid "Spoilers"
msgstr "Spoiler" msgstr "Spoiler"
#: c:\source\oc\server-3.0\htdocs\okapi/services/caches/formatters/gpxfile.tpl.php:120 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/caches/formatters/gpxfile.tpl.php:122
msgid "Image descriptions" msgid "Image descriptions"
msgstr "Bildbeschreibungen" msgstr "Bildbeschreibungen"
#: c:\source\oc\server-3.0\htdocs\okapi/services/caches/formatters/gpxfile.tpl.php:128 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/caches/formatters/gpxfile.tpl.php:130
msgid "The cache probably is located in the following protection areas:" msgid "The cache probably is located in the following protection areas:"
msgstr "" msgstr ""
"Der Geocache befindet sich wahrscheinlich in den folgenden Schutzgebieten:" "Der Geocache befindet sich wahrscheinlich in den folgenden Schutzgebieten:"
#: c:\source\oc\server-3.0\htdocs\okapi/services/logs/submit.php:70 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/logs/submit.php:70
msgid "" msgid ""
"You are trying to publish a log entry with a date in future. Cache log " "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." "entries are allowed to be published in the past, but NOT in the future."
@@ -121,7 +151,7 @@ msgstr ""
"Das Datum deines Logeintrags liegt in der Zukunft. Cache-Logs können nur für " "Das Datum deines Logeintrags liegt in der Zukunft. Cache-Logs können nur für "
"die Vergangenheit oder für heute eingetragen werden." "die Vergangenheit oder für heute eingetragen werden."
#: c:\source\oc\server-3.0\htdocs\okapi/services/logs/submit.php:92 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/logs/submit.php:92
#, php-format #, php-format
msgid "" msgid ""
"However, your cache rating was ignored, because %s does not have a rating " "However, your cache rating was ignored, because %s does not have a rating "
@@ -129,7 +159,7 @@ msgid ""
msgstr "" msgstr ""
"Deine Cachewertung wurde jedoch ignoriert, weil %s kein Bewertungssystem hat." "Deine Cachewertung wurde jedoch ignoriert, weil %s kein Bewertungssystem hat."
#: c:\source\oc\server-3.0\htdocs\okapi/services/logs/submit.php:111 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/logs/submit.php:111
#, php-format #, php-format
msgid "" msgid ""
"However, your cache recommendation was ignored, because %s does not allow " "However, your cache recommendation was ignored, because %s does not allow "
@@ -138,7 +168,7 @@ msgstr ""
"Deine Empfehlung wurde jedoch ignoriert, weil auf %s keine Event-Caches " "Deine Empfehlung wurde jedoch ignoriert, weil auf %s keine Event-Caches "
"empfohlen werden können." "empfohlen werden können."
#: c:\source\oc\server-3.0\htdocs\okapi/services/logs/submit.php:125 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/logs/submit.php:125
#, php-format #, php-format
msgid "" msgid ""
"However, your \"needs maintenance\" flag was ignored, because %s does not " "However, your \"needs maintenance\" flag was ignored, because %s does not "
@@ -147,7 +177,7 @@ msgstr ""
"Deine Angabe \"benötigt Wartung\" wurde jedoch ignoriert, weil es diese " "Deine Angabe \"benötigt Wartung\" wurde jedoch ignoriert, weil es diese "
"Funktion bei %s nicht gibt." "Funktion bei %s nicht gibt."
#: c:\source\oc\server-3.0\htdocs\okapi/services/logs/submit.php:145 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/logs/submit.php:145
msgid "" msgid ""
"This cache is an Event cache. You cannot \"Find\" it (but you can attend it, " "This cache is an Event cache. You cannot \"Find\" it (but you can attend it, "
"or comment on it)!" "or comment on it)!"
@@ -155,7 +185,7 @@ msgstr ""
"Dies ist ein Event-Cache. Du kannst ihn nicht \"finden\" (aber du kannst am " "Dies ist ein Event-Cache. Du kannst ihn nicht \"finden\" (aber du kannst am "
"Event teilnehmen oder einen Hinweis loggen)." "Event teilnehmen oder einen Hinweis loggen)."
#: c:\source\oc\server-3.0\htdocs\okapi/services/logs/submit.php:150 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/logs/submit.php:150
msgid "" msgid ""
"This cache is NOT an Event cache. You cannot \"Attend\" it (but you can find " "This cache is NOT an Event cache. You cannot \"Attend\" it (but you can find "
"it, or comment on it)!" "it, or comment on it)!"
@@ -163,26 +193,26 @@ msgstr ""
"Dies ist KEIN Event-Cache. Du kannst an ihm nicht \"teilnehmen\" (aber du " "Dies ist KEIN Event-Cache. Du kannst an ihm nicht \"teilnehmen\" (aber du "
"kannst ihn finden oder kommentieren)." "kannst ihn finden oder kommentieren)."
#: c:\source\oc\server-3.0\htdocs\okapi/services/logs/submit.php:155 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/logs/submit.php:155
msgid "Your have to supply some text for your comment." msgid "Your have to supply some text for your comment."
msgstr "Du musst einen Text für dein Hinweislog eingeben!" msgstr "Du musst einen Text für dein Hinweislog eingeben!"
#: c:\source\oc\server-3.0\htdocs\okapi/services/logs/submit.php:168 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/logs/submit.php:168
msgid "This cache requires a password. You didn't provide one!" msgid "This cache requires a password. You didn't provide one!"
msgstr "" msgstr ""
"Dieser Cache kann nur mit Kennwort geloggt werden, aber du hast keines " "Dieser Cache kann nur mit Kennwort geloggt werden, aber du hast keines "
"angegeben." "angegeben."
#: c:\source\oc\server-3.0\htdocs\okapi/services/logs/submit.php:170 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/logs/submit.php:170
msgid "Invalid password!" msgid "Invalid password!"
msgstr "Ungültiges Kennwort!" msgstr "Ungültiges Kennwort!"
#: c:\source\oc\server-3.0\htdocs\okapi/services/logs/submit.php:282 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/logs/submit.php:285
msgid "You have already submitted a log entry with exactly the same contents." msgid "You have already submitted a log entry with exactly the same contents."
msgstr "" msgstr ""
"Du hast bereits einen Logeintrag mit genau dem gleichen Inhalt gemacht." "Du hast bereits einen Logeintrag mit genau dem gleichen Inhalt gemacht."
#: c:\source\oc\server-3.0\htdocs\okapi/services/logs/submit.php:305 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/logs/submit.php:308
msgid "" msgid ""
"You have already submitted a \"Found it\" log entry once. Now you may submit " "You have already submitted a \"Found it\" log entry once. Now you may submit "
"\"Comments\" only!" "\"Comments\" only!"
@@ -190,53 +220,53 @@ msgstr ""
"Du hast diesen Cache bereits als gefunden geloggt. Ein zweites Fundlog ist " "Du hast diesen Cache bereits als gefunden geloggt. Ein zweites Fundlog ist "
"nicht möglich, aber du kannst stattdessen einen Hinweis loggen." "nicht möglich, aber du kannst stattdessen einen Hinweis loggen."
#: c:\source\oc\server-3.0\htdocs\okapi/services/logs/submit.php:307 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/logs/submit.php:310
msgid "You are the owner of this cache. You may submit \"Comments\" only!" msgid "You are the owner of this cache. You may submit \"Comments\" only!"
msgstr "" msgstr ""
"Als Besitzer des Caches kannst du nur Hinweise loggen, keine Funde oder " "Als Besitzer des Caches kannst du nur Hinweise loggen, keine Funde oder "
"Nichtfunde." "Nichtfunde."
#: c:\source\oc\server-3.0\htdocs\okapi/services/logs/submit.php:325 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/logs/submit.php:328
msgid "You have already rated this cache once. Your rating cannot be changed." msgid "You have already rated this cache once. Your rating cannot be changed."
msgstr "" msgstr ""
"Du hast diesen Cache bereits bewertet. Deine Bewertung ist nicht änderbar." "Du hast diesen Cache bereits bewertet. Deine Bewertung ist nicht änderbar."
#: c:\source\oc\server-3.0\htdocs\okapi/services/logs/submit.php:342 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/logs/submit.php:345
msgid "You have already recommended this cache once." msgid "You have already recommended this cache once."
msgstr "Du hast diesen Cache bereits empfohlen." msgstr "Du hast diesen Cache bereits empfohlen."
#: c:\source\oc\server-3.0\htdocs\okapi/services/logs/submit.php:352 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/logs/submit.php:355
msgid "You don't have any recommendations to give. Find more caches first!" msgid "You don't have any recommendations to give. Find more caches first!"
msgstr "" msgstr ""
"Du musst mehr Caches finden, um eine weitere Bewertung abgeben zu können!" "Du musst mehr Caches finden, um eine weitere Bewertung abgeben zu können!"
#: c:\source\oc\server-3.0\htdocs\okapi/services/logs/submit.php:395 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/logs/submit.php:398
msgid "Event caches cannot \"need maintenance\"." msgid "Event caches cannot \"need maintenance\"."
msgstr "Event-Caches können keine \"Wartung benötigen\"." msgstr "Event-Caches können keine \"Wartung benötigen\"."
#: c:\source\oc\server-3.0\htdocs\okapi/services/logs/submit.php:525 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/logs/submit.php:528
msgid "Your cache log entry was posted successfully." msgid "Your cache log entry was posted successfully."
msgstr "Dein Log wurde veröffentlicht." msgstr "Dein Log wurde veröffentlicht."
#: c:\source\oc\server-3.0\htdocs\okapi/views/apps/authorize.tpl.php:5 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/views/apps/authorize.tpl.php:5
msgid "Authorization Form" msgid "Authorization Form"
msgstr "Authorisierungs-Formular" msgstr "Authorisierungs-Formular"
#: c:\source\oc\server-3.0\htdocs\okapi/views/apps/authorize.tpl.php:46 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/views/apps/authorize.tpl.php:46
msgid "Expired request" msgid "Expired request"
msgstr "Anfrage abgelaufen" msgstr "Anfrage abgelaufen"
#: c:\source\oc\server-3.0\htdocs\okapi/views/apps/authorize.tpl.php:47 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/views/apps/authorize.tpl.php:47
msgid "Unfortunately, the request has expired. Please try again." msgid "Unfortunately, the request has expired. Please try again."
msgstr "" msgstr ""
"Die Anfrage ist wegen Zeitüberschreitung abgelaufen. Bitte versuche es noch " "Die Anfrage ist wegen Zeitüberschreitung abgelaufen. Bitte versuche es noch "
"einmal." "einmal."
#: c:\source\oc\server-3.0\htdocs\okapi/views/apps/authorize.tpl.php:49 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/views/apps/authorize.tpl.php:49
msgid "External application is requesting access..." msgid "External application is requesting access..."
msgstr "Eine externe Anwendung wünscht Zugriff ..." msgstr "Eine externe Anwendung wünscht Zugriff ..."
#: c:\source\oc\server-3.0\htdocs\okapi/views/apps/authorize.tpl.php:50 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/views/apps/authorize.tpl.php:50
#, php-format #, php-format
msgid "" msgid ""
"<b>%s</b> wants to access your <b>%s</b> account. Do you agree to grant " "<b>%s</b> wants to access your <b>%s</b> account. Do you agree to grant "
@@ -245,28 +275,29 @@ msgstr ""
"<b>%s</b> möchte auf dein <b>%s</b>-Benutzerkonto zugreifen. Möchtest du " "<b>%s</b> möchte auf dein <b>%s</b>-Benutzerkonto zugreifen. Möchtest du "
"dieser Anwendung Zugriff gewähren?" "dieser Anwendung Zugriff gewähren?"
#: c:\source\oc\server-3.0\htdocs\okapi/views/apps/authorize.tpl.php:53 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/views/apps/authorize.tpl.php:53
msgid "I agree" msgid "I agree"
msgstr "Ja" msgstr "Ja"
#: c:\source\oc\server-3.0\htdocs\okapi/views/apps/authorize.tpl.php:54 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/views/apps/authorize.tpl.php:54
msgid "Decline" msgid "Decline"
msgstr "Nein" msgstr "Nein"
#: c:\source\oc\server-3.0\htdocs\okapi/views/apps/authorize.tpl.php:56 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/views/apps/authorize.tpl.php:56
#, php-format #, php-format
msgid "" msgid ""
"\n" "\n"
"\t\t\t\t\t<p>Once permission is granted it is valid until its withdrawal on\n" " <p>Once permission is granted it is valid until its "
"\t\t\t\t\tthe <a href='%s'>applications management</a> page.</p>\n" "withdrawal on\n"
"\t\t\t\t\t<p>The application will access your acount via <a href='%s'>the " " the <a href='%s'>applications management</a> page.</p>\n"
"OKAPI Framework</a>.\n" " <p>The application will access your acount via <a "
"\t\t\t\t\tIf you allow this request application will be able to access all " "href='%s'>the OKAPI Framework</a>.\n"
"methods delivered\n" " If you allow this request application will be able to "
"\t\t\t\t\tby the OKAPI Framework, i.e. post log entries on geocaches in your " "access all methods delivered\n"
"name.\n" " by the OKAPI Framework, i.e. post log entries on "
"\t\t\t\t\tYou can revoke this permission at any moment.</p>\n" "geocaches in your name.\n"
"\t\t\t\t" " You can revoke this permission at any moment.</p>\n"
" "
msgstr "" msgstr ""
"\n" "\n"
"\t\t\t\t\t<p>Wenn die Erlaubnis erteilt wurde, ist sie so lange gültig, bis " "\t\t\t\t\t<p>Wenn die Erlaubnis erteilt wurde, ist sie so lange gültig, bis "
@@ -281,23 +312,23 @@ msgstr ""
"\t\t\t\t\tDu kannst diese Erlaubnis jederzeit widerrufen.</p>\n" "\t\t\t\t\tDu kannst diese Erlaubnis jederzeit widerrufen.</p>\n"
"\t\t\t\t" "\t\t\t\t"
#: c:\source\oc\server-3.0\htdocs\okapi/views/apps/authorized.tpl.php:5 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/views/apps/authorized.tpl.php:5
msgid "Authorization Succeeded" msgid "Authorization Succeeded"
msgstr "Authorisierung erfolgreich" msgstr "Authorisierung erfolgreich"
#: c:\source\oc\server-3.0\htdocs\okapi/views/apps/authorized.tpl.php:28 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/views/apps/authorized.tpl.php:28
msgid "Access successfully granted" msgid "Access successfully granted"
msgstr "Zugang wurde gewährt" msgstr "Zugang wurde gewährt"
#: c:\source\oc\server-3.0\htdocs\okapi/views/apps/authorized.tpl.php:29 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/views/apps/authorized.tpl.php:29
#, php-format #, php-format
msgid "" msgid ""
"\n" "\n"
"\t\t\t\t<p><b>You've just granted %s application access to your %s account.</" " <p><b>You've just granted %s application access to your %s "
"b>\n" "account.</b>\n"
"\t\t\t\tTo complete the operation, go back to %s and enter the following PIN " " To complete the operation, go back to %s and enter the "
"code:</p>\n" "following PIN code:</p>\n"
"\t\t\t" " "
msgstr "" msgstr ""
"\n" "\n"
"\t\t\t\t<p><b>Du hast der Anwendung \"%s\" Zugriff auf dein %s-Benutzerkonto " "\t\t\t\t<p><b>Du hast der Anwendung \"%s\" Zugriff auf dein %s-Benutzerkonto "
@@ -306,26 +337,26 @@ msgstr ""
"PIN-Code ein:</p>\n" "PIN-Code ein:</p>\n"
"\t\t\t" "\t\t\t"
#: c:\source\oc\server-3.0\htdocs\okapi/views/apps/index.tpl.php:5 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/views/apps/index.tpl.php:5
msgid "My Apps" msgid "My Apps"
msgstr "Meine Apps" msgstr "Meine Apps"
#: c:\source\oc\server-3.0\htdocs\okapi/views/apps/index.tpl.php:29 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/views/apps/index.tpl.php:29
msgid "Your external applications" msgid "Your external applications"
msgstr "Deine externen Anwendungen" msgstr "Deine externen Anwendungen"
#: c:\source\oc\server-3.0\htdocs\okapi/views/apps/index.tpl.php:31 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/views/apps/index.tpl.php:31
#, php-format #, php-format
msgid "" msgid ""
"\n" "\n"
"\t\t\t\t\t<p>This is the list of applications which you granted access to " " <p>This is the list of applications which you granted "
"your <b>%s</b> account.\n" "access to your <b>%s</b> account.\n"
"\t\t\t\t\tThis page gives you the abbility to revoke all previously granted " " This page gives you the abbility to revoke all "
"privileges.\n" "previously granted privileges.\n"
"\t\t\t\t\tOnce you click \"remove\" the application will no longer be able " " Once you click \"remove\" the application will no longer "
"to perform any\n" "be able to perform any\n"
"\t\t\t\t\tactions on your behalf.</p>\n" " actions on your behalf.</p>\n"
"\t\t\t\t" " "
msgstr "" msgstr ""
"\n" "\n"
"\t\t\t\t\t<p>Dies ist eine Liste der Anwendungen, denen du Zugriff auf dein " "\t\t\t\t\t<p>Dies ist eine Liste der Anwendungen, denen du Zugriff auf dein "
@@ -336,21 +367,21 @@ msgstr ""
"Aktionen mehr unter deinem \t\t\t\t\tBenutzername ausführen können.</p>\n" "Aktionen mehr unter deinem \t\t\t\t\tBenutzername ausführen können.</p>\n"
"\t\t\t\t" "\t\t\t\t"
#: c:\source\oc\server-3.0\htdocs\okapi/views/apps/index.tpl.php:45 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/views/apps/index.tpl.php:45
msgid "remove" msgid "remove"
msgstr "entfernen" msgstr "entfernen"
#: c:\source\oc\server-3.0\htdocs\okapi/views/apps/index.tpl.php:50 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/views/apps/index.tpl.php:50
#, php-format #, php-format
msgid "" msgid ""
"\n" "\n"
"\t\t\t\t\t<p>Thanks to the <a href='%s'>OKAPI Framework</a> you can grant " " <p>Thanks to the <a href='%s'>OKAPI Framework</a> you "
"external applications\n" "can grant external applications\n"
"\t\t\t\t\taccess to your <b>%s</b> account. Currently no applications are " " access to your <b>%s</b> account. Currently no "
"authorized to act\n" "applications are authorized to act\n"
"\t\t\t\t\ton your behalf. Once you start using external Opencaching " " on your behalf. Once you start using external "
"applications, they will appear here.</p>\n" "Opencaching applications, they will appear here.</p>\n"
"\t\t\t\t" " "
msgstr "" msgstr ""
"\n" "\n"
"\t\t\t\t\t<p>Die <a href='%s'>OKAPI-Schnittstelle</a> ermöglichst es dir, " "\t\t\t\t\t<p>Die <a href='%s'>OKAPI-Schnittstelle</a> ermöglichst es dir, "

View File

@@ -2,11 +2,11 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: OKAPI\n" "Project-Id-Version: OKAPI\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2013-07-19 08:41+0100\n" "POT-Creation-Date: 2014-01-23 15:53+0100\n"
"PO-Revision-Date: 2013-07-19 08:46+0100\n" "PO-Revision-Date: 2014-01-23 16:04+0100\n"
"Last-Translator: faina09 <stefanocotterli@gmail.com>\n" "Last-Translator: Wojciech Rygielski <rygielski@mimuw.edu.pl>\n"
"Language-Team: following <following@online.de>\n" "Language-Team: following <following@online.de>\n"
"Language: Italian\n" "Language: it\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
@@ -14,22 +14,31 @@ msgstr ""
"X-Poedit-Basepath: .\n" "X-Poedit-Basepath: .\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Poedit-SourceCharset: UTF-8\n" "X-Poedit-SourceCharset: UTF-8\n"
"X-Generator: Poedit 1.5.7\n" "X-Generator: Poedit 1.6.3\n"
"X-Poedit-SearchPath-0: c:\\source\\okapi\\following2\n" "X-Poedit-SearchPath-0: c:\\source\\okapi\\following2\n"
"X-Poedit-SearchPath-1: D:\\PRIV\\Projekty\\EclipseWorkspace\\opencaching-api" "X-Poedit-SearchPath-1: D:\\PRIV\\Projekty\\EclipseWorkspace\\opencaching-api"
"\\okapi\n" "\\okapi\n"
"X-Poedit-SearchPath-2: C:\\Users\\stefano.cotterli\\Desktop\\opencaching-" "X-Poedit-SearchPath-2: C:\\Users\\stefano.cotterli\\Desktop\\opencaching-"
"api\n" "api\n"
#: C:\Users\stefano.cotterli\Desktop\opencaching-api/okapi/services/caches/geocaches.php:956 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/caches/geocaches.php:957
msgid "Stage" msgid "Stage"
msgstr "Passo" msgstr "Passo"
#: C:\Users\stefano.cotterli\Desktop\opencaching-api/okapi/services/caches/geocaches.php:1111 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/caches/geocaches.php:986
msgid "User location"
msgstr ""
#: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/caches/geocaches.php:989
#, php-format
msgid "Your own custom coordinates for the %s geocache"
msgstr ""
#: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/caches/geocaches.php:1148
msgid "National Park / Landscape" msgid "National Park / Landscape"
msgstr "Parco Nazionale / Paesaggio" msgstr "Parco Nazionale / Paesaggio"
#: C:\Users\stefano.cotterli\Desktop\opencaching-api/okapi/services/caches/geocaches.php:1263 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/caches/geocaches.php:1300
#, php-format #, php-format
msgid "" msgid ""
"This <a href='%s'>geocache</a> description comes from the <a href='%s'>%s</" "This <a href='%s'>geocache</a> description comes from the <a href='%s'>%s</"
@@ -38,7 +47,7 @@ msgstr ""
"La deescrizione di questa <a href='%s'>geocache</a>proviene dal sito <a " "La deescrizione di questa <a href='%s'>geocache</a>proviene dal sito <a "
"href='%s'>%s</a>." "href='%s'>%s</a>."
#: C:\Users\stefano.cotterli\Desktop\opencaching-api/okapi/services/caches/geocaches.php:1275 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/caches/geocaches.php:1312
#, php-format #, php-format
msgid "" msgid ""
"&copy; <a href='%s'>%s</a>, <a href='%s'>%s</a>, <a href='http://" "&copy; <a href='%s'>%s</a>, <a href='%s'>%s</a>, <a href='http://"
@@ -46,7 +55,7 @@ msgid ""
"%s; all log entries &copy; their authors" "%s; all log entries &copy; their authors"
msgstr "" msgstr ""
#: C:\Users\stefano.cotterli\Desktop\opencaching-api/okapi/services/caches/geocaches.php:1286 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/caches/geocaches.php:1323
#, php-format #, php-format
msgid "" msgid ""
"&copy; <a href='%s'>%s</a>, <a href='%s'>%s</a>, <a href='http://" "&copy; <a href='%s'>%s</a>, <a href='%s'>%s</a>, <a href='http://"
@@ -54,62 +63,83 @@ msgid ""
"log entries &copy; their authors" "log entries &copy; their authors"
msgstr "" msgstr ""
#: C:\Users\stefano.cotterli\Desktop\opencaching-api/okapi/services/caches/formatters/gpxfile.tpl.php:31 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/caches/formatters/gpx.php:360
#: C:\Users\stefano.cotterli\Desktop\opencaching-api/okapi/services/caches/formatters/gpxfile.tpl.php:60 msgid ""
"<b>Geocache coordinates have been changed.</b> They have been replaced with "
"your own custom coordinates which you have provided for this geocache."
msgstr ""
#: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/caches/formatters/gpx.php:366
msgid ""
"<b>Geocache coordinates have been changed.</b> Currently they point to one "
"of the alternate waypoints originally described as:"
msgstr ""
#: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/caches/formatters/gpx.php:379
msgid "Original geocache location"
msgstr ""
#: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/caches/formatters/gpx.php:381
#, php-format
msgid "Original (owner-supplied) location of the %s geocache"
msgstr ""
#: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/caches/formatters/gpxfile.tpl.php:30
#: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/caches/formatters/gpxfile.tpl.php:62
msgid "hidden by" msgid "hidden by"
msgstr "nascosta da" msgstr "nascosta da"
#: C:\Users\stefano.cotterli\Desktop\opencaching-api/okapi/services/caches/formatters/gpxfile.tpl.php:62 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/caches/formatters/gpxfile.tpl.php:64
#, php-format #, php-format
msgid "%d recommendation" msgid "%d recommendation"
msgid_plural "%d recommendations" msgid_plural "%d recommendations"
msgstr[0] "%d raccomandazione" msgstr[0] "%d raccomandazione"
msgstr[1] "%d raccomandazioni" msgstr[1] "%d raccomandazioni"
#: C:\Users\stefano.cotterli\Desktop\opencaching-api/okapi/services/caches/formatters/gpxfile.tpl.php:63 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/caches/formatters/gpxfile.tpl.php:65
#, php-format #, php-format
msgid "found %d time" msgid "found %d time"
msgid_plural "found %d times" msgid_plural "found %d times"
msgstr[0] "trovata %d volta" msgstr[0] "trovata %d volta"
msgstr[1] "trovata %d volte" msgstr[1] "trovata %d volte"
#: C:\Users\stefano.cotterli\Desktop\opencaching-api/okapi/services/caches/formatters/gpxfile.tpl.php:66 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/caches/formatters/gpxfile.tpl.php:68
#, php-format #, php-format
msgid "%d trackable" msgid "%d trackable"
msgid_plural "%d trackables" msgid_plural "%d trackables"
msgstr[0] "%d travel bug" msgstr[0] "%d travel bug"
msgstr[1] "%d travel bugs" msgstr[1] "%d travel bugs"
#: C:\Users\stefano.cotterli\Desktop\opencaching-api/okapi/services/caches/formatters/gpxfile.tpl.php:70 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/caches/formatters/gpxfile.tpl.php:72
msgid "Personal notes" msgid "Personal notes"
msgstr "Note personali" msgstr "Note personali"
#: C:\Users\stefano.cotterli\Desktop\opencaching-api/okapi/services/caches/formatters/gpxfile.tpl.php:74 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/caches/formatters/gpxfile.tpl.php:76
msgid "Attributes" msgid "Attributes"
msgstr "Attributi" msgstr "Attributi"
#: C:\Users\stefano.cotterli\Desktop\opencaching-api/okapi/services/caches/formatters/gpxfile.tpl.php:78 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/caches/formatters/gpxfile.tpl.php:80
msgid "Trackables" msgid "Trackables"
msgstr "Travel bugs" msgstr "Travel bugs"
#: C:\Users\stefano.cotterli\Desktop\opencaching-api/okapi/services/caches/formatters/gpxfile.tpl.php:88 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/caches/formatters/gpxfile.tpl.php:90
#: C:\Users\stefano.cotterli\Desktop\opencaching-api/okapi/services/caches/formatters/gpxfile.tpl.php:104 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/caches/formatters/gpxfile.tpl.php:106
msgid "Images" msgid "Images"
msgstr "Immagini" msgstr "Immagini"
#: C:\Users\stefano.cotterli\Desktop\opencaching-api/okapi/services/caches/formatters/gpxfile.tpl.php:111 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/caches/formatters/gpxfile.tpl.php:113
msgid "Spoilers" msgid "Spoilers"
msgstr "Spoiler" msgstr "Spoiler"
#: C:\Users\stefano.cotterli\Desktop\opencaching-api/okapi/services/caches/formatters/gpxfile.tpl.php:120 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/caches/formatters/gpxfile.tpl.php:122
msgid "Image descriptions" msgid "Image descriptions"
msgstr "Descrizione immagine" msgstr "Descrizione immagine"
#: C:\Users\stefano.cotterli\Desktop\opencaching-api/okapi/services/caches/formatters/gpxfile.tpl.php:128 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/caches/formatters/gpxfile.tpl.php:130
msgid "The cache probably is located in the following protection areas:" msgid "The cache probably is located in the following protection areas:"
msgstr "La cache probabilmente &egrave; situata nelle seguenti aree protette:" msgstr "La cache probabilmente &egrave; situata nelle seguenti aree protette:"
#: C:\Users\stefano.cotterli\Desktop\opencaching-api/okapi/services/logs/submit.php:70 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/logs/submit.php:70
msgid "" msgid ""
"You are trying to publish a log entry with a date in future. Cache log " "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." "entries are allowed to be published in the past, but NOT in the future."
@@ -117,7 +147,7 @@ msgstr ""
"Hai cercato di pubblicare un log con una data nel futuro. E' permsso loggare " "Hai cercato di pubblicare un log con una data nel futuro. E' permsso loggare "
"una cache con una data passata, ma NON futura." "una cache con una data passata, ma NON futura."
#: C:\Users\stefano.cotterli\Desktop\opencaching-api/okapi/services/logs/submit.php:92 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/logs/submit.php:92
#, php-format #, php-format
msgid "" msgid ""
"However, your cache rating was ignored, because %s does not have a rating " "However, your cache rating was ignored, because %s does not have a rating "
@@ -126,7 +156,7 @@ msgstr ""
"Tuttavia, la tua valutazione della cache sarà ignorata, perché %s non ha un " "Tuttavia, la tua valutazione della cache sarà ignorata, perché %s non ha un "
"sistema di valutazione." "sistema di valutazione."
#: C:\Users\stefano.cotterli\Desktop\opencaching-api/okapi/services/logs/submit.php:111 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/logs/submit.php:111
#, php-format #, php-format
msgid "" msgid ""
"However, your cache recommendation was ignored, because %s does not allow " "However, your cache recommendation was ignored, because %s does not allow "
@@ -135,7 +165,7 @@ msgstr ""
"Tuttavia, la tua valutazione della cache sarà ignorata, perché %s non ha " "Tuttavia, la tua valutazione della cache sarà ignorata, perché %s non ha "
"permette la valutazione di cache evento." "permette la valutazione di cache evento."
#: C:\Users\stefano.cotterli\Desktop\opencaching-api/okapi/services/logs/submit.php:125 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/logs/submit.php:125
#, php-format #, php-format
msgid "" msgid ""
"However, your \"needs maintenance\" flag was ignored, because %s does not " "However, your \"needs maintenance\" flag was ignored, because %s does not "
@@ -144,7 +174,7 @@ msgstr ""
"Tuttavia, la tua segnalazione di \"manutenzione necessaria\" sarà ignorata, " "Tuttavia, la tua segnalazione di \"manutenzione necessaria\" sarà ignorata, "
"perché %s non supporta questa opzione." "perché %s non supporta questa opzione."
#: C:\Users\stefano.cotterli\Desktop\opencaching-api/okapi/services/logs/submit.php:145 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/logs/submit.php:145
msgid "" msgid ""
"This cache is an Event cache. You cannot \"Find\" it (but you can attend it, " "This cache is an Event cache. You cannot \"Find\" it (but you can attend it, "
"or comment on it)!" "or comment on it)!"
@@ -152,7 +182,7 @@ msgstr ""
"Questa cache è una cache Evento. Non puoi \"Trovarla\" (ma puoi partecipare, " "Questa cache è una cache Evento. Non puoi \"Trovarla\" (ma puoi partecipare, "
"o commentare l'evento)!" "o commentare l'evento)!"
#: C:\Users\stefano.cotterli\Desktop\opencaching-api/okapi/services/logs/submit.php:150 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/logs/submit.php:150
msgid "" msgid ""
"This cache is NOT an Event cache. You cannot \"Attend\" it (but you can find " "This cache is NOT an Event cache. You cannot \"Attend\" it (but you can find "
"it, or comment on it)!" "it, or comment on it)!"
@@ -160,23 +190,23 @@ msgstr ""
"Questa cache NON è una cache Evento. Non puoi \"Partecipare\" (ma puoi " "Questa cache NON è una cache Evento. Non puoi \"Partecipare\" (ma puoi "
"trovarla, o inserirci un commento)!" "trovarla, o inserirci un commento)!"
#: C:\Users\stefano.cotterli\Desktop\opencaching-api/okapi/services/logs/submit.php:155 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/logs/submit.php:155
msgid "Your have to supply some text for your comment." msgid "Your have to supply some text for your comment."
msgstr "Devi inserire del testo per il tuo commento." msgstr "Devi inserire del testo per il tuo commento."
#: C:\Users\stefano.cotterli\Desktop\opencaching-api/okapi/services/logs/submit.php:168 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/logs/submit.php:168
msgid "This cache requires a password. You didn't provide one!" msgid "This cache requires a password. You didn't provide one!"
msgstr "Questa cache richiede una password, ma non ne hai fornita nessuuna." msgstr "Questa cache richiede una password, ma non ne hai fornita nessuuna."
#: C:\Users\stefano.cotterli\Desktop\opencaching-api/okapi/services/logs/submit.php:170 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/logs/submit.php:170
msgid "Invalid password!" msgid "Invalid password!"
msgstr "Password non valida!" msgstr "Password non valida!"
#: C:\Users\stefano.cotterli\Desktop\opencaching-api/okapi/services/logs/submit.php:282 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/logs/submit.php:285
msgid "You have already submitted a log entry with exactly the same contents." msgid "You have already submitted a log entry with exactly the same contents."
msgstr "Hai già inserito un log esattamente con lo stesso contenuto." msgstr "Hai già inserito un log esattamente con lo stesso contenuto."
#: C:\Users\stefano.cotterli\Desktop\opencaching-api/okapi/services/logs/submit.php:305 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/logs/submit.php:308
msgid "" msgid ""
"You have already submitted a \"Found it\" log entry once. Now you may submit " "You have already submitted a \"Found it\" log entry once. Now you may submit "
"\"Comments\" only!" "\"Comments\" only!"
@@ -184,49 +214,49 @@ msgstr ""
"Hai già inserito un log \"Trovata\". Adesso puoi inserire solamente " "Hai già inserito un log \"Trovata\". Adesso puoi inserire solamente "
"\"Commenti\"!" "\"Commenti\"!"
#: C:\Users\stefano.cotterli\Desktop\opencaching-api/okapi/services/logs/submit.php:307 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/logs/submit.php:310
msgid "You are the owner of this cache. You may submit \"Comments\" only!" msgid "You are the owner of this cache. You may submit \"Comments\" only!"
msgstr "Sei il proprietario della cache. Puoi inserire solamente \"Commenti\"!" msgstr "Sei il proprietario della cache. Puoi inserire solamente \"Commenti\"!"
#: C:\Users\stefano.cotterli\Desktop\opencaching-api/okapi/services/logs/submit.php:325 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/logs/submit.php:328
msgid "You have already rated this cache once. Your rating cannot be changed." msgid "You have already rated this cache once. Your rating cannot be changed."
msgstr "" msgstr ""
"Hai giàvalutato questa cache. La tua valutazione non può essere cambiata." "Hai giàvalutato questa cache. La tua valutazione non può essere cambiata."
#: C:\Users\stefano.cotterli\Desktop\opencaching-api/okapi/services/logs/submit.php:342 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/logs/submit.php:345
msgid "You have already recommended this cache once." msgid "You have already recommended this cache once."
msgstr "Hai già raccmandato questa cache." msgstr "Hai già raccmandato questa cache."
#: C:\Users\stefano.cotterli\Desktop\opencaching-api/okapi/services/logs/submit.php:352 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/logs/submit.php:355
msgid "You don't have any recommendations to give. Find more caches first!" msgid "You don't have any recommendations to give. Find more caches first!"
msgstr "" msgstr ""
"Non hai possibilità di dare raccomandazioni. Prima devio trovare altre cache!" "Non hai possibilità di dare raccomandazioni. Prima devio trovare altre cache!"
#: C:\Users\stefano.cotterli\Desktop\opencaching-api/okapi/services/logs/submit.php:395 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/logs/submit.php:398
msgid "Event caches cannot \"need maintenance\"." msgid "Event caches cannot \"need maintenance\"."
msgstr "Le cache evento non necessitano di manutenzione!" msgstr "Le cache evento non necessitano di manutenzione!"
#: C:\Users\stefano.cotterli\Desktop\opencaching-api/okapi/services/logs/submit.php:525 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/logs/submit.php:528
msgid "Your cache log entry was posted successfully." msgid "Your cache log entry was posted successfully."
msgstr "Il tuo log sulla cache è stato correttamente inserito." msgstr "Il tuo log sulla cache è stato correttamente inserito."
#: C:\Users\stefano.cotterli\Desktop\opencaching-api/okapi/views/apps/authorize.tpl.php:5 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/views/apps/authorize.tpl.php:5
msgid "Authorization Form" msgid "Authorization Form"
msgstr "Modulo di autorizzazione" msgstr "Modulo di autorizzazione"
#: C:\Users\stefano.cotterli\Desktop\opencaching-api/okapi/views/apps/authorize.tpl.php:46 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/views/apps/authorize.tpl.php:46
msgid "Expired request" msgid "Expired request"
msgstr "Richiesta scaduta" msgstr "Richiesta scaduta"
#: C:\Users\stefano.cotterli\Desktop\opencaching-api/okapi/views/apps/authorize.tpl.php:47 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/views/apps/authorize.tpl.php:47
msgid "Unfortunately, the request has expired. Please try again." msgid "Unfortunately, the request has expired. Please try again."
msgstr "Sfortunatamente, la richiesta è scaduta. Per favore riprova." msgstr "Sfortunatamente, la richiesta è scaduta. Per favore riprova."
#: C:\Users\stefano.cotterli\Desktop\opencaching-api/okapi/views/apps/authorize.tpl.php:49 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/views/apps/authorize.tpl.php:49
msgid "External application is requesting access..." msgid "External application is requesting access..."
msgstr "Una applicazione esterna sta richiedendo l'accesso." msgstr "Una applicazione esterna sta richiedendo l'accesso."
#: C:\Users\stefano.cotterli\Desktop\opencaching-api/okapi/views/apps/authorize.tpl.php:50 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/views/apps/authorize.tpl.php:50
#, php-format #, php-format
msgid "" msgid ""
"<b>%s</b> wants to access your <b>%s</b> account. Do you agree to grant " "<b>%s</b> wants to access your <b>%s</b> account. Do you agree to grant "
@@ -235,122 +265,118 @@ msgstr ""
"<b>%s</b> vuole accedere al tuo account <b>%s</b>. Sei d'accordo nel " "<b>%s</b> vuole accedere al tuo account <b>%s</b>. Sei d'accordo nel "
"concedere l'accesso a questa applicazione?" "concedere l'accesso a questa applicazione?"
#: C:\Users\stefano.cotterli\Desktop\opencaching-api/okapi/views/apps/authorize.tpl.php:53 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/views/apps/authorize.tpl.php:53
msgid "I agree" msgid "I agree"
msgstr "Sono d'accordo" msgstr "Sono d'accordo"
#: C:\Users\stefano.cotterli\Desktop\opencaching-api/okapi/views/apps/authorize.tpl.php:54 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/views/apps/authorize.tpl.php:54
msgid "Decline" msgid "Decline"
msgstr "No" msgstr "No"
#: C:\Users\stefano.cotterli\Desktop\opencaching-api/okapi/views/apps/authorize.tpl.php:56 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/views/apps/authorize.tpl.php:56
#, php-format #, php-format
msgid "" msgid ""
"\n" "\n"
"\t\t\t\t\t<p>Once permission is granted it is valid until its withdrawal on\n" " <p>Once permission is granted it is valid until its "
"\t\t\t\t\tthe <a href='%s'>applications management</a> page.</p>\n" "withdrawal on\n"
"\t\t\t\t\t<p>The application will access your acount via <a href='%s'>the " " the <a href='%s'>applications management</a> page.</p>\n"
"OKAPI Framework</a>.\n" " <p>The application will access your acount via <a "
"\t\t\t\t\tIf you allow this request application will be able to access all " "href='%s'>the OKAPI Framework</a>.\n"
"methods delivered\n" " If you allow this request application will be able to "
"\t\t\t\t\tby the OKAPI Framework, i.e. post log entries on geocaches in your " "access all methods delivered\n"
"name.\n" " by the OKAPI Framework, i.e. post log entries on "
"\t\t\t\t\tYou can revoke this permission at any moment.</p>\n" "geocaches in your name.\n"
"\t\t\t\t" " You can revoke this permission at any moment.</p>\n"
" "
msgstr "" msgstr ""
"\n" "\n"
"\t\t\t\t\t<p>Una volta concessa, l'autorizzazoine è valida finché non venga\n" "<p>Una volta concessa, l'autorizzazoine è valida finché non venga\n"
"\t\t\t\t\trevocata nella pagina di <a href='%s'>gestione applicazioni</a>.</" "revocata nella pagina di <a href='%s'>gestione applicazioni</a>.</p>\n"
"p>\n" "<p>L'applicazione accederà al tuo account tramite il <a href='%s'>frameword "
"\t\t\t\t\t<p>L'applicazione accederà al tuo account tramite il <a " "OKAPI</a>.\n"
"href='%s'>frameword OKAPI</a>.\n" "Se permetti questa richiesta, l'applicazione potrà accedere a tutti i metodi "
"\t\t\t\t\tSe permetti questa richiesta, l'applicazione potrà accedere a " "forniti\n"
"tutti i metodi forniti\n" "dal framework OKAPI, per es. postare i log delle geocache a tuo nome..\n"
"\t\t\t\t\tdal framework OKAPI, per es. postare i log delle geocache a tuo " "Puoi revocare questo permesso in qualsiasi momento.</p>"
"nome..\n"
"\t\t\t\t\tPuoi revocare questo permesso in qualsiasi momento.</p>\n"
"\t\t\t\t"
#: C:\Users\stefano.cotterli\Desktop\opencaching-api/okapi/views/apps/authorized.tpl.php:5 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/views/apps/authorized.tpl.php:5
msgid "Authorization Succeeded" msgid "Authorization Succeeded"
msgstr "Autorizzazione concessa" msgstr "Autorizzazione concessa"
#: C:\Users\stefano.cotterli\Desktop\opencaching-api/okapi/views/apps/authorized.tpl.php:28 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/views/apps/authorized.tpl.php:28
msgid "Access successfully granted" msgid "Access successfully granted"
msgstr "Accesso correttamente consentito" msgstr "Accesso correttamente consentito"
#: C:\Users\stefano.cotterli\Desktop\opencaching-api/okapi/views/apps/authorized.tpl.php:29 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/views/apps/authorized.tpl.php:29
#, php-format #, php-format
msgid "" msgid ""
"\n" "\n"
"\t\t\t\t<p><b>You've just granted %s application access to your %s account.</" " <p><b>You've just granted %s application access to your %s "
"b>\n" "account.</b>\n"
"\t\t\t\tTo complete the operation, go back to %s and enter the following PIN " " To complete the operation, go back to %s and enter the "
"code:</p>\n" "following PIN code:</p>\n"
"\t\t\t" " "
msgstr "" msgstr ""
"\n" "\n"
"\t\t\t\t<p><b>Hai appena concesso all'applicazione \"%s\" l'accesso al tuo " "<p><b>Hai appena concesso all'applicazione \"%s\" l'accesso al tuo account "
"account %s.</b>\n" "%s.</b>\n"
"\t\t\t\tPer completare l'operazione, riorna a %s e inserisci il seguente " "Per completare l'operazione, riorna a %s e inserisci il seguente codice PIN:"
"codice PIN:</p>\n" "</p>"
"\t\t\t"
#: C:\Users\stefano.cotterli\Desktop\opencaching-api/okapi/views/apps/index.tpl.php:5 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/views/apps/index.tpl.php:5
msgid "My Apps" msgid "My Apps"
msgstr "Mie App" msgstr "Mie App"
#: C:\Users\stefano.cotterli\Desktop\opencaching-api/okapi/views/apps/index.tpl.php:29 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/views/apps/index.tpl.php:29
msgid "Your external applications" msgid "Your external applications"
msgstr "Le tue applicazioni esterne" msgstr "Le tue applicazioni esterne"
#: C:\Users\stefano.cotterli\Desktop\opencaching-api/okapi/views/apps/index.tpl.php:31 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/views/apps/index.tpl.php:31
#, php-format #, php-format
msgid "" msgid ""
"\n" "\n"
"\t\t\t\t\t<p>This is the list of applications which you granted access to " " <p>This is the list of applications which you granted "
"your <b>%s</b> account.\n" "access to your <b>%s</b> account.\n"
"\t\t\t\t\tThis page gives you the abbility to revoke all previously granted " " This page gives you the abbility to revoke all "
"privileges.\n" "previously granted privileges.\n"
"\t\t\t\t\tOnce you click \"remove\" the application will no longer be able " " Once you click \"remove\" the application will no longer "
"to perform any\n" "be able to perform any\n"
"\t\t\t\t\tactions on your behalf.</p>\n" " actions on your behalf.</p>\n"
"\t\t\t\t" " "
msgstr "" msgstr ""
"\n" "\n"
"\t\t\t\t\t<p>Questa è la lista delle applicazioni a cui hai concesso " "<p>Questa è la lista delle applicazioni a cui hai concesso l'accesso al tuo "
"l'accesso al tuo account <b>%s</b.\n" "account <b>%s</b.\n"
"\t\t\t\t\tQuesta pagina ti da la possibilità di revocare tutti privilegi " "Questa pagina ti da la possibilità di revocare tutti privilegi "
"precedentemente concessi.\n" "precedentemente concessi.\n"
"\t\t\t\t\tQundo clicchi su \"rimuovi\" all'applicazione non sarà più " "Qundo clicchi su \"rimuovi\" all'applicazione non sarà più concesso eseguire "
"concesso eseguire nessuna\n" "nessuna\n"
" \t\t\t\t\tazione per tuo conto.</p>\n" "azione per tuo conto.</p>"
"\t\t\t\t"
#: C:\Users\stefano.cotterli\Desktop\opencaching-api/okapi/views/apps/index.tpl.php:45 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/views/apps/index.tpl.php:45
msgid "remove" msgid "remove"
msgstr "entfernen" msgstr "entfernen"
#: C:\Users\stefano.cotterli\Desktop\opencaching-api/okapi/views/apps/index.tpl.php:50 #: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/views/apps/index.tpl.php:50
#, php-format #, php-format
msgid "" msgid ""
"\n" "\n"
"\t\t\t\t\t<p>Thanks to the <a href='%s'>OKAPI Framework</a> you can grant " " <p>Thanks to the <a href='%s'>OKAPI Framework</a> you "
"external applications\n" "can grant external applications\n"
"\t\t\t\t\taccess to your <b>%s</b> account. Currently no applications are " " access to your <b>%s</b> account. Currently no "
"authorized to act\n" "applications are authorized to act\n"
"\t\t\t\t\ton your behalf. Once you start using external Opencaching " " on your behalf. Once you start using external "
"applications, they will appear here.</p>\n" "Opencaching applications, they will appear here.</p>\n"
"\t\t\t\t" " "
msgstr "" msgstr ""
"\n" "\n"
"\t\t\t\t\t<p>Grazie al <a href='%s'>framework OKAPI</a>puoi concedere ad " "<p>Grazie al <a href='%s'>framework OKAPI</a>puoi concedere ad applicazioni "
"applicazioni esterne\n" "esterne\n"
"\t\t\t\t\t l'accesso al tuo account <b>%s</b>. Attualmente non ci son " "l'accesso al tuo account <b>%s</b>. Attualmente non ci son applicazioni "
"applicazioni autorizzate\n" "autorizzate\n"
"\t\t\t\t\tad agire per tuo conto. Quando userai applicazioni esterne a " "ad agire per tuo conto. Quando userai applicazioni esterne a Opencaching, "
"Opencaching, queste appariranno qui</p>\n" "queste appariranno qui</p>\n"
"\t\t\t\t" " "
#~ msgid "Recommending is allowed only for 'Found it' logtypes." #~ msgid "Recommending is allowed only for 'Found it' logtypes."
#~ msgstr "Le raccomandazioni sono ammesse solo per i log di tipo 'Trovata'" #~ msgstr "Le raccomandazioni sono ammesse solo per i log di tipo 'Trovata'"

View File

@@ -4,53 +4,53 @@ namespace okapi;
class Locales class Locales
{ {
public static $languages = array( public static $languages = array(
'pl' => array('lang' => 'pl', 'locale' => 'pl_PL.utf8', 'name' => 'Polish'), 'pl' => array('lang' => 'pl', 'locale' => 'pl_PL.utf8', 'name' => 'Polish'),
'en' => array('lang' => 'en', 'locale' => 'en_US.utf8', 'name' => 'English'), 'en' => array('lang' => 'en', 'locale' => 'en_US.utf8', 'name' => 'English'),
'nl' => array('lang' => 'nl', 'locale' => 'nl_NL.utf8', 'name' => 'Dutch'), 'nl' => array('lang' => 'nl', 'locale' => 'nl_NL.utf8', 'name' => 'Dutch'),
'de' => array('lang' => 'de', 'locale' => 'de_DE.utf8', 'name' => 'German'), 'de' => array('lang' => 'de', 'locale' => 'de_DE.utf8', 'name' => 'German'),
'it' => array('lang' => 'it', 'locale' => 'it_IT.utf8', 'name' => 'Italian'), 'it' => array('lang' => 'it', 'locale' => 'it_IT.utf8', 'name' => 'Italian'),
); );
/** /**
* Get the list of locales that should be installed on the system in order * Get the list of locales that should be installed on the system in order
* for all translations to work properly. * for all translations to work properly.
*/ */
public static function get_required_locales() public static function get_required_locales()
{ {
$arr = array('POSIX'); $arr = array('POSIX');
foreach (self::$languages as $key => $value) foreach (self::$languages as $key => $value)
$arr[] = $value['locale']; $arr[] = $value['locale'];
return $arr; return $arr;
} }
/** /**
* Get the list of locales installed on the current system. * Get the list of locales installed on the current system.
*/ */
public static function get_installed_locales() public static function get_installed_locales()
{ {
$arr = array(); $arr = array();
foreach (explode("\n", shell_exec("locale -a")) as $item) foreach (explode("\n", shell_exec("locale -a")) as $item)
if ($item) if ($item)
$arr[] = $item; $arr[] = $item;
return $arr; return $arr;
} }
private static function get_locale_for_language($lang) private static function get_locale_for_language($lang)
{ {
if (isset(self::$languages[$lang])) if (isset(self::$languages[$lang]))
return self::$languages[$lang]['locale']; return self::$languages[$lang]['locale'];
return null; return null;
} }
public static function get_best_locale($langprefs) public static function get_best_locale($langprefs)
{ {
foreach ($langprefs as $lang) foreach ($langprefs as $lang)
{ {
$locale = self::get_locale_for_language($lang); $locale = self::get_locale_for_language($lang);
if ($locale != null) if ($locale != null)
return $locale; return $locale;
} }
return self::$languages['en']['locale']; return self::$languages['en']['locale'];
} }
} }

View File

@@ -2,9 +2,9 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: OKAPI\n" "Project-Id-Version: OKAPI\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2013-04-10 01:08+0100\n" "POT-Creation-Date: 2014-01-23 16:05+0100\n"
"PO-Revision-Date: 2013-04-10 01:12+0100\n" "PO-Revision-Date: 2014-08-09 03:47+0100\n"
"Last-Translator: Wojciech Rygielski <rygielski@mimuw.edu.pl>\n" "Last-Translator: Harrie Klomp <harrie@harrieklomp.be>\n"
"Language-Team: \n" "Language-Team: \n"
"Language: nl_NL\n" "Language: nl_NL\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@@ -13,50 +13,96 @@ msgstr ""
"X-Poedit-KeywordsList: _;gettext;gettext_noop\n" "X-Poedit-KeywordsList: _;gettext;gettext_noop\n"
"X-Poedit-Basepath: D:\\PRIV\\Projekty\\EclipseWorkspace\\opencaching-api\\\n" "X-Poedit-Basepath: D:\\PRIV\\Projekty\\EclipseWorkspace\\opencaching-api\\\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 1.5.5\n" "X-Generator: Poedit 1.5.4\n"
"X-Poedit-SearchPath-0: .\n" "X-Poedit-SearchPath-0: .\n"
# For additional waypoints. As in "Stage 1: Parking". # For additional waypoints. As in "Stage 1: Parking".
#: okapi/services/caches/geocaches.php:846 #: okapi/services/caches/geocaches.php:957
msgid "Stage" msgid "Stage"
msgstr "Etappe" msgstr "Etappe"
#: okapi/services/caches/geocaches.php:1009 #: okapi/services/caches/geocaches.php:986
msgid "User location"
msgstr "Gebruikers locatie"
#: okapi/services/caches/geocaches.php:989
#, php-format
msgid "Your own custom coordinates for the %s geocache"
msgstr "Eigen gecorrigeerde coördinaten voor %s geocache"
#: okapi/services/caches/geocaches.php:1148
msgid "National Park / Landscape"
msgstr "Nationaal park / Landschap"
#: okapi/services/caches/geocaches.php:1300
#, php-format #, php-format
msgid "" msgid ""
"This <a href='%s'>geocache</a> description comes from the <a href='%s'>%s</" "This <a href='%s'>geocache</a> description comes from the <a href='%s'>%s</"
"a> site." "a> site."
msgstr "" msgstr ""
"Deze <a href='%s'>geocache</a> beschrijving komt van de <a href='%s'>%s</a> "
"\"\"site."
#: okapi/services/caches/geocaches.php:1021 #: okapi/services/caches/geocaches.php:1312
#, php-format #, fuzzy, php-format
msgid "" msgid ""
"&copy; <a href='%s'>%s</a>, <a href='%s'>%s</a>, <a href='http://" "&copy; <a href='%s'>%s</a>, <a href='%s'>%s</a>, <a href='http://"
"creativecommons.org/licenses/by-nc-nd/3.0/de/deed.en'>CC-BY-NC-ND</a>, as of " "creativecommons.org/licenses/by-nc-nd/3.0/de/deed.en'>CC-BY-NC-ND</a>, as of "
"%s; all log entries &copy; their authors" "%s; all log entries &copy; their authors"
msgstr "" msgstr ""
"&copy; <a href='%s'>%s</a>, <a href='%s'>%s</a>, <a href='http://"
"creativecommons.org/licenses/by-nc-nd/3.0/nl/deed.en'>CC-BY-NC-ND</a>, as of "
"%s; all log entries &copy; their authors"
#: okapi/services/caches/geocaches.php:1032 #: okapi/services/caches/geocaches.php:1323
#, php-format #, fuzzy, php-format
msgid "" msgid ""
"&copy; <a href='%s'>%s</a>, <a href='%s'>%s</a>, <a href='http://" "&copy; <a href='%s'>%s</a>, <a href='%s'>%s</a>, <a href='http://"
"creativecommons.org/licenses/by-nc-nd/3.0/de/deed.en'>CC-BY-NC-ND</a>; all " "creativecommons.org/licenses/by-nc-nd/3.0/de/deed.en'>CC-BY-NC-ND</a>; all "
"log entries &copy; their authors" "log entries &copy; their authors"
msgstr "" msgstr ""
"&copy; <a href='%s'>%s</a>, <a href='%s'>%s</a>, <a href='http://"
"creativecommons.org/licenses/by-nc-nd/3.0/nl/deed.en'>CC-BY-NC-ND</a>; all "
"log entries &copy; their authors"
#: okapi/services/caches/formatters/gpxfile.tpl.php:31 #: okapi/services/caches/formatters/gpx.php:360
#: okapi/services/caches/formatters/gpxfile.tpl.php:48 msgid ""
"<b>Geocache coordinates have been changed.</b> They have been replaced with "
"your own custom coordinates which you have provided for this geocache."
msgstr ""
"Geocache coördinaten zijn veranderd.</b> Deze zijn aangepast door "
"coördinaten die je zelf hebt gewijzigd voor deze geocache."
#: okapi/services/caches/formatters/gpx.php:366
msgid ""
"<b>Geocache coordinates have been changed.</b> Currently they point to one "
"of the alternate waypoints originally described as:"
msgstr ""
"Geocache coördinaten zijn veranderd.</b> Momenteel wijzen zij een van de "
"alternatieve waypoints oorspronkelijk beschreven als:"
#: okapi/services/caches/formatters/gpx.php:379
msgid "Original geocache location"
msgstr "Orginele geocache locatie"
#: okapi/services/caches/formatters/gpx.php:381
#, php-format
msgid "Original (owner-supplied) location of the %s geocache"
msgstr "Originele (opgegeven door plaatser) locatie van de %s geocache"
#: okapi/services/caches/formatters/gpxfile.tpl.php:30
#: okapi/services/caches/formatters/gpxfile.tpl.php:62
msgid "hidden by" msgid "hidden by"
msgstr "geplaatst door" msgstr "geplaatst door"
#: okapi/services/caches/formatters/gpxfile.tpl.php:50 #: okapi/services/caches/formatters/gpxfile.tpl.php:64
#, php-format #, php-format
msgid "%d recommendation" msgid "%d recommendation"
msgid_plural "%d recommendations" msgid_plural "%d recommendations"
msgstr[0] "%d aanbeveling" msgstr[0] "%d aanbeveling"
msgstr[1] "%d aanbevelingen" msgstr[1] "%d aanbevelingen"
#: okapi/services/caches/formatters/gpxfile.tpl.php:51 #: okapi/services/caches/formatters/gpxfile.tpl.php:65
#, php-format #, php-format
msgid "found %d time" msgid "found %d time"
msgid_plural "found %d times" msgid_plural "found %d times"
@@ -64,37 +110,42 @@ msgstr[0] "%d keer gevonden"
msgstr[1] "%d keren gevonden" msgstr[1] "%d keren gevonden"
# Generic term for trackable items (like geocoins and travelbugs). # Generic term for trackable items (like geocoins and travelbugs).
#: okapi/services/caches/formatters/gpxfile.tpl.php:54 #: okapi/services/caches/formatters/gpxfile.tpl.php:68
#, php-format #, php-format
msgid "%d trackable" msgid "%d trackable"
msgid_plural "%d trackables" msgid_plural "%d trackables"
msgstr[0] "%d trackable" msgstr[0] "%d trackable"
msgstr[1] "%d trackables" msgstr[1] "%d trackables"
#: okapi/services/caches/formatters/gpxfile.tpl.php:58 #: okapi/services/caches/formatters/gpxfile.tpl.php:72
msgid "Personal notes" msgid "Personal notes"
msgstr "Persoonlijke notities" msgstr "Persoonlijke notities"
#: okapi/services/caches/formatters/gpxfile.tpl.php:62 #: okapi/services/caches/formatters/gpxfile.tpl.php:76
msgid "Attributes" msgid "Attributes"
msgstr "Attributen" msgstr "Attributen"
#: okapi/services/caches/formatters/gpxfile.tpl.php:66 #: okapi/services/caches/formatters/gpxfile.tpl.php:80
msgid "Trackables" msgid "Trackables"
msgstr "Trackables" msgstr "Trackables"
#: okapi/services/caches/formatters/gpxfile.tpl.php:84 #: okapi/services/caches/formatters/gpxfile.tpl.php:90
#: okapi/services/caches/formatters/gpxfile.tpl.php:106
msgid "Images" msgid "Images"
msgstr "Afbeeldingen" msgstr "Afbeeldingen"
#: okapi/services/caches/formatters/gpxfile.tpl.php:91 #: okapi/services/caches/formatters/gpxfile.tpl.php:113
msgid "Spoilers" msgid "Spoilers"
msgstr "Spoilers" msgstr "Spoilers"
#: okapi/services/caches/formatters/gpxfile.tpl.php:99 #: okapi/services/caches/formatters/gpxfile.tpl.php:122
msgid "Image descriptions" msgid "Image descriptions"
msgstr "Afbeelding omschrijving" msgstr "Afbeelding omschrijving"
#: okapi/services/caches/formatters/gpxfile.tpl.php:130
msgid "The cache probably is located in the following protection areas:"
msgstr "De cache is waarschijnlijk in een volgend beschermd gebied geplaatst:"
#: okapi/services/logs/submit.php:70 #: okapi/services/logs/submit.php:70
msgid "" msgid ""
"You are trying to publish a log entry with a date in future. Cache log " "You are trying to publish a log entry with a date in future. Cache log "
@@ -112,40 +163,59 @@ msgstr ""
"De cachewaardering is genegeerd, omdat %s geen waarderingen in het systeem " "De cachewaardering is genegeerd, omdat %s geen waarderingen in het systeem "
"heeft." "heeft."
#: okapi/services/logs/submit.php:113 #: okapi/services/logs/submit.php:111
#, php-format
msgid ""
"However, your cache recommendation was ignored, because %s does not allow "
"recommending event caches."
msgstr ""
"De cachewaardering is genegeerd, omdat %s geen waarderingen op evenementen "
"ondersteund."
#: okapi/services/logs/submit.php:125
#, php-format #, php-format
msgid "" msgid ""
"However, your \"needs maintenance\" flag was ignored, because %s does not " "However, your \"needs maintenance\" flag was ignored, because %s does not "
"support this feature." "support this feature."
msgstr "" msgstr ""
"Maar jouw \"Heeft onderhoud nodig\" log is genegeerd omdat %s dit niet "
"ondersteunt."
# 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. # Missing "you can attend it"
#: okapi/services/logs/submit.php:131 #: okapi/services/logs/submit.php:145
msgid "" msgid ""
"This cache is an Event cache. You cannot \"Find it\"! (But - you may " "This cache is an Event cache. You cannot \"Find\" it (but you can attend it, "
"\"Comment\" on it.)" "or comment on it)!"
msgstr "" msgstr ""
"Dit is een eventcache. Deze kan niet als \"Gevonden\" gelogd worden. Maar " "Dit is een evenement. Deze kan niet als \"Gevonden\" gelogd worden. (maar "
"wel als \"Notitie\"." "wel deelgenomen of een notitie plaatsen)."
#: okapi/services/logs/submit.php:133 #: okapi/services/logs/submit.php:150
msgid ""
"This cache is NOT an Event cache. You cannot \"Attend\" it (but you can find "
"it, or comment on it)!"
msgstr ""
"Deze cache is GEEN evenement. Je kunt niet \"Deelnemen\" (maar wel als "
"gevonden loggen of een notitie plaatsen)"
#: okapi/services/logs/submit.php:155
msgid "Your have to supply some text for your comment." msgid "Your have to supply some text for your comment."
msgstr "Er dient enige tekst ingevuld te worden." msgstr "Er dient enige tekst ingevuld te worden."
#: okapi/services/logs/submit.php:146 #: okapi/services/logs/submit.php:168
msgid "This cache requires a password. You didn't provide one!" msgid "This cache requires a password. You didn't provide one!"
msgstr "Deze cache vraagt om een wachtwoord. Deze is niet opgegeven!" msgstr "Deze cache vraagt om een wachtwoord. Deze is niet opgegeven!"
#: okapi/services/logs/submit.php:148 #: okapi/services/logs/submit.php:170
msgid "Invalid password!" msgid "Invalid password!"
msgstr "Verkeerd wachtwoord!" 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. # 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:260 #: okapi/services/logs/submit.php:285
msgid "You have already submitted a log entry with exactly the same contents." msgid "You have already submitted a log entry with exactly the same contents."
msgstr "Deze log is reeds met dezelfde tekst verzonden." msgstr "Deze log is reeds met dezelfde tekst verzonden."
#: okapi/services/logs/submit.php:279 #: okapi/services/logs/submit.php:308
msgid "" msgid ""
"You have already submitted a \"Found it\" log entry once. Now you may submit " "You have already submitted a \"Found it\" log entry once. Now you may submit "
"\"Comments\" only!" "\"Comments\" only!"
@@ -153,25 +223,29 @@ msgstr ""
"Je hebt deze cache al als \"Gevonden\" gelogd. Je kunt nu wel een \"Notitie" "Je hebt deze cache al als \"Gevonden\" gelogd. Je kunt nu wel een \"Notitie"
"\" plaatsen." "\" plaatsen."
# The English text was changed from "you cannot rate it" to "you cannot find it". The translation remained. #: okapi/services/logs/submit.php:310
#: okapi/services/logs/submit.php:281
msgid "You are the owner of this cache. You may submit \"Comments\" only!" msgid "You are the owner of this cache. You may submit \"Comments\" only!"
msgstr "" msgstr ""
"Je bent eigenaar van deze cache. Je kunt alleen een \"Notitie\" plaatsen."
#: okapi/services/logs/submit.php:299 #: okapi/services/logs/submit.php:328
msgid "You have already rated this cache once. Your rating cannot be changed." msgid "You have already rated this cache once. Your rating cannot be changed."
msgstr "" msgstr ""
"Je hebt deze cache al gewaardeerd. De waardering kan niet veranderd worden." "Je hebt deze cache al gewaardeerd. De waardering kan niet veranderd worden."
#: okapi/services/logs/submit.php:316 #: okapi/services/logs/submit.php:345
msgid "You have already recommended this cache once." msgid "You have already recommended this cache once."
msgstr "" msgstr "Je hebt al een aanbeveling op deze cache gegeven."
#: okapi/services/logs/submit.php:323 #: okapi/services/logs/submit.php:355
msgid "You don't have any recommendations to give. Find more caches first!" msgid "You don't have any recommendations to give. Find more caches first!"
msgstr "" msgstr "Je kunt geen aanbeveling meer geven. Vind eerst meer caches."
#: okapi/services/logs/submit.php:491 #: okapi/services/logs/submit.php:398
msgid "Event caches cannot \"need maintenance\"."
msgstr "Evenementen hebben geen \"onderhoud nodig"
#: okapi/services/logs/submit.php:528
msgid "Your cache log entry was posted successfully." msgid "Your cache log entry was posted successfully."
msgstr "De log is succesvol verzonden." msgstr "De log is succesvol verzonden."
@@ -208,35 +282,33 @@ msgstr "Toestemmen"
msgid "Decline" msgid "Decline"
msgstr "Afwijzen" msgstr "Afwijzen"
# This should begin with "\n", but you may ignore the rest of \n and \r characters. # This should begin with "\n", but you may ignore the rest of \n characters.
# This message is shown to the user when external application is trying to get user's permission to access his account. # 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 # Sample: http://i.imgur.com/ZCJNT.png
#: okapi/views/apps/authorize.tpl.php:56 #: okapi/views/apps/authorize.tpl.php:56
#, php-format #, php-format
msgid "" msgid ""
"\n" "\n"
"\t\t\t\t\t<p>Once permission is granted it is valid until its withdrawal on\n" " <p>Once permission is granted it is valid until its "
"\t\t\t\t\tthe <a href='%s'>applications management</a> page.</p>\n" "withdrawal on\n"
"\t\t\t\t\t<p>The application will access your acount via <a href='%s'>the " " the <a href='%s'>applications management</a> page.</p>\n"
"OKAPI Framework</a>.\n" " <p>The application will access your acount via <a "
"\t\t\t\t\tIf you allow this request application will be able to access all " "href='%s'>the OKAPI Framework</a>.\n"
"methods delivered\n" " If you allow this request application will be able to "
"\t\t\t\t\tby the OKAPI Framework, i.e. post log entries on geocaches in your " "access all methods delivered\n"
"name.\n" " by the OKAPI Framework, i.e. post log entries on "
"\t\t\t\t\tYou can revoke this permission at any moment.</p>\n" "geocaches in your name.\n"
"\t\t\t\t" " You can revoke this permission at any moment.</p>\n"
" "
msgstr "" msgstr ""
"\n" "\n"
"\t\t\t\t\t<p>Wanneer toegestemd is blijft deze geldig tot intrekking op\n" "<p>Wanneer toegestemd is blijft deze geldig tot intrekking op\n"
"\t\t\t\t\tde <a href='%s'>toepassingsbeheer</a> pagina.</p>\n" "de <a href='%s'>toepassingsbeheer</a> pagina.</p>\n"
"\t\t\t\t\t<p>De toepassing zal toegang krijgen via jouw account op <a " "<p>De toepassing zal toegang krijgen via jouw account op <a href='%s'>the "
"href='%s'>the OKAPI Framework</a>.\n" "OKAPI Framework</a>.\n"
"\t\t\t\t\tWanneer je toestemming geeft voor deze toepassing zullen de " "Wanneer je toestemming geeft voor deze toepassing zullen de mogelijkheden\n"
"mogelijkheden\n" "van OKAPI Framework toegepast worden, b.v. het loggen van een cache.\n"
"\t\t\t\t\tvan OKAPI Framework toegepast worden, b.v. het loggen van een " "De toestemming kan elk moment ingetrokken worden.</p>"
"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 #: okapi/views/apps/authorized.tpl.php:5
msgid "Authorization Succeeded" msgid "Authorization Succeeded"
@@ -251,18 +323,16 @@ msgstr "Met succes toegang verleend"
#, php-format #, php-format
msgid "" msgid ""
"\n" "\n"
"\t\t\t\t<p><b>You've just granted %s application access to your %s account.</" " <p><b>You've just granted %s application access to your %s "
"b>\n" "account.</b>\n"
"\t\t\t\tTo complete the operation, go back to %s and enter the following PIN " " To complete the operation, go back to %s and enter the "
"code:</p>\n" "following PIN code:</p>\n"
"\t\t\t" " "
msgstr "" msgstr ""
"\n" "\n"
"\t\t\t\t<p><b>Je hebt toegang verleent voor %s toepassing op jouw %s account." "<p><b>Je hebt toegang verleent voor %s toepassing op jouw %s account.</b>\n"
"</b>\n" "Om de actie te voltooien, ga terug naar %s en gebruik de volgende PIN code:</"
"\t\t\t\tOm de aktie te voltooien, ga terug naar %s en gebruik de volgende " "p>"
"PIN code:</p>\n"
"\t\t\t"
#: okapi/views/apps/index.tpl.php:5 #: okapi/views/apps/index.tpl.php:5
msgid "My Apps" msgid "My Apps"
@@ -273,29 +343,25 @@ msgid "Your external applications"
msgstr "Jouw externe toepassingen" msgstr "Jouw externe toepassingen"
# This will be shown when user visits /okapi/apps page. # This will be shown when user visits /okapi/apps page.
# Sample: http://i.imgur.com/ZCJNT.png
#: okapi/views/apps/index.tpl.php:31 #: okapi/views/apps/index.tpl.php:31
#, php-format #, php-format
msgid "" msgid ""
"\n" "\n"
"\t\t\t\t\t<p>This is the list of applications which you granted access to " " <p>This is the list of applications which you granted "
"your <b>%s</b> account.\n" "access to your <b>%s</b> account.\n"
"\t\t\t\t\tThis page gives you the abbility to revoke all previously granted " " This page gives you the abbility to revoke all "
"privileges.\n" "previously granted privileges.\n"
"\t\t\t\t\tOnce you click \"remove\" the application will no longer be able " " Once you click \"remove\" the application will no longer "
"to perform any\n" "be able to perform any\n"
"\t\t\t\t\tactions on your behalf.</p>\n" " actions on your behalf.</p>\n"
"\t\t\t\t" " "
msgstr "" msgstr ""
"\n" "\n"
"\t\t\t\t\t<p>Dit is een lijst met toegestane toepassingen op jouw <b>%s</b> " "<p>Dit is een lijst met toegestane toepassingen op jouw <b>%s</b> account.\n"
"account.\n" "Op deze pagina kun je alle toestemmingen intrekken die gegeven zijn.\n"
"\t\t\t\t\tOp deze pagina kun je alle toestemmingen intrekken die gegeven " "Met een klik op \"verwijderen\" zal de toepassing verwijderen en is dan ook "
"zijn.\n" "niet meer beschikbaar\n"
"\t\t\t\t\tMet een klik op \"verwijderen\" zal de toepassing verwijderen en " "oor anderen.</p>"
"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 #: okapi/views/apps/index.tpl.php:45
msgid "remove" msgid "remove"
@@ -305,22 +371,21 @@ msgstr "verwijderen"
#, php-format #, php-format
msgid "" msgid ""
"\n" "\n"
"\t\t\t\t\t<p>Thanks to the <a href='%s'>OKAPI Framework</a> you can grant " " <p>Thanks to the <a href='%s'>OKAPI Framework</a> you "
"external applications\n" "can grant external applications\n"
"\t\t\t\t\taccess to your <b>%s</b> account. Currently no applications are " " access to your <b>%s</b> account. Currently no "
"authorized to act\n" "applications are authorized to act\n"
"\t\t\t\t\ton your behalf. Once you start using external Opencaching " " on your behalf. Once you start using external "
"applications, they will appear here.</p>\n" "Opencaching applications, they will appear here.</p>\n"
"\t\t\t\t" " "
msgstr "" msgstr ""
"\n" "\n"
"\t\t\t\t\t<p>Dankzij het <a href='%s'>OKAPI Framework</a> kun je toegang " "<p>Dankzij het <a href='%s'>OKAPI Framework</a> kun je toegang verlenen via "
"verlenen via externe\n" "externe\n"
"\t\t\t\t\ttoepassingen op een <b>%s</b> account. Momenteel zijn op dit " "toepassingen op een <b>%s</b> account. Momenteel zijn op dit account nog "
"account nog geen externe\n" "geen externe\n"
"\t\t\t\t\ttoepassingen actief. Geactiveerde Opencaching toepassingen " "toepassingen actief. Geactiveerde Opencaching toepassingen zullen hier "
"zullen hier getoond worden.</p>\n" "getoond worden.</p>"
"\t\t\t\t"
#~ msgid "" #~ msgid ""
#~ "This cache is archived. Only admins and the owner are allowed to add a " #~ "This cache is archived. Only admins and the owner are allowed to add a "

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: OKAPI\n" "Project-Id-Version: OKAPI\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2013-07-11 08:28+0100\n" "POT-Creation-Date: 2014-01-23 15:48+0100\n"
"PO-Revision-Date: 2013-07-11 08:35+0100\n" "PO-Revision-Date: 2014-01-23 15:49+0100\n"
"Last-Translator: Wojciech Rygielski <rygielski@mimuw.edu.pl>\n" "Last-Translator: Wojciech Rygielski <rygielski@mimuw.edu.pl>\n"
"Language-Team: \n" "Language-Team: \n"
"Language: pl_PL\n" "Language: pl_PL\n"
@@ -15,26 +15,35 @@ msgstr ""
"\\okapi\n" "\\okapi\n"
"Plural-Forms: nplurals=3; plural= n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " "Plural-Forms: nplurals=3; plural= n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
"|| n%100>=20) ? 1 : 2;\n" "|| n%100>=20) ? 1 : 2;\n"
"X-Poedit-SourceCharset: utf-8\n" "X-Poedit-SourceCharset: UTF-8\n"
"X-Generator: Poedit 1.5.5\n" "X-Generator: Poedit 1.6.3\n"
"X-Poedit-SearchPath-0: .\n" "X-Poedit-SearchPath-0: .\n"
#: services/caches/geocaches.php:956 #: services/caches/geocaches.php:957
msgid "Stage" msgid "Stage"
msgstr "Etap" msgstr "Etap"
#: services/caches/geocaches.php:1111 #: services/caches/geocaches.php:986
msgid "User location"
msgstr "Współrzędne użytkownika"
#: services/caches/geocaches.php:989
#, php-format
msgid "Your own custom coordinates for the %s geocache"
msgstr "Twoje osobiste współrzędne skrzynki %s"
#: services/caches/geocaches.php:1148
msgid "National Park / Landscape" msgid "National Park / Landscape"
msgstr "Park narodowy lub krajobrazowy" msgstr "Park narodowy lub krajobrazowy"
#: services/caches/geocaches.php:1263 #: services/caches/geocaches.php:1300
#, php-format #, php-format
msgid "" msgid ""
"This <a href='%s'>geocache</a> description comes from the <a href='%s'>%s</" "This <a href='%s'>geocache</a> description comes from the <a href='%s'>%s</"
"a> site." "a> site."
msgstr "Opis <a href='%s'>skrzynki</a> pochodzi z serwisu <a href='%s'>%s</a>." msgstr "Opis <a href='%s'>skrzynki</a> pochodzi z serwisu <a href='%s'>%s</a>."
#: services/caches/geocaches.php:1275 #: services/caches/geocaches.php:1312
#, php-format #, php-format
msgid "" msgid ""
"&copy; <a href='%s'>%s</a>, <a href='%s'>%s</a>, <a href='http://" "&copy; <a href='%s'>%s</a>, <a href='%s'>%s</a>, <a href='http://"
@@ -45,7 +54,7 @@ msgstr ""
"creativecommons.org/licenses/by-nc-nd/3.0/de/deed.pl'>CC-BY-NC-ND</a>, w " "creativecommons.org/licenses/by-nc-nd/3.0/de/deed.pl'>CC-BY-NC-ND</a>, w "
"dniu %s. Prawa autorskie wpisów do logów należą do ich autorów." "dniu %s. Prawa autorskie wpisów do logów należą do ich autorów."
#: services/caches/geocaches.php:1286 #: services/caches/geocaches.php:1323
#, php-format #, php-format
msgid "" msgid ""
"&copy; <a href='%s'>%s</a>, <a href='%s'>%s</a>, <a href='http://" "&copy; <a href='%s'>%s</a>, <a href='%s'>%s</a>, <a href='http://"
@@ -56,12 +65,37 @@ msgstr ""
"creativecommons.org/licenses/by-nc-nd/3.0/de/deed.pl'>CC-BY-NC-ND</a>. Prawa " "creativecommons.org/licenses/by-nc-nd/3.0/de/deed.pl'>CC-BY-NC-ND</a>. Prawa "
"autorskie wpisów do logów należą do ich autorów." "autorskie wpisów do logów należą do ich autorów."
#: services/caches/formatters/gpxfile.tpl.php:31 #: services/caches/formatters/gpx.php:360
#: services/caches/formatters/gpxfile.tpl.php:60 msgid ""
"<b>Geocache coordinates have been changed.</b> They have been replaced with "
"your own custom coordinates which you have provided for this geocache."
msgstr ""
"<b>Współrzędne skrzynki zostały zmienione.</b> Zostały zastąpione Twoimi "
"osobistymi współrzędnymi, które wprowadziłeś na stronie tej skrzynki."
#: services/caches/formatters/gpx.php:366
msgid ""
"<b>Geocache coordinates have been changed.</b> Currently they point to one "
"of the alternate waypoints originally described as:"
msgstr ""
"<b>Współrzędne skrzynki zostały zmienione.</b> Aktualnie wskazują one na "
"jeden z dodatkowych waypointów, oryginalnie opisanego jako:"
#: services/caches/formatters/gpx.php:379
msgid "Original geocache location"
msgstr "Oryginalne współrzędne skrzynki"
#: services/caches/formatters/gpx.php:381
#, php-format
msgid "Original (owner-supplied) location of the %s geocache"
msgstr "Oryginalne współrzędne skrzynki %s (podane przez autora)"
#: services/caches/formatters/gpxfile.tpl.php:30
#: services/caches/formatters/gpxfile.tpl.php:62
msgid "hidden by" msgid "hidden by"
msgstr "ukryta przez" msgstr "ukryta przez"
#: services/caches/formatters/gpxfile.tpl.php:62 #: services/caches/formatters/gpxfile.tpl.php:64
#, php-format #, php-format
msgid "%d recommendation" msgid "%d recommendation"
msgid_plural "%d recommendations" msgid_plural "%d recommendations"
@@ -69,7 +103,7 @@ msgstr[0] "%d rekomendacja"
msgstr[1] "%d rekomendacje" msgstr[1] "%d rekomendacje"
msgstr[2] "%d rekomendacji" msgstr[2] "%d rekomendacji"
#: services/caches/formatters/gpxfile.tpl.php:63 #: services/caches/formatters/gpxfile.tpl.php:65
#, php-format #, php-format
msgid "found %d time" msgid "found %d time"
msgid_plural "found %d times" msgid_plural "found %d times"
@@ -77,7 +111,7 @@ msgstr[0] "znaleziona %d raz"
msgstr[1] "znaleziona %d razy" msgstr[1] "znaleziona %d razy"
msgstr[2] "znaleziona %d razy" msgstr[2] "znaleziona %d razy"
#: services/caches/formatters/gpxfile.tpl.php:66 #: services/caches/formatters/gpxfile.tpl.php:68
#, php-format #, php-format
msgid "%d trackable" msgid "%d trackable"
msgid_plural "%d trackables" msgid_plural "%d trackables"
@@ -85,32 +119,32 @@ msgstr[0] "%d GeoKret (lub TravelBug)"
msgstr[1] "%d GeoKrety (lub TravelBugi)" msgstr[1] "%d GeoKrety (lub TravelBugi)"
msgstr[2] "%d GeoKretów (lub TravelBugów)" msgstr[2] "%d GeoKretów (lub TravelBugów)"
#: services/caches/formatters/gpxfile.tpl.php:70 #: services/caches/formatters/gpxfile.tpl.php:72
msgid "Personal notes" msgid "Personal notes"
msgstr "Osobiste notatki" msgstr "Osobiste notatki"
#: services/caches/formatters/gpxfile.tpl.php:74 #: services/caches/formatters/gpxfile.tpl.php:76
msgid "Attributes" msgid "Attributes"
msgstr "Atrybuty" msgstr "Atrybuty"
#: services/caches/formatters/gpxfile.tpl.php:78 #: services/caches/formatters/gpxfile.tpl.php:80
msgid "Trackables" msgid "Trackables"
msgstr "Geokrety, Travelbugi itp." msgstr "Geokrety, Travelbugi itp."
#: services/caches/formatters/gpxfile.tpl.php:88 #: services/caches/formatters/gpxfile.tpl.php:90
#: services/caches/formatters/gpxfile.tpl.php:104 #: services/caches/formatters/gpxfile.tpl.php:106
msgid "Images" msgid "Images"
msgstr "Obrazki" msgstr "Obrazki"
#: services/caches/formatters/gpxfile.tpl.php:111 #: services/caches/formatters/gpxfile.tpl.php:113
msgid "Spoilers" msgid "Spoilers"
msgstr "Spoilery" msgstr "Spoilery"
#: services/caches/formatters/gpxfile.tpl.php:120 #: services/caches/formatters/gpxfile.tpl.php:122
msgid "Image descriptions" msgid "Image descriptions"
msgstr "Opisy obrazków" msgstr "Opisy obrazków"
#: services/caches/formatters/gpxfile.tpl.php:128 #: services/caches/formatters/gpxfile.tpl.php:130
msgid "The cache probably is located in the following protection areas:" msgid "The cache probably is located in the following protection areas:"
msgstr "Prawdopodobnie skrzynka znajduje się na terenie obszarów chronionych:" msgstr "Prawdopodobnie skrzynka znajduje się na terenie obszarów chronionych:"
@@ -177,11 +211,11 @@ msgstr "Ta skrzynka wymaga podania hasła. Nie wpisałeś go."
msgid "Invalid password!" msgid "Invalid password!"
msgstr "Niepoprawne hasło!" msgstr "Niepoprawne hasło!"
#: services/logs/submit.php:282 #: services/logs/submit.php:285
msgid "You have already submitted a log entry with exactly the same contents." 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ą." msgstr "Już opublikowałeś wcześniej wpis z dokładnie taką samą treścią."
#: services/logs/submit.php:305 #: services/logs/submit.php:308
msgid "" msgid ""
"You have already submitted a \"Found it\" log entry once. Now you may submit " "You have already submitted a \"Found it\" log entry once. Now you may submit "
"\"Comments\" only!" "\"Comments\" only!"
@@ -189,30 +223,30 @@ msgstr ""
"Już opublikowałeś jeden wpis typu \"Znaleziona\" dla tej skrzynki. Teraz " "Już opublikowałeś jeden wpis typu \"Znaleziona\" dla tej skrzynki. Teraz "
"możesz dodawać jedynie \"Komentarze\"!" "możesz dodawać jedynie \"Komentarze\"!"
#: services/logs/submit.php:307 #: services/logs/submit.php:310
msgid "You are the owner of this cache. You may submit \"Comments\" only!" msgid "You are the owner of this cache. You may submit \"Comments\" only!"
msgstr "" msgstr ""
"Jesteś właścicielem tej skrzynki. Możesz przesyłać jedynie \"Komentarze\"." "Jesteś właścicielem tej skrzynki. Możesz przesyłać jedynie \"Komentarze\"."
#: services/logs/submit.php:325 #: services/logs/submit.php:328
msgid "You have already rated this cache once. Your rating cannot be changed." 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." msgstr "Już oceniłeś tę skrzynkę. Ocena nie może być zmieniona."
#: services/logs/submit.php:342 #: services/logs/submit.php:345
msgid "You have already recommended this cache once." msgid "You have already recommended this cache once."
msgstr "Już raz zarekomendowałeś tę skrzynkę." msgstr "Już raz zarekomendowałeś tę skrzynkę."
#: services/logs/submit.php:352 #: services/logs/submit.php:355
msgid "You don't have any recommendations to give. Find more caches first!" msgid "You don't have any recommendations to give. Find more caches first!"
msgstr "" msgstr ""
"Aktualnie nie możesz wystawić kolejnej rekomendacji. Znajdź najpierw więcej " "Aktualnie nie możesz wystawić kolejnej rekomendacji. Znajdź najpierw więcej "
"skrzynek!" "skrzynek!"
#: services/logs/submit.php:395 #: services/logs/submit.php:398
msgid "Event caches cannot \"need maintenance\"." msgid "Event caches cannot \"need maintenance\"."
msgstr "Skrzynki typu Wydarzenie nie mogą \"potrzebować serwisu\"." msgstr "Skrzynki typu Wydarzenie nie mogą \"potrzebować serwisu\"."
#: services/logs/submit.php:525 #: services/logs/submit.php:528
msgid "Your cache log entry was posted successfully." msgid "Your cache log entry was posted successfully."
msgstr "Twój wpis do logbooka został opublikowany pomyślnie." msgstr "Twój wpis do logbooka został opublikowany pomyślnie."
@@ -253,16 +287,17 @@ msgstr "Odmawiam"
#, php-format #, php-format
msgid "" msgid ""
"\n" "\n"
"\t\t\t\t\t<p>Once permission is granted it is valid until its withdrawal on\n" " <p>Once permission is granted it is valid until its "
"\t\t\t\t\tthe <a href='%s'>applications management</a> page.</p>\n" "withdrawal on\n"
"\t\t\t\t\t<p>The application will access your acount via <a href='%s'>the " " the <a href='%s'>applications management</a> page.</p>\n"
"OKAPI Framework</a>.\n" " <p>The application will access your acount via <a "
"\t\t\t\t\tIf you allow this request application will be able to access all " "href='%s'>the OKAPI Framework</a>.\n"
"methods delivered\n" " If you allow this request application will be able to "
"\t\t\t\t\tby the OKAPI Framework, i.e. post log entries on geocaches in your " "access all methods delivered\n"
"name.\n" " by the OKAPI Framework, i.e. post log entries on "
"\t\t\t\t\tYou can revoke this permission at any moment.</p>\n" "geocaches in your name.\n"
"\t\t\t\t" " You can revoke this permission at any moment.</p>\n"
" "
msgstr "" msgstr ""
"\n" "\n"
"<p>Raz udzielona zgoda jest ważna aż do momentu jej wycofania na stronie <a " "<p>Raz udzielona zgoda jest ważna aż do momentu jej wycofania na stronie <a "
@@ -285,11 +320,11 @@ msgstr "Pomyślnie dałeś dostęp"
#, php-format #, php-format
msgid "" msgid ""
"\n" "\n"
"\t\t\t\t<p><b>You've just granted %s application access to your %s account.</" " <p><b>You've just granted %s application access to your %s "
"b>\n" "account.</b>\n"
"\t\t\t\tTo complete the operation, go back to %s and enter the following PIN " " To complete the operation, go back to %s and enter the "
"code:</p>\n" "following PIN code:</p>\n"
"\t\t\t" " "
msgstr "" msgstr ""
"\n" "\n"
"<p><b>Właśnie dałeś dostęp aplikacji %s do Twojego konta %s.</b>\n" "<p><b>Właśnie dałeś dostęp aplikacji %s do Twojego konta %s.</b>\n"
@@ -308,14 +343,14 @@ msgstr "Twoje zewnętrzne aplikacje"
#, php-format #, php-format
msgid "" msgid ""
"\n" "\n"
"\t\t\t\t\t<p>This is the list of applications which you granted access to " " <p>This is the list of applications which you granted "
"your <b>%s</b> account.\n" "access to your <b>%s</b> account.\n"
"\t\t\t\t\tThis page gives you the abbility to revoke all previously granted " " This page gives you the abbility to revoke all "
"privileges.\n" "previously granted privileges.\n"
"\t\t\t\t\tOnce you click \"remove\" the application will no longer be able " " Once you click \"remove\" the application will no longer "
"to perform any\n" "be able to perform any\n"
"\t\t\t\t\tactions on your behalf.</p>\n" " actions on your behalf.</p>\n"
"\t\t\t\t" " "
msgstr "" msgstr ""
"\n" "\n"
"<p>Następującym aplikacjom zezwoliłeś na dostęp do swojego konta <b>%s</b>.\n" "<p>Następującym aplikacjom zezwoliłeś na dostęp do swojego konta <b>%s</b>.\n"
@@ -332,13 +367,13 @@ msgstr "usuń"
#, php-format #, php-format
msgid "" msgid ""
"\n" "\n"
"\t\t\t\t\t<p>Thanks to the <a href='%s'>OKAPI Framework</a> you can grant " " <p>Thanks to the <a href='%s'>OKAPI Framework</a> you "
"external applications\n" "can grant external applications\n"
"\t\t\t\t\taccess to your <b>%s</b> account. Currently no applications are " " access to your <b>%s</b> account. Currently no "
"authorized to act\n" "applications are authorized to act\n"
"\t\t\t\t\ton your behalf. Once you start using external Opencaching " " on your behalf. Once you start using external "
"applications, they will appear here.</p>\n" "Opencaching applications, they will appear here.</p>\n"
"\t\t\t\t" " "
msgstr "" msgstr ""
"\n" "\n"
"<p>Dzięki platformie <a href='%s'>OKAPI</a> możesz dawać zewnętrznym " "<p>Dzięki platformie <a href='%s'>OKAPI</a> możesz dawać zewnętrznym "
@@ -348,6 +383,30 @@ msgstr ""
"w Twoim imieniu. Gdy zaczniesz korzystać z zewnętrznych aplikacji, ich lista " "w Twoim imieniu. Gdy zaczniesz korzystać z zewnętrznych aplikacji, ich lista "
"pojawi się tutaj.</p>" "pojawi się tutaj.</p>"
#~ msgid ""
#~ "\n"
#~ "\t\t\t\t\t\t\t\t<b>Warning: Changed coordinates.</b> These are not the "
#~ "original\n"
#~ "\t\t\t\t\t\t\t\tcoordinates of this geocache (as supplied by the owner). "
#~ "They\n"
#~ "\t\t\t\t\t\t\t\thave been replaced with other coordinates:\n"
#~ "\t\t\t\t\t\t\t"
#~ msgstr ""
#~ "\n"
#~ "<b>Uwaga: Zmienione współrzędne.</b> To nie są oryginalne współrzędne tej "
#~ "skrzynki (takie, jakie podał jej autor). Współrzędne zostały nadpisane "
#~ "innymi współrzędnymi:"
#~ msgid ""
#~ "<b>Geocache's coordinates has been changed</b> to point to the user "
#~ "supplied value."
#~ msgstr ""
#~ "<b>Współrzędne skrzynki zostały zmienione</b> na własną wartość "
#~ "wprowadzoną przez użytkownika."
#~ msgid "User-supplied location of the %s geocache"
#~ msgstr "Współrzędne skrzynki %s wprowadzone przez użytkownika"
#~ msgid "Recommending is allowed only for 'Found it' logtypes." #~ msgid "Recommending is allowed only for 'Found it' logtypes."
#~ msgstr "Rekomendacje są dozwolone jedynie z wpisem \"Znaleziona\"." #~ msgstr "Rekomendacje są dozwolone jedynie z wpisem \"Znaleziona\"."

View File

@@ -839,6 +839,9 @@ class OAuthServer {
if( ! $timestamp ) if( ! $timestamp )
throw new OAuthMissingParameterException('oauth_timestamp'); throw new OAuthMissingParameterException('oauth_timestamp');
// Cast to integer. See issue #314.
$timestamp = $timestamp + 0;
// verify that timestamp is recentish // verify that timestamp is recentish
$now = time(); $now = time();
if (abs($now - $timestamp) > $this->timestamp_threshold) { if (abs($now - $timestamp) > $this->timestamp_threshold) {

View File

@@ -6,186 +6,187 @@ use Exception;
class OkapiServiceRunner class OkapiServiceRunner
{ {
# #
# This the list of all available OKAPI methods. All methods on this list become # 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 # 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 # 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 # 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 # 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). # (you may describe it as "internal" and accessible to selected consumer keys only).
# #
public static $all_names = array( public static $all_names = array(
# Valid format: ^services/[0-9a-z_/]*$ (it means you may use only alphanumeric # Valid format: ^services/[0-9a-z_/]*$ (it means you may use only alphanumeric
# characters and the "_" sign in your method names). # characters and the "_" sign in your method names).
'services/apisrv/installation', 'services/apisrv/installation',
'services/apisrv/installations', 'services/apisrv/installations',
'services/apisrv/stats', 'services/apisrv/stats',
'services/apiref/method', 'services/apiref/method',
'services/apiref/method_index', 'services/apiref/method_index',
'services/apiref/issue', 'services/apiref/issue',
'services/attrs/attribute_index', 'services/attrs/attribute_index',
'services/attrs/attribute', 'services/attrs/attribute',
'services/attrs/attributes', 'services/attrs/attributes',
'services/oauth/request_token', 'services/oauth/request_token',
'services/oauth/authorize', 'services/oauth/authorize',
'services/oauth/access_token', 'services/oauth/access_token',
'services/caches/search/all', 'services/caches/search/all',
'services/caches/search/bbox', 'services/caches/search/bbox',
'services/caches/search/nearest', 'services/caches/search/nearest',
'services/caches/search/by_urls', 'services/caches/search/by_urls',
'services/caches/search/save', 'services/caches/search/save',
'services/caches/shortcuts/search_and_retrieve', 'services/caches/shortcuts/search_and_retrieve',
'services/caches/geocache', 'services/caches/geocache',
'services/caches/geocaches', 'services/caches/geocaches',
'services/caches/mark', 'services/caches/mark',
'services/caches/formatters/gpx', 'services/caches/save_personal_notes',
'services/caches/formatters/garmin', 'services/caches/formatters/gpx',
'services/caches/map/tile', 'services/caches/formatters/garmin',
'services/logs/entries', 'services/caches/map/tile',
'services/logs/entry', 'services/logs/entries',
'services/logs/logs', 'services/logs/entry',
'services/logs/userlogs', 'services/logs/logs',
'services/logs/submit', 'services/logs/userlogs',
'services/users/user', 'services/logs/submit',
'services/users/users', 'services/users/user',
'services/users/by_usernames', 'services/users/users',
'services/users/by_username', 'services/users/by_usernames',
'services/users/by_internal_id', 'services/users/by_username',
'services/users/by_internal_ids', 'services/users/by_internal_id',
'services/replicate/changelog', 'services/users/by_internal_ids',
'services/replicate/fulldump', 'services/replicate/changelog',
'services/replicate/info', 'services/replicate/fulldump',
); 'services/replicate/info',
);
/** Check if method exists. */ /** Check if method exists. */
public static function exists($service_name) public static function exists($service_name)
{ {
return in_array($service_name, self::$all_names); return in_array($service_name, self::$all_names);
} }
/** Get method options (is consumer required etc.). */ /** Get method options (is consumer required etc.). */
public static function options($service_name) public static function options($service_name)
{ {
if (!self::exists($service_name)) if (!self::exists($service_name))
throw new Exception(); throw new Exception();
require_once($GLOBALS['rootpath']."okapi/$service_name.php"); require_once($GLOBALS['rootpath']."okapi/$service_name.php");
try try
{ {
return call_user_func(array('\\okapi\\'. return call_user_func(array('\\okapi\\'.
str_replace('/', '\\', $service_name).'\\WebService', 'options')); str_replace('/', '\\', $service_name).'\\WebService', 'options'));
} catch (Exception $e) } catch (Exception $e)
{ {
throw new Exception("Make sure you've declared your WebService class ". throw new Exception("Make sure you've declared your WebService class ".
"in an valid namespace (".'okapi\\'.str_replace('/', '\\', $service_name)."); ". "in an valid namespace (".'okapi\\'.str_replace('/', '\\', $service_name)."); ".
$e->getMessage()); $e->getMessage());
} }
} }
/** /**
* Get method documentation file contents (stuff within the XML file). * Get method documentation file contents (stuff within the XML file).
* If you're looking for a parsed representation, use services/apiref/method. * If you're looking for a parsed representation, use services/apiref/method.
*/ */
public static function docs($service_name) public static function docs($service_name)
{ {
if (!self::exists($service_name)) if (!self::exists($service_name))
throw new Exception(); throw new Exception();
try { try {
return file_get_contents("$service_name.xml", true); return file_get_contents("$service_name.xml", true);
} catch (Exception $e) { } catch (Exception $e) {
throw new Exception("Missing documentation file: $service_name.xml"); throw new Exception("Missing documentation file: $service_name.xml");
} }
} }
/** /**
* Execute the method and return the result. * Execute the method and return the result.
* *
* OKAPI methods return OkapiHttpResponses, but some MAY also return * OKAPI methods return OkapiHttpResponses, but some MAY also return
* PHP objects (see OkapiRequest::construct_inside_request for details). * PHP objects (see OkapiRequest::construct_inside_request for details).
* *
* If $request must be consistent with given method's options (must * If $request must be consistent with given method's options (must
* include Consumer and Token, if they are required). * include Consumer and Token, if they are required).
*/ */
public static function call($service_name, OkapiRequest $request) public static function call($service_name, OkapiRequest $request)
{ {
Okapi::init_internals(); Okapi::init_internals();
if (!self::exists($service_name)) if (!self::exists($service_name))
throw new Exception("Method does not exist: '$service_name'"); throw new Exception("Method does not exist: '$service_name'");
$options = self::options($service_name); $options = self::options($service_name);
if ($options['min_auth_level'] >= 2 && $request->consumer == null) if ($options['min_auth_level'] >= 2 && $request->consumer == null)
{ {
throw new Exception("Method '$service_name' called with mismatched OkapiRequest: ". throw new Exception("Method '$service_name' called with mismatched OkapiRequest: ".
"\$request->consumer MAY NOT be empty for Level 2 and Level 3 methods. Provide ". "\$request->consumer MAY NOT be empty for Level 2 and Level 3 methods. Provide ".
"a dummy Consumer if you have to."); "a dummy Consumer if you have to.");
} }
if ($options['min_auth_level'] >= 3 && $request->token == null) if ($options['min_auth_level'] >= 3 && $request->token == null)
{ {
throw new Exception("Method '$service_name' called with mismatched OkapiRequest: ". throw new Exception("Method '$service_name' called with mismatched OkapiRequest: ".
"\$request->token MAY NOT be empty for Level 3 methods."); "\$request->token MAY NOT be empty for Level 3 methods.");
} }
$time_started = microtime(true); $time_started = microtime(true);
Okapi::gettext_domain_init(); Okapi::gettext_domain_init();
try try
{ {
require_once($GLOBALS['rootpath']."okapi/$service_name.php"); require_once($GLOBALS['rootpath']."okapi/$service_name.php");
$response = call_user_func(array('\\okapi\\'. $response = call_user_func(array('\\okapi\\'.
str_replace('/', '\\', $service_name).'\\WebService', 'call'), $request); str_replace('/', '\\', $service_name).'\\WebService', 'call'), $request);
Okapi::gettext_domain_restore(); Okapi::gettext_domain_restore();
} catch (Exception $e) { } catch (Exception $e) {
Okapi::gettext_domain_restore(); Okapi::gettext_domain_restore();
throw $e; throw $e;
} }
$runtime = microtime(true) - $time_started; $runtime = microtime(true) - $time_started;
# Log the request to the stats table. Only valid requests (these which didn't end up # Log the request to the stats table. Only valid requests (these which didn't end up
# with an exception) are logged. # with an exception) are logged.
self::save_stats($service_name, $request, $runtime); self::save_stats($service_name, $request, $runtime);
return $response; return $response;
} }
/** /**
* For internal use only. The stats table can be used to store any kind of * For internal use only. The stats table can be used to store any kind of
* runtime-stats data, i.e. not only regarding services. This is a special * runtime-stats data, i.e. not only regarding services. This is a special
* version of save_stats which saves runtime stats under the name of $extra_name. * version of save_stats which saves runtime stats under the name of $extra_name.
* Note, that $request can be null. * Note, that $request can be null.
*/ */
public static function save_stats_extra($extra_name, $request, $runtime) public static function save_stats_extra($extra_name, $request, $runtime)
{ {
self::save_stats("extra/".$extra_name, $request, $runtime); self::save_stats("extra/".$extra_name, $request, $runtime);
} }
private static function save_stats($service_name, $request, $runtime) private static function save_stats($service_name, $request, $runtime)
{ {
# Getting rid of nulls. MySQL PRIMARY keys cannot contain nullable columns. # Getting rid of nulls. MySQL PRIMARY keys cannot contain nullable columns.
# Temp table doesn't have primary key, but other stats tables (which are # Temp table doesn't have primary key, but other stats tables (which are
# dependant on stats table) - do. # dependant on stats table) - do.
if ($request !== null) { if ($request !== null) {
$consumer_key = ($request->consumer != null) ? $request->consumer->key : 'anonymous'; $consumer_key = ($request->consumer != null) ? $request->consumer->key : 'anonymous';
$user_id = (($request->token != null) && ($request->token instanceof OkapiAccessToken)) ? $request->token->user_id : -1; $user_id = (($request->token != null) && ($request->token instanceof OkapiAccessToken)) ? $request->token->user_id : -1;
if ($request->is_http_request() && ($service_name[0] == 's')) # 's' for "services/", we don't want "extra/" included if ($request->is_http_request() && ($service_name[0] == 's')) # 's' for "services/", we don't want "extra/" included
$calltype = 'http'; $calltype = 'http';
else else
$calltype = 'internal'; $calltype = 'internal';
} else { } else {
$consumer_key = 'internal'; $consumer_key = 'internal';
$user_id = -1; $user_id = -1;
$calltype = 'internal'; $calltype = 'internal';
} }
Db::execute(" Db::execute("
insert into okapi_stats_temp (`datetime`, consumer_key, user_id, service_name, calltype, runtime) insert into okapi_stats_temp (`datetime`, consumer_key, user_id, service_name, calltype, runtime)
values ( values (
now(), now(),
'".mysql_real_escape_string($consumer_key)."', '".mysql_real_escape_string($consumer_key)."',
'".mysql_real_escape_string($user_id)."', '".mysql_real_escape_string($user_id)."',
'".mysql_real_escape_string($service_name)."', '".mysql_real_escape_string($service_name)."',
'".mysql_real_escape_string($calltype)."', '".mysql_real_escape_string($calltype)."',
'".mysql_real_escape_string($runtime)."' '".mysql_real_escape_string($runtime)."'
); );
"); ");
} }
} }

View File

@@ -15,32 +15,32 @@ use okapi\Cache;
class WebService class WebService
{ {
public static function options() public static function options()
{ {
return array( return array(
'min_auth_level' => 0 'min_auth_level' => 0
); );
} }
public static function call(OkapiRequest $request) public static function call(OkapiRequest $request)
{ {
$issue_id = $request->get_parameter('issue_id'); $issue_id = $request->get_parameter('issue_id');
if (!$issue_id) if (!$issue_id)
throw new ParamMissing('issue_id'); throw new ParamMissing('issue_id');
if ((!preg_match("/^[0-9]+$/", $issue_id)) || (strlen($issue_id) > 6)) if ((!preg_match("/^[0-9]+$/", $issue_id)) || (strlen($issue_id) > 6))
throw new InvalidParam('issue_id'); throw new InvalidParam('issue_id');
# In October 2013, Google Code feed at: # In October 2013, Google Code feed at:
# http://code.google.com/feeds/issues/p/opencaching-api/issues/$issue_id/comments/full # http://code.google.com/feeds/issues/p/opencaching-api/issues/$issue_id/comments/full
# stopped working. We are forced to respond with a simple placeholder. # stopped working. We are forced to respond with a simple placeholder.
$result = array( $result = array(
'id' => $issue_id + 0, 'id' => $issue_id + 0,
'last_updated' => null, 'last_updated' => null,
'title' => null, 'title' => null,
'url' => "https://code.google.com/p/opencaching-api/issues/detail?id=".$issue_id, 'url' => "https://code.google.com/p/opencaching-api/issues/detail?id=".$issue_id,
'comment_count' => null 'comment_count' => null
); );
return Okapi::formatted_response($request, $result); return Okapi::formatted_response($request, $result);
} }
} }

View File

@@ -1,33 +1,33 @@
<xml> <xml>
<brief>Retrieve information on given issue</brief> <brief>Retrieve information on given issue</brief>
<issue-id>11</issue-id> <issue-id>11</issue-id>
<desc> <desc>
<p><b>Important:</b> This method stopped working properly in October 2013. <p><b>Important:</b> This method stopped working properly in October 2013.
Now, it returns a simple placeholder. Now, it returns a simple placeholder.
<a href='https://code.google.com/p/opencaching-api/issues/detail?id=288'>Read more</a>.</p> <a href='https://code.google.com/p/opencaching-api/issues/detail?id=288'>Read more</a>.</p>
<p>OKAPI is trying to be as <b>integrated</b> with its <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. <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 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>. <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 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> sure if we want them displayed on our documentation pages).</p>
</desc> </desc>
<req name='issue_id'> <req name='issue_id'>
ID of an Issue. ID of an Issue.
</req> </req>
<common-format-params/> <common-format-params/>
<returns> <returns>
<p>A dictionary of the following structure:</p> <p>A dictionary of the following structure:</p>
<ul> <ul>
<li><b>id</b> - number of the issue created for this method,</li> <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><b>last_updated</b> - date and time (ISO 8601) when the issue was last updated
<b>or null</b> if unknown,</li> <b>or null</b> if unknown,</li>
<li><b>title</b> - issue title <b>or null</b> if unknown,</li> <li><b>title</b> - issue title <b>or null</b> if unknown,</li>
<li><b>url</b> - URL of the issue page,</li> <li><b>url</b> - URL of the issue page,</li>
<li><b>comment_count</b> - total number of submitted comments <b>or null</b> if unknown.</li> <li><b>comment_count</b> - total number of submitted comments <b>or null</b> if unknown.</li>
</ul> </ul>
<p>Note, that this will respond with HTTP 400 if we fail to retrieve data from <p>Note, that this will respond with HTTP 400 if we fail to retrieve data from
the Google Code site.</p> the Google Code site.</p>
</returns> </returns>
</xml> </xml>

View File

@@ -14,158 +14,216 @@ use okapi\OkapiInternalConsumer;
class WebService class WebService
{ {
public static function options() public static function options()
{ {
return array( return array(
'min_auth_level' => 0 'min_auth_level' => 0
); );
} }
private static function arg_desc($arg_node) private static function arg_desc($arg_node)
{ {
$attrs = $arg_node->attributes(); $attrs = $arg_node->attributes();
return array( return array(
'name' => (string)$attrs['name'], 'name' => (string)$attrs['name'],
'is_required' => $arg_node->getName() == 'req', 'is_required' => $arg_node->getName() == 'req',
'is_deprecated' => (isset($attrs['class']) && (strpos($attrs['class'], 'deprecated') !== false)), 'is_deprecated' => (isset($attrs['class']) && (strpos($attrs['class'], 'deprecated') !== false)),
'class' => 'public', 'class' => 'public',
'description' => 'description' =>
(isset($attrs['default']) ? ("<p>Default value: <b>".$attrs['default']."</b></p>") : ""). (isset($attrs['default']) ? ("<p>Default value: <b>".$attrs['default']."</b></p>") : "").
self::get_inner_xml($arg_node), self::get_inner_xml($arg_node),
); );
} }
private static function get_inner_xml($node) private static function get_inner_xml($node)
{ {
/* Fetch as <some-node>content</some-node>, extract content. */ /* Fetch as <some-node>content</some-node>, extract content. */
$s = $node->asXML(); $s = $node->asXML();
$start = strpos($s, ">") + 1; $start = strpos($s, ">") + 1;
$length = strlen($s) - $start - (3 + strlen($node->getName())); $length = strlen($s) - $start - (3 + strlen($node->getName()));
$s = substr($s, $start, $length); $s = substr($s, $start, $length);
/* Find and replace %okapi:plugins%. */ /* Find and replace %okapi:plugins%. */
$s = preg_replace_callback("~%OKAPI:([a-z:]+)%~", array("self", "plugin_callback"), $s); $s = preg_replace_callback('~%OKAPI:([a-z:/_#]+)%~', array("self", "plugin_callback"), $s);
return $s; return $s;
} }
public static function plugin_callback($matches) /**
{ * You can use the following syntax:
$input = $matches[1]; *
$arr = explode(":", $input); * <a href="%OKAPI:docurl:fragment%">any text</a> - to reference fragment of introducing
$plugin_name = $arr[0]; * documentation
*
* <a href="%OKAPI:methodref:methodname%">any text</a> - to reference any other method
*
* <a href="%OKAPI:methodref:methodname#html_anchor%">any text</a> - to reference
* any HTML anchor in other method
*
* <a href="%OKAPI:methodref:#html_anchor%">any text</a> - to reference any HTML
* anchor within current document
*
* <a href="%OKAPI:methodargref:methodname#argument_name%">any text</a> - to
* reference argument of another method
*
* <a href="%OKAPI:methodargref:#argument_name%">any text</a> - to reference
* argument within current method
*
* <a href="%OKAPI:methodretref:methodname#returned_key%">any text</a> - to
* reference returned value of another method
*
* <a href="%OKAPI:methodretref:#returned_key%">any text</a> - to reference
* returned value within current method
*
* NOTE!
*
* Since returned JSON dictionaries are not standardized (they are simply plain
* HTML in the docs), to reference returned values you must manually create an
* anchor prefixed with ret_, i.e. (HTML snippet): <li
* id="ret_alt_wpts"><p><b>alt_wpts</b> - list of alternate/additional
* waypoints</...> and access it with (HTML snippet): <a
* href="%OKAPI:methodretref:#alt_wpts%">any text</a>.
*/
public static function plugin_callback($matches)
{
$input = $matches[1];
$arr = explode(":", $input);
$plugin_name = $arr[0];
switch ($plugin_name) { switch ($plugin_name) {
case 'docurl': case 'docurl':
$fragment = $arr[1]; $fragment = $arr[1];
return Settings::get('SITE_URL')."okapi/introduction.html#".$fragment; return Settings::get('SITE_URL')."okapi/introduction.html#".$fragment;
default: case 'methodref':
throw new Exception("Unknown plugin: ".$input); case 'methodargref':
} case 'methodretref':
} $elements = explode('#', $arr[1]);
$result = '';
if ($elements[0] != '')
{
$result .= Settings::get('SITE_URL')."okapi/".$elements[0].'.html';
}
if (count($elements) > 1)
{
$result .= '#';
switch ($plugin_name) {
case 'methodargref':
$result .= 'arg_';
break;
case 'methodretref':
$result .= 'ret_';
break;
}
$result .= $elements[1];
}
return $result;
default:
throw new Exception("Unknown plugin: ".$input);
}
}
public static function call(OkapiRequest $request) public static function call(OkapiRequest $request)
{ {
$methodname = $request->get_parameter('name'); $methodname = $request->get_parameter('name');
if (!$methodname) if (!$methodname)
throw new ParamMissing('name'); throw new ParamMissing('name');
if (!preg_match("#^services/[0-9a-z_/]*$#", $methodname)) if (!preg_match("#^services/[0-9a-z_/]*$#", $methodname))
throw new InvalidParam('name'); throw new InvalidParam('name');
if (!OkapiServiceRunner::exists($methodname)) if (!OkapiServiceRunner::exists($methodname))
throw new InvalidParam('name', "Method does not exist: '$methodname'."); throw new InvalidParam('name', "Method does not exist: '$methodname'.");
$options = OkapiServiceRunner::options($methodname); $options = OkapiServiceRunner::options($methodname);
if (!isset($options['min_auth_level'])) if (!isset($options['min_auth_level']))
throw new Exception("Method $methodname is missing a required 'min_auth_level' option!"); throw new Exception("Method $methodname is missing a required 'min_auth_level' option!");
$docs = simplexml_load_string(OkapiServiceRunner::docs($methodname)); $docs = simplexml_load_string(OkapiServiceRunner::docs($methodname));
$exploded = explode("/", $methodname); $exploded = explode("/", $methodname);
$result = array( $result = array(
'name' => $methodname, 'name' => $methodname,
'short_name' => end($exploded), 'short_name' => end($exploded),
'ref_url' => Settings::get('SITE_URL')."okapi/$methodname.html", 'ref_url' => Settings::get('SITE_URL')."okapi/$methodname.html",
'auth_options' => array( 'auth_options' => array(
'min_auth_level' => $options['min_auth_level'], 'min_auth_level' => $options['min_auth_level'],
'oauth_consumer' => $options['min_auth_level'] >= 2, 'oauth_consumer' => $options['min_auth_level'] >= 2,
'oauth_token' => $options['min_auth_level'] >= 3, 'oauth_token' => $options['min_auth_level'] >= 3,
) )
); );
if (!$docs->brief) if (!$docs->brief)
throw new Exception("Missing <brief> element in the $methodname.xml file."); throw new Exception("Missing <brief> element in the $methodname.xml file.");
if ($docs->brief != self::get_inner_xml($docs->brief)) if ($docs->brief != self::get_inner_xml($docs->brief))
throw new Exception("The <brief> element may not contain HTML markup ($methodname.xml)."); throw new Exception("The <brief> element may not contain HTML markup ($methodname.xml).");
if (strlen($docs->brief) > 80) if (strlen($docs->brief) > 80)
throw new Exception("The <brief> description may not be longer than 80 characters ($methodname.xml)."); throw new Exception("The <brief> description may not be longer than 80 characters ($methodname.xml).");
if (strpos($docs->brief, "\n") !== false) if (strpos($docs->brief, "\n") !== false)
throw new Exception("The <brief> element may not contain new-lines ($methodname.xml)."); throw new Exception("The <brief> element may not contain new-lines ($methodname.xml).");
if (substr(trim($docs->brief), -1) == '.') if (substr(trim($docs->brief), -1) == '.')
throw new Exception("The <brief> element should not end with a dot ($methodname.xml)."); throw new Exception("The <brief> element should not end with a dot ($methodname.xml).");
$result['brief_description'] = self::get_inner_xml($docs->brief); $result['brief_description'] = self::get_inner_xml($docs->brief);
if ($docs->{'issue-id'}) if ($docs->{'issue-id'})
$result['issue_id'] = (string)$docs->{'issue-id'}; $result['issue_id'] = (string)$docs->{'issue-id'};
else else
$result['issue_id'] = null; $result['issue_id'] = null;
if (!$docs->desc) if (!$docs->desc)
throw new Exception("Missing <desc> element in the $methodname.xml file."); throw new Exception("Missing <desc> element in the $methodname.xml file.");
$result['description'] = self::get_inner_xml($docs->desc); $result['description'] = self::get_inner_xml($docs->desc);
$result['arguments'] = array(); $result['arguments'] = array();
foreach ($docs->req as $arg) { $result['arguments'][] = self::arg_desc($arg); } foreach ($docs->req as $arg) { $result['arguments'][] = self::arg_desc($arg); }
foreach ($docs->opt 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) foreach ($docs->{'import-params'} as $import_desc)
{ {
$attrs = $import_desc->attributes(); $attrs = $import_desc->attributes();
$referenced_methodname = $attrs['method']; $referenced_methodname = $attrs['method'];
$referenced_method_info = OkapiServiceRunner::call('services/apiref/method', $referenced_method_info = OkapiServiceRunner::call('services/apiref/method',
new OkapiInternalRequest(new OkapiInternalConsumer(), null, array('name' => $referenced_methodname))); new OkapiInternalRequest(new OkapiInternalConsumer(), null, array('name' => $referenced_methodname)));
$include_list = isset($attrs['params']) ? explode("|", $attrs['params']) : null; $include_list = isset($attrs['params']) ? explode("|", $attrs['params']) : null;
$exclude_list = isset($attrs['except']) ? explode("|", $attrs['except']) : array(); $exclude_list = isset($attrs['except']) ? explode("|", $attrs['except']) : array();
foreach ($referenced_method_info['arguments'] as $arg) foreach ($referenced_method_info['arguments'] as $arg)
{ {
if ($arg['class'] == 'common-formatting') if ($arg['class'] == 'common-formatting')
continue; continue;
if (($include_list === null) && (count($exclude_list) == 0)) if (($include_list === null) && (count($exclude_list) == 0))
{ {
$arg['description'] = "<i>Inherited from <a href='".$referenced_method_info['ref_url']. $arg['description'] = "<i>Inherited from <a href='".$referenced_method_info['ref_url'].
"'>".$referenced_method_info['name']."</a> method.</i>"; "'>".$referenced_method_info['name']."</a> method.</i>";
} }
elseif ( elseif (
(($include_list === null) || in_array($arg['name'], $include_list)) (($include_list === null) || in_array($arg['name'], $include_list))
&& (!in_array($arg['name'], $exclude_list)) && (!in_array($arg['name'], $exclude_list))
) { ) {
$arg['description'] = "<i>Same as in the <a href='".$referenced_method_info['ref_url']. $arg['description'] = "<i>Same as in the <a href='".$referenced_method_info['ref_url'].
"'>".$referenced_method_info['name']."</a> method.</i>"; "'>".$referenced_method_info['name']."</a> method.</i>";
} else { } else {
continue; continue;
} }
$arg['class'] = 'inherited'; $arg['class'] = 'inherited';
$result['arguments'][] = $arg; $result['arguments'][] = $arg;
} }
} }
if ($docs->{'common-format-params'}) if ($docs->{'common-format-params'})
{ {
$result['arguments'][] = array( $result['arguments'][] = array(
'name' => 'format', 'name' => 'format',
'is_required' => false, 'is_required' => false,
'is_deprecated' => false, 'is_deprecated' => false,
'class' => 'common-formatting', 'class' => 'common-formatting',
'description' => "<i>Standard <a href='".Settings::get('SITE_URL')."okapi/introduction.html#common-formatting'>common formatting</a> argument.</i>" 'description' => "<i>Standard <a href='".Settings::get('SITE_URL')."okapi/introduction.html#common-formatting'>common formatting</a> argument.</i>"
); );
$result['arguments'][] = array( $result['arguments'][] = array(
'name' => 'callback', 'name' => 'callback',
'is_required' => false, 'is_required' => false,
'is_deprecated' => false, 'is_deprecated' => false,
'class' => 'common-formatting', 'class' => 'common-formatting',
'description' => "<i>Standard <a href='".Settings::get('SITE_URL')."okapi/introduction.html#common-formatting'>common formatting</a> argument.</i>" 'description' => "<i>Standard <a href='".Settings::get('SITE_URL')."okapi/introduction.html#common-formatting'>common formatting</a> argument.</i>"
); );
} }
foreach ($result['arguments'] as &$arg_ref) foreach ($result['arguments'] as &$arg_ref)
if ($arg_ref['is_deprecated']) if ($arg_ref['is_deprecated'])
$arg_ref['class'] .= " deprecated"; $arg_ref['class'] .= " deprecated";
if (!$docs->returns) if (!$docs->returns)
throw new Exception("Missing <returns> element in the $methodname.xml file. ". throw new Exception("Missing <returns> element in the $methodname.xml file. ".
"If your method does not return anything, you should document in nonetheless."); "If your method does not return anything, you should document in nonetheless.");
$result['returns'] = self::get_inner_xml($docs->returns); $result['returns'] = self::get_inner_xml($docs->returns);
return Okapi::formatted_response($request, $result); return Okapi::formatted_response($request, $result);
} }
} }

View File

@@ -1,55 +1,55 @@
<xml> <xml>
<brief>Get information on a given OKAPI service method</brief> <brief>Get information on a given OKAPI service method</brief>
<issue-id>13</issue-id> <issue-id>13</issue-id>
<desc> <desc>
<p>This method allows you to <b>access API documentation</b> (the same which you <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> are reading just now). Given a method name, it returns a complete method description.</p>
</desc> </desc>
<req name='name'> <req name='name'>
Name of a method (begins with "services/"). Name of a method (begins with "services/").
</req> </req>
<common-format-params/> <common-format-params/>
<returns> <returns>
<p>A dictionary of the following structure:</p> <p>A dictionary of the following structure:</p>
<ul> <ul>
<li><b>name</b> - name of the method,</li> <li><b>name</b> - name of the method,</li>
<li><b>short_name</b> - name without a path,</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 <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 homepage Issue Tracker) associated with this method <b>or null</b> if this
method has associated issue,</li> method has associated issue,</li>
<li><b>description</b> - HTML-formatted description of what the method does,</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, <li><b>brief_description</b> - brief (max 80 characters), single-line,
plain-text description of what the method does,</li> 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>ref_url</b> - URL of the documentation page with method description,</li>
<li> <li>
<b>auth_options</b> - a dictionary which describes authentication <b>auth_options</b> - a dictionary which describes authentication
requirements for this method, it has a following structure: requirements for this method, it has a following structure:
<ul> <ul>
<li><b>min_auth_level</b> - integer, in range from 0 to 3, <li><b>min_auth_level</b> - integer, in range from 0 to 3,
see Introduction page.</li> see Introduction page.</li>
<li><b>oauth_consumer</b> - true, if requests are required to be signed <li><b>oauth_consumer</b> - true, if requests are required to be signed
with OAuth Consumer Key (min_auth_level >= 2),</li> with OAuth Consumer Key (min_auth_level >= 2),</li>
<li><b>oauth_token</b> - true, if requests are required to include an <li><b>oauth_token</b> - true, if requests are required to include an
OAuth Token (min_auth_level == 3).</li> OAuth Token (min_auth_level == 3).</li>
</ul> </ul>
</li> </li>
<li><b>arguments</b> - list of dictionaries, describes method <li><b>arguments</b> - list of dictionaries, describes method
arguments. Each dictionary has a following structure: arguments. Each dictionary has a following structure:
<ul> <ul>
<li><b>name</b> - name of an argument,</li> <li><b>name</b> - name of an argument,</li>
<li><b>is_required</b> - boolean, true if the argument is required,</li> <li><b>is_required</b> - boolean, true if the argument is required,</li>
<li><b>is_deprecated</b> - boolean, true if the argument is deprecated,</li> <li><b>is_deprecated</b> - boolean, true if the argument is deprecated,</li>
<li><b>description</b> - HTML-formatted description of an argument.</li> <li><b>description</b> - HTML-formatted description of an argument.</li>
<li> <li>
<p><b>class</b> - space separated list of the following values: <i>public</i>, <p><b>class</b> - space separated list of the following values: <i>public</i>,
<i>inherited</i>, <i>common-formatting</i> and <i>deprecated</i> <i>inherited</i>, <i>common-formatting</i> and <i>deprecated</i>
(other values might be introduced in future).</p> (other values might be introduced in future).</p>
<p>Currently these values do not mean anything specific. They are <p>Currently these values do not mean anything specific. They are
used for different coloring/styling in the documentation pages.</p> used for different coloring/styling in the documentation pages.</p>
</li> </li>
</ul> </ul>
</li> </li>
<li><b>returns</b> - HTML-formatted description method's return value.</li> <li><b>returns</b> - HTML-formatted description method's return value.</li>
</ul> </ul>
</returns> </returns>
</xml> </xml>

View File

@@ -15,33 +15,33 @@ use okapi\OkapiInternalRequest;
class WebService class WebService
{ {
public static function options() public static function options()
{ {
return array( return array(
'min_auth_level' => 0 'min_auth_level' => 0
); );
} }
public static function call(OkapiRequest $request) public static function call(OkapiRequest $request)
{ {
$methodnames = OkapiServiceRunner::$all_names; $methodnames = OkapiServiceRunner::$all_names;
sort($methodnames); sort($methodnames);
$cache_key = "api_ref/method_index#".md5(implode("#", $methodnames)); $cache_key = "api_ref/method_index#".md5(implode("#", $methodnames));
$results = Cache::get($cache_key); $results = Cache::get($cache_key);
if ($results == null) if ($results == null)
{ {
$results = array(); $results = array();
foreach ($methodnames as $methodname) foreach ($methodnames as $methodname)
{ {
$info = OkapiServiceRunner::call('services/apiref/method', new OkapiInternalRequest( $info = OkapiServiceRunner::call('services/apiref/method', new OkapiInternalRequest(
new OkapiInternalConsumer(), null, array('name' => $methodname))); new OkapiInternalConsumer(), null, array('name' => $methodname)));
$results[] = array( $results[] = array(
'name' => $info['name'], 'name' => $info['name'],
'brief_description' => $info['brief_description'], 'brief_description' => $info['brief_description'],
); );
} }
Cache::set($cache_key, $results, 3600); Cache::set($cache_key, $results, 3600);
} }
return Okapi::formatted_response($request, $results); return Okapi::formatted_response($request, $results);
} }
} }

View File

@@ -1,17 +1,17 @@
<xml> <xml>
<brief>Get a list of OKAPI methods with brief descriptions</brief> <brief>Get a list of OKAPI methods with brief descriptions</brief>
<issue-id>12</issue-id> <issue-id>12</issue-id>
<desc> <desc>
<p>Get a list of OKAPI methods with brief descriptions.</p> <p>Get a list of OKAPI methods with brief descriptions.</p>
</desc> </desc>
<common-format-params/> <common-format-params/>
<returns> <returns>
A list of dictionaries, each of which contains one API A list of dictionaries, each of which contains one API
method description in the following format: method description in the following format:
<ul> <ul>
<li><b>name</b> - name of a method,</li> <li><b>name</b> - name of a method,</li>
<li><b>brief_description</b> - brief (max 80 characters), single-line, <li><b>brief_description</b> - brief (max 80 characters), single-line,
plain-text description of what the method does.</li> plain-text description of what the method does.</li>
</ul> </ul>
</returns> </returns>
</xml> </xml>

View File

@@ -13,20 +13,20 @@ use okapi\OkapiInternalRequest;
class WebService class WebService
{ {
public static function options() public static function options()
{ {
return array( return array(
'min_auth_level' => 0 'min_auth_level' => 0
); );
} }
public static function call(OkapiRequest $request) public static function call(OkapiRequest $request)
{ {
$result = array(); $result = array();
$result['site_url'] = Settings::get('SITE_URL'); $result['site_url'] = Settings::get('SITE_URL');
$result['okapi_base_url'] = $result['site_url']."okapi/"; $result['okapi_base_url'] = $result['site_url']."okapi/";
$result['site_name'] = Okapi::get_normalized_site_name(); $result['site_name'] = Okapi::get_normalized_site_name();
$result['okapi_revision'] = Okapi::$revision; $result['okapi_revision'] = Okapi::$revision;
return Okapi::formatted_response($request, $result); return Okapi::formatted_response($request, $result);
} }
} }

View File

@@ -1,31 +1,31 @@
<xml> <xml>
<brief>Get information on this OKAPI installation</brief> <brief>Get information on this OKAPI installation</brief>
<issue-id>14</issue-id> <issue-id>14</issue-id>
<desc> <desc>
Retrieve some basic information about this OKAPI installation. Retrieve some basic information about this OKAPI installation.
</desc> </desc>
<common-format-params/> <common-format-params/>
<returns> <returns>
<p>A dictionary of the following structure:</p> <p>A dictionary of the following structure:</p>
<ul> <ul>
<li> <li>
<b>site_url</b> - URL of the Opencaching site which is running <b>site_url</b> - URL of the Opencaching site which is running
the OKAPI installation (usually this looks like the OKAPI installation (usually this looks like
"http://www.opencaching.<i>xx</i>/", where <b>xx</b> is a top "http://www.opencaching.<i>xx</i>/", where <b>xx</b> is a top
level domain of a country). level domain of a country).
</li> </li>
<li> <li>
<b>okapi_base_url</b> - URL of the OKAPI installation (usually this is <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 <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 that); this value is to be used as a prefix when constructing service
method URLs, method URLs,
</li> </li>
<li> <li>
<b>site_name</b> - international name of the Opencaching site, <b>site_name</b> - international name of the Opencaching site,
</li> </li>
<li><b>okapi_revision</b> - integer, an SVN revision of the OKAPI project <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 installed on this Opencaching site, <b>or null</b>, when could not
determine revision number.</li> determine revision number.</li>
</ul> </ul>
</returns> </returns>
</xml> </xml>

View File

@@ -15,104 +15,104 @@ use okapi\OkapiInternalRequest;
class WebService class WebService
{ {
public static function options() public static function options()
{ {
return array( return array(
'min_auth_level' => 0 'min_auth_level' => 0
); );
} }
public static function call(OkapiRequest $request) public static function call(OkapiRequest $request)
{ {
# The list of installations is periodically refreshed by contacting OKAPI # The list of installations is periodically refreshed by contacting OKAPI
# repository. This method usually displays the cached version of it. # repository. This method usually displays the cached version of it.
$cachekey = 'apisrv/installations'; $cachekey = 'apisrv/installations';
$backupkey = 'apisrv/installations-backup'; $backupkey = 'apisrv/installations-backup';
$results = Cache::get($cachekey); $results = Cache::get($cachekey);
if (!$results) if (!$results)
{ {
# Download the current list of OKAPI servers. # Download the current list of OKAPI servers.
try try
{ {
$opts = array( $opts = array(
'http' => array( 'http' => array(
'method' => "GET", 'method' => "GET",
'timeout' => 5.0 'timeout' => 5.0
) )
); );
$context = stream_context_create($opts); $context = stream_context_create($opts);
$xml = file_get_contents("http://opencaching-api.googlecode.com/svn/trunk/etc/installations.xml", $xml = file_get_contents("http://opencaching-api.googlecode.com/svn/trunk/etc/installations.xml",
false, $context); false, $context);
} }
catch (ErrorException $e) catch (ErrorException $e)
{ {
# Google failed on us. Try to respond with a backup list. # Google failed on us. Try to respond with a backup list.
$results = Cache::get($backupkey); $results = Cache::get($backupkey);
if ($results) if ($results)
{ {
Cache::set($cachekey, $results, 12 * 3600); # so to retry no earlier than after 12 hours Cache::set($cachekey, $results, 12 * 3600); # so to retry no earlier than after 12 hours
return Okapi::formatted_response($request, $results); return Okapi::formatted_response($request, $results);
} }
# Backup has expired (or have never been cached). If we're on a development # 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. # server then probably it's okay. In production this SHOULD NOT happen.
$results = array( $results = array(
array( array(
'site_url' => Settings::get('SITE_URL'), 'site_url' => Settings::get('SITE_URL'),
'site_name' => "Unable to retrieve!", 'site_name' => "Unable to retrieve!",
'okapi_base_url' => Settings::get('SITE_URL')."okapi/", 'okapi_base_url' => Settings::get('SITE_URL')."okapi/",
) )
); );
Cache::set($cachekey, $results, 12 * 3600); # so to retry no earlier than after 12 hours Cache::set($cachekey, $results, 12 * 3600); # so to retry no earlier than after 12 hours
return Okapi::formatted_response($request, $results); return Okapi::formatted_response($request, $results);
} }
$doc = simplexml_load_string($xml); $doc = simplexml_load_string($xml);
$results = array(); $results = array();
$i_was_included = false; $i_was_included = false;
foreach ($doc->installation as $inst) foreach ($doc->installation as $inst)
{ {
$site_url = (string)$inst[0]['site_url']; $site_url = (string)$inst[0]['site_url'];
if ($inst[0]['okapi_base_url']) if ($inst[0]['okapi_base_url'])
$okapi_base_url = (string)$inst[0]['okapi_base_url']; $okapi_base_url = (string)$inst[0]['okapi_base_url'];
else else
$okapi_base_url = $site_url."okapi/"; $okapi_base_url = $site_url."okapi/";
if ($inst[0]['site_name']) if ($inst[0]['site_name'])
$site_name = (string)$inst[0]['site_name']; $site_name = (string)$inst[0]['site_name'];
else else
$site_name = Okapi::get_normalized_site_name($site_url); $site_name = Okapi::get_normalized_site_name($site_url);
$results[] = array( $results[] = array(
'site_url' => $site_url, 'site_url' => $site_url,
'site_name' => $site_name, 'site_name' => $site_name,
'okapi_base_url' => $okapi_base_url, 'okapi_base_url' => $okapi_base_url,
); );
if ($site_url == Settings::get('SITE_URL')) if ($site_url == Settings::get('SITE_URL'))
$i_was_included = true; $i_was_included = true;
} }
# If running on a local development installation, then include the local # If running on a local development installation, then include the local
# installation URL. # installation URL.
if (!$i_was_included) if (!$i_was_included)
{ {
$results[] = array( $results[] = array(
'site_url' => Settings::get('SITE_URL'), 'site_url' => Settings::get('SITE_URL'),
'site_name' => "DEVELSITE", 'site_name' => "DEVELSITE",
'okapi_base_url' => Settings::get('SITE_URL')."okapi/", 'okapi_base_url' => Settings::get('SITE_URL')."okapi/",
); );
# Contact OKAPI developers in order to get added to the official sites list! # 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 it for one day. Also, save a backup (valid for 30 days).
Cache::set($cachekey, $results, 86400); Cache::set($cachekey, $results, 86400);
Cache::set($backupkey, $results, 86400*30); Cache::set($backupkey, $results, 86400*30);
} }
return Okapi::formatted_response($request, $results); return Okapi::formatted_response($request, $results);
} }
} }

View File

@@ -1,33 +1,33 @@
<xml> <xml>
<brief>Get the list of all public OKAPI installations</brief> <brief>Get the list of all public OKAPI installations</brief>
<issue-id>39</issue-id> <issue-id>39</issue-id>
<desc> <desc>
Get the list of all public OKAPI installations. Keep in mind that Get the list of all public OKAPI installations. Keep in mind that
OKAPI installations might differ slightly. If you plan on using OKAPI installations might differ slightly. If you plan on using
multiple OKAPI installations in your application (which is a very multiple OKAPI installations in your application (which is a very
good thing!) you should test it against the one with the <b>lowest</b> good thing!) you should test it against the one with the <b>lowest</b>
OKAPI revision number. OKAPI revision number.
</desc> </desc>
<common-format-params/> <common-format-params/>
<returns> <returns>
<p>A dictionary of the following structure:</p> <p>A dictionary of the following structure:</p>
<ul> <ul>
<li> <li>
<b>site_url</b> - URL of the Opencaching site which is running <b>site_url</b> - URL of the Opencaching site which is running
the OKAPI installation (usually this looks like the OKAPI installation (usually this looks like
"http://www.opencaching.<i>xx</i>/", where <b>xx</b> is a top "http://www.opencaching.<i>xx</i>/", where <b>xx</b> is a top
level domain of a country). level domain of a country).
</li> </li>
<li> <li>
<b>site_name</b> - universal name for this site (should be fine <b>site_name</b> - universal name for this site (should be fine
for all languages), for all languages),
</li> </li>
<li> <li>
<b>okapi_base_url</b> - URL of the OKAPI installation (usually this is <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 <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 that); this value is to be used as a prefix when constructing service
method URLs. method URLs.
</li> </li>
</ul> </ul>
</returns> </returns>
</xml> </xml>

View File

@@ -15,48 +15,48 @@ use okapi\Settings;
class WebService class WebService
{ {
public static function options() public static function options()
{ {
return array( return array(
'min_auth_level' => 0 'min_auth_level' => 0
); );
} }
public static function call(OkapiRequest $request) public static function call(OkapiRequest $request)
{ {
$cachekey = "apisrv/stats"; $cachekey = "apisrv/stats";
$result = Cache::get($cachekey); $result = Cache::get($cachekey);
if (!$result) if (!$result)
{ {
$result = array( $result = array(
'cache_count' => 0 + Db::select_value(" 'cache_count' => 0 + Db::select_value("
select count(*) from caches where status in (1,2,3) select count(*) from caches where status in (1,2,3)
"), "),
'user_count' => 0 + Db::select_value(" 'user_count' => 0 + Db::select_value("
select count(*) from ( select count(*) from (
select distinct user_id select distinct user_id
from cache_logs from cache_logs
where where
type in (1,2) type in (1,2)
and ".((Settings::get('OC_BRANCH') == 'oc.pl') ? "deleted = 0" : "true")." and ".((Settings::get('OC_BRANCH') == 'oc.pl') ? "deleted = 0" : "true")."
UNION DISTINCT UNION DISTINCT
select distinct user_id select distinct user_id
from caches from caches
) as t; ) as t;
"), "),
'apps_count' => 0 + Db::select_value("select count(*) from okapi_consumers;"), 'apps_count' => 0 + Db::select_value("select count(*) from okapi_consumers;"),
'apps_active' => 0 + Db::select_value(" 'apps_active' => 0 + Db::select_value("
select count(distinct s.consumer_key) select count(distinct s.consumer_key)
from from
okapi_stats_hourly s, okapi_stats_hourly s,
okapi_consumers c okapi_consumers c
where where
s.consumer_key = c.`key` s.consumer_key = c.`key`
and s.period_start > date_add(now(), interval -30 day) and s.period_start > date_add(now(), interval -30 day)
"), "),
); );
Cache::set($cachekey, $result, 86400); # cache it for one day Cache::set($cachekey, $result, 86400); # cache it for one day
} }
return Okapi::formatted_response($request, $result); return Okapi::formatted_response($request, $result);
} }
} }

View File

@@ -1,20 +1,20 @@
<xml> <xml>
<brief>Get some basic stats about the site</brief> <brief>Get some basic stats about the site</brief>
<issue-id>43</issue-id> <issue-id>43</issue-id>
<desc> <desc>
Retrieve some basic statistics about this OKAPI installation. Retrieve some basic statistics about this OKAPI installation.
If you want some more stats, post a comment! If you want some more stats, post a comment!
</desc> </desc>
<common-format-params/> <common-format-params/>
<returns> <returns>
<p>A dictionary of the following structure:</p> <p>A dictionary of the following structure:</p>
<ul> <ul>
<li><b>cache_count</b> - approximate total number of geocaches stored at this site,</li> <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>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 <li><b>apps_count</b> - approximate total number of all OKAPI applications (number of
registered API Keys).</li> registered API Keys).</li>
<li><b>apps_active</b> - approximate number of active OKAPI applications (the ones which issued <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> at least one, non-anonymous OKAPI request during the last month).</li>
</ul> </ul>
</returns> </returns>
</xml> </xml>

View File

@@ -18,272 +18,272 @@ use SimpleXMLElement;
class AttrHelper class AttrHelper
{ {
/** /**
* By default, when DEBUG mode is enabled, the attributes.xml file is * By default, when DEBUG mode is enabled, the attributes.xml file is
* reloaded practically on every request. If you don't want that, you can * reloaded practically on every request. If you don't want that, you can
* temporarilly disable this behavior by settings this to false. * temporarilly disable this behavior by settings this to false.
*/ */
private static $RELOAD_ON_DEBUG = true; private static $RELOAD_ON_DEBUG = true;
private static $attr_dict = null; private static $attr_dict = null;
/** /**
* Return the cache key suffix to be used for caching. This should be used * Return the cache key suffix to be used for caching. This should be used
* In order for the $RELOAD_ON_DEBUG to work properly when switching to/from * In order for the $RELOAD_ON_DEBUG to work properly when switching to/from
* DEBUG mode. * DEBUG mode.
*/ */
private static function cache_key_suffix() private static function cache_key_suffix()
{ {
return (self::$RELOAD_ON_DEBUG) ? "#DBG" : ""; return (self::$RELOAD_ON_DEBUG) ? "#DBG" : "";
} }
/** Return the timeout to be used for attribute caching. */ /** Return the timeout to be used for attribute caching. */
private static function ttl() private static function ttl()
{ {
return (Settings::get('DEBUG') && self::$RELOAD_ON_DEBUG) ? 2 : 86400; return (Settings::get('DEBUG') && self::$RELOAD_ON_DEBUG) ? 2 : 86400;
} }
/** /**
* Forces an immediate refresh of the current attributes from the * Forces an immediate refresh of the current attributes from the
* attribute-definitions.xml file. * attribute-definitions.xml file.
*/ */
public static function refresh_now() public static function refresh_now()
{ {
try try
{ {
$path = $GLOBALS['rootpath']."okapi/services/attrs/attribute-definitions.xml"; $path = $GLOBALS['rootpath']."okapi/services/attrs/attribute-definitions.xml";
$xml = file_get_contents($path); $xml = file_get_contents($path);
self::refresh_from_string($xml); self::refresh_from_string($xml);
} }
catch (Exception $e) catch (Exception $e)
{ {
# Failed to read or parse the file (i.e. after a syntax error was # Failed to read or parse the file (i.e. after a syntax error was
# commited). Let's check when the last successful parse occured. # commited). Let's check when the last successful parse occured.
self::init_from_cache(false); self::init_from_cache(false);
if (self::$attr_dict === null) if (self::$attr_dict === null)
{ {
# That's bad! We don't have ANY copy of the data AND we failed # That's bad! We don't have ANY copy of the data AND we failed
# to parse it. We will use a fake, empty data. # to parse it. We will use a fake, empty data.
$cache_key = "attrhelper/dict#".Okapi::$revision.self::cache_key_suffix(); $cache_key = "attrhelper/dict#".Okapi::$revision.self::cache_key_suffix();
$cachedvalue = array( $cachedvalue = array(
'attr_dict' => array(), 'attr_dict' => array(),
); );
Cache::set($cache_key, $cachedvalue, self::ttl()); Cache::set($cache_key, $cachedvalue, self::ttl());
} }
return; return;
} }
} }
/** /**
* Refresh all attributes from the given XML. Usually, this file is * Refresh all attributes from the given XML. Usually, this file is
* downloaded from Google Code (using refresh_now). * downloaded from Google Code (using refresh_now).
*/ */
public static function refresh_from_string($xml) public static function refresh_from_string($xml)
{ {
/* The attribute-definitions.xml file defines relationships between /* The attribute-definitions.xml file defines relationships between
* attributes originating from various OC installations. Each * attributes originating from various OC installations. Each
* installation uses internal IDs of its own. Which "attribute schema" * installation uses internal IDs of its own. Which "attribute schema"
* is being used in THIS installation? */ * is being used in THIS installation? */
$my_schema = Settings::get('ORIGIN_URL'); $my_schema = Settings::get('ORIGIN_URL');
$doc = simplexml_load_string($xml); $doc = simplexml_load_string($xml);
$cachedvalue = array( $cachedvalue = array(
'attr_dict' => array(), 'attr_dict' => array(),
); );
# Build cache attributes dictionary # Build cache attributes dictionary
$all_internal_ids = array(); $all_internal_ids = array();
foreach ($doc->attr as $attrnode) foreach ($doc->attr as $attrnode)
{ {
$attr = array( $attr = array(
'acode' => (string)$attrnode['acode'], 'acode' => (string)$attrnode['acode'],
'gc_equivs' => array(), 'gc_equivs' => array(),
'internal_id' => null, 'internal_id' => null,
'names' => array(), 'names' => array(),
'descriptions' => array(), 'descriptions' => array(),
'is_discontinued' => true 'is_discontinued' => true
); );
foreach ($attrnode->groundspeak as $gsnode) foreach ($attrnode->groundspeak as $gsnode)
{ {
$attr['gc_equivs'][] = array( $attr['gc_equivs'][] = array(
'id' => (int)$gsnode['id'], 'id' => (int)$gsnode['id'],
'inc' => in_array((string)$gsnode['inc'], array("true", "1")) ? 1 : 0, 'inc' => in_array((string)$gsnode['inc'], array("true", "1")) ? 1 : 0,
'name' => (string)$gsnode['name'] 'name' => (string)$gsnode['name']
); );
} }
foreach ($attrnode->opencaching as $ocnode) foreach ($attrnode->opencaching as $ocnode)
{ {
/* If it is used by at least one OC node, then it's NOT discontinued. */ /* If it is used by at least one OC node, then it's NOT discontinued. */
$attr['is_discontinued'] = false; $attr['is_discontinued'] = false;
if ((string)$ocnode['schema'] == $my_schema) if ((string)$ocnode['schema'] == $my_schema)
{ {
/* It is used by THIS OC node. */ /* It is used by THIS OC node. */
$internal_id = (int)$ocnode['id']; $internal_id = (int)$ocnode['id'];
if (isset($all_internal_ids[$internal_id])) if (isset($all_internal_ids[$internal_id]))
throw new Exception("The internal attribute ".$internal_id. throw new Exception("The internal attribute ".$internal_id.
" has multiple assigments to OKAPI attributes."); " has multiple assigments to OKAPI attributes.");
$all_internal_ids[$internal_id] = true; $all_internal_ids[$internal_id] = true;
if (!is_null($attr['internal_id'])) if (!is_null($attr['internal_id']))
throw new Exception("There are multiple internal IDs for the ". throw new Exception("There are multiple internal IDs for the ".
$attr['acode']." attribute."); $attr['acode']." attribute.");
$attr['internal_id'] = $internal_id; $attr['internal_id'] = $internal_id;
} }
} }
foreach ($attrnode->lang as $langnode) foreach ($attrnode->lang as $langnode)
{ {
$lang = (string)$langnode['id']; $lang = (string)$langnode['id'];
foreach ($langnode->name as $namenode) foreach ($langnode->name as $namenode)
{ {
if (isset($attr['names'][$lang])) if (isset($attr['names'][$lang]))
throw new Exception("Duplicate ".$lang." name of attribute ".$attr['acode']); throw new Exception("Duplicate ".$lang." name of attribute ".$attr['acode']);
$attr['names'][$lang] = (string)$namenode; $attr['names'][$lang] = (string)$namenode;
} }
foreach ($langnode->desc as $descnode) foreach ($langnode->desc as $descnode)
{ {
if (isset($attr['descriptions'][$lang])) if (isset($attr['descriptions'][$lang]))
throw new Exception("Duplicate ".$lang." description of attribute ".$attr['acode']); throw new Exception("Duplicate ".$lang." description of attribute ".$attr['acode']);
$xml = $descnode->asxml(); /* contains "<desc>" and "</desc>" */ $xml = $descnode->asxml(); /* contains "<desc>" and "</desc>" */
$innerxml = preg_replace("/(^[^>]+>)|(<[^<]+$)/us", "", $xml); $innerxml = preg_replace("/(^[^>]+>)|(<[^<]+$)/us", "", $xml);
$attr['descriptions'][$lang] = self::cleanup_string($innerxml); $attr['descriptions'][$lang] = self::cleanup_string($innerxml);
} }
} }
$cachedvalue['attr_dict'][$attr['acode']] = $attr; $cachedvalue['attr_dict'][$attr['acode']] = $attr;
} }
$cache_key = "attrhelper/dict#".Okapi::$revision.self::cache_key_suffix(); $cache_key = "attrhelper/dict#".Okapi::$revision.self::cache_key_suffix();
Cache::set($cache_key, $cachedvalue, self::ttl()); Cache::set($cache_key, $cachedvalue, self::ttl());
self::$attr_dict = $cachedvalue['attr_dict']; self::$attr_dict = $cachedvalue['attr_dict'];
} }
/** /**
* Object to be used for forward-compatibility (see the attributes method). * Object to be used for forward-compatibility (see the attributes method).
*/ */
public static function get_unknown_placeholder($acode) public static function get_unknown_placeholder($acode)
{ {
return array( return array(
'acode' => $acode, 'acode' => $acode,
'gc_equivs' => array(), 'gc_equivs' => array(),
'internal_id' => null, 'internal_id' => null,
'names' => array( 'names' => array(
'en' => "Unknown attribute" 'en' => "Unknown attribute"
), ),
'descriptions' => array( 'descriptions' => array(
'en' => ( 'en' => (
"This attribute ($acode) is unknown at ".Okapi::get_normalized_site_name(). "This attribute ($acode) is unknown at ".Okapi::get_normalized_site_name().
". It might not exist, or it may be a new attribute, recognized ". ". It might not exist, or it may be a new attribute, recognized ".
"only in newer OKAPI installations. Perhaps ".Okapi::get_normalized_site_name(). "only in newer OKAPI installations. Perhaps ".Okapi::get_normalized_site_name().
" needs to have its OKAPI updated?" " needs to have its OKAPI updated?"
) )
), ),
'is_discontinued' => true 'is_discontinued' => true
); );
} }
/** /**
* Initialize all the internal attributes (if not yet initialized). This * Initialize all the internal attributes (if not yet initialized). This
* loads attribute values from the cache. If they are not present in the * loads attribute values from the cache. If they are not present in the
* cache, it will read and parse them from attribute-definitions.xml file. * cache, it will read and parse them from attribute-definitions.xml file.
*/ */
private static function init_from_cache($allow_refreshing=true) private static function init_from_cache($allow_refreshing=true)
{ {
if (self::$attr_dict !== null) if (self::$attr_dict !== null)
{ {
/* Already initialized. */ /* Already initialized. */
return; return;
} }
$cache_key = "attrhelper/dict#".Okapi::$revision.self::cache_key_suffix(); $cache_key = "attrhelper/dict#".Okapi::$revision.self::cache_key_suffix();
$cachedvalue = Cache::get($cache_key); $cachedvalue = Cache::get($cache_key);
if ($cachedvalue === null) if ($cachedvalue === null)
{ {
# I.e. after Okapi::$revision is changed, or cache got invalidated. # I.e. after Okapi::$revision is changed, or cache got invalidated.
if ($allow_refreshing) if ($allow_refreshing)
{ {
self::refresh_now(); self::refresh_now();
self::init_from_cache(false); self::init_from_cache(false);
return; return;
} }
else else
{ {
$cachedvalue = array( $cachedvalue = array(
'attr_dict' => array(), 'attr_dict' => array(),
); );
} }
} }
self::$attr_dict = $cachedvalue['attr_dict']; self::$attr_dict = $cachedvalue['attr_dict'];
} }
/** /**
* Return a dictionary of all attributes. The format is INTERNAL and PRIVATE, * Return a dictionary of all attributes. The format is INTERNAL and PRIVATE,
* it is NOT the same as in the "attributes" method (but it is quite similar). * it is NOT the same as in the "attributes" method (but it is quite similar).
*/ */
public static function get_attrdict() public static function get_attrdict()
{ {
self::init_from_cache(); self::init_from_cache();
return self::$attr_dict; return self::$attr_dict;
} }
/** "\n\t\tBla blabla\n\t\t<b>bla</b>bla.\n\t" => "Bla blabla <b>bla</b>bla." */ /** "\n\t\tBla blabla\n\t\t<b>bla</b>bla.\n\t" => "Bla blabla <b>bla</b>bla." */
private static function cleanup_string($s) private static function cleanup_string($s)
{ {
return preg_replace('/(^\s+)|(\s+$)/us', "", preg_replace('/\s+/us', " ", $s)); return preg_replace('/(^\s+)|(\s+$)/us', "", preg_replace('/\s+/us', " ", $s));
} }
/** /**
* Get the mapping table between internal attribute id => OKAPI A-code. * Get the mapping table between internal attribute id => OKAPI A-code.
* The result is cached! * The result is cached!
*/ */
public static function get_internal_id_to_acode_mapping() public static function get_internal_id_to_acode_mapping()
{ {
static $mapping = null; static $mapping = null;
if ($mapping !== null) if ($mapping !== null)
return $mapping; return $mapping;
$cache_key = "attrhelper/id2acode/".Okapi::$revision.self::cache_key_suffix(); $cache_key = "attrhelper/id2acode/".Okapi::$revision.self::cache_key_suffix();
$mapping = Cache::get($cache_key); $mapping = Cache::get($cache_key);
if (!$mapping) if (!$mapping)
{ {
self::init_from_cache(); self::init_from_cache();
$mapping = array(); $mapping = array();
foreach (self::$attr_dict as $acode => &$attr_ref) foreach (self::$attr_dict as $acode => &$attr_ref)
$mapping[$attr_ref['internal_id']] = $acode; $mapping[$attr_ref['internal_id']] = $acode;
Cache::set($cache_key, $mapping, self::ttl()); Cache::set($cache_key, $mapping, self::ttl());
} }
return $mapping; return $mapping;
} }
/** /**
* Get the mapping: A-codes => attribute name. The language for the name * Get the mapping: A-codes => attribute name. The language for the name
* is selected based on the $langpref parameter. The result is cached! * is selected based on the $langpref parameter. The result is cached!
*/ */
public static function get_acode_to_name_mapping($langpref) public static function get_acode_to_name_mapping($langpref)
{ {
static $mapping = null; static $mapping = null;
if ($mapping !== null) if ($mapping !== null)
return $mapping; return $mapping;
$cache_key = md5(serialize(array("attrhelper/acode2name", $langpref, $cache_key = md5(serialize(array("attrhelper/acode2name", $langpref,
Okapi::$revision, self::cache_key_suffix()))); Okapi::$revision, self::cache_key_suffix())));
$mapping = Cache::get($cache_key); $mapping = Cache::get($cache_key);
if (!$mapping) if (!$mapping)
{ {
self::init_from_cache(); self::init_from_cache();
$mapping = array(); $mapping = array();
foreach (self::$attr_dict as $acode => &$attr_ref) foreach (self::$attr_dict as $acode => &$attr_ref)
{ {
$mapping[$acode] = Okapi::pick_best_language( $mapping[$acode] = Okapi::pick_best_language(
$attr_ref['names'], $langpref); $attr_ref['names'], $langpref);
} }
Cache::set($cache_key, $mapping, self::ttl()); Cache::set($cache_key, $mapping, self::ttl());
} }
return $mapping; return $mapping;
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -17,45 +17,45 @@ use okapi\services\attrs\AttrHelper;
class WebService class WebService
{ {
public static function options() public static function options()
{ {
return array( return array(
'min_auth_level' => 1 'min_auth_level' => 1
); );
} }
public static function call(OkapiRequest $request) public static function call(OkapiRequest $request)
{ {
# Read the parameters. # Read the parameters.
$acode = $request->get_parameter('acode'); $acode = $request->get_parameter('acode');
if ($acode === null) throw new ParamMissing('acode'); if ($acode === null) throw new ParamMissing('acode');
$langpref = $request->get_parameter('langpref'); $langpref = $request->get_parameter('langpref');
if (!$langpref) $langpref = "en"; if (!$langpref) $langpref = "en";
$fields = $request->get_parameter('fields'); $fields = $request->get_parameter('fields');
if (!$fields) $fields = "name"; if (!$fields) $fields = "name";
$forward_compatible = $request->get_parameter('forward_compatible'); $forward_compatible = $request->get_parameter('forward_compatible');
if (!$forward_compatible) $forward_compatible = "true"; if (!$forward_compatible) $forward_compatible = "true";
# Pass them all to the attributes method. # Pass them all to the attributes method.
$params = array( $params = array(
'acodes' => $acode, 'acodes' => $acode,
'langpref' => $langpref, 'langpref' => $langpref,
'fields' => $fields, 'fields' => $fields,
'forward_compatible' => $forward_compatible 'forward_compatible' => $forward_compatible
); );
$results = OkapiServiceRunner::call('services/attrs/attributes', $results = OkapiServiceRunner::call('services/attrs/attributes',
new OkapiInternalRequest($request->consumer, $request->token, $params)); new OkapiInternalRequest($request->consumer, $request->token, $params));
$result = $results[$acode]; $result = $results[$acode];
if ($result === null) if ($result === null)
{ {
/* Note, this can happen only when $forward_compatible is false. */ /* Note, this can happen only when $forward_compatible is false. */
throw new InvalidParam('acode', "Unknown A-code."); throw new InvalidParam('acode', "Unknown A-code.");
} }
return Okapi::formatted_response($request, $result); return Okapi::formatted_response($request, $result);
} }
} }

View File

@@ -1,134 +1,134 @@
<xml> <xml>
<brief>Retrieve data on a single attribute</brief> <brief>Retrieve data on a single attribute</brief>
<issue-id>268</issue-id> <issue-id>268</issue-id>
<desc> <desc>
<p>Retrieve data on a single OKAPI geocache-attribute.</p> <p>Retrieve data on a single OKAPI geocache-attribute.</p>
<p>OKAPI attributes are identified by an unique ID called an <b>A-code</b>. <p>OKAPI attributes are identified by an unique ID called an <b>A-code</b>.
All OKAPI attributes are shared among all OKAPI servers. Once an attribute is All OKAPI attributes are shared among all OKAPI servers. Once an attribute is
published (e.g. via the <b>attribute_index</b> method), it won't published (e.g. via the <b>attribute_index</b> method), it won't
disappear in any of the future OKAPI revisions, nor will its meaning change. disappear in any of the future OKAPI revisions, nor will its meaning change.
Some attributes may get discontinued in the future, but they will remain accessible Some attributes may get discontinued in the future, but they will remain accessible
by their original A-code.</p> by their original A-code.</p>
</desc> </desc>
<req name='acode'> <req name='acode'>
The A-code of the attribute you're interested in. The A-code of the attribute you're interested in.
</req> </req>
<opt name='langpref' default='en'> <opt name='langpref' default='en'>
<p>Pipe-separated list of ISO 639-1 language codes. This indicates the <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 order of preference in which language will be chosen for fields like
<b>name</b> and <b>description</b>.</p> <b>name</b> and <b>description</b>.</p>
</opt> </opt>
<opt name='fields' default='name'> <opt name='fields' default='name'>
<p>Pipe-separated list of field names which you are interested with. <p>Pipe-separated list of field names which you are interested with.
See below for the list of available fields.</p> See below for the list of available fields.</p>
</opt> </opt>
<opt name='forward_compatible' default='true'> <opt name='forward_compatible' default='true'>
By default, OKAPI will return an empty placeholder if you ask for an By default, OKAPI will return an empty placeholder if you ask for an
unknown attribute. If you'd like to catch such errors and handle them unknown attribute. If you'd like to catch such errors and handle them
differently, then you may change this behavior by setting this parameter differently, then you may change this behavior by setting this parameter
to <b>false</b>. Then, OKAPI will return HTTP 400 error response, to <b>false</b>. Then, OKAPI will return HTTP 400 error response,
instead of the placeholder (note that it behaves differently in the instead of the placeholder (note that it behaves differently in the
<b>attributes</b> method). <b>attributes</b> method).
</opt> </opt>
<common-format-params/> <common-format-params/>
<returns> <returns>
<p>A dictionary of fields you have selected in the <b>fields</b> <p>A dictionary of fields you have selected in the <b>fields</b>
parameter. Available fields:</p> parameter. Available fields:</p>
<ul> <ul>
<li> <li>
<p><b>acode</b> - string, the A-code. Unique identifier of the <p><b>acode</b> - string, the A-code. Unique identifier of the
attribute.</p> attribute.</p>
</li> </li>
<li> <li>
<p><b>name</b> - plaintext string, name of the attribute (language is <p><b>name</b> - plaintext string, name of the attribute (language is
selected based on your <b>langpref</b> parameter),</p> selected based on your <b>langpref</b> parameter),</p>
<p>If you think your language is missing, then feel free to add missing <p>If you think your language is missing, then feel free to add missing
translations directly to OKAPI repository. See translations directly to OKAPI repository. See
<a href='https://code.google.com/p/opencaching-api/source/browse/trunk/okapi/services/attrs/attribute-definitions.xml'>here</a>.</p> <a href='https://code.google.com/p/opencaching-api/source/browse/trunk/okapi/services/attrs/attribute-definitions.xml'>here</a>.</p>
</li> </li>
<li> <li>
<p><b>names</b> - a dictionary of all known names of the attribute, in <p><b>names</b> - a dictionary of all known names of the attribute, in
various languages (ISO 639-1 language code is used as dictionary various languages (ISO 639-1 language code is used as dictionary
key).</p> key).</p>
<p>If you think your language is missing, then feel free to add missing <p>If you think your language is missing, then feel free to add missing
translations directly to OKAPI repository. See translations directly to OKAPI repository. See
<a href='https://code.google.com/p/opencaching-api/source/browse/trunk/okapi/services/attrs/attribute-definitions.xml'>here</a>.</p> <a href='https://code.google.com/p/opencaching-api/source/browse/trunk/okapi/services/attrs/attribute-definitions.xml'>here</a>.</p>
</li> </li>
<li> <li>
<b>description</b> - HTML string, description of the attribute (language is <b>description</b> - HTML string, description of the attribute (language is
selected based on your <b>langpref</b> parameter), selected based on your <b>langpref</b> parameter),
<p>If you think your language is missing, then feel free to add missing <p>If you think your language is missing, then feel free to add missing
translations directly to OKAPI repository. See translations directly to OKAPI repository. See
<a href='https://code.google.com/p/opencaching-api/source/browse/trunk/okapi/services/attrs/attribute-definitions.xml'>here</a>.</p> <a href='https://code.google.com/p/opencaching-api/source/browse/trunk/okapi/services/attrs/attribute-definitions.xml'>here</a>.</p>
</li> </li>
<li> <li>
<p><b>descriptions</b> - a dictionary of all known descriptions of the <p><b>descriptions</b> - a dictionary of all known descriptions of the
attribute, in various languages (ISO 639-1 language code is used as attribute, in various languages (ISO 639-1 language code is used as
dictionary key).</p> dictionary key).</p>
<p>If you think your language is missing, then feel free to add missing <p>If you think your language is missing, then feel free to add missing
translations directly to OKAPI repository. See translations directly to OKAPI repository. See
<a href='https://code.google.com/p/opencaching-api/source/browse/trunk/okapi/services/attrs/attribute-definitions.xml'>here</a>.</p> <a href='https://code.google.com/p/opencaching-api/source/browse/trunk/okapi/services/attrs/attribute-definitions.xml'>here</a>.</p>
</li> </li>
<li> <li>
<p><b>gc_equivs</b> - a list of Geocaching.com (Groundspeak) <p><b>gc_equivs</b> - a list of Geocaching.com (Groundspeak)
attributes, which have exactly the same (or a very similar) meaning. Each attributes, which have exactly the same (or a very similar) meaning. Each
attribute is described as a dictionary of the following structure:</p> attribute is described as a dictionary of the following structure:</p>
<ul> <ul>
<li><b>id</b> - ID of the Geocaching.com attribute,</li> <li><b>id</b> - ID of the Geocaching.com attribute,</li>
<li> <li>
<b>inc</b> - integer, either 1 or 0. See Geocaching.com's <b>inc</b> - integer, either 1 or 0. See Geocaching.com's
XSD for details on its meaning, XSD for details on its meaning,
</li> </li>
<li> <li>
<b>name</b> - the name of the attribute (as it is included <b>name</b> - the name of the attribute (as it is included
in Geocaching.com GPX files). in Geocaching.com GPX files).
</li> </li>
</ul> </ul>
<p>Note that one gc_equivs list may have multiple items on it, and <p>Note that one gc_equivs list may have multiple items on it, and
that one Geocaching.com ID may be present in many gc_equivs.</p> that one Geocaching.com ID may be present in many gc_equivs.</p>
</li> </li>
<li> <li>
<p><b>is_locally_used</b> - boolean, indicates if the attribute is <i>currently</i> <p><b>is_locally_used</b> - boolean, indicates if the attribute is <i>currently</i>
used by <i>this</i> Opencaching server. Or, to be more specific, <b>true</b> used by <i>this</i> Opencaching server. Or, to be more specific, <b>true</b>
means that the attribute can currently be included in the <b>attr_acodes</b> field means that the attribute can currently be included in the <b>attr_acodes</b> field
of the <b>geocache</b> method in this OKAPI installation.</p> of the <b>geocache</b> method in this OKAPI installation.</p>
<p>Note that this flag can change in time. Some attributes may get <p>Note that this flag can change in time. Some attributes may get
introduced into other installations, whereas other attributes may introduced into other installations, whereas other attributes may
(temporarily or permanently) stop being used. In general, we are aiming (temporarily or permanently) stop being used. In general, we are aiming
towards global unification of all attributes between all OC nodes, towards global unification of all attributes between all OC nodes,
but this process will take time (and probably it will never but this process will take time (and probably it will never
be 100% complete).</p> be 100% complete).</p>
</li> </li>
<li> <li>
<p><b>local_icon_url</b> - an URL pointing to an image associated with <p><b>local_icon_url</b> - an URL pointing to an image associated with
this particular attribute in the local OC server, <b>or null</b> if the this particular attribute in the local OC server, <b>or null</b> if the
current server does not have any image for this attribute.</p> current server does not have any image for this attribute.</p>
<p>Please note, that each OC server uses a different image set for their <p>Please note, that each OC server uses a different image set for their
attributes. All these images come in various sizes and can change over time. attributes. All these images come in various sizes and can change over time.
In other words, if you want to use this attribute, then you must always be In other words, if you want to use this attribute, then you must always be
prepared to receive <b>null</b>, or an image of unexpected dimensions.</p> prepared to receive <b>null</b>, or an image of unexpected dimensions.</p>
</li> </li>
<li> <li>
<p><b>is_discontinued</b> - boolean, indicates if the attribute is discontinued. <p><b>is_discontinued</b> - boolean, indicates if the attribute is discontinued.
This means that it is no longer in use at OC servers which run current OKAPI This means that it is no longer in use at OC servers which run current OKAPI
versions, i.e. geocaches are no longer tagged with this attribute. However, versions, i.e. geocaches are no longer tagged with this attribute. However,
it may still be in use at servers which have not been updated yet to the it may still be in use at servers which have not been updated yet to the
current OKAPI version.</p> current OKAPI version.</p>
<p><b>Important:</b> This flag can change in time. Discontinued attributes can <p><b>Important:</b> This flag can change in time. Discontinued attributes can
"come back to life" later. You can never be 100% sure you will not "come back to life" later. You can never be 100% sure you will not
encounter them in OKAPI responses, so this field is purely informative.</p> encounter them in OKAPI responses, so this field is purely informative.</p>
</li> </li>
</ul> </ul>
</returns> </returns>
</xml> </xml>

View File

@@ -18,56 +18,56 @@ use okapi\services\attrs\AttrHelper;
class WebService class WebService
{ {
public static function options() public static function options()
{ {
return array( return array(
'min_auth_level' => 1 'min_auth_level' => 1
); );
} }
public static function call(OkapiRequest $request) public static function call(OkapiRequest $request)
{ {
# Read the parameters. # Read the parameters.
$langpref = $request->get_parameter('langpref'); $langpref = $request->get_parameter('langpref');
if (!$langpref) $langpref = "en"; if (!$langpref) $langpref = "en";
$fields = $request->get_parameter('fields'); $fields = $request->get_parameter('fields');
if (!$fields) $fields = "name"; if (!$fields) $fields = "name";
$only_locally_used = $request->get_parameter('only_locally_used'); $only_locally_used = $request->get_parameter('only_locally_used');
if (!$only_locally_used) $only_locally_used = "false"; if (!$only_locally_used) $only_locally_used = "false";
$only_locally_used = ($only_locally_used == "true"); $only_locally_used = ($only_locally_used == "true");
# Get the list of attributes and filter the A-codes based on the # Get the list of attributes and filter the A-codes based on the
# parameters. # parameters.
require_once 'attr_helper.inc.php'; require_once 'attr_helper.inc.php';
$attrdict = AttrHelper::get_attrdict(); $attrdict = AttrHelper::get_attrdict();
$acodes = array(); $acodes = array();
foreach ($attrdict as $acode => &$attr_ref) foreach ($attrdict as $acode => &$attr_ref)
{ {
if ($only_locally_used && ($attr_ref['internal_id'] === null)) { if ($only_locally_used && ($attr_ref['internal_id'] === null)) {
/* Skip. */ /* Skip. */
continue; continue;
} }
$acodes[] = $acode; $acodes[] = $acode;
} }
# Retrieve the attribute objects and return the results. # Retrieve the attribute objects and return the results.
if (count($acodes) > 0) { if (count($acodes) > 0) {
$params = array( $params = array(
'acodes' => implode("|", $acodes), 'acodes' => implode("|", $acodes),
'langpref' => $langpref, 'langpref' => $langpref,
'fields' => $fields, 'fields' => $fields,
); );
$results = OkapiServiceRunner::call('services/attrs/attributes', $results = OkapiServiceRunner::call('services/attrs/attributes',
new OkapiInternalRequest($request->consumer, $request->token, $params)); new OkapiInternalRequest($request->consumer, $request->token, $params));
} else { } else {
$results = new ArrayObject(); $results = new ArrayObject();
} }
return Okapi::formatted_response($request, $results); return Okapi::formatted_response($request, $results);
} }
} }

View File

@@ -1,35 +1,35 @@
<xml> <xml>
<brief>Get the list of all OKAPI attributes (A-codes)</brief> <brief>Get the list of all OKAPI attributes (A-codes)</brief>
<issue-id>270</issue-id> <issue-id>270</issue-id>
<desc> <desc>
<p>This method returns <b>all</b> currently defined OKAPI geocache-attributes. <p>This method returns <b>all</b> currently defined OKAPI geocache-attributes.
It is useful when you want to cache the data on the client-side.</p> It is useful when you want to cache the data on the client-side.</p>
<p>Keep in mind that the number of attributes will grow. Hence, <b>if your application <p>Keep in mind that the number of attributes will grow. Hence, <b>if your application
uses multiple OKAPI servers, then it's best to use one of the frequently uses multiple OKAPI servers, then it's best to use one of the frequently
updated servers for pre-caching attribute data</b> (currently, Opencaching.PL updated servers for pre-caching attribute data</b> (currently, Opencaching.PL
is the most frequently updated installation). Also, you should never assume is the most frequently updated installation). Also, you should never assume
that you have the complete list cached: A new attribute may be created at any time!</p> that you have the complete list cached: A new attribute may be created at any time!</p>
<p>Once an attribute is published via this method, it won't disappear, nor <p>Once an attribute is published via this method, it won't disappear, nor
will its meaning change (names and descriptions can be slightly altered though).</p> will its meaning change (names and descriptions can be slightly altered though).</p>
</desc> </desc>
<opt name='langpref' default='en'> <opt name='langpref' default='en'>
Works the same as in the <b>attribute</b> method. Works the same as in the <b>attribute</b> method.
</opt> </opt>
<opt name='fields' default='name'> <opt name='fields' default='name'>
Works the same as in the <b>attribute</b> method. Works the same as in the <b>attribute</b> method.
</opt> </opt>
<opt name='only_locally_used' default='false'> <opt name='only_locally_used' default='false'>
<p>By default, all known attributes are returned - including those which <p>By default, all known attributes are returned - including those which
were used a couple of years back, or those which are used by other OC nodes were used a couple of years back, or those which are used by other OC nodes
(and may perhaps be used by the local OC node in the future). If you're (and may perhaps be used by the local OC node in the future). If you're
interested only in the attributes *currently used* within *this* Opencaching interested only in the attributes *currently used* within *this* Opencaching
installation, then you may set this parameter to <b>true</b>.</p> installation, then you may set this parameter to <b>true</b>.</p>
</opt> </opt>
<common-format-params/> <common-format-params/>
<returns> <returns>
A dictionary. All A-codes will be mapped to dictionary keys, and A dictionary. All A-codes will be mapped to dictionary keys, and
each value will be an object, as described in the <b>attribute</b> method. each value will be an object, as described in the <b>attribute</b> method.
</returns> </returns>
</xml> </xml>

View File

@@ -18,120 +18,120 @@ use okapi\Db;
class WebService class WebService
{ {
public static function options() public static function options()
{ {
return array( return array(
'min_auth_level' => 1 'min_auth_level' => 1
); );
} }
private static $valid_field_names = array( private static $valid_field_names = array(
'acode', 'name', 'names', 'description', 'descriptions', 'gc_equivs', 'acode', 'name', 'names', 'description', 'descriptions', 'gc_equivs',
'is_locally_used', 'is_deprecated', 'local_icon_url', 'is_discontinued' 'is_locally_used', 'is_deprecated', 'local_icon_url', 'is_discontinued'
); );
public static function call(OkapiRequest $request) public static function call(OkapiRequest $request)
{ {
# Read the parameters. # Read the parameters.
$acodes = $request->get_parameter('acodes'); $acodes = $request->get_parameter('acodes');
if (!$acodes) throw new ParamMissing('acodes'); if (!$acodes) throw new ParamMissing('acodes');
$acodes = explode("|", $acodes); $acodes = explode("|", $acodes);
$langpref = $request->get_parameter('langpref'); $langpref = $request->get_parameter('langpref');
if (!$langpref) $langpref = "en"; if (!$langpref) $langpref = "en";
$langpref = explode("|", $langpref); $langpref = explode("|", $langpref);
$fields = $request->get_parameter('fields'); $fields = $request->get_parameter('fields');
if (!$fields) $fields = "name"; if (!$fields) $fields = "name";
$fields = explode("|", $fields); $fields = explode("|", $fields);
foreach ($fields as $field) foreach ($fields as $field)
{ {
if (!in_array($field, self::$valid_field_names)) if (!in_array($field, self::$valid_field_names))
throw new InvalidParam('fields', "'$field' is not a valid field code."); throw new InvalidParam('fields', "'$field' is not a valid field code.");
} }
$forward_compatible = $request->get_parameter('forward_compatible'); $forward_compatible = $request->get_parameter('forward_compatible');
if (!$forward_compatible) $forward_compatible = "true"; if (!$forward_compatible) $forward_compatible = "true";
if (!in_array($forward_compatible, array("true", "false"))) if (!in_array($forward_compatible, array("true", "false")))
throw new InvalidParam('forward_compatible'); throw new InvalidParam('forward_compatible');
$forward_compatible = ($forward_compatible == "true"); $forward_compatible = ($forward_compatible == "true");
# Load the attributes (all of them). # Load the attributes (all of them).
require_once 'attr_helper.inc.php'; require_once 'attr_helper.inc.php';
$attrdict = AttrHelper::get_attrdict(); $attrdict = AttrHelper::get_attrdict();
# For each A-code, check if it exists, filter its fields and add it # For each A-code, check if it exists, filter its fields and add it
# to the results. # to the results.
$results = array(); $results = array();
foreach ($acodes as $acode) foreach ($acodes as $acode)
{ {
/* Please note, that the $attr variable from the $attrdict dictionary /* Please note, that the $attr variable from the $attrdict dictionary
* below is NOT fully compatible with the interface of the "attribute" * below is NOT fully compatible with the interface of the "attribute"
* method. Some of $attr's fields are private and should not be exposed, * method. Some of $attr's fields are private and should not be exposed,
* other fields don't exist and have to be added dynamically! */ * other fields don't exist and have to be added dynamically! */
if (isset($attrdict[$acode])) { if (isset($attrdict[$acode])) {
$attr = $attrdict[$acode]; $attr = $attrdict[$acode];
} elseif ($forward_compatible) { } elseif ($forward_compatible) {
$attr = AttrHelper::get_unknown_placeholder($acode); $attr = AttrHelper::get_unknown_placeholder($acode);
} else { } else {
$results[$acode] = null; $results[$acode] = null;
continue; continue;
} }
# Fill langpref-specific fields. # Fill langpref-specific fields.
$attr['name'] = Okapi::pick_best_language($attr['names'], $langpref); $attr['name'] = Okapi::pick_best_language($attr['names'], $langpref);
$attr['description'] = Okapi::pick_best_language($attr['descriptions'], $langpref); $attr['description'] = Okapi::pick_best_language($attr['descriptions'], $langpref);
# Fill some other fields (not kept in the cached attrdict). # Fill some other fields (not kept in the cached attrdict).
$attr['is_locally_used'] = ($attr['internal_id'] !== null); $attr['is_locally_used'] = ($attr['internal_id'] !== null);
$attr['is_deprecated'] = $attr['is_discontinued']; // deprecated and undocumetned field, see issue 70 $attr['is_deprecated'] = $attr['is_discontinued']; // deprecated and undocumetned field, see issue 70
# Add to results. # Add to results.
$results[$acode] = $attr; $results[$acode] = $attr;
} }
# If the user wanted local_icon_urls, fetch them now. (We cannot cache them # If the user wanted local_icon_urls, fetch them now. (We cannot cache them
# in the $attrdict because currently we have no way of knowing then they # in the $attrdict because currently we have no way of knowing then they
# change.) # change.)
if (in_array('local_icon_url', $fields)) if (in_array('local_icon_url', $fields))
{ {
$tmp = Db::select_all(" $tmp = Db::select_all("
select id, icon_large select id, icon_large
from cache_attrib from cache_attrib
"); ");
$map = array(); $map = array();
foreach ($tmp as &$row_ref) { foreach ($tmp as &$row_ref) {
$map[$row_ref['id']] = &$row_ref; $map[$row_ref['id']] = &$row_ref;
} }
$prefix = Settings::get('SITE_URL'); $prefix = Settings::get('SITE_URL');
foreach ($results as &$attr_ref) { foreach ($results as &$attr_ref) {
$internal_id = $attr_ref['internal_id']; $internal_id = $attr_ref['internal_id'];
if (isset($map[$internal_id])) { if (isset($map[$internal_id])) {
$row = $map[$internal_id]; $row = $map[$internal_id];
$attr_ref['local_icon_url'] = $prefix.$row['icon_large']; $attr_ref['local_icon_url'] = $prefix.$row['icon_large'];
} else { } else {
$attr_ref['local_icon_url'] = null; $attr_ref['local_icon_url'] = null;
} }
} }
} }
# Filter the fields. # Filter the fields.
foreach ($results as &$attr_ref) { foreach ($results as &$attr_ref) {
$clean_row = array(); $clean_row = array();
foreach ($fields as $field) foreach ($fields as $field)
$clean_row[$field] = $attr_ref[$field]; $clean_row[$field] = $attr_ref[$field];
$attr_ref = $clean_row; $attr_ref = $clean_row;
} }
return Okapi::formatted_response($request, $results); return Okapi::formatted_response($request, $results);
} }
} }

View File

@@ -1,30 +1,30 @@
<xml> <xml>
<brief>Retrieve data on multiple attributes at once</brief> <brief>Retrieve data on multiple attributes at once</brief>
<issue-id>269</issue-id> <issue-id>269</issue-id>
<desc> <desc>
<p>This method works like the <b>attribute</b> method, but <p>This method works like the <b>attribute</b> method, but
with multiple A-codes instead of only one. Read the docs for with multiple A-codes instead of only one. Read the docs for
the <b>attribute</b> method first!</p> the <b>attribute</b> method first!</p>
</desc> </desc>
<req name='acodes'> <req name='acodes'>
Pipe-separated list of A-codes you're interested in. Pipe-separated list of A-codes you're interested in.
</req> </req>
<opt name='langpref' default='en'> <opt name='langpref' default='en'>
Works the same as in the <b>attribute</b> method. Works the same as in the <b>attribute</b> method.
</opt> </opt>
<opt name='fields' default='name'> <opt name='fields' default='name'>
Works the same as in the <b>attribute</b> method. Works the same as in the <b>attribute</b> method.
</opt> </opt>
<opt name='forward_compatible' default='true'> <opt name='forward_compatible' default='true'>
This has a similar meaning as in the <b>attribute</b> method, but works This has a similar meaning as in the <b>attribute</b> method, but works
differently. If set to <b>false</b>, OKAPI will return <b>null</b>s differently. If set to <b>false</b>, OKAPI will return <b>null</b>s
for unknown keys (A-codes). You will still receive an HTTP 200 response for unknown keys (A-codes). You will still receive an HTTP 200 response
though! though!
</opt> </opt>
<common-format-params/> <common-format-params/>
<returns> <returns>
<p>A dictionary. A-codes you provide will be mapped to dictionary keys, <p>A dictionary. A-codes you provide will be mapped to dictionary keys,
and each value will be an object, as described in the <b>attribute</b> and each value will be an object, as described in the <b>attribute</b>
method.</p> method.</p>
</returns> </returns>
</xml> </xml>

View File

@@ -20,198 +20,202 @@ use \Exception;
class WebService class WebService
{ {
private static $shutdown_function_registered = false; private static $shutdown_function_registered = false;
private static $files_to_unlink = array(); private static $files_to_unlink = array();
public static function options() public static function options()
{ {
return array( return array(
'min_auth_level' => 1 'min_auth_level' => 1
); );
} }
public static function call(OkapiRequest $request) public static function call(OkapiRequest $request)
{ {
$cache_codes = $request->get_parameter('cache_codes'); $cache_codes = $request->get_parameter('cache_codes');
if ($cache_codes === null) throw new ParamMissing('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. # 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! # All of the queries below have to be ready for $cache_codes to be empty!
$langpref = $request->get_parameter('langpref'); $langpref = $request->get_parameter('langpref');
if (!$langpref) $langpref = "en"; if (!$langpref) $langpref = "en";
$images = $request->get_parameter('images'); $images = $request->get_parameter('images');
if (!$images) $images = "all"; if (!$images) $images = "all";
if (!in_array($images, array("none", "all", "spoilers", "nonspoilers"))) if (!in_array($images, array("none", "all", "spoilers", "nonspoilers")))
throw new InvalidParam('images'); throw new InvalidParam('images');
$location_source = $request->get_parameter('location_source');
$location_change_prefix = $request->get_parameter('location_change_prefix');
# Start creating ZIP archive. # Start creating ZIP archive.
$tempfilename = Okapi::get_var_dir()."/garmin".time().rand(100000,999999).".zip"; $tempfilename = Okapi::get_var_dir()."/garmin".time().rand(100000,999999).".zip";
$zip = new ZipArchive(); $zip = new ZipArchive();
if ($zip->open($tempfilename, ZIPARCHIVE::CREATE) !== true) if ($zip->open($tempfilename, ZIPARCHIVE::CREATE) !== true)
throw new Exception("ZipArchive class could not create temp file $tempfilename. Check permissions!"); throw new Exception("ZipArchive class could not create temp file $tempfilename. Check permissions!");
# Create basic structure # Create basic structure
$zip->addEmptyDir("Garmin"); $zip->addEmptyDir("Garmin");
$zip->addEmptyDir("Garmin/GPX"); $zip->addEmptyDir("Garmin/GPX");
$zip->addEmptyDir("Garmin/GeocachePhotos"); $zip->addEmptyDir("Garmin/GeocachePhotos");
# Include a GPX file compatible with Garmin devices. It should include all # Include a GPX file compatible with Garmin devices. It should include all
# Geocaching.com (groundspeak:) and Opencaching.com (ox:) extensions. It will # Geocaching.com (groundspeak:) and Opencaching.com (ox:) extensions. It will
# also include image references (actual images will be added as separate files later) # 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). # and personal data (if the method was invoked using Level 3 Authentication).
$zip->addFromString("Garmin/GPX/opencaching".time().rand(100000,999999).".gpx", $zip->addFromString("Garmin/GPX/opencaching".time().rand(100000,999999).".gpx",
OkapiServiceRunner::call('services/caches/formatters/gpx', new OkapiInternalRequest( OkapiServiceRunner::call('services/caches/formatters/gpx', new OkapiInternalRequest(
$request->consumer, $request->token, array( $request->consumer, $request->token, array(
'cache_codes' => $cache_codes, 'cache_codes' => $cache_codes,
'langpref' => $langpref, 'langpref' => $langpref,
'ns_ground' => 'true', 'ns_ground' => 'true',
'ns_ox' => 'true', 'ns_ox' => 'true',
'images' => 'ox:all', 'images' => 'ox:all',
'attrs' => 'ox:tags', 'attrs' => 'ox:tags',
'trackables' => 'desc:count', 'trackables' => 'desc:count',
'alt_wpts' => 'true', 'alt_wpts' => 'true',
'recommendations' => 'desc:count', 'recommendations' => 'desc:count',
'latest_logs' => 'true', 'latest_logs' => 'true',
'lpc' => 'all', 'lpc' => 'all',
'my_notes' => ($request->token != null) ? "desc:text" : "none" 'my_notes' => ($request->token != null) ? "desc:text" : "none",
)))->get_body()); 'location_source' => $location_source,
'location_change_prefix' => $location_change_prefix
)))->get_body());
# Then, include all the images. # Then, include all the images.
$caches = OkapiServiceRunner::call('services/caches/geocaches', new OkapiInternalRequest( $caches = OkapiServiceRunner::call('services/caches/geocaches', new OkapiInternalRequest(
$request->consumer, $request->token, array('cache_codes' => $cache_codes, $request->consumer, $request->token, array('cache_codes' => $cache_codes,
'langpref' => $langpref, 'fields' => "images"))); 'langpref' => $langpref, 'fields' => "images")));
if (count($caches) > 50) if (count($caches) > 50)
throw new InvalidParam('cache_codes', "The maximum number of caches allowed to be downloaded with this method is 50."); throw new InvalidParam('cache_codes', "The maximum number of caches allowed to be downloaded with this method is 50.");
if ($images != 'none') if ($images != 'none')
{ {
$supported_extensions = array('jpg', 'jpeg', 'gif', 'png', 'bmp'); $supported_extensions = array('jpg', 'jpeg', 'gif', 'png', 'bmp');
foreach ($caches as $cache_code => $dict) foreach ($caches as $cache_code => $dict)
{ {
$imgs = $dict['images']; $imgs = $dict['images'];
if (count($imgs) == 0) if (count($imgs) == 0)
continue; continue;
$dir = "Garmin/GeocachePhotos/".$cache_code[strlen($cache_code) - 1]; $dir = "Garmin/GeocachePhotos/".$cache_code[strlen($cache_code) - 1];
$zip->addEmptyDir($dir); # fails silently if it already exists $zip->addEmptyDir($dir); # fails silently if it already exists
$dir .= "/".$cache_code[strlen($cache_code) - 2]; $dir .= "/".$cache_code[strlen($cache_code) - 2];
$zip->addEmptyDir($dir); $zip->addEmptyDir($dir);
$dir .= "/".$cache_code; $dir .= "/".$cache_code;
$zip->addEmptyDir($dir); $zip->addEmptyDir($dir);
foreach ($imgs as $no => $img) foreach ($imgs as $no => $img)
{ {
if ($images == 'spoilers' && (!$img['is_spoiler'])) if ($images == 'spoilers' && (!$img['is_spoiler']))
continue; continue;
if ($images == 'nonspoilers' && $img['is_spoiler']) if ($images == 'nonspoilers' && $img['is_spoiler'])
continue; continue;
$tmp = false; $tmp = false;
foreach ($supported_extensions as $ext) foreach ($supported_extensions as $ext)
{ {
if (strtolower(substr($img['url'], strlen($img['url']) - strlen($ext) - 1)) != ".".$ext) if (strtolower(substr($img['url'], strlen($img['url']) - strlen($ext) - 1)) != ".".$ext)
{ {
$tmp = true; $tmp = true;
continue; continue;
} }
} }
if (!$tmp) if (!$tmp)
continue; # unsupported file extension continue; # unsupported file extension
if ($img['is_spoiler']) { if ($img['is_spoiler']) {
$zip->addEmptyDir($dir."/Spoilers"); $zip->addEmptyDir($dir."/Spoilers");
$zippath = $dir."/Spoilers/".$img['unique_caption'].".jpg"; $zippath = $dir."/Spoilers/".$img['unique_caption'].".jpg";
} else { } else {
$zippath = $dir."/".$img['unique_caption'].".jpg"; $zippath = $dir."/".$img['unique_caption'].".jpg";
} }
# The safest way would be to use the URL, but that would be painfully slow! # 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). # That's why we're trying to access files directly (and fail silently on error).
# This was tested on OCPL server only. # This was tested on OCPL server only.
# Note: Oliver Dietz (oc.de) replied that images with 'local' set to 0 could not # 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. # be accessed locally. But all the files have 'local' set to 1 anyway.
$syspath = Settings::get('IMAGES_DIR')."/".$img['uuid'].".jpg"; $syspath = Settings::get('IMAGES_DIR')."/".$img['uuid'].".jpg";
if (file_exists($syspath)) if (file_exists($syspath))
{ {
$file = file_get_contents($syspath); $file = file_get_contents($syspath);
if ($file) if ($file)
$zip->addFromString($zippath, $file); $zip->addFromString($zippath, $file);
} }
else else
{ {
# If file exists, but does not end with ".jpg", we will create # If file exists, but does not end with ".jpg", we will create
# JPEG version of it and store it in the cache. # JPEG version of it and store it in the cache.
$cache_key = "jpg#".$img['uuid']; $cache_key = "jpg#".$img['uuid'];
$jpeg_contents = Cache::get($cache_key); $jpeg_contents = Cache::get($cache_key);
if ($jpeg_contents === null) if ($jpeg_contents === null)
{ {
foreach ($supported_extensions as $ext) foreach ($supported_extensions as $ext)
{ {
$syspath_other = Settings::get('IMAGES_DIR')."/".$img['uuid'].".".$ext; $syspath_other = Settings::get('IMAGES_DIR')."/".$img['uuid'].".".$ext;
if (file_exists($syspath_other)) if (file_exists($syspath_other))
{ {
try try
{ {
$image = imagecreatefromstring(file_get_contents($syspath_other)); $image = imagecreatefromstring(file_get_contents($syspath_other));
ob_start(); ob_start();
imagejpeg($image); imagejpeg($image);
$jpeg_contents = ob_get_clean(); $jpeg_contents = ob_get_clean();
imagedestroy($image); imagedestroy($image);
} }
catch (Exception $e) catch (Exception $e)
{ {
# GD couldn't parse the file. We will skip it, and cache # GD couldn't parse the file. We will skip it, and cache
# the "false" value as the contents. This way, we won't # the "false" value as the contents. This way, we won't
# attempt to parse it during the next 24 hours. # attempt to parse it during the next 24 hours.
$jpeg_contents = false; $jpeg_contents = false;
} }
Cache::set($cache_key, $jpeg_contents, 86400); Cache::set($cache_key, $jpeg_contents, 86400);
break; break;
} }
} }
} }
if ($jpeg_contents) # This can be "null" *or* "false"! if ($jpeg_contents) # This can be "null" *or* "false"!
$zip->addFromString($zippath, $jpeg_contents); $zip->addFromString($zippath, $jpeg_contents);
} }
} }
} }
} }
$zip->close(); $zip->close();
# The result could be big. Bigger than our memory limit. We will # The result could be big. Bigger than our memory limit. We will
# return an open file stream instead of a string. We also should # return an open file stream instead of a string. We also should
# set a higher time limit, because downloading this response may # set a higher time limit, because downloading this response may
# take some time over slow network connections (and I'm not sure # take some time over slow network connections (and I'm not sure
# what is the PHP's default way of handling such scenario). # what is the PHP's default way of handling such scenario).
set_time_limit(600); set_time_limit(600);
$response = new OkapiHttpResponse(); $response = new OkapiHttpResponse();
$response->content_type = "application/zip"; $response->content_type = "application/zip";
$response->content_disposition = 'attachment; filename="results.zip"'; $response->content_disposition = 'attachment; filename="results.zip"';
$response->stream_length = filesize($tempfilename); $response->stream_length = filesize($tempfilename);
$response->body = fopen($tempfilename, "rb"); $response->body = fopen($tempfilename, "rb");
$response->allow_gzip = false; $response->allow_gzip = false;
self::add_file_to_unlink($tempfilename); self::add_file_to_unlink($tempfilename);
return $response; return $response;
} }
private static function add_file_to_unlink($filename) private static function add_file_to_unlink($filename)
{ {
if (!self::$shutdown_function_registered) if (!self::$shutdown_function_registered)
register_shutdown_function(array("okapi\\services\\caches\\formatters\\garmin\\WebService", "unlink_temporary_files")); register_shutdown_function(array("okapi\\services\\caches\\formatters\\garmin\\WebService", "unlink_temporary_files"));
self::$files_to_unlink[] = $filename; self::$files_to_unlink[] = $filename;
} }
public static function unlink_temporary_files() public static function unlink_temporary_files()
{ {
foreach (self::$files_to_unlink as $filename) foreach (self::$files_to_unlink as $filename)
@unlink($filename); @unlink($filename);
self::$files_to_unlink = array(); self::$files_to_unlink = array();
} }
} }

View File

@@ -1,47 +1,55 @@
<xml> <xml>
<brief>Retrieve ZIP file for Garmin devices</brief> <brief>Retrieve ZIP file for Garmin devices</brief>
<issue-id>99</issue-id> <issue-id>99</issue-id>
<desc> <desc>
<p>Produce a ZIP file with content compatible with Geocaching-enabled Garmin <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 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 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 (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> 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 <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 with the chosen geocaches. Options for the GPX file are fixed, use the
services/caches/formatters/gpx method if services/caches/formatters/gpx method if
you want a custom-tailored GPX file.</p> you want a custom-tailored GPX file.</p>
<p><b>Important note:</b> The contents of the returned ZIP archive may change in the <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 future. You should not parse the contents, nor assume that they have any specific
structure.</p> structure.</p>
<p><b>Important note:</b> We might be increasing required authentication level for <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 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>, 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> 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 <p><b>Note:</b> All non-JPEG images will be skipped. Currently OKAPI does not convert
other types of images to JPEG.</p> other types of images to JPEG.</p>
</desc> </desc>
<req name='cache_codes'> <req name='cache_codes'>
<p>Pipe-separated list of cache codes which you are interested in. <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 No more than 50 codes are allowed. (This limit is smaller than usual, because
we're afraid about the sizes of ZIP files produced.) 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> This CAN be an empty string (it will still result in a valid ZIP file).</p>
</req> </req>
<opt name='langpref' default='en'> <opt name='langpref' default='en'>
<p>Pipe-separated list of ISO 639-1 language codes. This indicates the <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> order of preference in which language will be chosen for GPX entities.</p>
</opt> </opt>
<opt name='images' default='all'> <opt name='images' default='all'>
<p>One of the following values:</p> <p>One of the following values:</p>
<ul> <ul>
<li><b>none</b> - no images will be included in the result,</li> <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>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>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> <li><b>nonspoilers</b> - only non-spoiler images will be included in the result.</li>
</ul> </ul>
</opt> </opt>
<returns> <opt name="location_source" default='default-coords'>
<p>ZIP file. You should extract it's contents directly into the root Same as in the <a href="%OKAPI:methodargref:services/caches/formatters/gpx#location_source%">
directory of your Garmin's internal memory storage.</p> services/caches/formatters/gpx</a> method.
</returns> </opt>
<opt name="location_change_prefix" default="#">
Same as in the <a href="%OKAPI:methodargref:services/caches/formatters/gpx#location_change_prefix%">
services/caches/formatters/gpx</a> method.
</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> </xml>

View File

@@ -19,297 +19,383 @@ use okapi\services\attrs\AttrHelper;
class WebService class WebService
{ {
public static function options() public static function options()
{ {
return array( return array(
'min_auth_level' => 1 'min_auth_level' => 1
); );
} }
/** Maps OKAPI cache type codes to Geocaching.com GPX cache types. */ /** Maps OKAPI cache type codes to Geocaching.com GPX cache types. */
public static $cache_GPX_types = array( public static $cache_GPX_types = array(
'Traditional' => 'Traditional Cache', 'Traditional' => 'Traditional Cache',
'Multi' => 'Multi-Cache', 'Multi' => 'Multi-Cache',
'Quiz' => 'Unknown Cache', 'Quiz' => 'Unknown Cache',
'Event' => 'Event Cache', 'Event' => 'Event Cache',
'Virtual' => 'Virtual Cache', 'Virtual' => 'Virtual Cache',
'Webcam' => 'Webcam Cache', 'Webcam' => 'Webcam Cache',
'Moving' => 'Unknown Cache', 'Moving' => 'Unknown Cache',
'Math/Physics' => 'Unknown Cache', 'Math/Physics' => 'Unknown Cache',
'Drive-In' => 'Traditional Cache', 'Drive-In' => 'Traditional Cache',
'Own' => 'Unknown Cache', 'Podcast' => 'Unknown Cache',
'Other' => 'Unknown Cache' 'Own' => 'Unknown Cache',
); 'Other' => 'Unknown Cache'
);
/** Maps OKAPI's 'size2' values to geocaching.com size codes. */ /** Maps OKAPI's 'size2' values to geocaching.com size codes. */
public static $cache_GPX_sizes = array( public static $cache_GPX_sizes = array(
'none' => 'Virtual', 'none' => 'Virtual',
'nano' => 'Micro', 'nano' => 'Micro',
'micro' => 'Micro', 'micro' => 'Micro',
'small' => 'Small', 'small' => 'Small',
'regular' => 'Regular', 'regular' => 'Regular',
'large' => 'Large', 'large' => 'Large',
'xlarge' => 'Large', 'xlarge' => 'Large',
'other' => 'Other', 'other' => 'Other',
); );
public static function call(OkapiRequest $request) public static function call(OkapiRequest $request)
{ {
$vars = array(); $vars = array();
# Validating arguments. We will also assign some of them to the # Validating arguments. We will also assign some of them to the
# $vars variable which we will use later in the GPS template. # $vars variable which we will use later in the GPS template.
$cache_codes = $request->get_parameter('cache_codes'); $cache_codes = $request->get_parameter('cache_codes');
if ($cache_codes === null) throw new ParamMissing('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. # 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! # All of the queries below have to be ready for $cache_codes to be empty!
$langpref = $request->get_parameter('langpref'); $langpref = $request->get_parameter('langpref');
if (!$langpref) $langpref = "en"; if (!$langpref) $langpref = "en";
foreach (array('ns_ground', 'ns_gsak', 'ns_ox', 'latest_logs', 'alt_wpts', 'mark_found') as $param) foreach (array('ns_ground', 'ns_gsak', 'ns_ox', 'latest_logs', 'alt_wpts', 'mark_found') as $param)
{ {
$val = $request->get_parameter($param); $val = $request->get_parameter($param);
if (!$val) $val = "false"; if (!$val) $val = "false";
elseif (!in_array($val, array("true", "false"))) elseif (!in_array($val, array("true", "false")))
throw new InvalidParam($param); throw new InvalidParam($param);
$vars[$param] = ($val == "true"); $vars[$param] = ($val == "true");
} }
if ($vars['latest_logs'] && (!$vars['ns_ground'])) 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."); throw new BadRequest("In order for 'latest_logs' to work you have to also include 'ns_ground' extensions.");
$tmp = $request->get_parameter('my_notes'); $tmp = $request->get_parameter('my_notes');
$vars['my_notes'] = array(); $vars['my_notes'] = array();
if ($tmp && $tmp != 'none') { if ($tmp && $tmp != 'none') {
$tmp = explode('|', $tmp); $tmp = explode('|', $tmp);
foreach ($tmp as $elem) { foreach ($tmp as $elem) {
if ($elem == 'none') { if ($elem == 'none') {
/* pass */ /* pass */
} elseif (in_array($elem, array('desc:text', 'gc:personal_note'))) { } elseif (in_array($elem, array('desc:text', 'gc:personal_note'))) {
if (in_array('none', $tmp)) { if (in_array('none', $tmp)) {
throw new InvalidParam( throw new InvalidParam(
'my_notes', "You cannot mix 'none' and '$elem'" 'my_notes', "You cannot mix 'none' and '$elem'"
); );
} }
if ($request->token == null) { if ($request->token == null) {
throw new BadRequest( throw new BadRequest(
"Level 3 Authentication is required to access my_notes data." "Level 3 Authentication is required to access my_notes data."
); );
} }
$vars['my_notes'][] = $elem; $vars['my_notes'][] = $elem;
} else { } else {
throw new InvalidParam('my_notes', "Invalid list entry: '$elem'"); throw new InvalidParam('my_notes', "Invalid list entry: '$elem'");
} }
} }
} }
$images = $request->get_parameter('images'); $images = $request->get_parameter('images');
if (!$images) $images = 'descrefs:nonspoilers'; if (!$images) $images = 'descrefs:nonspoilers';
if (!in_array($images, array('none', 'descrefs:thumblinks', 'descrefs:nonspoilers', 'descrefs:all', 'ox:all'))) if (!in_array($images, array('none', 'descrefs:thumblinks', 'descrefs:nonspoilers', 'descrefs:all', 'ox:all')))
throw new InvalidParam('images', "'$images'"); throw new InvalidParam('images', "'$images'");
$vars['images'] = $images; $vars['images'] = $images;
$tmp = $request->get_parameter('attrs'); $tmp = $request->get_parameter('attrs');
if (!$tmp) $tmp = 'desc:text'; if (!$tmp) $tmp = 'desc:text';
$tmp = explode("|", $tmp); $tmp = explode("|", $tmp);
$vars['attrs'] = array(); $vars['attrs'] = array();
foreach ($tmp as $elem) foreach ($tmp as $elem)
{ {
if ($elem == 'none') { if ($elem == 'none') {
/* pass */ /* pass */
} elseif (in_array($elem, array('desc:text', 'ox:tags', 'gc:attrs', 'gc_ocde:attrs'))) { } elseif (in_array($elem, array('desc:text', 'ox:tags', 'gc:attrs', 'gc_ocde:attrs'))) {
if ($elem == 'gc_ocde:attrs' && Settings::get('OC_BRANCH') != 'oc.de') if ($elem == 'gc_ocde:attrs' && Settings::get('OC_BRANCH') != 'oc.de')
$vars['attrs'][] = 'gc:attrs'; $vars['attrs'][] = 'gc:attrs';
else else
$vars['attrs'][] = $elem; $vars['attrs'][] = $elem;
} else { } else {
throw new InvalidParam('attrs', "Invalid list entry: '$elem'"); throw new InvalidParam('attrs', "Invalid list entry: '$elem'");
} }
} }
$protection_areas = $request->get_parameter('protection_areas'); $protection_areas = $request->get_parameter('protection_areas');
if (!$protection_areas || $protection_areas == 'desc:auto') if (!$protection_areas || $protection_areas == 'desc:auto')
{ {
if (Settings::get('OC_BRANCH') == 'oc.de') $protection_areas = 'desc:text'; if (Settings::get('OC_BRANCH') == 'oc.de') $protection_areas = 'desc:text';
else $protection_areas = 'none'; else $protection_areas = 'none';
} }
if (!in_array($protection_areas, array('none', 'desc:text'))) if (!in_array($protection_areas, array('none', 'desc:text')))
throw new InvalidParam('protection_areas',"'$protection_areas'"); throw new InvalidParam('protection_areas',"'$protection_areas'");
$vars['protection_areas'] = $protection_areas; $vars['protection_areas'] = $protection_areas;
$tmp = $request->get_parameter('trackables'); $tmp = $request->get_parameter('trackables');
if (!$tmp) $tmp = 'none'; if (!$tmp) $tmp = 'none';
if (!in_array($tmp, array('none', 'desc:list', 'desc:count'))) if (!in_array($tmp, array('none', 'desc:list', 'desc:count')))
throw new InvalidParam('trackables', "'$tmp'"); throw new InvalidParam('trackables', "'$tmp'");
$vars['trackables'] = $tmp; $vars['trackables'] = $tmp;
$tmp = $request->get_parameter('recommendations'); $tmp = $request->get_parameter('recommendations');
if (!$tmp) $tmp = 'none'; if (!$tmp) $tmp = 'none';
if (!in_array($tmp, array('none', 'desc:count'))) if (!in_array($tmp, array('none', 'desc:count')))
throw new InvalidParam('recommendations', "'$tmp'"); throw new InvalidParam('recommendations', "'$tmp'");
$vars['recommendations'] = $tmp; $vars['recommendations'] = $tmp;
$lpc = $request->get_parameter('lpc'); $lpc = $request->get_parameter('lpc');
if ($lpc === null) $lpc = 10; # will be checked in services/caches/geocaches call if ($lpc === null) $lpc = 10; # will be checked in services/caches/geocaches call
$user_uuid = $request->get_parameter('user_uuid'); $user_uuid = $request->get_parameter('user_uuid');
# Which fields of the services/caches/geocaches method do we need? # location_source (part 1 of 2)
$fields = 'code|name|location|date_created|url|type|status|size|size2|oxsize'. $location_source = $request->get_parameter('location_source');
'|difficulty|terrain|description|hint2|rating|owner|url|internal_id'. if (!$location_source)
'|protection_areas'; {
if ($vars['images'] != 'none') $location_source = 'default-coords';
$fields .= "|images"; }
if (count($vars['attrs']) > 0) # Make sure location_source has prefix alt_wpt:
$fields .= "|attrnames|attr_acodes"; if ($location_source != 'default-coords' && strncmp($location_source, 'alt_wpt:', 8) != 0)
if ($vars['trackables'] == 'desc:list') {
$fields .= "|trackables"; throw new InvalidParam('location_source', '\''.$location_source.'\'');
elseif ($vars['trackables'] == 'desc:count') }
$fields .= "|trackables_count";
if ($vars['alt_wpts'] == 'true')
$fields .= "|alt_wpts";
if ($vars['recommendations'] != 'none')
$fields .= "|recommendations|founds";
if (count($vars['my_notes']) > 0)
$fields .= "|my_notes";
if ($vars['latest_logs'])
$fields .= "|latest_logs";
if ($vars['mark_found'])
$fields .= "|is_found";
$vars['caches'] = OkapiServiceRunner::call( # Make sure we have sufficient authorization
'services/caches/geocaches', new OkapiInternalRequest( if ($location_source == 'alt_wpt:user-coords' && $request->token == null)
$request->consumer, $request->token, array( {
'cache_codes' => $cache_codes, throw new BadRequest("Level 3 Authentication is required to access 'alt_wpt:user-coords'.");
'langpref' => $langpref, }
'fields' => $fields,
'lpc' => $lpc,
'user_uuid' => $user_uuid,
'log_fields' => 'uuid|date|user|type|comment|internal_id|was_recommended'
)
)
);
# Get all the other data need. # Which fields of the services/caches/geocaches method do we need?
$vars['installation'] = OkapiServiceRunner::call( $fields = 'code|name|location|date_created|url|type|status|size|size2|oxsize'.
'services/apisrv/installation', new OkapiInternalRequest( '|difficulty|terrain|description|hint2|rating|owner|url|internal_id'.
new OkapiInternalConsumer(), null, array() '|protection_areas|short_description';
) if ($vars['images'] != 'none')
); $fields .= "|images";
$vars['cache_GPX_types'] = self::$cache_GPX_types; if (count($vars['attrs']) > 0)
$vars['cache_GPX_sizes'] = self::$cache_GPX_sizes; $fields .= "|attrnames|attr_acodes";
if ($vars['trackables'] == 'desc:list')
$fields .= "|trackables";
elseif ($vars['trackables'] == 'desc:count')
$fields .= "|trackables_count";
if ($vars['alt_wpts'] == 'true' || $location_source != 'default-coords')
$fields .= "|alt_wpts";
if ($vars['recommendations'] != 'none')
$fields .= "|recommendations|founds";
if (count($vars['my_notes']) > 0)
$fields .= "|my_notes";
if ($vars['latest_logs'])
$fields .= "|latest_logs";
if ($vars['mark_found'])
$fields .= "|is_found";
if (count($vars['attrs']) > 0) $vars['caches'] = OkapiServiceRunner::call(
{ 'services/caches/geocaches', new OkapiInternalRequest(
/* The user asked for some kind of attribute output. We'll fetch all $request->consumer, $request->token, array(
* the data we MAY need. This is often far too much, but thanks to 'cache_codes' => $cache_codes,
* caching, it will work fast. */ 'langpref' => $langpref,
'fields' => $fields,
'lpc' => $lpc,
'user_uuid' => $user_uuid,
'log_fields' => 'uuid|date|user|type|comment|internal_id|was_recommended'
)
)
);
$vars['attr_index'] = OkapiServiceRunner::call( # Get rid of invalid cache references.
'services/attrs/attribute_index', new OkapiInternalRequest(
$request->consumer, $request->token, array(
'only_locally_used' => 'true',
'langpref' => $langpref,
'fields' => 'name|gc_equivs'
)
)
);
# prepare GS attribute data $valid = array();
foreach ($vars['caches'] as $key => &$ref) {
if ($ref !== null) {
$valid[$key] = &$ref;
}
}
$vars['caches'] = &$valid;
unset($valid);
$vars['gc_attrs'] = in_array('gc:attrs', $vars['attrs']); # Get all the other data need.
$vars['gc_ocde_attrs'] = in_array('gc_ocde:attrs', $vars['attrs']);
if ($vars['gc_attrs'] || $vars['gc_ocde_attrs'])
{
if ($vars['gc_ocde_attrs'])
{
# As this is an OCDE compatibility feature, we use the same Pseudo-GS
# attribute names here as OCDE. Note that this code is specific to OCDE
# database; OCPL stores attribute names in a different way and may use
# different names for equivalent attributes.
$ocde_attrnames = Db::select_group_by('id'," $vars['installation'] = OkapiServiceRunner::call(
select id, name 'services/apisrv/installation', new OkapiInternalRequest(
from cache_attrib new OkapiInternalConsumer(), null, array()
"); )
$attr_dict = AttrHelper::get_attrdict(); );
} $vars['cache_GPX_types'] = self::$cache_GPX_types;
$vars['cache_GPX_sizes'] = self::$cache_GPX_sizes;
foreach ($vars['caches'] as &$cache) if (count($vars['attrs']) > 0)
{ {
$cache['gc_attrs'] = array(); /* The user asked for some kind of attribute output. We'll fetch all
foreach ($cache['attr_acodes'] as $acode) * the data we MAY need. This is often far too much, but thanks to
{ * caching, it will work fast. */
$has_gc_equivs = false;
foreach ($vars['attr_index'][$acode]['gc_equivs'] as $gc)
{
# The assignment via GC-ID as array key will prohibit duplicate
# GC attributes, which can result from
# - assigning the same GC ID to multiple A-Codes,
# - contradicting attributes in one OC listing, e.g. 24/4 + not 24/7.
$cache['gc_attrs'][$gc['id']] = $gc; $vars['attr_index'] = OkapiServiceRunner::call(
$has_gc_equivs = true; 'services/attrs/attribute_index', new OkapiInternalRequest(
} $request->consumer, $request->token, array(
if (!$has_gc_equivs && $vars['gc_ocde_attrs']) 'only_locally_used' => 'true',
{ 'langpref' => $langpref,
# Generate an OCDE pseudo-GS attribute; 'fields' => 'name|gc_equivs'
# see http://code.google.com/p/opencaching-api/issues/detail?id=190 and )
# http://code.google.com/p/opencaching-api/issues/detail?id=271. )
# );
# Groundspeak uses ID 1..65 (as of June, 2013), and OCDE makeshift
# IDs start at 106, so there is space for 40 new GS attributes.
$internal_id = $attr_dict[$acode]['internal_id']; # prepare GS attribute data
$cache['gc_attrs'][100 + $internal_id] = array(
'inc' => 1,
'name' => $ocde_attrnames[$internal_id][0]['name'],
);
}
}
}
}
}
/* OC sites always used internal user_ids in their generated GPX files. $vars['gc_attrs'] = in_array('gc:attrs', $vars['attrs']);
* This might be considered an error in itself (Groundspeak's XML namespace $vars['gc_ocde_attrs'] = in_array('gc_ocde:attrs', $vars['attrs']);
* doesn't allow that), but it very common (Garmin's OpenCaching.COM if ($vars['gc_attrs'] || $vars['gc_ocde_attrs'])
* also does that). Therefore, for backward-compatibility reasons, OKAPI {
* will do it the same way. See issue 174. if ($vars['gc_ocde_attrs'])
* {
* Currently, the caches method does not expose "owner.internal_id" and # As this is an OCDE compatibility feature, we use the same Pseudo-GS
* "latest_logs.user.internal_id" fields, we will read them manually # attribute names here as OCDE. Note that this code is specific to OCDE
* from the database here. */ # database; OCPL stores attribute names in a different way and may use
# different names for equivalent attributes.
$dict = array(); $ocde_attrnames = Db::select_group_by('id',"
foreach ($vars['caches'] as &$cache_ref) select id, name
{ from cache_attrib
$dict[$cache_ref['owner']['uuid']] = true; ");
if (isset($cache_ref['latest_logs'])) $attr_dict = AttrHelper::get_attrdict();
foreach ($cache_ref['latest_logs'] as &$log_ref) }
$dict[$log_ref['user']['uuid']] = true;
}
$rs = Db::query("
select uuid, user_id
from user
where uuid in ('".implode("','", array_map('mysql_real_escape_string', array_keys($dict)))."')
");
while ($row = mysql_fetch_assoc($rs))
$dict[$row['uuid']] = $row['user_id'];
$vars['user_uuid_to_internal_id'] = &$dict;
unset($dict);
$response = new OkapiHttpResponse(); foreach ($vars['caches'] as &$cache_ref)
$response->content_type = "application/gpx; charset=utf-8"; {
$response->content_disposition = 'attachment; filename="results.gpx"'; $cache_ref['gc_attrs'] = array();
ob_start(); foreach ($cache_ref['attr_acodes'] as $acode)
Okapi::gettext_domain_init(explode("|", $langpref)); # Consumer gets properly localized GPX file. {
include 'gpxfile.tpl.php'; $has_gc_equivs = false;
Okapi::gettext_domain_restore(); foreach ($vars['attr_index'][$acode]['gc_equivs'] as $gc)
$response->body = ob_get_clean(); {
return $response; # The assignment via GC-ID as array key will prohibit duplicate
} # GC attributes, which can result from
# - assigning the same GC ID to multiple A-Codes,
# - contradicting attributes in one OC listing, e.g. 24/4 + not 24/7.
$cache_ref['gc_attrs'][$gc['id']] = $gc;
$has_gc_equivs = true;
}
if (!$has_gc_equivs && $vars['gc_ocde_attrs'])
{
# Generate an OCDE pseudo-GS attribute;
# see http://code.google.com/p/opencaching-api/issues/detail?id=190 and
# http://code.google.com/p/opencaching-api/issues/detail?id=271.
#
# Groundspeak uses ID 1..65 (as of June, 2013), and OCDE makeshift
# IDs start at 106, so there is space for 40 new GS attributes.
$internal_id = $attr_dict[$acode]['internal_id'];
$cache_ref['gc_attrs'][100 + $internal_id] = array(
'inc' => 1,
'name' => $ocde_attrnames[$internal_id][0]['name'],
);
}
}
}
}
}
/* OC sites always used internal user_ids in their generated GPX files.
* This might be considered an error in itself (Groundspeak's XML namespace
* doesn't allow that), but it very common (Garmin's OpenCaching.COM
* also does that). Therefore, for backward-compatibility reasons, OKAPI
* will do it the same way. See issue 174.
*
* Currently, the caches method does not expose "owner.internal_id" and
* "latest_logs.user.internal_id" fields, we will read them manually
* from the database here. */
$dict = array();
foreach ($vars['caches'] as &$cache_ref)
{
$dict[$cache_ref['owner']['uuid']] = true;
if (isset($cache_ref['latest_logs']))
foreach ($cache_ref['latest_logs'] as &$log_ref)
$dict[$log_ref['user']['uuid']] = true;
}
$rs = Db::query("
select uuid, user_id
from user
where uuid in ('".implode("','", array_map('mysql_real_escape_string', array_keys($dict)))."')
");
while ($row = mysql_fetch_assoc($rs))
$dict[$row['uuid']] = $row['user_id'];
$vars['user_uuid_to_internal_id'] = &$dict;
unset($dict);
# location_source (part 2 of 2)
if ($location_source != 'default-coords')
{
$location_change_prefix = $request->get_parameter('location_change_prefix');
if (!$location_change_prefix) {
$location_change_prefix = '# ';
}
# lets find requested coords
foreach ($vars['caches'] as &$cache_ref)
{
foreach ($cache_ref['alt_wpts'] as $alt_wpt_key => $alt_wpt)
{
if ('alt_wpt:'.$alt_wpt['type'] == $location_source)
{
# Switch locations between primary wpt and alternate wpt.
# Also alter the cache name and make sure to append a proper
# notice.
$original_location = $cache_ref['location'];
$cache_ref['location'] = $alt_wpt['location'];
$cache_ref['name_2'] = $location_change_prefix.$cache_ref['name'];
if ($location_source == "alt_wpt:user-coords") {
# In case of "user-coords", replace the default warning with a custom-tailored one.
$cache_ref['warning_prefix'] = _(
"<b>Geocache coordinates have been changed.</b> They have been replaced with ".
"your own custom coordinates which you have provided for this geocache."
);
} else {
# Default warning
$cache_ref['warning_prefix'] = _(
"<b>Geocache coordinates have been changed.</b> Currently they ".
"point to one of the alternate waypoints originally described as:"
) . " " . $alt_wpt['description'];
}
# remove current alt waypoint
unset($cache_ref['alt_wpts'][$alt_wpt_key]);
# add original location as alternate
if ($vars['alt_wpts'])
{
$cache_ref['alt_wpts'][] = array(
'name' => $cache_ref['code'].'-DEFAULT-COORDS',
'location' => $original_location,
'type' => 'default-coords',
'type_name' => _("Original geocache location"),
'sym' => 'Block, Blue',
'description' => sprintf(_("Original (owner-supplied) location of the %s geocache"), $cache_ref['code']),
);
}
break;
}
}
}
}
$response = new OkapiHttpResponse();
$response->content_type = "application/gpx; charset=utf-8";
$response->content_disposition = 'attachment; filename="results.gpx"';
ob_start();
Okapi::gettext_domain_init(explode("|", $langpref)); # Consumer gets properly localized GPX file.
include 'gpxfile.tpl.php';
Okapi::gettext_domain_restore();
$response->body = ob_get_clean();
return $response;
}
} }

View File

@@ -1,198 +1,257 @@
<xml> <xml>
<brief>Retrieve geocaches in GPX format</brief> <brief>Retrieve geocaches in GPX format</brief>
<issue-id>40</issue-id> <issue-id>40</issue-id>
<desc> <desc>
<p>Produces a standards-compliant Geocaching GPX file.</p> <p>Produces a standards-compliant Geocaching GPX file.</p>
<p>Unlike the services/caches/geocaches method responses, GPX files cannot <p>Unlike the services/caches/geocaches method responses, GPX files cannot
contain names and descriptions in separate multiple languages. This method contain names and descriptions in separate multiple languages. This method
will attempt to choose the best language based on your preference will attempt to choose the best language based on your preference
(see <b>langpref</b> argument).</p> (see <b>langpref</b> argument).</p>
<p>Remember that a full-blown GPX file is much larger than a basic one. <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 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 waypoints takes ~50 times less space than the same list with cache descriptions
and log entries included.</p> and log entries included.</p>
<p>Note, that GPX files may contain <a href='%OKAPI:docurl:html%'>unvalidated <p>Note, that GPX files may contain <a href='%OKAPI:docurl:html%'>unvalidated
HTML</a>.</p> HTML</a>.</p>
</desc> </desc>
<req name='cache_codes'> <req name='cache_codes'>
<p>Pipe-separated list of cache codes which you are interested in. <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 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 result in an empty, but valid, GPX file). All invalid cache codes will
be skipped without any notice!</p> be skipped without any notice!</p>
</req> </req>
<opt name='langpref' default='en'> <opt name='langpref' default='en'>
<p>Pipe-separated list of ISO 639-1 language codes. This indicates the <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> order of preference in which language will be chosen for GPX entities.</p>
</opt> </opt>
<opt name='ns_ground' default='false'> <opt name='ns_ground' default='false'>
Boolean. If <b>true</b> then response will include Boolean. If <b>true</b> then response will include
<a href='http://www.groundspeak.com/cache/1/0/1/cache.xsd'>Groundspeak's <a href='http://www.groundspeak.com/cache/1/0/1/cache.xsd'>Groundspeak's
GPX extension</a>. This namespace declares an extra &lt;cache&gt; element GPX extension</a>. This namespace declares an extra &lt;cache&gt; element
used by <a href='http://www.geocaching.com/'>geocaching.com</a> and <b>many others</b>. 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 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. be used in every geocaching GPX file. You probably want to have it.
</opt> </opt>
<opt name='ns_gsak' default='false'> <opt name='ns_gsak' default='false'>
<p>Boolean. If <b>true</b> then response will include <p>Boolean. If <b>true</b> then response will include
<a href='http://www.gsak.net/xmlv1/5/gsak.xsd'>GSAK GPX extension</a>. <a href='http://www.gsak.net/xmlv1/5/gsak.xsd'>GSAK GPX extension</a>.
This namespace declares an extra &lt;wptExtension&gt; element, This namespace declares an extra &lt;wptExtension&gt; element,
which allows including "waypoint inheritance" information (parent-child relations). which allows including "waypoint inheritance" information (parent-child relations).
This in turn allows us to include "alternate waypoints" in the response.</p> 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 <p>We know of <b>at least</b> <a href='http://www.gsak.net/'>one tool</a> that
makes use of this.</p> makes use of this.</p>
</opt> </opt>
<opt name='ns_ox' default='false'> <opt name='ns_ox' default='false'>
Boolean. If <b>true</b> then response will include Garmin's 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 <a href='http://www.opencaching.com/xmlschemas/opencaching/1/0/opencaching.xsd'>OpenCaching.com
GPX extension</a>. This namespace declares an extra &lt;opencaching&gt; element GPX extension</a>. This namespace declares an extra &lt;opencaching&gt; element
used by <a href='http://www.opencaching.com/'>Garmin's Opencaching.com</a>. used by <a href='http://www.opencaching.com/'>Garmin's Opencaching.com</a>.
The element includes information on cache difficulty, ratings, tags and images. The element includes information on cache difficulty, ratings, tags and images.
It is used within Garmin's Geocaching-enabled handheld GPS devices. It is used within Garmin's Geocaching-enabled handheld GPS devices.
</opt> </opt>
<opt name='images' default='descrefs:nonspoilers'> <opt name='images' default='descrefs:nonspoilers'>
<p>Which images to include (and how to include them). One of the following values:</p> <p>Which images to include (and how to include them). One of the following values:</p>
<ul> <ul>
<li><b>none</b> - no images will be included,</li> <li><b>none</b> - no images will be included,</li>
<li><b>descrefs:thumblinks</b> - all images will be included as "thumbnail" <li><b>descrefs:thumblinks</b> - all images will be included as "thumbnail"
&lt;img&gt; references at the end of each cache description, with a &lt;img&gt; references at the end of each cache description, with a
replacement image used for spoilers; the thumbnails are linked to the replacement image used for spoilers; the thumbnails are linked to the
large images,</li> large images,</li>
<li><b>descrefs:nonspoilers</b> - all non-spoiler images will be included <li><b>descrefs:nonspoilers</b> - all non-spoiler images will be included
as &lt;img&gt; references at the end of each cache description,</li> as &lt;img&gt; references at the end of each cache description,</li>
<li><b>descrefs:all</b> - all images will be included (including spoilers) <li><b>descrefs:all</b> - all images will be included (including spoilers)
as &lt;img&gt; references at the end of each cache description,</li> as &lt;img&gt; references at the end of each cache description,</li>
<li><b>ox:all</b> - all images will be included (including spoilers) <li><b>ox:all</b> - all images will be included (including spoilers)
as Garmin's &lt;ox:image&gt; references.</li> as Garmin's &lt;ox:image&gt; references.</li>
</ul> </ul>
<p>Note: When using "descrefs:" mode, remember to set <b>ns_ground</b> to <b>true</b>. <p>Note: When using "descrefs:" mode, remember to set <b>ns_ground</b> to <b>true</b>.
The default value is "descrefs:nonspoilers" for compatibilty reasons, but for The default value is "descrefs:nonspoilers" for compatibilty reasons, but for
most applications "descrefs:thumblinks" will be the better choice.</p> most applications "descrefs:thumblinks" will be the better choice.</p>
<p>Note: When using "ox:" mode, remember to set <b>ns_ox</b> to <b>true</b>. You must also <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 include JPEG files along the GPX for this to work properly - see
services/caches/formatters/garmin for more information.</p> services/caches/formatters/garmin for more information.</p>
<p>In the future, more generic ways of including images in GPX files may emerge.</p> <p>In the future, more generic ways of including images in GPX files may emerge.</p>
</opt> </opt>
<opt name='attrs' default='desc:text'> <opt name='attrs' default='desc:text'>
<p>This argument controls whether cache attributes are included and how they are included. <p>This argument controls whether cache attributes are included and how they are included.
Pipe-separated list consisting of any set of the following values:</p> Pipe-separated list consisting of any set of the following values:</p>
<ul> <ul>
<li><b>desc:text</b> - attributes will be listed (in plain-text) within <li><b>desc:text</b> - attributes will be listed (in plain-text) within
the cache description (<b>ns_ground</b> has to be <b>true</b>),</li> the cache description (<b>ns_ground</b> has to be <b>true</b>),</li>
<li><b>ox:tags</b> - attributes will be listed as Garmin's <i>ox:tag</i> <li><b>ox:tags</b> - attributes will be listed as Garmin's <i>ox:tag</i>
elements (<b>ns_ox</b> has to be <b>true</b>),</li> elements (<b>ns_ox</b> has to be <b>true</b>),</li>
<li> <li>
<p><b>gc:attrs</b> - attributes will be listed as Groundspeak's <p><b>gc:attrs</b> - attributes will be listed as Groundspeak's
<i>groundspeak:attribute</i> elements (<b>ns_ground</b> has to be <b>true</b>), <i>groundspeak:attribute</i> elements (<b>ns_ground</b> has to be <b>true</b>),
compatible with Geocaching.com (see the list compatible with Geocaching.com (see the list
<a href='http://www.geocaching.com/about/icons.aspx'>here</a>).</p> <a href='http://www.geocaching.com/about/icons.aspx'>here</a>).</p>
<p>Note that most Opencaching attributes don't have Geocaching.com counterparts. <p>Note that most Opencaching attributes don't have Geocaching.com counterparts.
Such attributes cannot be included as gc:attrs and will be ignored.</p> Such attributes cannot be included as gc:attrs and will be ignored.</p>
</li> </li>
<li> <li>
<p><b>gc_ocde:attrs</b> - attributes will be listed as Groundspeak's <p><b>gc_ocde:attrs</b> - attributes will be listed as Groundspeak's
<i>groundspeak:attribute</i> elements (<b>ns_ground</b> has to be <b>true</b>). <i>groundspeak:attribute</i> elements (<b>ns_ground</b> has to be <b>true</b>).
Opencaching attributes which have no Geocaching.com counterparts will be Opencaching attributes which have no Geocaching.com counterparts will be
included according to an Opencaching.DE convention, using "makeshift IDs" included according to an Opencaching.DE convention, using "makeshift IDs"
which may change in the future.</p> which may change in the future.</p>
<p>Note that this option is supported only by some OC sites. Other sites will <p>Note that this option is supported only by some OC sites. Other sites will
handle <b>gc_ocde:attrs</b> in the same way as <b>gc:attrs</b>.</p> handle <b>gc_ocde:attrs</b> in the same way as <b>gc:attrs</b>.</p>
</li> </li>
</ul> </ul>
<p>If you don't want any attributes to be included, you must set the <b>attrs</b> <p>If you don't want any attributes to be included, you must set the <b>attrs</b>
parameter to <b>none</b>. Using an empty string won't work this way - it will parameter to <b>none</b>. Using an empty string won't work this way - it will
trigger the default (<b>desc:text</b>) to be selected, for backward-compatibility.</p> trigger the default (<b>desc:text</b>) to be selected, for backward-compatibility.</p>
</opt> </opt>
<opt name='protection_areas' default='desc:auto'> <opt name='protection_areas' default='desc:auto'>
<p>This argument controls whether protection area information is included and how <p>This argument controls whether protection area information is included and how
it is included.</p> it is included.</p>
<ul> <ul>
<li><b>desc:text</b> - if the cache (probably) is located within one or more <li><b>desc:text</b> - if the cache (probably) is located within one or more
protection areas, e.g. a nature reserve, a list of the protection areas will protection areas, e.g. a nature reserve, a list of the protection areas will
be included in the cache description.</li> be included in the cache description.</li>
<li><b>desc:auto</b> - protection area information may be included in the <li><b>desc:auto</b> - protection area information may be included in the
cache description, depending on the installation.</li> cache description, depending on the installation.</li>
<li><b>none</b> - no protection area information will be included in the <li><b>none</b> - no protection area information will be included in the
GPX data.</li> GPX data.</li>
</ul> </ul>
<p>Note that information on protection areas may be incomplete or outdated <p>Note that information on protection areas may be incomplete or outdated
or completely missing on some installations.</p> or completely missing on some installations.</p>
</opt> </opt>
<opt name='trackables' default='none'> <opt name='trackables' default='none'>
<p>This argument controls whether information on trackables is included and how it is included. <p>This argument controls whether information on trackables is included and how it is included.
One of the following values:</p> One of the following values:</p>
<ul> <ul>
<li><b>none</b> - no trackables-data will be included,</li> <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: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> <li><b>desc:count</b> - total number of trackables will be included (in plain-text) within the cache description.</li>
</ul> </ul>
<p>Note: When using "desc:" mode, remember to set <b>ns_ground</b> to <b>true</b>.</p> <p>Note: When using "desc:" mode, remember to set <b>ns_ground</b> to <b>true</b>.</p>
</opt> </opt>
<opt name='recommendations' default='none'> <opt name='recommendations' default='none'>
<p>This argument controls whether information on recommendations is included and how it is included. <p>This argument controls whether information on recommendations is included and how it is included.
One of the following values:</p> One of the following values:</p>
<ul> <ul>
<li><b>none</b> - no recommendations-info will be included,</li> <li><b>none</b> - no recommendations-info will be included,</li>
<li><b>desc:count</b> - number of recommendations (and founds) will be included <li><b>desc:count</b> - number of recommendations (and founds) will be included
(in plain-text) within the cache description.</li> (in plain-text) within the cache description.</li>
</ul> </ul>
<p>Note: When using "desc:" mode, remember to set <b>ns_ground</b> to <b>true</b>.</p> <p>Note: When using "desc:" mode, remember to set <b>ns_ground</b> to <b>true</b>.</p>
</opt> </opt>
<opt name='my_notes' default='none'> <opt name='my_notes' default='none'>
<p>Allows you to include personal user's notes on each cache. This parameter <p>Allows you to include personal user's notes on each cache. This parameter
takes either <b>none</b> (default value), or pipe separeted list of the takes either <b>none</b> (default value), or pipe separeted list of the
following values:</p> following values:</p>
<ul> <ul>
<li><b>desc:text</b> - include personal notes as part of the cache <li><b>desc:text</b> - include personal notes as part of the cache
description.</li> description.</li>
<li> <li>
<p><b>gc:personal_note</b> - include personal notes as <p><b>gc:personal_note</b> - include personal notes as
&lt;groundspeak:personal_note&gt; element (under &lt;groundspeak:personal_note&gt; element (under
&lt;groundspeak:cache&gt; element).</p> &lt;groundspeak:cache&gt; element).</p>
<p><b>Warning:</b> This element is not a part of <p><b>Warning:</b> This element is not a part of
<i>http://www.groundspeak.com/cache/1/0/1</i> groundspeak schema, but it <i>http://www.groundspeak.com/cache/1/0/1</i> groundspeak schema, but it
is a <i>http://www.groundspeak.com/cache/1/0/2</i> schema element. Using is a <i>http://www.groundspeak.com/cache/1/0/2</i> schema element. Using
this option will generate formally invalid XML file.</p> this option will generate formally invalid XML file.</p>
</li> </li>
</ul> </ul>
<p>Note: You need to use Level 3 Authentication in order to set it to anything else than "none".</p> <p>Note: You need to use Level 3 Authentication in order to set it to anything else than "none".</p>
</opt> </opt>
<opt name='latest_logs' default='false'> <opt name='latest_logs' default='false'>
<p>Boolean. If <b>true</b> then response will include a couple of <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> 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> <p>You <b>must</b> set <b>ns_ground</b> argument to <b>true</b> if you want to use this.</p>
</opt> </opt>
<opt name='lpc' default='10'> <opt name='lpc' default='10'>
Log-entries per cache - the number of log entries included in each returned cache. 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> 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. and <b>latest_logs</b> arguments must be set to <b>true</b> in order for the logs to be included.
</opt> </opt>
<opt name='alt_wpts' default='false'> <opt name='alt_wpts' default='false'>
<p>Boolean. If <b>true</b> then we will include additional (alternate) waypoints in the response. <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> 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 <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 waypoints and their geocaches. Please note that not many applications are currently able to
properly understand GSAK metadata.</p> properly understand GSAK metadata.</p>
</opt> </opt>
<opt name='mark_found' default='false'> <opt name='mark_found' default='false'>
<p>Boolean. If <b>true</b> then all caches which are already found, will be marked appropriately <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> (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> <p>This field requires you to use the <b>user_uuid</b> parameter (or Level 3 Authentication).</p>
</opt> </opt>
<opt name="user_uuid"> <opt name="user_uuid">
<p>User'd ID. Required to mark found caches using <b>Level 1</b> Authentication.</p> <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> <p>If you use <b>Level 3</b> Authentication, you shouldn't use this parameter. Or, to be exact:</p>
<ul> <ul>
<li><b>user_uuid</b> will be extracted from the Access Token you use. You don't have to provide it.</li> <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 <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> will respond with HTTP 400 error.</li>
</ul> </ul>
</opt> </opt>
<returns> <opt name="location_source" default='default-coords'>
<p>GPX file. All invalid cache codes will be skipped without any notice!</p> <p>If you supply a value, then it must be prefixed with <i>alt_wpt:</i>,
</returns> and should match one of alternate waypoint types documented in the
<a href="%OKAPI:methodretref:services/caches/geocache#alt_wpts%">alt_wpts</a>
field returned by <a href="%OKAPI:methodref:services/caches/geocache%">services/caches/geocache</a>
method. If the type doesn't match, it will be ignored.</p>
<p>By default, the <b>lat</b> and <b>lon</b> attributes of the <b>&lt;wpt&gt;</b> element
will return the default coordinates of the cache, as supplied by the owner of the cache.
This option allows you to replace these coordinates with other set of coordinates, loaded
from the <b>alt_wpts</b> field mentioned above.</p>
<p>This may have some advantages in some scenarios. For example, if your user
is driving a car, he might be more interested in the coordinates of the
nearest suitable parking spot than in the coordinates of the geocache itself.
If you set <b>location_source</b> to <b>alt_wpt:parking</b> then you'll be
pre-fetching parking locations (if provided) without the need of loading all
other <b>alternate waypoints</b> in your requests.</p>
<p>Other use case: setting this to <b>alt_wpt:user-coords</b> allows
personalized results in some OC installations.</p>
<ul>
<li>If the type provided doesn't match any alternate waypoints, the default
coordinates of the geocache will be used.</li>
<li>
<p>If the type matches exactly one alternate waypoint, then:</p>
<ul>
<li>The <b>lat</b> and <b>lon</b> attributes of the "primary"
<b>&lt;wpt&gt;</b> element will be replaced with the location of
the matched alternate waypoint,</li>
<li>The name of the geocache will be prefixed with the value suppiled in
<b><a href="%OKAPI:methodargref:#location_change_prefix%">location_change_prefix</a></b>
parameter,</li>
<li>The desription of the waypoint will be included in the
header of the cache description,</li>
<li>The matched alternate waypoint will be removed from the list
of alternate waypoins (in order to avoid duplicate locations),</li>
<li>Extra alternate waypoint will be created, with the original
location of the geocache.</li>
</ul>
</li>
<li>If the type provided matches multiple alternate waypoints, then
the first one will be chosen.</li>
</ul>
</opt>
<opt name="location_change_prefix" default="#">
<p>Prefix to be added to the geocache name, in case its location has been changed due to
the <b><a href="%OKAPI:methodargref:#location_source%">location_source</a></b> parameter
matching any of the alternate waypoint.</p>
</opt>
<returns>
<p>GPX file. All invalid cache codes will be skipped without any notice!</p>
</returns>
</xml> </xml>

View File

@@ -16,187 +16,192 @@ http://www.groundspeak.com/cache/1/0/1 http://www.groundspeak.com/cache/1/0/1/ca
http://geocaching.com.au/geocache/1 http://geocaching.com.au/geocache/1/geocache.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 http://www.gsak.net/xmlv1/5 http://www.gsak.net/xmlv1/5/gsak.xsd
"> ">
<name><?= $vars['installation']['site_name'] ?> Geocache Search Results</name> <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'] . ($vars['alt_wpts'] && $vars['ns_gsak'] ? ' (HasChildren)' : '') ?></desc> <desc><?= $vars['installation']['site_name'] ?> Geocache Search Results, downloaded via OKAPI - <?= $vars['installation']['okapi_base_url'] . ($vars['alt_wpts'] && $vars['ns_gsak'] ? ' (HasChildren)' : '') ?></desc>
<author><?= $vars['installation']['site_name'] ?></author> <author><?= $vars['installation']['site_name'] ?></author>
<url><?= $vars['installation']['site_url'] ?></url> <url><?= $vars['installation']['site_url'] ?></url>
<urlname><?= $vars['installation']['site_name'] ?></urlname> <urlname><?= $vars['installation']['site_name'] ?></urlname>
<time><?= date('c') ?></time> <time><?= date('c') ?></time>
<? foreach ($vars['caches'] as $c) { ?> <? 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']); ?>
<? list($lat, $lon) = explode("|", $c['location']); ?> <wpt lat="<?= $lat ?>" lon="<?= $lon ?>">
<wpt lat="<?= $lat ?>" lon="<?= $lon ?>"> <time><?= $c['date_created'] ?></time>
<time><?= $c['date_created'] ?></time> <name><?= $c['code'] ?></name>
<name><?= $c['code'] ?></name> <desc><?= Okapi::xmlescape(isset($c['name_2']) ? $c['name_2'] : $c['name']) ?> <?= _("hidden by") ?> <?= Okapi::xmlescape($c['owner']['username']) ?> :: <?= ucfirst($c['type']) ?> Cache (<?= $c['difficulty'] ?>/<?= $c['terrain'] ?><? if ($c['size'] !== null) { echo "/".$c['size']; } else { echo "/X"; } ?>/<?= $c['rating'] ?>)</desc>
<desc><?= Okapi::xmlescape($c['name']) ?> <?= _("hidden by") ?> <?= Okapi::xmlescape($c['owner']['username']) ?> :: <?= 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>
<url><?= $c['url'] ?></url> <urlname><?= Okapi::xmlescape($c['name']) ?></urlname>
<urlname><?= Okapi::xmlescape($c['name']) ?></urlname> <sym><?= ($vars['mark_found'] && $c['is_found']) ? "Geocache Found" : "Geocache" ?></sym>
<sym><?= ($vars['mark_found'] && $c['is_found']) ? "Geocache Found" : "Geocache" ?></sym> <type>Geocache|<?= $vars['cache_GPX_types'][$c['type']] ?></type>
<type>Geocache|<?= $vars['cache_GPX_types'][$c['type']] ?></type> <? if ($vars['ns_ground']) { /* Does user want us to include Groundspeak's <cache> element? */ ?>
<? 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: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><?= Okapi::xmlescape(isset($c['name_2']) ? $c['name_2'] : $c['name']) ?></groundspeak:name>
<groundspeak:name><?= Okapi::xmlescape($c['name']) ?></groundspeak:name> <groundspeak:placed_by><?= Okapi::xmlescape($c['owner']['username']) ?></groundspeak:placed_by>
<groundspeak:placed_by><?= Okapi::xmlescape($c['owner']['username']) ?></groundspeak:placed_by> <groundspeak:owner id="<?= $vars['user_uuid_to_internal_id'][$c['owner']['uuid']] ?>"><?= Okapi::xmlescape($c['owner']['username']) ?></groundspeak:owner>
<groundspeak:owner id="<?= $vars['user_uuid_to_internal_id'][$c['owner']['uuid']] ?>"><?= Okapi::xmlescape($c['owner']['username']) ?></groundspeak:owner> <groundspeak:type><?= $vars['cache_GPX_types'][$c['type']] ?></groundspeak:type>
<groundspeak:type><?= $vars['cache_GPX_types'][$c['type']] ?></groundspeak:type> <groundspeak:container><?= $vars['cache_GPX_sizes'][$c['size2']] ?></groundspeak:container>
<groundspeak:container><?= $vars['cache_GPX_sizes'][$c['size2']] ?></groundspeak:container> <? if ($vars['gc_attrs'] || $vars['gc_ocde_attrs']) { /* Does user want us to include groundspeak:attributes? */ ?>
<? if ($vars['gc_attrs'] || $vars['gc_ocde_attrs']) { /* Does user want us to include groundspeak:attributes? */ ?> <groundspeak:attributes>
<groundspeak:attributes> <?
<? foreach ($c['gc_attrs'] as $gc_id => $gc_attr) {
foreach ($c['gc_attrs'] as $gc_id => $gc_attr) { print "<groundspeak:attribute id=\"".$gc_id."\" ";
print "<groundspeak:attribute id=\"".$gc_id."\" "; print "inc=\"".$gc_attr['inc']."\">";
print "inc=\"".$gc_attr['inc']."\">"; print Okapi::xmlescape($gc_attr['name']);
print Okapi::xmlescape($gc_attr['name']); print "</groundspeak:attribute>";
print "</groundspeak:attribute>"; }
} ?>
?> </groundspeak:attributes>
</groundspeak:attributes> <? } ?>
<? } ?> <groundspeak:difficulty><?= $c['difficulty'] ?></groundspeak:difficulty>
<groundspeak:difficulty><?= $c['difficulty'] ?></groundspeak:difficulty> <groundspeak:terrain><?= $c['terrain'] ?></groundspeak:terrain>
<groundspeak:terrain><?= $c['terrain'] ?></groundspeak:terrain> <? if ($c['short_description']) { ?>
<groundspeak:long_description html="True"> <groundspeak:short_description html="False"><?= Okapi::xmlescape($c['short_description']) ?></groundspeak:short_description>
&lt;p&gt; <? } ?>
&lt;a href="<?= $c['url'] ?>"&gt;<?= Okapi::xmlescape($c['name']) ?>&lt;/a&gt; <groundspeak:long_description html="True">
<?= _("hidden by") ?> &lt;a href='<?= $c['owner']['profile_url'] ?>'&gt;<?= Okapi::xmlescape($c['owner']['username']) ?>&lt;/a&gt;&lt;br/&gt; <? if (isset($c['warning_prefix'])) { ?>
<? if ($vars['recommendations'] == 'desc:count') { /* Does user want us to include recommendations count? */ ?> &lt;p style='font-size: 120%'&gt;<?= Okapi::xmlescape($c['warning_prefix']) ?>&lt;/p&gt;
<?= sprintf(ngettext("%d recommendation", "%d recommendations", $c['recommendations']), $c['recommendations']) ?> <? } ?>
(<?= sprintf(ngettext("found %d time", "found %d times", $c['founds']), $c['founds']) ?>). &lt;p&gt;
<? } ?> &lt;a href="<?= $c['url'] ?>"&gt;<?= Okapi::xmlescape($c['name']) ?>&lt;/a&gt;
<? if ($vars['trackables'] == 'desc:count') { /* Does user want us to include trackables count? */ ?> <?= _("hidden by") ?> &lt;a href='<?= $c['owner']['profile_url'] ?>'&gt;<?= Okapi::xmlescape($c['owner']['username']) ?>&lt;/a&gt;&lt;br/&gt;
<?= sprintf(ngettext("%d trackable", "%d trackables", $c['trackables_count']), $c['trackables_count']) ?>. <? if ($vars['recommendations'] == 'desc:count') { /* Does user want us to include recommendations count? */ ?>
<? } ?> <?= sprintf(ngettext("%d recommendation", "%d recommendations", $c['recommendations']), $c['recommendations']) ?>
&lt;/p&gt; (<?= sprintf(ngettext("found %d time", "found %d times", $c['founds']), $c['founds']) ?>).
<? if ((in_array('desc:text', $vars['my_notes'])) && ($c['my_notes'] != null)) { /* Does user want us to include personal notes? */ ?> <? } ?>
&lt;p&gt;&lt;b&gt;<?= _("Personal notes") ?>:&lt;/b&gt;&lt;br&gt;<?= Okapi::xmlescape(nl2br($c['my_notes'])) ?>&lt;/p&gt; <? 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']) ?>.
<? } ?>
&lt;/p&gt;
<? if ((in_array('desc:text', $vars['my_notes'])) && ($c['my_notes'] != null)) { /* Does user want us to include personal notes? */ ?>
&lt;p&gt;&lt;b&gt;<?= _("Personal notes") ?>:&lt;/b&gt;&lt;br&gt;<?= Okapi::xmlescape(nl2br($c['my_notes'])) ?>&lt;/p&gt;
<? } ?>
<? if (in_array('desc:text', $vars['attrs']) && count($c['attrnames']) > 0) { /* Does user want us to include attributes? */ ?> <? if (in_array('desc:text', $vars['attrs']) && count($c['attrnames']) > 0) { /* Does user want us to include attributes? */ ?>
&lt;p&gt;<?= _("Attributes") ?>:&lt;/p&gt; &lt;p&gt;<?= _("Attributes") ?>:&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;<?= implode("&lt;/li&gt;&lt;li&gt;", $c['attrnames']) ?>&lt;/li&gt;&lt;/ul&gt; &lt;ul&gt;&lt;li&gt;<?= implode("&lt;/li&gt;&lt;li&gt;", $c['attrnames']) ?>&lt;/li&gt;&lt;/ul&gt;
<? } ?> <? } ?>
<? if ($vars['trackables'] == 'desc:list' && count($c['trackables']) > 0) { /* Does user want us to include trackables list? */ ?> <? if ($vars['trackables'] == 'desc:list' && count($c['trackables']) > 0) { /* Does user want us to include trackables list? */ ?>
&lt;p&gt;<?= _("Trackables") ?>:&lt;/p&gt; &lt;p&gt;<?= _("Trackables") ?>:&lt;/p&gt;
&lt;ul&gt; &lt;ul&gt;
<? foreach ($c['trackables'] as $t) { ?> <? foreach ($c['trackables'] as $t) { ?>
&lt;li&gt;&lt;a href='<?= Okapi::xmlescape($t['url']) ?>'&gt;<?= Okapi::xmlescape($t['name']) ?>&lt;/a&gt; (<?= $t['code'] ?>)&lt;/li&gt; &lt;li&gt;&lt;a href='<?= Okapi::xmlescape($t['url']) ?>'&gt;<?= Okapi::xmlescape($t['name']) ?>&lt;/a&gt; (<?= $t['code'] ?>)&lt;/li&gt;
<? } ?> <? } ?>
&lt;/ul&gt; &lt;/ul&gt;
<? } ?> <? } ?>
<?= Okapi::xmlescape($c['description']) ?> <?= Okapi::xmlescape($c['description']) ?>
<? if ((strpos($vars['images'], "descrefs:") === 0) && count($c['images']) > 0) { /* Does user want us to include <img> references in cache descriptions? */ <? if ((strpos($vars['images'], "descrefs:") === 0) && count($c['images']) > 0) { /* Does user want us to include <img> references in cache descriptions? */
if ($vars['images'] == "descrefs:thumblinks") { ?> if ($vars['images'] == "descrefs:thumblinks") { ?>
&lt;h2&gt;<?= _("Images") ?> (<?= count($c['images']) ?>)&lt;/h2&gt; &lt;h2&gt;<?= _("Images") ?> (<?= count($c['images']) ?>)&lt;/h2&gt;
&lt;div&gt; &lt;div&gt;
<? foreach ($c['images'] as $img) { ?> <? foreach ($c['images'] as $img) { ?>
&lt;div style='float:left; padding:6px'&gt;&lt;a href='<?= Okapi::xmlescape($img['url']) ?>'&gt;&lt;img src='<?= Okapi::xmlescape($img['thumb_url']) ?>'&gt;&lt;/a&gt;&lt;br&gt; &lt;div style='float:left; padding:6px'&gt;&lt;a href='<?= Okapi::xmlescape($img['url']) ?>'&gt;&lt;img src='<?= Okapi::xmlescape($img['thumb_url']) ?>'&gt;&lt;/a&gt;&lt;br&gt;
<?= Okapi::xmlescape($img['caption']) ?>&lt;/div&gt; <?= Okapi::xmlescape($img['caption']) ?>&lt;/div&gt;
<? } ?> <? } ?>
&lt;/div&gt; &lt;/div&gt;
<? } else { <? } else {
# We will split images into two subcategories: spoilers and nonspoilers. # We will split images into two subcategories: spoilers and nonspoilers.
$spoilers = array(); $spoilers = array();
$nonspoilers = array(); $nonspoilers = array();
foreach ($c['images'] as $img) foreach ($c['images'] as $img)
if ($img['is_spoiler']) $spoilers[] = $img; if ($img['is_spoiler']) $spoilers[] = $img;
else $nonspoilers[] = $img; else $nonspoilers[] = $img;
?> ?>
<? if (count($nonspoilers) > 0) { ?> <? if (count($nonspoilers) > 0) { ?>
&lt;h2&gt;<?= _("Images") ?> (<?= count($nonspoilers) ?>)&lt;/h2&gt; &lt;h2&gt;<?= _("Images") ?> (<?= count($nonspoilers) ?>)&lt;/h2&gt;
<? foreach ($nonspoilers as $img) { ?> <? foreach ($nonspoilers as $img) { ?>
&lt;p&gt;&lt;img src='<?= Okapi::xmlescape($img['url']) ?>'&gt;&lt;br&gt; &lt;p&gt;&lt;img src='<?= Okapi::xmlescape($img['url']) ?>'&gt;&lt;br&gt;
<?= Okapi::xmlescape($img['caption']) ?>&lt;/p&gt; <?= Okapi::xmlescape($img['caption']) ?>&lt;/p&gt;
<? } ?> <? } ?>
<? } ?> <? } ?>
<? if (count($spoilers) > 0 && $vars['images'] == 'descrefs:all') { ?> <? if (count($spoilers) > 0 && $vars['images'] == 'descrefs:all') { ?>
&lt;h2&gt;<?= _("Spoilers") ?> (<?= count($spoilers) ?>)&lt;/h2&gt; &lt;h2&gt;<?= _("Spoilers") ?> (<?= count($spoilers) ?>)&lt;/h2&gt;
<? foreach ($spoilers as $img) { ?> <? foreach ($spoilers as $img) { ?>
&lt;p&gt;&lt;img src='<?= Okapi::xmlescape($img['url']) ?>'&gt;&lt;br&gt; &lt;p&gt;&lt;img src='<?= Okapi::xmlescape($img['url']) ?>'&gt;&lt;br&gt;
<?= Okapi::xmlescape($img['caption']) ?>&lt;/p&gt; <?= Okapi::xmlescape($img['caption']) ?>&lt;/p&gt;
<? } ?> <? } ?>
<? } ?> <? } ?>
<? } ?> <? } ?>
<? } ?> <? } ?>
<? if ((strpos($vars['images'], "ox:") === 0) && count($c['images']) > 0) { /* Include image descriptions (for ox:image numbers)? */ ?> <? if ((strpos($vars['images'], "ox:") === 0) && count($c['images']) > 0) { /* Include image descriptions (for ox:image numbers)? */ ?>
&lt;p&gt;<?= _("Image descriptions") ?>:&lt;/p&gt; &lt;p&gt;<?= _("Image descriptions") ?>:&lt;/p&gt;
&lt;ul&gt; &lt;ul&gt;
<? foreach ($c['images'] as $no => $img) { ?> <? foreach ($c['images'] as $no => $img) { ?>
&lt;li&gt;<?= $img['unique_caption'] ?>. <?= Okapi::xmlescape($img['caption']) ?>&lt;/li&gt; &lt;li&gt;<?= $img['unique_caption'] ?>. <?= Okapi::xmlescape($img['caption']) ?>&lt;/li&gt;
<? } ?> <? } ?>
&lt;/ul&gt; &lt;/ul&gt;
<? } ?> <? } ?>
<? if ($vars['protection_areas'] == 'desc:text' && count($c['protection_areas'])) { ?> <? if ($vars['protection_areas'] == 'desc:text' && count($c['protection_areas'])) { ?>
&lt;p&gt;<?= _("The cache probably is located in the following protection areas:") ?>&lt;/p&gt; &lt;p&gt;<?= _("The cache probably is located in the following protection areas:") ?>&lt;/p&gt;
&lt;ul&gt; &lt;ul&gt;
<? foreach($c['protection_areas'] as $protection_area) { ?> <? foreach($c['protection_areas'] as $protection_area) { ?>
&lt;li&gt;<?= Okapi::xmlescape($protection_area['type'])." - ".Okapi::xmlescape($protection_area['name']) ?>&lt;/li&gt; &lt;li&gt;<?= Okapi::xmlescape($protection_area['type'])." - ".Okapi::xmlescape($protection_area['name']) ?>&lt;/li&gt;
<? } ?> <? } ?>
&lt;/ul;&gt; &lt;/ul;&gt;
<? } ?> <? } ?>
</groundspeak:long_description> </groundspeak:long_description>
<groundspeak:encoded_hints><?= Okapi::xmlescape($c['hint2']) ?></groundspeak:encoded_hints> <groundspeak:encoded_hints><?= Okapi::xmlescape($c['hint2']) ?></groundspeak:encoded_hints>
<? if ((in_array('gc:personal_note', $vars['my_notes'])) && ($c['my_notes'] != null)) { /* Does user want us to include personal notes? -> Issue 294 */ ?> <? if ((in_array('gc:personal_note', $vars['my_notes'])) && ($c['my_notes'] != null)) { /* Does user want us to include personal notes? -> Issue 294 */ ?>
<groundspeak:personal_note><?= Okapi::xmlescape($c['my_notes']) ?></groundspeak:personal_note> <groundspeak:personal_note><?= Okapi::xmlescape($c['my_notes']) ?></groundspeak:personal_note>
<? } ?> <? } ?>
<? if ($vars['latest_logs']) { /* Does user want us to include latest log entries? */ ?> <? if ($vars['latest_logs']) { /* Does user want us to include latest log entries? */ ?>
<groundspeak:logs> <groundspeak:logs>
<? foreach ($c['latest_logs'] as $log) { ?> <? foreach ($c['latest_logs'] as $log) { ?>
<groundspeak:log id="<?= $log['internal_id'] ?>"> <groundspeak:log id="<?= $log['internal_id'] ?>">
<groundspeak:date><?= $log['date'] ?></groundspeak:date> <groundspeak:date><?= $log['date'] ?></groundspeak:date>
<groundspeak:type><?= $log['type'] ?></groundspeak:type> <groundspeak:type><?= $log['type'] ?></groundspeak:type>
<groundspeak:finder id="<?= $vars['user_uuid_to_internal_id'][$log['user']['uuid']] ?>"><?= Okapi::xmlescape($log['user']['username']) ?></groundspeak:finder> <groundspeak:finder id="<?= $vars['user_uuid_to_internal_id'][$log['user']['uuid']] ?>"><?= Okapi::xmlescape($log['user']['username']) ?></groundspeak:finder>
<groundspeak:text encoded="False"><?= $log['was_recommended'] ? "(*) ": "" ?><?= Okapi::xmlescape($log['comment']) ?></groundspeak:text> <groundspeak:text encoded="False"><?= $log['was_recommended'] ? "(*) ": "" ?><?= Okapi::xmlescape($log['comment']) ?></groundspeak:text>
</groundspeak:log> </groundspeak:log>
<? } ?> <? } ?>
</groundspeak:logs> </groundspeak:logs>
<? } ?> <? } ?>
<? /* groundspeak:travelbugs - does it actually DO anything? WRTODO */ ?> <? /* groundspeak:travelbugs - does it actually DO anything? WRTODO */ ?>
</groundspeak:cache> </groundspeak:cache>
<? } ?> <? } ?>
<? if ($vars['ns_ox']) { /* Does user want us to include Garmin's <opencaching> element? */ ?> <? 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:opencaching xmlns:ox="http://www.opencaching.com/xmlschemas/opencaching/1/0">
<ox:ratings> <ox:ratings>
<? if ($c['rating'] !== null) { ?><ox:awesomeness><?= $c['rating'] ?></ox:awesomeness><? } ?> <? if ($c['rating'] !== null) { ?><ox:awesomeness><?= $c['rating'] ?></ox:awesomeness><? } ?>
<ox:difficulty><?= $c['difficulty'] ?></ox:difficulty> <ox:difficulty><?= $c['difficulty'] ?></ox:difficulty>
<? if ($c['oxsize'] !== null) { ?><ox:size><?= $c['oxsize'] ?></ox:size><? } ?> <? if ($c['oxsize'] !== null) { ?><ox:size><?= $c['oxsize'] ?></ox:size><? } ?>
<ox:terrain><?= $c['terrain'] ?></ox:terrain> <ox:terrain><?= $c['terrain'] ?></ox:terrain>
</ox:ratings> </ox:ratings>
<? if (in_array('ox:tags', $vars['attrs']) && count($c['attrnames']) > 0) { /* Does user want us to include ox:tags? */ ?> <? if (in_array('ox:tags', $vars['attrs']) && 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> <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? */ ?> <? if ((strpos($vars['images'], "ox:") === 0) && count($c['images']) > 0) { /* Does user want us to include ox:image references? */ ?>
<ox:images> <ox:images>
<? foreach ($c['images'] as $no => $img) { ?> <? foreach ($c['images'] as $no => $img) { ?>
<ox:image> <ox:image>
<ox:name><?= $img['unique_caption'] ?>.jpg</ox:name> <ox:name><?= $img['unique_caption'] ?>.jpg</ox:name>
<ox:size>0</ox:size> <ox:size>0</ox:size>
<ox:required>false</ox:required> <ox:required>false</ox:required>
<ox:spoiler><?= ($img['is_spoiler'] ? "true" : "false") ?></ox:spoiler> <ox:spoiler><?= ($img['is_spoiler'] ? "true" : "false") ?></ox:spoiler>
</ox:image> </ox:image>
<? } ?> <? } ?>
</ox:images> </ox:images>
<? } ?> <? } ?>
</ox:opencaching> </ox:opencaching>
<? } ?> <? } ?>
</wpt> </wpt>
<? if ($vars['alt_wpts']) { ?> <? if ($vars['alt_wpts']) { ?>
<? foreach ($c['alt_wpts'] as $wpt) { ?> <? foreach ($c['alt_wpts'] as $wpt) { ?>
<? list($lat, $lon) = explode("|", $wpt['location']); ?> <? list($lat, $lon) = explode("|", $wpt['location']); ?>
<wpt lat="<?= $lat ?>" lon="<?= $lon ?>"> <wpt lat="<?= $lat ?>" lon="<?= $lon ?>">
<time><?= $c['date_created'] ?></time> <time><?= $c['date_created'] ?></time>
<name><?= Okapi::xmlescape($wpt['name']) ?></name> <name><?= Okapi::xmlescape($wpt['name']) ?></name>
<cmt><?= Okapi::xmlescape($wpt['description']) ?></cmt> <cmt><?= Okapi::xmlescape($wpt['description']) ?></cmt>
<desc><?= Okapi::xmlescape($wpt['type_name']) ?></desc> <desc><?= Okapi::xmlescape($wpt['type_name']) ?></desc>
<url><?= $c['url'] ?></url> <url><?= $c['url'] ?></url>
<urlname><?= Okapi::xmlescape($c['name']) ?></urlname> <urlname><?= Okapi::xmlescape($c['name']) ?></urlname>
<sym><?= $wpt['sym'] ?></sym> <sym><?= $wpt['sym'] ?></sym>
<type>Waypoint|<?= $wpt['sym'] ?></type> <type>Waypoint|<?= $wpt['sym'] ?></type>
<? if ($vars['ns_gsak']) { ?> <? if ($vars['ns_gsak']) { ?>
<gsak:wptExtension xmlns:gsak="http://www.gsak.net/xmlv1/5"> <gsak:wptExtension xmlns:gsak="http://www.gsak.net/xmlv1/5">
<gsak:Parent><?= $c['code'] ?></gsak:Parent> <gsak:Parent><?= $c['code'] ?></gsak:Parent>
</gsak:wptExtension> </gsak:wptExtension>
<? } ?> <? } ?>
</wpt> </wpt>
<? } ?> <? } ?>
<? } ?> <? } ?>
<? } ?> <? } ?>
</gpx> </gpx>

View File

@@ -12,51 +12,51 @@ use okapi\services\caches\search\SearchAssistant;
class WebService class WebService
{ {
public static function options() public static function options()
{ {
return array( return array(
'min_auth_level' => 1 'min_auth_level' => 1
); );
} }
public static function call(OkapiRequest $request) public static function call(OkapiRequest $request)
{ {
$cache_code = $request->get_parameter('cache_code'); $cache_code = $request->get_parameter('cache_code');
if (!$cache_code) throw new ParamMissing('cache_code'); if (!$cache_code) throw new ParamMissing('cache_code');
if (strpos($cache_code, "|") !== false) throw new InvalidParam('cache_code'); if (strpos($cache_code, "|") !== false) throw new InvalidParam('cache_code');
$langpref = $request->get_parameter('langpref'); $langpref = $request->get_parameter('langpref');
if (!$langpref) $langpref = "en"; if (!$langpref) $langpref = "en";
$fields = $request->get_parameter('fields'); $fields = $request->get_parameter('fields');
if (!$fields) $fields = "code|name|location|type|status"; if (!$fields) $fields = "code|name|location|type|status";
$log_fields = $request->get_parameter('log_fields'); $log_fields = $request->get_parameter('log_fields');
if (!$log_fields) $log_fields = "uuid|date|user|type|comment"; if (!$log_fields) $log_fields = "uuid|date|user|type|comment";
$lpc = $request->get_parameter('lpc'); $lpc = $request->get_parameter('lpc');
if (!$lpc) $lpc = 10; if (!$lpc) $lpc = 10;
$attribution_append = $request->get_parameter('attribution_append'); $attribution_append = $request->get_parameter('attribution_append');
if (!$attribution_append) $attribution_append = 'full'; if (!$attribution_append) $attribution_append = 'full';
$params = array( $params = array(
'cache_codes' => $cache_code, 'cache_codes' => $cache_code,
'langpref' => $langpref, 'langpref' => $langpref,
'fields' => $fields, 'fields' => $fields,
'attribution_append' => $attribution_append, 'attribution_append' => $attribution_append,
'lpc' => $lpc, 'lpc' => $lpc,
'log_fields' => $log_fields 'log_fields' => $log_fields
); );
$my_location = $request->get_parameter('my_location'); $my_location = $request->get_parameter('my_location');
if ($my_location) if ($my_location)
$params['my_location'] = $my_location; $params['my_location'] = $my_location;
$user_uuid = $request->get_parameter('user_uuid'); $user_uuid = $request->get_parameter('user_uuid');
if ($user_uuid) if ($user_uuid)
$params['user_uuid'] = $user_uuid; $params['user_uuid'] = $user_uuid;
# There's no need to validate the fields/lpc parameters as the 'geocaches' # 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). # method does this (it will raise a proper exception on invalid values).
$results = OkapiServiceRunner::call('services/caches/geocaches', new OkapiInternalRequest( $results = OkapiServiceRunner::call('services/caches/geocaches', new OkapiInternalRequest(
$request->consumer, $request->token, $params)); $request->consumer, $request->token, $params));
$result = $results[$cache_code]; $result = $results[$cache_code];
if ($result === null) if ($result === null)
throw new InvalidParam('cache_code', "This cache does not exist."); throw new InvalidParam('cache_code', "This cache does not exist.");
return Okapi::formatted_response($request, $result); return Okapi::formatted_response($request, $result);
} }
} }

View File

@@ -1,413 +1,440 @@
<xml> <xml>
<brief>Retrieve information on a single geocache</brief> <brief>Retrieve information on a single geocache</brief>
<issue-id>19</issue-id> <issue-id>19</issue-id>
<desc> <desc>
<p>Retrieve information on a single geocache.</p> <p>Retrieve information on a single geocache.</p>
</desc> </desc>
<req name='cache_code'>Unique code of the geocache</req> <req name='cache_code'>Unique code of the geocache</req>
<opt name='langpref' default='en'> <opt name='langpref' default='en'>
<p>Pipe-separated list of ISO 639-1 language codes. This indicates the <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 order of preference in which language will be chosen for fields like
<b>name</b> and <b>description</b>.</p> <b>name</b> and <b>description</b>.</p>
<p>Please note, that you may also access caches' descriptions in all <p>Please note, that you may also access caches' descriptions in all
available languages. If you want to do this, you should use fields 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>names</b>, <b>descriptions</b> (etc.) instead of <b>name</b> and
<b>description</b> (etc.).</p> <b>description</b> (etc.).</p>
</opt> </opt>
<opt name='fields' default='code|name|location|type|status'> <opt name='fields' default='code|name|location|type|status'>
<p>Pipe-separated list of field names which you are interested with. <p>Pipe-separated list of field names which you are interested with.
Selected fields will be included in the response. See below for the Selected fields will be included in the response. See below for the
list of available fields.</p> list of available fields.</p>
</opt> </opt>
<opt name='attribution_append' default='full'> <opt name='attribution_append' default='full'>
<p>By default, OKAPI appends the value of <b>attribution_note</b> field to all the <p>By default, OKAPI appends the value of <b>attribution_note</b> field to all the
cache descriptions. If you would like to display the attribution note separately, cache descriptions. If you would like to display the attribution note separately,
you may use this parameter. Use one of the following values:</p> you may use this parameter. Use one of the following values:</p>
<ul> <ul>
<li> <li>
<b>full</b> - OKAPI will append the attribution note to the descriptions of the <b>full</b> - OKAPI will append the attribution note to the descriptions of the
geocache. The language of the note will match the language of the description geocache. The language of the note will match the language of the description
(i.e. each value in <b>descriptions</b> may contain attribution notes in (i.e. each value in <b>descriptions</b> may contain attribution notes in
a different language). a different language).
</li> </li>
<li> <li>
<p><b>none</b> - OKAPI will not append the attribution note to geocache <p><b>none</b> - OKAPI will not append the attribution note to geocache
descriptions. You will use the <b>attribution_note</b> instead (which depends descriptions. You will use the <b>attribution_note</b> instead (which depends
on the <b>langpref</b> parameter).</p> on the <b>langpref</b> parameter).</p>
<p><b>Important note:</b> You are still <b>required</b> to display the
<b>attribution_note</b> field in some other way.</p>
</li>
<li>
<b>static</b> - OKAPI will append a "static" attribution note to the descriptions
of the geocache. This might equal the "full" attribution note, but not necessarilly,
since the "full" note may contain a current date
(<a href='https://code.google.com/p/opencaching-api/issues/detail?id=178'>why?</a>),
and the "static" note will not. This is implemented primarily for internal use
(i.e. the replicate module). Usually you should not use it, because on some
installations the static note will not be sufficient to meet the data license
requirements.
</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='log_fields' default='uuid|date|user|type|comment'>
Pipe-separated list of log fields to include in the <b>latest_logs</b> field.
For valid field names, see <b>logs/entry</b> method.
</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. Currently available fields:</p>
<ul>
<li><b>code</b> - unique Opencaching 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><b>Event</b>,</li>
<li><i>(any other value is valid too)</i></li>
</ul>
<p><b>Event</b> is a peculiar type of geocache which is NOT a geocache
at all, but it <b>is</b> stored as a geocache in OC database (this design
decision is old as the OC network itself!). Just keep in mind, that
in case of Event Caches, some fields may have a little different meaning
than you would tell by their name.</p>
</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>gc_code</b> - Geocaching.com code (GC code) of the geocache
<b>or null</b> if the cache is not listed on GC or the GC code is
unknown.</p>
<p>Please note that this information is supplied by cache owners
and it is often missing, obsolete or otherwise incorrect.</p>
</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 querying 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 querying 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 querying OKAPI.</p>
</li>
<li>
<p><b>bearing3</b> - string, the absolute bearing to the cache, represented as a typical direction
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 querying OKAPI.</p>
</li>
<li>
<p><b>is_found</b> - boolean, true if the 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). Please note, that this will also return <b>true</b>
for attended Event Caches.</p>
</li>
<li>
<p><b>is_not_found</b> - boolean, true if the 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>
<p><b>is_watched</b> - boolean, true if the user is watching this cache. You may consider highlighting
such geocaches in some fashion, as the users may use this functionality to temporarily mark geocaches
of particular interest (i.e. geocaches they plan to find today).</p>
<p>This is private data. You will need Level 3 Authentication to access this field.</p>
</li>
<li>
<p><b>is_ignored</b> - boolean, true if the user is ignoring this cache. (See also <b>exclude_ignored</b>
parameter of all search methods.)</p>
<p>This is private data. You will need Level 3 Authentication to access this field.</p>
</li>
<li>
<p><b>founds</b> - number of times the geocache was successfully found
(or attended, in case of Event Caches),</p>
</li>
<li>
<p><b>notfounds</b> - number of times the geocache was not found
(in case of Event Caches this will always be zero),</p>
</li>
<li>
<p><b>willattends</b> - in case of Event Caches, this is the number of
"Will attend" log entries. In case of any other cache type, it will
be <b>zero</b> (not null, for <a href='https://code.google.com/p/opencaching-api/issues/detail?id=233'>backward
compatibility</a>),</p>
</li>
<li class='deprecated'>
<b>size</b> - deprecated
(<a href='http://code.google.com/p/opencaching-api/issues/detail?id=155'>why?</a>),
use <b>size2</b> instead. Float (between 1 and 5), size rating of the container, or
<b>null</b> if geocache does not have a container,
</li>
<li>
<p><b>size2</b> - string indicating the size of the container, so called
"size2 code". One of the following values:
'none', 'nano', 'micro', 'small', 'regular', 'large', 'xlarge', 'other'.</p>
</li>
<li>
<p><b>oxsize</b> - float (between 1 and 5) or null, this is a size rating
variant, compatible with the one used by opencaching.com (and lately,
Garmin GPS devices).</p>
<p><b>Note:</b> The mapping is undocumented and may change without notice.</p>
<p><b>Note:</b> Some of OC's size values cannot be properly mapped to <b>oxsize</b>,
i.e. the 'other' size.</p>
</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>
<b>trip_time</b> - float, approximate total amount of time needed to
find the cache, in hours; this will usually include the time to reach the
cache location <em>and</em> go back (from/to a parking spot, etc.);
<b>null</b> if unknown,
</li>
<li><b>trip_distance</b> - float, approximate total distance needed to
find the cache, in kilometers; this will usually include the time to reach the
cache location <em>and</em> go back (from/to a parking spot, etc.);
<b>null</b> if unknown,
</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> - <a href='%OKAPI:docurl:html%'>HTML string</a>,
description of the cache,</li>
<li><b>descriptions</b> - a dictionary (language code =&gt;
<a href='%OKAPI:docurl:html%'>HTML string</a>) of cache descriptions,</li>
<li class="deprecated"><b>hint</b> - <a href='%OKAPI:docurl:html%'>HTML-encoded</a>
string, cache hints/spoilers; deprecated
(<a href="http://code.google.com/p/opencaching-api/issues/detail?id=261">why?</a>),
use <b>hint2</b> instead,</li>
<li class="deprecated"><b>hints</b> - a dictionary (language code =&gt;
<a href='%OKAPI:docurl:html%'>HTML-encoded</a> string) of cache hints/spoilers;
deprecated, use hints2 instead,</li>
<li><b>hint2</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>hints2</b> - a dictionary (language code =&gt;
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> - URL of the image,</li>
<li><b>thumb_url</b> - URL of a small (thumb) version of the image,</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>preview_image</b> - On some installations, owners may select one of the <b>images</b>
(see above) as a preview image. You are encouraged to display it as a 'teaser'
for this cache. On other installations this functionality is disabled and you
will always get the <b>null</b> value here.</p>
<p>The value of <b>preview_image</b> is either <b>null</b> or a dictionary describing <p><b>Important note:</b> You are still <b>required</b> to display the
an image. The structure of this dictionary is the same as of a single entry on <b>attribution_note</b> field in some other way.</p>
the <b>images</b> list described above.</p> </li>
</li> <li>
<li> <b>static</b> - OKAPI will append a "static" attribution note to the descriptions
<p><b>attr_acodes</b> - unordered list of OKAPI geocache-attribute IDs (A-codes) with of the geocache. This might equal the "full" attribution note, but not necessarilly,
which the cache was tagged. Use the <b>attrs</b> module to retrieve information on since the "full" note may contain a current date
these attributes.</p> (<a href='https://code.google.com/p/opencaching-api/issues/detail?id=178'>why?</a>),
</li> and the "static" note will not. This is implemented primarily for internal use
<li> (i.e. the replicate module). Usually you should not use it, because on some
<b>attrnames</b> - if you don't want to dig into <b>attr_acodes</b> just now, then installations the static note will not be sufficient to meet the data license
you can use these as a simple alternative. <b>attrnames</b> is an unordered list of requirements.
names of the attributes with which the cache was tagged; the language will be </li>
selected based on the <b>langpref</b> parameter. </ul>
</li> </opt>
<li> <opt name='lpc' default='10'>
<p><b>attribution_note</b> - the proper attribution note for the cache listing according Log-entries per cache - the number of logs returned in the <b>latest_logs</b> field.
to the local OC site's Data License. By default, this note is also appended to geocache This should be an integer or a special "all" value. Please note, that you must include
descriptions (see the <b>attribution_append</b> parameter).</p> the <b>latest_logs</b> field in <b>fields</b> in order to see the log entries.
</li> </opt>
<li> <opt name='log_fields' default='uuid|date|user|type|comment'>
<p><b>latest_logs</b> - a couple of latest log entries in the cache. Pipe-separated list of log fields to include in the <b>latest_logs</b> field.
The format is the same as the one returned by the services/logs/logs method.</p> For valid field names, see <b>logs/entry</b> method.
<p>Notice: The number of logs returned can be set with the <b>lpc</b> option.</p> </opt>
</li> <opt name='my_location'>
<li> <p>The reference point for cache distance and bearing calculation (typically - the user's location),
<p><b>my_notes</b> - user's notes on the cache, in plain-text, or <b>null</b> in the "lat|lon" format. This parameter is required when accessing <b>distance</b> and/or <b>bearing</b>
if user hadn't defined any notes.</p> fields.</p>
<p>This field requires you to use the <b>Level 3</b> Authentication.</p> <p>Use positive numbers for latitudes in the northern hemisphere and
</li> longitudes in the eastern hemisphere (and negative for southern and
<li><b>trackables_count</b> - a total number of trackables hidden within the cache.</li> western hemispheres accordingly). These are full degrees with a dot
<li> as a decimal point (ex. "54.3|22.3").</p>
<p><b>trackables</b> - list of dictionaries, each dictionary represents one </opt>
trackable hidden within the cache container; each dictionary has the <opt name="user_uuid">
following structure:</p> <p>User'd ID. Required to access fields like <b>is_found</b> using <b>Level 1</b> Authentication.</p>
<ul> <p>Please note that if you want to access private fields (like <b>my_notes</b>), then
<li><b>code</b> - code of the trackable,</li> this parameter won't help you. You have to use <b>Level 3</b> Authentication instead.</p>
<li><b>name</b> - plain text name of the trackable,</li> <p>If you use <b>Level 3</b> Authentication, you shouldn't use this parameter. Or, to be exact:</p>
<li><b>url</b> - trackable's own webpage address <b>or null</b>, if OKAPI <ul>
cannot automatically determine this address.</li> <li><b>user_uuid</b> will be extracted from the Access Token you use. You don't have to provide it.</li>
</ul> <li>If you provide <b>user_uuid</b>, then it HAS TO match your Access Token. If it doesn't, OKAPI
</li> will respond with HTTP 400 error.</li>
<li> </ul>
<p><b>alt_wpts</b> - list of alternate/additional waypoints associated </opt>
with this geocache. Each item is a dictionary of the following structure:</p> <common-format-params/>
<ul> <returns>
<li><b>name</b> - plain-text, short, unique "codename" for the waypoint,</li> <p>A dictionary of fields you have selected. Currently available fields:</p>
<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>type</b> - string, unique identifier for the type of waypoint; one
of the following: <b>parking</b>, <b>path</b>, <b>stage</b>,
<b>physical-stage</b>, <b>virtual-stage</b>, <b>final</b>, <b>poi</b>, <b>other</b>;
more types may be added; unknown types should be treated as <b>other</b>,
</li>
<li><b>type_name</b> - string, the human-readable name of the waypoint type,
e.g. "Parking area" or "Final location"; the language will be selected
based on the langpref argument,
</li>
<li>
<b>sym</b> - string, one of commonly recognized waypoint symbol
names, originally introduced by Garmin in their devices and GPX
files (e.g. "Flag, Green" or "Parking Area"),
</li>
<li><b>description</b> - plain-text longer description of the waypoint.</li>
</ul>
</li>
<li>
<p><b>country</b> - name of the country the cache is placed in;
may be empty ("") if the country is unknown.</p>
<p><b>Note:</b> This data is user-supplied and is not validated in
any way. Consider using external geocoding services instead. Also,
currently you have no way of knowing in which language it will appear
in (but it *may* start to vary on the value of your <b>langpref</b>
parameter in the future).</p>
</li>
<li>
<p><b>state</b> - name of the state the cache is placed in;
may be empty ("") if the state is unknown.</p>
<p><b>Note:</b> On some installations this data is user-supplied and
is not validated in any way. Other installations calculate it from
cache coordinates but may have problems in border regions.
Consider using external geocoding services instead. Also,
currently you have no way of knowing in which language it will appear
in (but it *may* start to vary on the value of your <b>langpref</b>
parameter in the future).</p>
</li>
<li>
<p><b>protection_areas</b> - list of dictionaries, each representing a
protection area in which the cache probably is located; each dictionary
has the following structure:</p>
<ul>
<li><b>type</b> - the type of the protection area, e.g.
"National Nature Reserve",</li>
<li><b>name</b> - the name of the protection area, e.g.
"East Dartmoor Woods and Heaths".</li>
</ul>
<p>The types and names currently are in a local language of the OC site but
may be translated depending on the <b>langpref</b> parameter in the future.</p>
<p>Note that this information is not authoritative. It may be outdated
or incomplete, and it is completely missing on some OC installations.</p>
</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 listed at the Opencaching site,</li>
<li><b>date_hidden</b> - date and time (ISO 8601) when
<ul>
<li>the geocache was first hidden (for physical caches), or </li>
<li>the geocache was first published (for virtual caches), or</li>
<li>the event takes place (for event caches),</li>
</ul>
</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>
<p>For example, for <i>geocache?cache_code=OP3D96&amp;fields=type</i> <ul>
query, the result might look something link this:</p> <li><b>code</b> - unique Opencaching code of the geocache,</li>
<pre>{"type": "Traditional"}</pre> <li><b>name</b> - name of the geocache,</li>
<p>If given cache code does not exist, the method will <li><b>names</b> - a dictionary (language code => plain-text string) of
respond with an HTTP 400 error.</p> names of the geocache (at this time, there will be only one language,
</returns> 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><b>Event</b>,</li>
<li><i>(any other value is valid too)</i></li>
</ul>
<p><b>Event</b> is a peculiar type of geocache which is NOT a geocache
at all, but it <b>is</b> stored as a geocache in OC database (this design
decision is old as the OC network itself!). Just keep in mind, that
in case of Event Caches, some fields may have a little different meaning
than you would tell by their name.</p>
</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>gc_code</b> - Geocaching.com code (GC code) of the geocache
<b>or null</b> if the cache is not listed on GC or the GC code is
unknown.</p>
<p>Please note that this information is supplied by cache owners
and it is often missing, obsolete or otherwise incorrect.</p>
</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 querying 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 querying 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 querying OKAPI.</p>
</li>
<li>
<p><b>bearing3</b> - string, the absolute bearing to the cache, represented as a typical direction
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 querying OKAPI.</p>
</li>
<li>
<p><b>is_found</b> - boolean, true if the 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). Please note, that this will also return <b>true</b>
for attended Event Caches.</p>
</li>
<li>
<p><b>is_not_found</b> - boolean, true if the 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>
<p><b>is_watched</b> - boolean, true if the user is watching this cache. You may consider highlighting
such geocaches in some fashion, as the users may use this functionality to temporarily mark geocaches
of particular interest (i.e. geocaches they plan to find today).</p>
<p>This is private data. You will need Level 3 Authentication to access this field.</p>
</li>
<li>
<p><b>is_ignored</b> - boolean, true if the user is ignoring this cache. (See also <b>exclude_ignored</b>
parameter of all search methods.)</p>
<p>This is private data. You will need Level 3 Authentication to access this field.</p>
</li>
<li>
<p><b>founds</b> - number of times the geocache was successfully found
(or attended, in case of Event Caches),</p>
</li>
<li>
<p><b>notfounds</b> - number of times the geocache was not found
(in case of Event Caches this will always be zero),</p>
</li>
<li>
<p><b>willattends</b> - in case of Event Caches, this is the number of
"Will attend" log entries. In case of any other cache type, it will
be <b>zero</b> (not null, for <a href='https://code.google.com/p/opencaching-api/issues/detail?id=233'>backward
compatibility</a>),</p>
</li>
<li class='deprecated'>
<b>size</b> - deprecated
(<a href='http://code.google.com/p/opencaching-api/issues/detail?id=155'>why?</a>),
use <b>size2</b> instead. Float (between 1 and 5), size rating of the container, or
<b>null</b> if geocache does not have a container,
</li>
<li>
<p><b>size2</b> - string indicating the size of the container, so called
"size2 code". One of the following values:
'none', 'nano', 'micro', 'small', 'regular', 'large', 'xlarge', 'other'.</p>
</li>
<li>
<p><b>oxsize</b> - float (between 1 and 5) or null, this is a size rating
variant, compatible with the one used by opencaching.com (and lately,
Garmin GPS devices).</p>
<p><b>Note:</b> The mapping is undocumented and may change without notice.</p>
<p><b>Note:</b> Some of OC's size values cannot be properly mapped to <b>oxsize</b>,
i.e. the 'other' size.</p>
</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>
<b>trip_time</b> - float, approximate total amount of time needed to
find the cache, in hours; this will usually include the time to reach the
cache location <em>and</em> go back (from/to a parking spot, etc.);
<b>null</b> if unknown,
</li>
<li><b>trip_distance</b> - float, approximate total distance needed to
find the cache, in kilometers; this will usually include the time to reach the
cache location <em>and</em> go back (from/to a parking spot, etc.);
<b>null</b> if unknown,
</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>short_description</b> - a plaintext string with a single line (very short)
description of the cache (kind of a "tagline text"),</li>
<li><b>short_descriptions</b> - a dictionary (language code =&gt;
plaintext string) of short cache descriptions,</li>
<li><b>description</b> - <a href='%OKAPI:docurl:html%'>HTML string</a>,
description of the cache,</li>
<li><b>descriptions</b> - a dictionary (language code =&gt;
<a href='%OKAPI:docurl:html%'>HTML string</a>) of cache descriptions,</li>
<li class="deprecated"><b>hint</b> - <a href='%OKAPI:docurl:html%'>HTML-encoded</a>
string, cache hints/spoilers; deprecated
(<a href="http://code.google.com/p/opencaching-api/issues/detail?id=261">why?</a>),
use <b>hint2</b> instead,</li>
<li class="deprecated"><b>hints</b> - a dictionary (language code =&gt;
<a href='%OKAPI:docurl:html%'>HTML-encoded</a> string) of cache hints/spoilers;
deprecated, use hints2 instead,</li>
<li><b>hint2</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>hints2</b> - a dictionary (language code =&gt;
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> - URL of the image,</li>
<li><b>thumb_url</b> - URL of a small (thumb) version of the image,</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>preview_image</b> - On some installations, owners may select one of the <b>images</b>
(see above) as a preview image. You are encouraged to display it as a 'teaser'
for this cache. On other installations this functionality is disabled and you
will always get the <b>null</b> value here.</p>
<p>The value of <b>preview_image</b> is either <b>null</b> or a dictionary describing
an image. The structure of this dictionary is the same as of a single entry on
the <b>images</b> list described above.</p>
</li>
<li>
<p><b>attr_acodes</b> - unordered list of OKAPI geocache-attribute IDs (A-codes) with
which the cache was tagged. Use the <b>attrs</b> module to retrieve information on
these attributes.</p>
</li>
<li>
<b>attrnames</b> - if you don't want to dig into <b>attr_acodes</b> just now, then
you can use these as a simple alternative. <b>attrnames</b> is an unordered list of
names of the attributes with which the cache was tagged; the language will be
selected based on the <b>langpref</b> parameter.
</li>
<li>
<p><b>attribution_note</b> - the proper attribution note for the cache listing according
to the local OC site's Data License. By default, this note is also appended to geocache
descriptions (see the <b>attribution_append</b> parameter).</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 id="ret_alt_wpts">
<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, short, unique "codename" for the waypoint,</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>
<p><b>type</b> - string, unique identifier for the type of waypoint;
one of the following:</p>
<ul>
<li><b>parking</b>, <b>path</b>, <b>stage</b>, <b>physical-stage</b>,
<b>virtual-stage</b>, <b>final</b>, <b>poi</b>, <b>other</b> - used by
OC itself, for detailed descriptions of these you'll have to refer to
external Opencaching documenation,</li>
<li><b>user-coords</b> - extra coordinates supplied <i>by the user who
had found the cache</i> (NOT the owner of the cache), most probably
pointing to the final location of the cache (e.g. a quiz cache);
this type of waypoint is available only in some installations and only
if you're using Level 3 Authentication,</li>
<li>more types may be added at any moment; unknown types should be
treated as <b>other</b>.</li>
</ul>
</li>
<li>
<b>type_name</b> - string, the human-readable name of the waypoint type,
e.g. "Parking area" or "Final location"; the language will be selected
based on the langpref argument,
</li>
<li>
<p><b>sym</b> - string, one of commonly recognized waypoint symbol
names, originally introduced by Garmin in their devices and GPX
files (e.g. "Flag, Green" or "Parking Area").</p>
<p>These symbol codes are only suggestions. They are understood by
Garmin-related software and devices. If you don't know how to display such
symbols, you are welcome to use any symbol you'd like in your
application.</p>
</li>
<li><b>description</b> - plain-text longer description of the waypoint.</li>
</ul>
</li>
<li>
<p><b>country</b> - name of the country the cache is placed in;
may be empty ("") if the country is unknown.</p>
<p><b>Note:</b> This data is user-supplied and is not validated in
any way. Consider using external geocoding services instead. Also,
currently you have no way of knowing in which language it will appear
in (but it *may* start to vary on the value of your <b>langpref</b>
parameter in the future).</p>
</li>
<li>
<p><b>state</b> - name of the state the cache is placed in;
may be empty ("") if the state is unknown.</p>
<p><b>Note:</b> On some installations this data is user-supplied and
is not validated in any way. Other installations calculate it from
cache coordinates but may have problems in border regions.
Consider using external geocoding services instead. Also,
currently you have no way of knowing in which language it will appear
in (but it *may* start to vary on the value of your <b>langpref</b>
parameter in the future).</p>
</li>
<li>
<p><b>protection_areas</b> - list of dictionaries, each representing a
protection area in which the cache probably is located; each dictionary
has the following structure:</p>
<ul>
<li><b>type</b> - the type of the protection area, e.g.
"National Nature Reserve",</li>
<li><b>name</b> - the name of the protection area, e.g.
"East Dartmoor Woods and Heaths".</li>
</ul>
<p>The types and names currently are in a local language of the OC site but
may be translated depending on the <b>langpref</b> parameter in the future.</p>
<p>Note that this information is not authoritative. It may be outdated
or incomplete, and it is completely missing on some OC installations.</p>
</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 listed at the Opencaching site,</li>
<li><b>date_hidden</b> - date and time (ISO 8601) when
<ul>
<li>the geocache was first hidden (for physical caches), or </li>
<li>the geocache was first published (for virtual caches), or</li>
<li>the event takes place (for event caches),</li>
</ul>
</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>
<p>For example, for <i>geocache?cache_code=OP3D96&amp;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 an HTTP 400 error.</p>
</returns>
</xml> </xml>

File diff suppressed because it is too large Load Diff

View File

@@ -1,51 +1,54 @@
<xml> <xml>
<brief>Retrieve information on multiple geocaches</brief> <brief>Retrieve information on multiple geocaches</brief>
<issue-id>20</issue-id> <issue-id>20</issue-id>
<desc> <desc>
<p>This method works like the services/caches/geocache method, but works <p>This method works like the services/caches/geocache method, but works
with multiple geocaches (instead of only one).</p> with multiple geocaches (instead of only one).</p>
</desc> </desc>
<req name='cache_codes'> <req name='cache_codes'>
<p>Pipe-separated list of cache cache codes. These represent the <p>Pipe-separated list of cache cache codes. These represent the
geocaches you are interested in. No more than 500 codes are allowed. 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 Unlike in the "geocache" method, this CAN be an empty string (it will
result in an empty, but valid, response).</p> result in an empty, but valid, response).</p>
</req> </req>
<opt name='langpref' default='en'> <opt name='langpref' default='en'>
<p>Pipe-separated list of ISO 639-1 language codes. This indicates the <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 order of preference in which language will be chosen for fields like
<b>name</b> and <b>description</b>.</p> <b>name</b> and <b>description</b>.</p>
<p>Please note, that you may also access caches' descriptions in all <p>Please note, that you may also access caches' descriptions in all
available languages. If you want to do this, you should use fields 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>names</b>, <b>descriptions</b> (etc.) instead of <b>name</b> and
<b>description</b> (etc.).</p> <b>description</b> (etc.).</p>
</opt> </opt>
<opt name='fields' default='code|name|location|type|status'> <opt name='fields' default='code|name|location|type|status'>
<p>Same as in the services/caches/geocache method. Pipe-separated list <p>Same as in the services/caches/geocache method. Pipe-separated list
of field names which you are interested with. of field names which you are interested with.
See services/caches/geocache method for a list available values.</p> See services/caches/geocache method for a list available values.</p>
</opt> </opt>
<opt name='attribution_append' default='full'> <opt name='attribution_append' default='full'>
<p>Same as in the services/caches/geocache method.</p> <p>Same as in the services/caches/geocache method.</p>
</opt> </opt>
<opt name='lpc' default='10'> <opt name='lpc' default='10'>
Same as in the services/caches/geocache method. Same as in the services/caches/geocache method.
</opt> </opt>
<opt name='log_fields' default='uuid|date|user|type|comment'> <opt name='log_fields' default='uuid|date|user|type|comment'>
Same as in the services/caches/geocache method. Same as in the services/caches/geocache method.
</opt> </opt>
<opt name='user_uuid'> <opt name='my_location'>
Same as in the services/caches/geocache method. Same as in the services/caches/geocache method.
</opt> </opt>
<common-format-params/> <opt name='user_uuid'>
<returns> Same as in the services/caches/geocache method.
<p>A dictionary. Cache codes you provide will be mapped to dictionary keys, </opt>
and each value will be a dictionary of fields you have selected.</p> <common-format-params/>
<p>For example, for <i>geocaches?cache_codes=OP3D96|OC124&amp;fields=type</i> <returns>
query, the result might look something link this:</p> <p>A dictionary. Cache codes you provide will be mapped to dictionary keys,
<pre>{"OP3D96": {"type": "Traditional"}, "OC124": null}</pre> and each value will be a dictionary of fields you have selected.</p>
<p>Value of <b>null</b> means that the given cache haven't been found. <p>For example, for <i>geocaches?cache_codes=OP3D96|OC124&amp;fields=type</i>
(This behavior is different than in the services/caches/geocache method, which query, the result might look something link this:</p>
responds with an HTTP 400 error in such case.)</p> <pre>{"OP3D96": {"type": "Traditional"}, "OC124": null}</pre>
</returns> <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 an HTTP 400 error in such case.)</p>
</returns>
</xml> </xml>

View File

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

View File

@@ -29,200 +29,200 @@ require_once($GLOBALS['rootpath']."okapi/services/caches/search/searching.inc.ph
class WebService class WebService
{ {
/** /**
* Should be always true. You may temporarily set it to false, when you're * Should be always true. You may temporarily set it to false, when you're
* testing/debugging the tile renderer. * testing/debugging the tile renderer.
*/ */
private static $USE_ETAGS_CACHE = true; private static $USE_ETAGS_CACHE = true;
/** /**
* Should be always true. You may temporarily set it to false, when you're * Should be always true. You may temporarily set it to false, when you're
* testing/debugging the tile renderer. * testing/debugging the tile renderer.
*/ */
private static $USE_IMAGE_CACHE = true; private static $USE_IMAGE_CACHE = true;
/** /**
* Should be always true. You may temporarily set it to false, when you're * Should be always true. You may temporarily set it to false, when you're
* testing/debugging. Grep the code to check when this flag is used. * testing/debugging. Grep the code to check when this flag is used.
*/ */
private static $USE_OTHER_CACHE = true; private static $USE_OTHER_CACHE = true;
public static function options() public static function options()
{ {
return array( return array(
'min_auth_level' => 3 'min_auth_level' => 3
); );
} }
private static function require_uint($request, $name, $min_value = 0) private static function require_uint($request, $name, $min_value = 0)
{ {
$val = $request->get_parameter($name); $val = $request->get_parameter($name);
if ($val === null) if ($val === null)
throw new ParamMissing($name); throw new ParamMissing($name);
$ret = intval($val); $ret = intval($val);
if ($ret < 0 || ("$ret" !== $val)) if ($ret < 0 || ("$ret" !== $val))
throw new InvalidParam($name, "Expecting non-negative integer."); throw new InvalidParam($name, "Expecting non-negative integer.");
return $ret; return $ret;
} }
public static function call(OkapiRequest $request) public static function call(OkapiRequest $request)
{ {
$checkpointA_started = microtime(true); $checkpointA_started = microtime(true);
# Make sure the request is internal. # Make sure the request is internal.
if (!in_array($request->consumer->key, array('internal', 'facade'))) if (!in_array($request->consumer->key, array('internal', 'facade')))
throw new BadRequest("Your Consumer Key has not been allowed to access this method."); throw new BadRequest("Your Consumer Key has not been allowed to access this method.");
# zoom, x, y - required tile-specific parameters. # zoom, x, y - required tile-specific parameters.
$zoom = self::require_uint($request, 'z'); $zoom = self::require_uint($request, 'z');
if ($zoom > 21) if ($zoom > 21)
throw new InvalidParam('z', "Maximum value for this parameter is 21."); throw new InvalidParam('z', "Maximum value for this parameter is 21.");
$x = self::require_uint($request, 'x'); $x = self::require_uint($request, 'x');
$y = self::require_uint($request, 'y'); $y = self::require_uint($request, 'y');
if ($x >= 1<<$zoom) if ($x >= 1<<$zoom)
throw new InvalidParam('x', "Should be in 0..".((1<<$zoom) - 1)."."); throw new InvalidParam('x', "Should be in 0..".((1<<$zoom) - 1).".");
if ($y >= 1<<$zoom) if ($y >= 1<<$zoom)
throw new InvalidParam('y', "Should be in 0..".((1<<$zoom) - 1)."."); throw new InvalidParam('y', "Should be in 0..".((1<<$zoom) - 1).".");
# Now, we will create a search set (or use one previously created). # Now, we will create a search set (or use one previously created).
# Instead of creating a new OkapiInternalRequest object, we will pass # Instead of creating a new OkapiInternalRequest object, we will pass
# the current request directly. We can do that, because we inherit all # the current request directly. We can do that, because we inherit all
# of the "save" method's parameters. # of the "save" method's parameters.
$search_set = OkapiServiceRunner::call('services/caches/search/save', $request); $search_set = OkapiServiceRunner::call('services/caches/search/save', $request);
$set_id = $search_set['set_id']; $set_id = $search_set['set_id'];
# Get caches which are present in the result set AND within the tile # Get caches which are present in the result set AND within the tile
# (+ those around the borders). # (+ those around the borders).
$rs = TileTree::query_fast($zoom, $x, $y, $set_id); $rs = TileTree::query_fast($zoom, $x, $y, $set_id);
$rows = array(); $rows = array();
if ($rs !== null) if ($rs !== null)
{ {
while ($row = mysql_fetch_row($rs)) while ($row = mysql_fetch_row($rs))
$rows[] = $row; $rows[] = $row;
unset($row); unset($row);
} }
OkapiServiceRunner::save_stats_extra("caches/map/tile/checkpointA", null, OkapiServiceRunner::save_stats_extra("caches/map/tile/checkpointA", null,
microtime(true) - $checkpointA_started); microtime(true) - $checkpointA_started);
$checkpointB_started = microtime(true); $checkpointB_started = microtime(true);
# Add dynamic, user-related flags. # Add dynamic, user-related flags.
if (count($rows) > 0) if (count($rows) > 0)
{ {
# Load user-related cache ids. # Load user-related cache ids.
$cache_key = "tileuser/".$request->token->user_id; $cache_key = "tileuser/".$request->token->user_id;
$user = self::$USE_OTHER_CACHE ? Cache::get($cache_key) : null; $user = self::$USE_OTHER_CACHE ? Cache::get($cache_key) : null;
if ($user === null) if ($user === null)
{ {
$user = array(); $user = array();
# Ignored caches. # Ignored caches.
$rs = Db::query(" $rs = Db::query("
select cache_id select cache_id
from cache_ignore from cache_ignore
where user_id = '".mysql_real_escape_string($request->token->user_id)."' where user_id = '".mysql_real_escape_string($request->token->user_id)."'
"); ");
$user['ignored'] = array(); $user['ignored'] = array();
while (list($cache_id) = mysql_fetch_row($rs)) while (list($cache_id) = mysql_fetch_row($rs))
$user['ignored'][$cache_id] = true; $user['ignored'][$cache_id] = true;
# Found caches. # Found caches.
$rs = Db::query(" $rs = Db::query("
select distinct cache_id select distinct cache_id
from cache_logs from cache_logs
where where
user_id = '".mysql_real_escape_string($request->token->user_id)."' user_id = '".mysql_real_escape_string($request->token->user_id)."'
and type = 1 and type = 1
and ".((Settings::get('OC_BRANCH') == 'oc.pl') ? "deleted = 0" : "true")." and ".((Settings::get('OC_BRANCH') == 'oc.pl') ? "deleted = 0" : "true")."
"); ");
$user['found'] = array(); $user['found'] = array();
while (list($cache_id) = mysql_fetch_row($rs)) while (list($cache_id) = mysql_fetch_row($rs))
$user['found'][$cache_id] = true; $user['found'][$cache_id] = true;
# Own caches. # Own caches.
$rs = Db::query(" $rs = Db::query("
select distinct cache_id select distinct cache_id
from caches from caches
where user_id = '".mysql_real_escape_string($request->token->user_id)."' where user_id = '".mysql_real_escape_string($request->token->user_id)."'
"); ");
$user['own'] = array(); $user['own'] = array();
while (list($cache_id) = mysql_fetch_row($rs)) while (list($cache_id) = mysql_fetch_row($rs))
$user['own'][$cache_id] = true; $user['own'][$cache_id] = true;
Cache::set($cache_key, $user, 30); Cache::set($cache_key, $user, 30);
} }
# Add extra flags to geocaches. # Add extra flags to geocaches.
foreach ($rows as &$row_ref) foreach ($rows as &$row_ref)
{ {
# Add the "found" flag (to indicate that this cache needs # Add the "found" flag (to indicate that this cache needs
# to be drawn as found) and the "own" flag (to indicate that # to be drawn as found) and the "own" flag (to indicate that
# the current user is the owner). # the current user is the owner).
if (isset($user['found'][$row_ref[0]])) if (isset($user['found'][$row_ref[0]]))
$row_ref[6] |= TileTree::$FLAG_FOUND; # $row[6] is "flags" $row_ref[6] |= TileTree::$FLAG_FOUND; # $row[6] is "flags"
if (isset($user['own'][$row_ref[0]])) if (isset($user['own'][$row_ref[0]]))
$row_ref[6] |= TileTree::$FLAG_OWN; # $row[6] is "flags" $row_ref[6] |= TileTree::$FLAG_OWN; # $row[6] is "flags"
} }
} }
# Compute the image hash/fingerprint. This will be used both for ETags # Compute the image hash/fingerprint. This will be used both for ETags
# and internal cache ($cache_key). # and internal cache ($cache_key).
$tile = new DefaultTileRenderer($zoom, $rows); $tile = new DefaultTileRenderer($zoom, $rows);
$image_fingerprint = $tile->get_unique_hash(); $image_fingerprint = $tile->get_unique_hash();
# Start creating response. # Start creating response.
$response = new OkapiHttpResponse(); $response = new OkapiHttpResponse();
$response->content_type = $tile->get_content_type(); $response->content_type = $tile->get_content_type();
$response->cache_control = "Cache-Control: private, max-age=600"; $response->cache_control = "Cache-Control: private, max-age=600";
$response->etag = 'W/"'.$image_fingerprint.'"'; $response->etag = 'W/"'.$image_fingerprint.'"';
# Check if the request didn't include the same ETag. # Check if the request didn't include the same ETag.
OkapiServiceRunner::save_stats_extra("caches/map/tile/checkpointB", null, OkapiServiceRunner::save_stats_extra("caches/map/tile/checkpointB", null,
microtime(true) - $checkpointB_started); microtime(true) - $checkpointB_started);
$checkpointC_started = microtime(true); $checkpointC_started = microtime(true);
if (self::$USE_ETAGS_CACHE && ($request->etag == $response->etag)) if (self::$USE_ETAGS_CACHE && ($request->etag == $response->etag))
{ {
# Hit. Report the content was unmodified. # Hit. Report the content was unmodified.
$response->etag = null; $response->etag = null;
$response->status = "304 Not Modified"; $response->status = "304 Not Modified";
return $response; return $response;
} }
# Check if the image was recently rendered and is kept in image cache. # Check if the image was recently rendered and is kept in image cache.
$cache_key = "tile/".$image_fingerprint; $cache_key = "tile/".$image_fingerprint;
$response->body = self::$USE_IMAGE_CACHE ? Cache::get($cache_key) : null; $response->body = self::$USE_IMAGE_CACHE ? Cache::get($cache_key) : null;
OkapiServiceRunner::save_stats_extra("caches/map/tile/checkpointC", null, OkapiServiceRunner::save_stats_extra("caches/map/tile/checkpointC", null,
microtime(true) - $checkpointC_started); microtime(true) - $checkpointC_started);
$checkpointD_started = microtime(true); $checkpointD_started = microtime(true);
if ($response->body !== null) if ($response->body !== null)
{ {
# Hit. We will use the cached version of the image. # Hit. We will use the cached version of the image.
return $response; return $response;
} }
# Miss. Render the image. Cache the result. # Miss. Render the image. Cache the result.
$response->body = $tile->render(); $response->body = $tile->render();
Cache::set_scored($cache_key, $response->body); Cache::set_scored($cache_key, $response->body);
OkapiServiceRunner::save_stats_extra("caches/map/tile/checkpointD", null, OkapiServiceRunner::save_stats_extra("caches/map/tile/checkpointD", null,
microtime(true) - $checkpointD_started); microtime(true) - $checkpointD_started);
return $response; return $response;
} }
} }

View File

@@ -1,18 +1,18 @@
<xml> <xml>
<brief>Get cache map tile</brief> <brief>Get cache map tile</brief>
<issue-id>150</issue-id> <issue-id>150</issue-id>
<desc> <desc>
<p><b>CLOSED BETA</b> version. Due to long-term performance tests, currently <p><b>CLOSED BETA</b> version. Due to long-term performance tests, currently
this method is not publicly accessible. Let us know if you're interested this method is not publicly accessible. Let us know if you're interested
in using it.</p> in using it.</p>
<p>Use this method to retrieve a tile-map of all caches included in your search <p>Use this method to retrieve a tile-map of all caches included in your search
result.</p> result.</p>
</desc> </desc>
<req name='z'>Zoom level (0..21).</req> <req name='z'>Zoom level (0..21).</req>
<req name='x'>Tile number on the X axis.</req> <req name='x'>Tile number on the X axis.</req>
<req name='y'>Tile number on the Y axis.</req> <req name='y'>Tile number on the Y axis.</req>
<import-params method="services/caches/search/save"/> <import-params method="services/caches/search/save"/>
<returns> <returns>
The PNG image with the requested map tile. The PNG image with the requested map tile.
</returns> </returns>
</xml> </xml>

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -17,74 +17,74 @@ use okapi\Settings;
class WebService class WebService
{ {
public static function options() public static function options()
{ {
return array( return array(
'min_auth_level' => 3 'min_auth_level' => 3
); );
} }
public static function call(OkapiRequest $request) public static function call(OkapiRequest $request)
{ {
# User is already verified (via OAuth), but we need to verify the # User is already verified (via OAuth), but we need to verify the
# cache code (check if it exists). We will simply call a geocache method # cache code (check if it exists). We will simply call a geocache method
# on it - this will also throw a proper exception if it doesn't exist. # on it - this will also throw a proper exception if it doesn't exist.
$cache_code = $request->get_parameter('cache_code'); $cache_code = $request->get_parameter('cache_code');
if ($cache_code == null) if ($cache_code == null)
throw new ParamMissing('cache_code'); throw new ParamMissing('cache_code');
$geocache = OkapiServiceRunner::call('services/caches/geocache', new OkapiInternalRequest( $geocache = OkapiServiceRunner::call('services/caches/geocache', new OkapiInternalRequest(
$request->consumer, $request->token, array('cache_code' => $cache_code, 'fields' => 'internal_id'))); $request->consumer, $request->token, array('cache_code' => $cache_code, 'fields' => 'internal_id')));
# watched # watched
if ($tmp = $request->get_parameter('watched')) if ($tmp = $request->get_parameter('watched'))
{ {
if (!in_array($tmp, array('true', 'false', 'unchanged'))) if (!in_array($tmp, array('true', 'false', 'unchanged')))
throw new InvalidParam('watched', $tmp); throw new InvalidParam('watched', $tmp);
if ($tmp == 'true') if ($tmp == 'true')
Db::execute(" Db::execute("
insert ignore into cache_watches (cache_id, user_id) insert ignore into cache_watches (cache_id, user_id)
values ( values (
'".mysql_real_escape_string($geocache['internal_id'])."', '".mysql_real_escape_string($geocache['internal_id'])."',
'".mysql_real_escape_string($request->token->user_id)."' '".mysql_real_escape_string($request->token->user_id)."'
); );
"); ");
elseif ($tmp == 'false') elseif ($tmp == 'false')
Db::execute(" Db::execute("
delete from cache_watches delete from cache_watches
where where
cache_id = '".mysql_real_escape_string($geocache['internal_id'])."' cache_id = '".mysql_real_escape_string($geocache['internal_id'])."'
and user_id = '".mysql_real_escape_string($request->token->user_id)."'; and user_id = '".mysql_real_escape_string($request->token->user_id)."';
"); ");
} }
# ignored # ignored
if ($tmp = $request->get_parameter('ignored')) if ($tmp = $request->get_parameter('ignored'))
{ {
if (!in_array($tmp, array('true', 'false', 'unchanged'))) if (!in_array($tmp, array('true', 'false', 'unchanged')))
throw new InvalidParam('ignored', $tmp); throw new InvalidParam('ignored', $tmp);
if ($tmp == 'true') if ($tmp == 'true')
Db::execute(" Db::execute("
insert ignore into cache_ignore (cache_id, user_id) insert ignore into cache_ignore (cache_id, user_id)
values ( values (
'".mysql_real_escape_string($geocache['internal_id'])."', '".mysql_real_escape_string($geocache['internal_id'])."',
'".mysql_real_escape_string($request->token->user_id)."' '".mysql_real_escape_string($request->token->user_id)."'
); );
"); ");
elseif ($tmp == 'false') elseif ($tmp == 'false')
Db::execute(" Db::execute("
delete from cache_ignore delete from cache_ignore
where where
cache_id = '".mysql_real_escape_string($geocache['internal_id'])."' cache_id = '".mysql_real_escape_string($geocache['internal_id'])."'
and user_id = '".mysql_real_escape_string($request->token->user_id)."' and user_id = '".mysql_real_escape_string($request->token->user_id)."'
"); ");
} }
$result = array( $result = array(
'success' => true, 'success' => true,
); );
return Okapi::formatted_response($request, $result); return Okapi::formatted_response($request, $result);
} }
} }

View File

@@ -1,33 +1,33 @@
<xml> <xml>
<brief>Mark cache as watched or ignored</brief> <brief>Mark cache as watched or ignored</brief>
<issue-id>166</issue-id> <issue-id>166</issue-id>
<desc> <desc>
<p>This method allows your users to mark the geocache as <b>watched</b> or <p>This method allows your users to mark the geocache as <b>watched</b> or
<b>ignored</b>. <b>ignored</b>.
Read the docs on separate parameters for details.</p> Read the docs on separate parameters for details.</p>
</desc> </desc>
<req name='cache_code'> <req name='cache_code'>
<p>Code of the geocache.</p> <p>Code of the geocache.</p>
</req> </req>
<opt name='watched' default='unchanged'> <opt name='watched' default='unchanged'>
<p>Mark (or unmark) the cache as watched. This should be <b>true</b>, <p>Mark (or unmark) the cache as watched. This should be <b>true</b>,
<b>false</b> or <b>unchanged</b>. You may access the current state of this <b>false</b> or <b>unchanged</b>. You may access the current state of this
flag with the <b>is_watched</b> field of the geocache method.</p> flag with the <b>is_watched</b> field of the geocache method.</p>
</opt> </opt>
<opt name='ignored' default='unchanged'> <opt name='ignored' default='unchanged'>
<p>Mark (or unmark) the cache as ignored. This should be <b>true</b>, <p>Mark (or unmark) the cache as ignored. This should be <b>true</b>,
<b>false</b> or <b>unchanged</b>. You may access the current state of this <b>false</b> or <b>unchanged</b>. You may access the current state of this
flag with the <b>is_ignored</b> field of the geocache method.</p> flag with the <b>is_ignored</b> field of the geocache method.</p>
</opt> </opt>
<common-format-params/> <common-format-params/>
<returns> <returns>
<p>A dictionary of the following structure:</p> <p>A dictionary of the following structure:</p>
<ul> <ul>
<li><b>success</b> - true, if all went well.</li> <li><b>success</b> - true, if all went well.</li>
</ul> </ul>
<p>Please note, that currently this will <b>always</b> be true! Nothing can go <p>Please note, that currently this will <b>always</b> be true! Nothing can go
wrong as long as you pass your parameters in a right way (and if you don't, wrong as long as you pass your parameters in a right way (and if you don't,
you will get an HTTP 400 response). If you have received an HTTP 200 response, you will get an HTTP 400 response). If you have received an HTTP 200 response,
then you may assume that all went well.</p> then you may assume that all went well.</p>
</returns> </returns>
</xml> </xml>

View File

@@ -20,17 +20,18 @@ require_once('searching.inc.php');
class WebService class WebService
{ {
public static function options() public static function options()
{ {
return array( return array(
'min_auth_level' => 1 'min_auth_level' => 1
); );
} }
public static function call(OkapiRequest $request) public static function call(OkapiRequest $request)
{ {
$search_params = SearchAssistant::get_common_search_params($request); $search_assistant = new SearchAssistant($request);
$result = SearchAssistant::get_common_search_result($search_params); $search_assistant->prepare_common_search_params();
return Okapi::formatted_response($request, $result); $result = $search_assistant->get_common_search_result();
} return Okapi::formatted_response($request, $result);
}
} }

View File

@@ -1,209 +1,209 @@
<xml> <xml>
<brief>Search for geocaches</brief> <brief>Search for geocaches</brief>
<issue-id>15</issue-id> <issue-id>15</issue-id>
<desc> <desc>
<p>Search for geocaches using some simple filters. <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> All of them are optional, but you surely want to use at least some of them.</p>
<ul> <ul>
<li>If you're looking for a way to keep your geocaching database in sync with ours, <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> 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. <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> This way, you can get much more data in <b>one request</b>.</li>
</ul> </ul>
</desc> </desc>
<opt name='type'> <opt name='type'>
<p>Pipe-separated list of cache type codes. If given, the results will be limited <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 to the given cache types. For a list of cache type codes, see services/caches/geocache
method.</p> method.</p>
<p><b>Notice:</b> If you want to include all cache types <b>except</b> <p><b>Notice:</b> If you want to include all cache types <b>except</b>
given ones, prepend your list with the "-" sign.</p> given ones, prepend your list with the "-" sign.</p>
</opt> </opt>
<opt name='status' default='Available'> <opt name='status' default='Available'>
<p>Pipe-separated list of status codes. Only caches matching any of the given <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 statuses will included in the response. For a list of cache status codes, see
services/caches/geocache method.</p> services/caches/geocache method.</p>
</opt> </opt>
<opt name='owner_uuid'> <opt name='owner_uuid'>
<p>Pipe-separated list of user IDs. If given, the list of returned caches <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> 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> 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 <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> owned by the given users, prepend your list with the "-" sign.</p>
</opt> </opt>
<opt name='name'> <opt name='name'>
<p>UTF-8 encoded string - the name (or part of the name) of the cache.</p> <p>UTF-8 encoded string - the name (or part of the name) of the cache.</p>
<p>Allowed wildcard characters:</p> <p>Allowed wildcard characters:</p>
<ul> <ul>
<li>asterisk ("*") will match any string of characters,</li> <li>asterisk ("*") will match any string of characters,</li>
<li>underscore ("_") will match any single character.</li> <li>underscore ("_") will match any single character.</li>
</ul> </ul>
<p>Name matching is case-insensitive. Maximum length for the <b>name</b> parameter <p>Name matching is case-insensitive. Maximum length for the <b>name</b> parameter
is 100 characters.</p> is 100 characters.</p>
<p><b>Examples:</b></p> <p><b>Examples:</b></p>
<ul> <ul>
<li>"name=the *" returns caches which name starts with "the " (or "The ", etc.),</li> <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" 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=*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> <li>"name=*tower_" will match "Water Towers" but will <b>not</b> match "Water Tower".</li>
</ul> </ul>
<p><b>Notice:</b> Cache code and name are two different things - if you want to <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> find a cache by its code, use the services/caches/geocache method.</p>
</opt> </opt>
<opt name='terrain' default='1-5'> <opt name='terrain' default='1-5'>
<p>A string "X-Y", where X and Y are integers between 1 and 5, and X &lt;= Y. <p>A string "X-Y", where X and Y are integers between 1 and 5, and X &lt;= Y.
Only caches with terrain rating between these numbers (inclusive) will be returned.</p> Only caches with terrain rating between these numbers (inclusive) will be returned.</p>
</opt> </opt>
<opt name='difficulty' default='1-5'> <opt name='difficulty' default='1-5'>
<p>A string "X-Y", where X and Y are integers between 1 and 5, and X &lt;= Y. <p>A string "X-Y", where X and Y are integers between 1 and 5, and X &lt;= Y.
Only caches with difficulty rating between these numbers (inclusive) will be returned.</p> Only caches with difficulty rating between these numbers (inclusive) will be returned.</p>
</opt> </opt>
<opt name='size' class='deprecated'> <opt name='size' class='deprecated'>
<p>Deprecated. Please use <b>size2</b> instead - this will allow you to <p>Deprecated. Please use <b>size2</b> instead - this will allow you to
differentiate "nano" vs. "micro" and "none" vs. "other" sizes differentiate "nano" vs. "micro" and "none" vs. "other" sizes
(<a href='http://code.google.com/p/opencaching-api/issues/detail?id=155'>details</a>).</p> (<a href='http://code.google.com/p/opencaching-api/issues/detail?id=155'>details</a>).</p>
<p>A string "X-Y", where X and Y are integers between 1 and 5, and X &lt;= Y. <p>A string "X-Y", where X and Y are integers between 1 and 5, and X &lt;= Y.
Only caches with size attribute between these numbers (inclusive) will be returned Only caches with size attribute between these numbers (inclusive) will be returned
(1 - micro, 5 - very big).</p> (1 - micro, 5 - very big).</p>
<p><b>Notice:</b> If you use this parameter, all caches which do not have a container <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. If you want caches (like Event or Virtual caches) will be excluded from the results. If you want caches
with no container included, append "|X" suffix to the value of this parameter with no container included, append "|X" suffix to the value of this parameter
(e.g. "3-5|X").</p> (e.g. "3-5|X").</p>
</opt> </opt>
<opt name='size2'> <opt name='size2'>
<p>Pipe-separated list of "size2 codes". Only matching caches will be <p>Pipe-separated list of "size2 codes". Only matching caches will be
included. The codes are: 'none', 'nano', 'micro', 'small', 'regular', included. The codes are: 'none', 'nano', 'micro', 'small', 'regular',
'large', 'xlarge', 'other'.</p> 'large', 'xlarge', 'other'.</p>
<p><b>Note:</b> Not all OC servers use all of these. I.e. OCPL does not <p><b>Note:</b> Not all OC servers use all of these. I.e. OCPL does not
have 'nano' nor 'other' caches (but it might have them in the future).</p> have 'nano' nor 'other' caches (but it might have them in the future).</p>
</opt> </opt>
<opt name='rating'> <opt name='rating'>
<p>A string "X-Y", where X and Y are integers between 1 and 5, and X &lt;= Y. <p>A string "X-Y", where X and Y are integers between 1 and 5, and X &lt;= Y.
Only caches with an overall rating between these numbers (inclusive) will be returned Only caches with an overall rating between these numbers (inclusive) will be returned
(1 - poor, 5 - excellent).</p> (1 - poor, 5 - excellent).</p>
<p><b>Notice:</b> If you use this parameter, all caches with too few votes will be <p><b>Notice:</b> If you use this parameter, all caches with too few votes will be
excluded from results. If you still want unrated caches included, append "|X" suffix excluded from results. If you still want unrated caches included, append "|X" suffix
to the value of this parameter (e.g. "3-5|X").</p> to the value of this parameter (e.g. "3-5|X").</p>
<p><b>Notice:</b> Some OC installations do not provide ratings for their geocaches. <p><b>Notice:</b> Some OC installations do not provide ratings for their geocaches.
On such installation, this parameter will be ignored.</p> On such installation, this parameter will be ignored.</p>
</opt> </opt>
<opt name='min_rcmds'> <opt name='min_rcmds'>
<p>There are two possible value-types for this argument:</p> <p>There are two possible value-types for this argument:</p>
<ul> <ul>
<li>Integer N. If <i>N</i> given, the result will contain only those caches which have <li>Integer N. If <i>N</i> given, the result will contain only those caches which have
at least <i>N</i> recommendations.</li> at least <i>N</i> recommendations.</li>
<li>Integer N with a percent sign appended (e.g. "20%"). The result will contain <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. 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> Please note, that this is useful only with conjunction with <b>min_founds</b>
parameter.</li> parameter.</li>
</ul> </ul>
</opt> </opt>
<opt name='min_founds'> <opt name='min_founds'>
<p>Integer N. If <i>N</i> given, the result will contain only those caches which have <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> been found at least <i>N</i> times. Useful if you want to skip "unverified" caches.</p>
</opt> </opt>
<opt name='max_founds'> <opt name='max_founds'>
<p>Integer N. If <i>N</i> given, the result will contain only those caches which have <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> been found at most <i>N</i> times. Useful for FTF hunters.</p>
</opt> </opt>
<opt name='modified_since'> <opt name='modified_since'>
<p>A date and time string. This should be in ISO 8601 format (currently any <p>A date and time string. This should be in ISO 8601 format (currently any
format acceptable by PHP's <a href='http://pl2.php.net/strtotime'>strtotime</a> format acceptable by PHP's <a href='http://pl2.php.net/strtotime'>strtotime</a>
function also will do, but most of them don't handle time zones properly, function also will do, but most of them don't handle time zones properly,
try to use ISO 8601!).</p> try to use ISO 8601!).</p>
<p>If given, the list of returned geocaches will be limited to those which were <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 created or modified since that date. (Note: currently caches do not get
"modified" when user makes a log entry.)</p> "modified" when user makes a log entry.)</p>
</opt> </opt>
<opt name='found_status' default='either'> <opt name='found_status' default='either'>
<p><b>Notice:</b> This parameter may be used only for requests signed with an Access Token <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> (you will need to use <b>Level 3</b> Authentication).</p>
<p>Should be one of the following:</p> <p>Should be one of the following:</p>
<ul> <ul>
<li><b>found_only</b> - only caches found by the user will be returned,</li> <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>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> <li><b>either</b> - all caches will be returned.</li>
</ul> </ul>
</opt> </opt>
<opt name='found_by'> <opt name='found_by'>
<p>User UUID. If given, the response will only include geocaches found by <p>User UUID. If given, the response will only include geocaches found by
the given user.</p> the given user.</p>
</opt> </opt>
<opt name='not_found_by'> <opt name='not_found_by'>
<p>User UUID. If given, the response will only include geocaches not found by <p>User UUID. If given, the response will only include geocaches not found by
the given user.</p> the given user.</p>
</opt> </opt>
<opt name='watched_only' default='false'> <opt name='watched_only' default='false'>
<p><b>Notice:</b> This parameter may be used only for requests signed with an Access Token <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> (you will need to use <b>Level 3</b> Authentication).</p>
<p>Boolean. If set to <b>true</b>, only caches which the user has marked as watched <p>Boolean. If set to <b>true</b>, only caches which the user has marked as watched
will be included in the result. This might be used to temporarily mark geocaches will be included in the result. This might be used to temporarily mark geocaches
of particular interest (e.g. "all the caches I plan to find today").</p> of particular interest (e.g. "all the caches I plan to find today").</p>
</opt> </opt>
<opt name='exclude_ignored' default='false'> <opt name='exclude_ignored' default='false'>
<p><b>Notice:</b> This parameter may be used only for requests signed <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> with an Access Token (you will need to use <b>Level 3</b> Authentication).</p>
<p>Boolean. If set to <b>true</b>, caches which the user has marked as ignored <p>Boolean. If set to <b>true</b>, caches which the user has marked as ignored
will not be included in the result.</p> will not be included in the result.</p>
</opt> </opt>
<opt name='exclude_my_own' default='false'> <opt name='exclude_my_own' default='false'>
<p><b>Notice:</b> This parameter may be used only for requests signed <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). 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> 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 <p>Boolean. If set to <b>true</b>, caches which the user is an owner of will
not be included in the result.</p> not be included in the result.</p>
</opt> </opt>
<opt name='with_trackables_only' default='false'> <opt name='with_trackables_only' default='false'>
Boolean. If set to <b>true</b>, only caches with at least one trackable Boolean. If set to <b>true</b>, only caches with at least one trackable
will be included in the result. will be included in the result.
</opt> </opt>
<opt name='ftf_hunter' default='false'> <opt name='ftf_hunter' default='false'>
Boolean. If set to <b>true</b>, only caches which have not yet been Boolean. If set to <b>true</b>, only caches which have not yet been
found <b>by anyone</b> will be included. found <b>by anyone</b> will be included.
</opt> </opt>
<opt name='set_and'> <opt name='set_and'>
<p>ID of a set previously created with the <b>search/save</b> method. <p>ID of a set previously created with the <b>search/save</b> method.
If given, the results are <a href='http://en.wikipedia.org/wiki/Logical_conjunction'>AND</a>ed If given, the results are <a href='http://en.wikipedia.org/wiki/Logical_conjunction'>AND</a>ed
together with this set.</p> together with this set.</p>
<p>If you want to list the set contents only, please note the default <p>If you want to list the set contents only, please note the default
value of the <b>status</b> parameter! You may want to override it value of the <b>status</b> parameter! You may want to override it
in order to include unavailable and/or archived geocaches within the set.</p> in order to include unavailable and/or archived geocaches within the set.</p>
</opt> </opt>
<opt name='limit' default='100'> <opt name='limit' default='100'>
<p>Integer in range 1..500. Maximum number of cache codes returned.</p> <p>Integer in range 1..500. Maximum number of cache codes returned.</p>
</opt> </opt>
<opt name='offset' default='0'> <opt name='offset' default='0'>
<p>An offset, the amount of items to skip at the beginning of the result. Combined with limit <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 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. 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> Have a peek at the <b>replicate</b> module if you need all the caches.</p>
</opt> </opt>
<opt name='order_by'> <opt name='order_by'>
<p>Pipe separated list of fields to order the results by. Prefix the field name with <p>Pipe separated list of fields to order the results by. Prefix the field name with
a '-' sign to indicate a descending order.</p> a '-' sign to indicate a descending order.</p>
<p>Currently, fields which you can order by include: <b>code</b>, <b>name</b>, <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> <b>founds</b>, <b>rcmds</b>, <b>rcmds%</b> (tell us if you want more).</p>
<p><b>Examples:</b></p> <p><b>Examples:</b></p>
<ul> <ul>
<li>to order by cache name use "order_by=name" or "order_by=+name",</li> <li>to order by cache name use "order_by=name" or "order_by=+name",</li>
<li>to have the most recommended caches caches in front, use "order_by=-rcmds%",</li> <li>to have the most recommended caches in front, use "order_by=-rcmds%",</li>
<li>multicolumn sorting is also allowed, ex. "order_by=-founds|name"</li> <li>multicolumn sorting is also allowed, ex. "order_by=-founds|name"</li>
</ul> </ul>
<p><b>Note:</b> Try to avoid executing separate OKAPI request every time you <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 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> sorting on client side. This will speed up things for both sides.</p>
<p><b>Note:</b> For most other methods (like <b>bbox</b> and <b>nearest</b>), <p><b>Note:</b> For most other methods (like <b>bbox</b> and <b>nearest</b>),
the default order is <i>by the distance from the center</i>. If you supply the default order is <i>by the distance from the center</i>. If you supply
custom <b>order_by</b> parameter, then we will try to order by your preference custom <b>order_by</b> parameter, then we will try to order by your preference
first, and then by the distance later.</p> first, and then by the distance later.</p>
</opt> </opt>
<common-format-params/> <common-format-params/>
<returns> <returns>
<p>A dictionary of the following structure:</p> <p>A dictionary of the following structure:</p>
<ul> <ul>
<li><b>results</b> - a list of cache codes,</li> <li><b>results</b> - a list of cache codes,</li>
<li><b>more</b> - boolean, <b>true</b> means that there were more <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 results for your query, but they were not returned because of the
<b>limit</b> parameter.</li> <b>limit</b> parameter.</li>
</ul> </ul>
</returns> </returns>
</xml> </xml>

View File

@@ -12,76 +12,83 @@ use okapi\services\caches\search\SearchAssistant;
class WebService class WebService
{ {
public static function options() public static function options()
{ {
return array( return array(
'min_auth_level' => 1 'min_auth_level' => 1
); );
} }
public static function call(OkapiRequest $request) public static function call(OkapiRequest $request)
{ {
# You may wonder, why there are no parameters like "bbox" or "center" in the # 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. # "search/all" method. This is *intentional* and should be kept this way.
# Such parameters would fall in conflict with each other and - in result - # Such parameters would fall in conflict with each other and - in result -
# make the documentation very fuzzy. That's why they were intentionally # make the documentation very fuzzy. That's why they were intentionally
# left out of the "search/all" method, and put in separate (individual) ones. # left out of the "search/all" method, and put in separate (individual) ones.
# It's much easier to grasp their meaning this way. # It's much easier to grasp their meaning this way.
$tmp = $request->get_parameter('bbox'); $tmp = $request->get_parameter('bbox');
if (!$tmp) if (!$tmp)
throw new ParamMissing('bbox'); throw new ParamMissing('bbox');
$parts = explode('|', $tmp); $parts = explode('|', $tmp);
if (count($parts) != 4) if (count($parts) != 4)
throw new InvalidParam('bbox', "Expecting 4 pipe-separated parts, got ".count($parts)."."); throw new InvalidParam('bbox', "Expecting 4 pipe-separated parts, got ".count($parts).".");
foreach ($parts as &$part_ref) foreach ($parts as &$part_ref)
{ {
if (!preg_match("/^-?[0-9]+(\.?[0-9]*)$/", $part_ref)) if (!preg_match("/^-?[0-9]+(\.?[0-9]*)$/", $part_ref))
throw new InvalidParam('bbox', "'$part_ref' is not a valid float number."); throw new InvalidParam('bbox', "'$part_ref' is not a valid float number.");
$part_ref = floatval($part_ref); $part_ref = floatval($part_ref);
} }
list($bbsouth, $bbwest, $bbnorth, $bbeast) = $parts; list($bbsouth, $bbwest, $bbnorth, $bbeast) = $parts;
if ($bbnorth <= $bbsouth) if ($bbnorth <= $bbsouth)
throw new InvalidParam('bbox', "Northern edge must be situated to the north of the southern edge."); throw new InvalidParam('bbox', "Northern edge must be situated to the north of the southern edge.");
if ($bbeast == $bbwest) if ($bbeast == $bbwest)
throw new InvalidParam('bbox', "Eastern edge longitude is the same as the western one."); throw new InvalidParam('bbox', "Eastern edge longitude is the same as the western one.");
if ($bbnorth > 90 || $bbnorth < -90 || $bbsouth > 90 || $bbsouth < -90) if ($bbnorth > 90 || $bbnorth < -90 || $bbsouth > 90 || $bbsouth < -90)
throw new InvalidParam('bbox', "Latitudes have to be within -90..90 range."); throw new InvalidParam('bbox', "Latitudes have to be within -90..90 range.");
if ($bbeast > 180 || $bbeast < -180 || $bbwest > 180 || $bbwest < -180) if ($bbeast > 180 || $bbeast < -180 || $bbwest > 180 || $bbwest < -180)
throw new InvalidParam('bbox', "Longitudes have to be within -180..180 range."); throw new InvalidParam('bbox', "Longitudes have to be within -180..180 range.");
# Construct SQL conditions for the specified bounding box. # Construct SQL conditions for the specified bounding box.
$where_conds = array(); $search_assistant = new SearchAssistant($request);
$where_conds[] = "caches.latitude between '".mysql_real_escape_string($bbsouth)."' and '".mysql_real_escape_string($bbnorth)."'"; $search_assistant->prepare_common_search_params();
if ($bbeast > $bbwest) $search_assistant->prepare_location_search_params();
{
# 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)."')";
}
# $where_conds = array();
# In the method description, we promised to return caches ordered by the *rough* $where_conds[] = $search_assistant->get_latitude_expr()." between '".mysql_real_escape_string($bbsouth)."' and '".mysql_real_escape_string($bbnorth)."'";
# distance from the center of the bounding box. We'll use ORDER BY with a simplified if ($bbeast > $bbwest)
# distance formula and combine it with the LIMIT clause to get the best results. {
# # Easy one.
$where_conds[] = $search_assistant->get_longitude_expr()." 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[] = "(".$search_assistant->get_longitude_expr()." > '".mysql_real_escape_string($bbwest)
."' or ".$search_assistant->get_longitude_expr()." < '".mysql_real_escape_string($bbeast)."')";
}
$center_lat = ($bbsouth + $bbnorth) / 2.0; #
$center_lon = ($bbwest + $bbeast) / 2.0; # 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.
#
$search_params = SearchAssistant::get_common_search_params($request); $center_lat = ($bbsouth + $bbnorth) / 2.0;
$search_params['where_conds'] = array_merge($where_conds, $search_params['where_conds']); $center_lon = ($bbwest + $bbeast) / 2.0;
$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); $search_params = $search_assistant->get_search_params();
$search_params['where_conds'] = array_merge($where_conds, $search_params['where_conds']);
$search_params['order_by'][] = Okapi::get_distance_sql($center_lat, $center_lon,
$search_assistant->get_latitude_expr(),
$search_assistant->get_longitude_expr()); # not replaced; added to the end!
$search_assistant->set_search_params($search_params);
return Okapi::formatted_response($request, $result); $result = $search_assistant->get_common_search_result();
}
return Okapi::formatted_response($request, $result);
}
} }

View File

@@ -1,34 +1,38 @@
<xml> <xml>
<brief>Search for caches within specified bounding box</brief> <brief>Search for caches within specified bounding box</brief>
<issue-id>16</issue-id> <issue-id>16</issue-id>
<desc> <desc>
<p>This method is similar to the search/all method, but the results <p>This method is similar to the search/all method, but the results
are restricted to the caches situated within a given bounding box are restricted to the caches situated within a given bounding box
(a rectangle on the map).</p> (a rectangle on the map).</p>
<p>Unless overriden, results are ordered by the distance from the center of the bounding <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 box. This means, that if you hit the limit of geocaches
returned, you will receive the ones that are in the middle of your returned, you will receive the ones that are in the middle of your
box, and miss the ones on the edges.</p> box, and miss the ones on the edges.</p>
<p>Usually you will want to use search_and_retrieve method instead of this one. <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> This way, you can get much more data in <b>one request</b>.</p>
</desc> </desc>
<req name='bbox'> <req name='bbox'>
<p>The bounding box within to search for caches. The box is defined <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> by a string in "<b>S|W|N|E</b>" format, where:</p>
<ul> <ul>
<li><b>S</b> stands for southern edge latitude of the box,</li> <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>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>N</b> stands for northern edge latitude of the box,</li>
<li><b>E</b> stands for eastern edge longitude of the box.</li> <li><b>E</b> stands for eastern edge longitude of the box.</li>
</ul> </ul>
<p>Use positive numbers for latitudes in the northern hemisphere and <p>Use positive numbers for latitudes in the northern hemisphere and
longitudes in the eastern hemisphere (and negative for southern and longitudes in the eastern hemisphere (and negative for southern and
western hemispheres accordingly). These are full degrees with a dot western hemispheres accordingly). These are full degrees with a dot
as a decimal point (ex. "48.7|15.8|54|24.9").</p> as a decimal point (ex. "48.7|15.8|54|24.9").</p>
</req> </req>
<import-params method='services/caches/search/all'/> <opt name='location_source' default='default-coords'>
<common-format-params/> Same as in the <a href="%OKAPI:methodargref:services/caches/search/nearest#location_source%">
<returns> services/caches/search/nearest</a> method.
<p>Same format as in the search/all method.</p> </opt>
</returns> <import-params method='services/caches/search/all'/>
<common-format-params/>
<returns>
<p>Same format as in the search/all method.</p>
</returns>
</xml> </xml>

View File

@@ -13,165 +13,165 @@ use okapi\Db;
class WebService class WebService
{ {
public static function options() public static function options()
{ {
return array( return array(
'min_auth_level' => 1 'min_auth_level' => 1
); );
} }
/** /**
* Returns one of: array('cache_code', 'OPXXXX'), array('internal_id', '12345'), * Returns one of: array('cache_code', 'OPXXXX'), array('internal_id', '12345'),
* array('uuid', 'A408C3...') or null. * array('uuid', 'A408C3...') or null.
*/ */
private static function get_cache_key($url) private static function get_cache_key($url)
{ {
# Determine our own domain. # Determine our own domain.
static $host = null; static $host = null;
static $length = null; static $length = null;
if ($host == null) if ($host == null)
{ {
$host = parse_url(Settings::get('SITE_URL'), PHP_URL_HOST); $host = parse_url(Settings::get('SITE_URL'), PHP_URL_HOST);
if (strpos($host, "www.") === 0) if (strpos($host, "www.") === 0)
$host = substr($host, 4); $host = substr($host, 4);
$length = strlen($host); $length = strlen($host);
} }
# Parse the URL # Parse the URL
$uri = parse_url($url); $uri = parse_url($url);
if ($uri == false) if ($uri == false)
return null; return null;
if ((!isset($uri['scheme'])) || (!in_array($uri['scheme'], array('http', 'https')))) if ((!isset($uri['scheme'])) || (!in_array($uri['scheme'], array('http', 'https'))))
return null; return null;
if ((!isset($uri['host'])) || (substr($uri['host'], -$length) != $host)) if ((!isset($uri['host'])) || (substr($uri['host'], -$length) != $host))
return null; return null;
if (!isset($uri['path'])) if (!isset($uri['path']))
return null; return null;
if (preg_match("#^/(O[A-Z][A-Z0-9]{4,5})$#", $uri['path'], $matches)) if (preg_match("#^/(O[A-Z][A-Z0-9]{4,5})$#", $uri['path'], $matches))
{ {
# Some servers allow "http://oc.xx/<cache_code>" shortcut. # Some servers allow "http://oc.xx/<cache_code>" shortcut.
return array('cache_code', $matches[1]); return array('cache_code', $matches[1]);
} }
$parts = array(); $parts = array();
if (isset($uri['query'])) if (isset($uri['query']))
$parts = array_merge($parts, explode('&', $uri['query'])); $parts = array_merge($parts, explode('&', $uri['query']));
if (isset($uri['fragment'])) if (isset($uri['fragment']))
$parts = array_merge($parts, explode('&', $uri['fragment'])); $parts = array_merge($parts, explode('&', $uri['fragment']));
foreach ($parts as $param) foreach ($parts as $param)
{ {
$item = explode('=', $param, 2); $item = explode('=', $param, 2);
if (count($item) != 2) if (count($item) != 2)
continue; continue;
$key = $item[0]; $key = $item[0];
$value = $item[1]; $value = $item[1];
if ($key == 'wp') if ($key == 'wp')
return array('cache_code', $value); return array('cache_code', $value);
if ($key == 'cacheid') if ($key == 'cacheid')
return array('internal_id', $value); return array('internal_id', $value);
if ($key == 'uuid') if ($key == 'uuid')
return array('uuid', $value); return array('uuid', $value);
} }
return null; return null;
} }
public static function call(OkapiRequest $request) public static function call(OkapiRequest $request)
{ {
# Retrieve the list of URLs to check. # Retrieve the list of URLs to check.
$tmp = $request->get_parameter('urls'); $tmp = $request->get_parameter('urls');
if (!$tmp) if (!$tmp)
throw new ParamMissing('urls'); throw new ParamMissing('urls');
$urls = explode('|', $tmp); $urls = explode('|', $tmp);
$as_dict = $request->get_parameter('as_dict'); $as_dict = $request->get_parameter('as_dict');
if (!$as_dict) $as_dict = 'false'; if (!$as_dict) $as_dict = 'false';
if (!in_array($as_dict, array('true', 'false'))) if (!in_array($as_dict, array('true', 'false')))
throw new InvalidParam('as_dict'); throw new InvalidParam('as_dict');
$as_dict = ($as_dict == 'true'); $as_dict = ($as_dict == 'true');
# Generate the lists of keys. # Generate the lists of keys.
$results = array(); $results = array();
$urls_with = array( $urls_with = array(
'cache_code' => array(), 'cache_code' => array(),
'internal_id' => array(), 'internal_id' => array(),
'uuid' => array() 'uuid' => array()
); );
foreach ($urls as &$url_ref) foreach ($urls as &$url_ref)
{ {
$key = self::get_cache_key($url_ref); $key = self::get_cache_key($url_ref);
if ($key != null) if ($key != null)
$urls_with[$key[0]][$url_ref] = $key[1]; $urls_with[$key[0]][$url_ref] = $key[1];
else else
$results[$url_ref] = null; $results[$url_ref] = null;
} }
# Include 'cache_code' references. # Include 'cache_code' references.
foreach ($urls_with['cache_code'] as $url => $cache_code) foreach ($urls_with['cache_code'] as $url => $cache_code)
$results[$url] = $cache_code; $results[$url] = $cache_code;
# Include 'internal_id' references. # Include 'internal_id' references.
$internal_ids = array_values($urls_with['internal_id']); $internal_ids = array_values($urls_with['internal_id']);
if (count($internal_ids) > 0) if (count($internal_ids) > 0)
{ {
$rs = Db::query(" $rs = Db::query("
select cache_id, wp_oc select cache_id, wp_oc
from caches from caches
where where
cache_id in ('".implode("','", array_map('mysql_real_escape_string', $internal_ids))."') cache_id in ('".implode("','", array_map('mysql_real_escape_string', $internal_ids))."')
and status in (1,2,3) and status in (1,2,3)
"); ");
$dict = array(); $dict = array();
while ($row = mysql_fetch_assoc($rs)) while ($row = mysql_fetch_assoc($rs))
$dict[$row['cache_id']] = $row['wp_oc']; $dict[$row['cache_id']] = $row['wp_oc'];
foreach ($urls_with['internal_id'] as $url => $internal_id) foreach ($urls_with['internal_id'] as $url => $internal_id)
{ {
if (isset($dict[$internal_id])) if (isset($dict[$internal_id]))
$results[$url] = $dict[$internal_id]; $results[$url] = $dict[$internal_id];
else else
$results[$url] = null; $results[$url] = null;
} }
} }
# Include 'uuid' references. # Include 'uuid' references.
$uuids = array_values($urls_with['uuid']); $uuids = array_values($urls_with['uuid']);
if (count($uuids) > 0) if (count($uuids) > 0)
{ {
$rs = Db::query(" $rs = Db::query("
select uuid, wp_oc select uuid, wp_oc
from caches from caches
where where
uuid in ('".implode("','", array_map('mysql_real_escape_string', $uuids))."') uuid in ('".implode("','", array_map('mysql_real_escape_string', $uuids))."')
and status in (1,2,3) and status in (1,2,3)
"); ");
$dict = array(); $dict = array();
while ($row = mysql_fetch_assoc($rs)) while ($row = mysql_fetch_assoc($rs))
$dict[$row['uuid']] = $row['wp_oc']; $dict[$row['uuid']] = $row['wp_oc'];
foreach ($urls_with['uuid'] as $url => $uuid) foreach ($urls_with['uuid'] as $url => $uuid)
{ {
if (isset($dict[$uuid])) if (isset($dict[$uuid]))
$results[$url] = $dict[$uuid]; $results[$url] = $dict[$uuid];
else else
$results[$url] = null; $results[$url] = null;
} }
} }
# Format the results according to the 'as_dict' parameter. # Format the results according to the 'as_dict' parameter.
if ($as_dict) if ($as_dict)
return Okapi::formatted_response($request, $results); return Okapi::formatted_response($request, $results);
else else
{ {
$cache_codes = array(); $cache_codes = array();
foreach ($results as $url => $cache_code) foreach ($results as $url => $cache_code)
if ($cache_code != null) if ($cache_code != null)
$cache_codes[$cache_code] = true; $cache_codes[$cache_code] = true;
$flattened = array('results' => array_keys($cache_codes)); $flattened = array('results' => array_keys($cache_codes));
return Okapi::formatted_response($request, $flattened); return Okapi::formatted_response($request, $flattened);
} }
} }
} }

View File

@@ -1,32 +1,32 @@
<xml> <xml>
<brief>Resolve cache references from given URLs</brief> <brief>Resolve cache references from given URLs</brief>
<issue-id>116</issue-id> <issue-id>116</issue-id>
<desc> <desc>
<p>Given a set of URLs, determine codes of geocaches referenced within them.</p> <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 <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 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, on a cache using OKAPI). Geocache pages may be referenced using various types of URLs,
only some of them contain the cache code.</p> only some of them contain the cache code.</p>
</desc> </desc>
<req name='urls'> <req name='urls'>
<p>Pipe-separated list of URLs. No more than 500.</p> <p>Pipe-separated list of URLs. No more than 500.</p>
</req> </req>
<opt name='as_dict' default='false'> <opt name='as_dict' default='false'>
<p>If <b>false</b>, then the result of this method will be compatible with <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> 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 <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 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> (cache code) or null (if no cache code was found for the given URL).</p>
</opt> </opt>
<common-format-params/> <common-format-params/>
<returns> <returns>
<p>As described in the <b>as_dict</b> parameter.</p> <p>As described in the <b>as_dict</b> parameter.</p>
<p>Examples:</p> <p>Examples:</p>
<p>For <i>by_urls?urls=url1|url2|url3</i> <p>For <i>by_urls?urls=url1|url2|url3</i>
query, the result might look something link this:</p> query, the result might look something link this:</p>
<pre>{"results": ["OP28F9"]}</pre> <pre>{"results": ["OP28F9"]}</pre>
<p>For <i>by_urls?urls=url1|url2|url3&amp;as_dict=true</i> <p>For <i>by_urls?urls=url1|url2|url3&amp;as_dict=true</i>
query, the result might look something link this:</p> query, the result might look something link this:</p>
<pre>{"url1": "OP28F9", "url2": null, "url3": "OP28F9"}</pre> <pre>{"url1": "OP28F9", "url2": null, "url3": "OP28F9"}</pre>
</returns> </returns>
</xml> </xml>

View File

@@ -12,75 +12,82 @@ use okapi\services\caches\search\SearchAssistant;
class WebService class WebService
{ {
public static function options() public static function options()
{ {
return array( return array(
'min_auth_level' => 1 'min_auth_level' => 1
); );
} }
public static function call(OkapiRequest $request) public static function call(OkapiRequest $request)
{ {
# You may wonder, why there are no parameters like "bbox" or "center" in the # 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. # "search/all" method. This is *intentional* and should be kept this way.
# Such parameters would fall in conflict with each other and - in result - # Such parameters would fall in conflict with each other and - in result -
# make the documentation very fuzzy. That's why they were intentionally # make the documentation very fuzzy. That's why they were intentionally
# left out of the "search/all" method, and put in separate (individual) ones. # left out of the "search/all" method, and put in separate (individual) ones.
# It's much easier to grasp their meaning this way. # It's much easier to grasp their meaning this way.
$tmp = $request->get_parameter('center'); $tmp = $request->get_parameter('center');
if (!$tmp) if (!$tmp)
throw new ParamMissing('center'); throw new ParamMissing('center');
$parts = explode('|', $tmp); $parts = explode('|', $tmp);
if (count($parts) != 2) if (count($parts) != 2)
throw new InvalidParam('center', "Expecting 2 pipe-separated parts, got ".count($parts)."."); throw new InvalidParam('center', "Expecting 2 pipe-separated parts, got ".count($parts).".");
foreach ($parts as &$part_ref) foreach ($parts as &$part_ref)
{ {
if (!preg_match("/^-?[0-9]+(\.?[0-9]*)$/", $part_ref)) if (!preg_match("/^-?[0-9]+(\.?[0-9]*)$/", $part_ref))
throw new InvalidParam('center', "'$part_ref' is not a valid float number."); throw new InvalidParam('center', "'$part_ref' is not a valid float number.");
$part_ref = floatval($part_ref); $part_ref = floatval($part_ref);
} }
list($center_lat, $center_lon) = $parts; list($center_lat, $center_lon) = $parts;
if ($center_lat > 90 || $center_lat < -90) if ($center_lat > 90 || $center_lat < -90)
throw new InvalidParam('center', "Latitudes have to be within -90..90 range."); throw new InvalidParam('center', "Latitudes have to be within -90..90 range.");
if ($center_lon > 180 || $center_lon < -180) if ($center_lon > 180 || $center_lon < -180)
throw new InvalidParam('center', "Longitudes have to be within -180..180 range."); 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* # 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 # 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. # 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"); $search_assistant = new SearchAssistant($request);
$search_assistant->prepare_common_search_params();
$search_assistant->prepare_location_search_params();
$distance_formula = Okapi::get_distance_sql(
$center_lat, $center_lon,
$search_assistant->get_latitude_expr(), $search_assistant->get_longitude_expr()
);
# 'radius' parameter is optional. If not given, we'll have to calculate the # 'radius' parameter is optional. If not given, we'll have to calculate the
# distance for every cache in the database. # distance for every cache in the database.
$where_conds = array(); $where_conds = array();
$radius = null; $radius = null;
if ($tmp = $request->get_parameter('radius')) if ($tmp = $request->get_parameter('radius'))
{ {
if (!preg_match("/^-?[0-9]+(\.?[0-9]*)$/", $tmp)) if (!preg_match("/^-?[0-9]+(\.?[0-9]*)$/", $tmp))
throw new InvalidParam('radius', "'$tmp' is not a valid float number."); throw new InvalidParam('radius', "'$tmp' is not a valid float number.");
$radius = floatval($tmp); $radius = floatval($tmp);
if ($radius <= 0) if ($radius <= 0)
throw new InvalidParam('radius', "Has to be a positive number."); throw new InvalidParam('radius', "Has to be a positive number.");
$radius *= 1000; # this one is given in kilemeters, converting to meters! $radius *= 1000; # this one is given in kilemeters, converting to meters!
$where_conds[] = "$distance_formula <= '".mysql_real_escape_string($radius)."'"; $where_conds[] = "$distance_formula <= '".mysql_real_escape_string($radius)."'";
} }
$search_params = SearchAssistant::get_common_search_params($request); $search_params = $search_assistant->get_search_params();
$search_params['where_conds'] = array_merge($where_conds, $search_params['where_conds']); $search_params['where_conds'] = array_merge($where_conds, $search_params['where_conds']);
$search_params['order_by'][] = $distance_formula; # not replaced; added to the end! $search_params['order_by'][] = $distance_formula; # not replaced; added to the end!
$search_assistant->set_search_params($search_params);
$result = SearchAssistant::get_common_search_result($search_params); $result = $search_assistant->get_common_search_result();
if ($radius == null) if ($radius == null)
{ {
# 'more' is meaningless in this case, we'll remove it. # 'more' is meaningless in this case, we'll remove it.
unset($result['more']); unset($result['more']);
} }
return Okapi::formatted_response($request, $result); return Okapi::formatted_response($request, $result);
} }
} }

View File

@@ -1,30 +1,44 @@
<xml> <xml>
<brief>Search for nearest geocaches</brief> <brief>Search for nearest geocaches</brief>
<issue-id>17</issue-id> <issue-id>17</issue-id>
<desc> <desc>
<p>Find the nearest geocaches. Unless overriden, results are ordered by the distance from <p>Find the nearest geocaches. Unless overriden, results are ordered by the distance from
the given center point.</p> the given center point.</p>
<p>Usually you will want to use search_and_retrieve method instead of this one. <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> This way, you can get much more data in <b>one request</b>.</p>
</desc> </desc>
<req name='center'> <req name='center'>
<p>The center point (typically - the user's location), in the <p>The center point (typically - the user's location), in the
"lat|lon" format.</p> "lat|lon" format.</p>
<p>Use positive numbers for latitudes in the northern hemisphere and <p>Use positive numbers for latitudes in the northern hemisphere and
longitudes in the eastern hemisphere (and negative for southern and longitudes in the eastern hemisphere (and negative for southern and
western hemispheres accordingly). These are full degrees with a dot western hemispheres accordingly). These are full degrees with a dot
as a decimal point (ex. "54.3|22.3").</p> as a decimal point (ex. "54.3|22.3").</p>
</req> </req>
<opt name='radius'> <opt name='radius'>
<p>Maximal distance (from the center point) for the cache to <p>Maximal distance (from the center point) for the cache to
be included in the results. Unlike in most other places, this be included in the results. Unlike in most other places, this
distance is given <b>in kilometers</b> instead of meters distance is given <b>in kilometers</b> instead of meters
(it can contain a floating point though).</p> (it can contain a floating point though).</p>
</opt> </opt>
<import-params method='services/caches/search/all'/> <opt name='location_source' default='default-coords'>
<common-format-params/> <p>In general, this parameter should take the same value as in the
<returns> <a href="%OKAPI:methodargref:services/caches/formatters/gpx#location_source%">
<p>Same format as in the search/all method. The <b>more</b> key will services/caches/formatters/gpx</a> method, but currently <u>only two values are
be skipped when no <b>radius</b> argument is given.</p> supported</u>: <b>default-coords</b> and <b>alt_wpt:user-coords</b>.</p>
</returns>
<p>Allows you to search among alternate locations of the geocache,
instead of the default one. Particularily useful with <b>alt_wpt:user-coords</b>
alternate waypoint.</p>
<p>Please note, that if you plan on using this option in conjunction
with <b>search_and_retrieve</b> method, then you'd probably want to use
the same option in your <i>retr_method</i> too (if available).</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> </xml>

View File

@@ -14,169 +14,171 @@ use okapi\services\caches\search\SearchAssistant;
class WebService class WebService
{ {
public static function options() public static function options()
{ {
return array( return array(
'min_auth_level' => 1 'min_auth_level' => 1
); );
} }
/** /**
* Get [set_id, date_created, expires] for the given params_hash * Get [set_id, date_created, expires] for the given params_hash
* (or [null, null, null] if not found). * (or [null, null, null] if not found).
*/ */
private static function find_param_set($params_hash, $ref_max_age) private static function find_param_set($params_hash, $ref_max_age)
{ {
$tmp = Db::select_row(" $tmp = Db::select_row("
select select
id as id, id as id,
unix_timestamp(date_created) as date_created, unix_timestamp(date_created) as date_created,
unix_timestamp(expires) as expires unix_timestamp(expires) as expires
from okapi_search_sets from okapi_search_sets
where where
params_hash = '".mysql_real_escape_string($params_hash)."' params_hash = '".mysql_real_escape_string($params_hash)."'
and date_add(date_created, interval '".mysql_real_escape_string($ref_max_age)."' second) > now() and date_add(date_created, interval '".mysql_real_escape_string($ref_max_age)."' second) > now()
order by id desc order by id desc
limit 1 limit 1
"); ");
if ($tmp === null) if ($tmp === null)
return array(null, null, null); return array(null, null, null);
return array($tmp['id'], $tmp['date_created'], $tmp['expires']); return array($tmp['id'], $tmp['date_created'], $tmp['expires']);
} }
public static function call(OkapiRequest $request) public static function call(OkapiRequest $request)
{ {
# "Cache control" parameters. # "Cache control" parameters.
$tmp = $request->get_parameter('min_store'); $tmp = $request->get_parameter('min_store');
if ($tmp === null) $tmp = "300"; if ($tmp === null) $tmp = "300";
$min_store = intval($tmp); $min_store = intval($tmp);
if (("$min_store" !== $tmp) ||($min_store < 0) || ($min_store > 64800)) if (("$min_store" !== $tmp) ||($min_store < 0) || ($min_store > 64800))
throw new InvalidParam('min_store', "Has to be in the 0..64800 range."); throw new InvalidParam('min_store', "Has to be in the 0..64800 range.");
$tmp = $request->get_parameter('ref_max_age'); $tmp = $request->get_parameter('ref_max_age');
if ($tmp === null) $tmp = "300"; if ($tmp === null) $tmp = "300";
if ($tmp == "nolimit") $tmp = "9999999"; if ($tmp == "nolimit") $tmp = "9999999";
$ref_max_age = intval($tmp); $ref_max_age = intval($tmp);
if (("$ref_max_age" !== $tmp) || ($ref_max_age < 300)) if (("$ref_max_age" !== $tmp) || ($ref_max_age < 300))
throw new InvalidParam('ref_max_age', "Has to be >=300."); throw new InvalidParam('ref_max_age', "Has to be >=300.");
# Search params. # Search params.
$search_params = SearchAssistant::get_common_search_params($request); $search_assistant = new SearchAssistant($request);
$tables = array_merge( $search_assistant->prepare_common_search_params();
array('caches'), $search_params = $search_assistant->get_search_params();
$search_params['extra_tables'] $tables = array_merge(
); array('caches'),
$where_conds = array_merge( $search_params['extra_tables']
array('caches.wp_oc is not null'), );
$search_params['where_conds'] $where_conds = array_merge(
); array('caches.wp_oc is not null'),
unset($search_params); $search_params['where_conds']
);
unset($search_params);
# Generate, or retrieve an existing set, and return the result. # Generate, or retrieve an existing set, and return the result.
# All user-supplied data in $tables and $where_conds MUST be escaped! # All user-supplied data in $tables and $where_conds MUST be escaped!
$result = self::get_set($tables, $where_conds, $min_store, $ref_max_age); $result = self::get_set($tables, $where_conds, $min_store, $ref_max_age);
return Okapi::formatted_response($request, $result); return Okapi::formatted_response($request, $result);
} }
/** /**
* Important: YOU HAVE TO make sure $tables and $where_conds don't contain * Important: YOU HAVE TO make sure $tables and $where_conds don't contain
* unescaped user-supplied data! * unescaped user-supplied data!
*/ */
public static function get_set($tables, $where_conds, $min_store, $ref_max_age) public static function get_set($tables, $where_conds, $min_store, $ref_max_age)
{ {
# Compute the "params hash". # Compute the "params hash".
$params_hash = md5(serialize(array($tables, $where_conds))); $params_hash = md5(serialize(array($tables, $where_conds)));
# Check if there exists an entry for this hash, which also meets the # Check if there exists an entry for this hash, which also meets the
# given freshness criteria. # given freshness criteria.
list($set_id, $date_created, $expires) = self::find_param_set($params_hash, $ref_max_age); list($set_id, $date_created, $expires) = self::find_param_set($params_hash, $ref_max_age);
if ($set_id === null) if ($set_id === null)
{ {
# To avoid generating the same results by multiple threads at once # To avoid generating the same results by multiple threads at once
# (the "tile" method uses the "save" method, so the problem is # (the "tile" method uses the "save" method, so the problem is
# quite real!), we will acquire a write-lock here. # quite real!), we will acquire a write-lock here.
$lock = OkapiLock::get("search-results-writer"); $lock = OkapiLock::get("search-results-writer");
$lock->acquire(); $lock->acquire();
try try
{ {
# Make sure we were the first to acquire the lock. # Make sure we were the first to acquire the lock.
list($set_id, $date_created, $expires) = self::find_param_set($params_hash, $ref_max_age); list($set_id, $date_created, $expires) = self::find_param_set($params_hash, $ref_max_age);
if ($set_id === null) if ($set_id === null)
{ {
# We are in the first thread which have acquired the lock. # We are in the first thread which have acquired the lock.
# We will proceed with result-set creation. Other threads # We will proceed with result-set creation. Other threads
# will be waiting until we finish. # will be waiting until we finish.
Db::execute(" Db::execute("
insert into okapi_search_sets (params_hash, date_created, expires) insert into okapi_search_sets (params_hash, date_created, expires)
values ( values (
'processing in progress', 'processing in progress',
now(), now(),
date_add(now(), interval '".mysql_real_escape_string($min_store + 60)."' second) date_add(now(), interval '".mysql_real_escape_string($min_store + 60)."' second)
) )
"); ");
$set_id = Db::last_insert_id(); $set_id = Db::last_insert_id();
$date_created = time(); $date_created = time();
$expires = $date_created + $min_store + 60; $expires = $date_created + $min_store + 60;
Db::execute(" Db::execute("
insert into okapi_search_results (set_id, cache_id) insert into okapi_search_results (set_id, cache_id)
select distinct select distinct
'".mysql_real_escape_string($set_id)."', '".mysql_real_escape_string($set_id)."',
caches.cache_id caches.cache_id
from ".implode(", ", $tables)." from ".implode(", ", $tables)."
where (".implode(") and (", $where_conds).") where (".implode(") and (", $where_conds).")
"); ");
# Lock barrier, to make sure the data is visible by other # Lock barrier, to make sure the data is visible by other
# sessions. See http://bugs.mysql.com/bug.php?id=36618 # sessions. See http://bugs.mysql.com/bug.php?id=36618
Db::execute("lock table okapi_search_results write"); Db::execute("lock table okapi_search_results write");
Db::execute("unlock tables"); Db::execute("unlock tables");
Db::execute(" Db::execute("
update okapi_search_sets update okapi_search_sets
set params_hash = '".mysql_real_escape_string($params_hash)."' set params_hash = '".mysql_real_escape_string($params_hash)."'
where id = '".mysql_real_escape_string($set_id)."' where id = '".mysql_real_escape_string($set_id)."'
"); ");
} else { } else {
# Some other thread acquired the lock before us and it has # Some other thread acquired the lock before us and it has
# generated the result set. We don't need to do anything. # generated the result set. We don't need to do anything.
} }
$lock->release(); $lock->release();
} }
catch (Exception $e) catch (Exception $e)
{ {
# SQL error? Make sure the lock is released and rethrow. # SQL error? Make sure the lock is released and rethrow.
$lock->release(); $lock->release();
throw $e; throw $e;
} }
} }
# If we got an old set, we may need to expand its lifetime in order to # If we got an old set, we may need to expand its lifetime in order to
# meet user's "min_store" criterium. # meet user's "min_store" criterium.
if (time() + $min_store > $expires) if (time() + $min_store > $expires)
{ {
Db::execute(" Db::execute("
update okapi_search_sets update okapi_search_sets
set expires = date_add(now(), interval '".mysql_real_escape_string($min_store + 60)."' second) set expires = date_add(now(), interval '".mysql_real_escape_string($min_store + 60)."' second)
where id = '".mysql_real_escape_string($set_id)."' where id = '".mysql_real_escape_string($set_id)."'
"); ");
} }
return array( return array(
'set_id' => "$set_id", 'set_id' => "$set_id",
'generated_at' => date('c', $date_created), 'generated_at' => date('c', $date_created),
'expires' => date('c', $expires), 'expires' => date('c', $expires),
); );
} }
} }

View File

@@ -1,36 +1,36 @@
<xml> <xml>
<brief>Save a search result set</brief> <brief>Save a search result set</brief>
<issue-id>163</issue-id> <issue-id>163</issue-id>
<desc> <desc>
<p>This works similar to the <b>search/all</b> method, but the returned <p>This works similar to the <b>search/all</b> method, but the returned
set of geocaches is temporarilly stored, instead of being directly set of geocaches is temporarilly stored, instead of being directly
returned to you.</p> returned to you.</p>
<p>You may want to use this method when you don't want your search <p>You may want to use this method when you don't want your search
results modified while the user is browsing through them, page by page. results modified while the user is browsing through them, page by page.
To view a portion of a saved search, use the <b>search/all</b> To view a portion of a saved search, use the <b>search/all</b>
method with proper <b>set_and</b> <u>and <b>status</b></u> (!) method with proper <b>set_and</b> <u>and <b>status</b></u> (!)
parameters.</p> parameters.</p>
</desc> </desc>
<opt name='min_store' default="300"> <opt name='min_store' default="300">
The amount of time (in seconds) after which you allow OKAPI to delete The amount of time (in seconds) after which you allow OKAPI to delete
the set (OKAPI <b>may</b> remove it, but doesn't have to). the set (OKAPI <b>may</b> remove it, but doesn't have to).
The maximum allowed value is 64800 (18 hours). The maximum allowed value is 64800 (18 hours).
</opt> </opt>
<opt name='ref_max_age' default="300"> <opt name='ref_max_age' default="300">
<p>If OKAPI finds an existing result set which was created for your <p>If OKAPI finds an existing result set which was created for your
search query, it may return the ID of this existing set (and possibly search query, it may return the ID of this existing set (and possibly
extend its lifetime so it fits your <b>min_store</b>). What is the extend its lifetime so it fits your <b>min_store</b>). What is the
maximum age of the existing result set which you are willing to accept?</p> maximum age of the existing result set which you are willing to accept?</p>
<p>This should be an integer (in seconds) <b>or</b> a special <p>This should be an integer (in seconds) <b>or</b> a special
<b>nolimit</b> value. It must be greater or equal to 300 (5 minutes).</p> <b>nolimit</b> value. It must be greater or equal to 300 (5 minutes).</p>
</opt> </opt>
<import-params method='services/caches/search/all' except="offset|limit|order_by"/> <import-params method='services/caches/search/all' except="offset|limit|order_by"/>
<common-format-params/> <common-format-params/>
<returns> <returns>
<p>A dictionary of the following structure:</p> <p>A dictionary of the following structure:</p>
<ul> <ul>
<li><b>set_id</b> - string, the identifier of your saved set, <li><b>set_id</b> - string, the identifier of your saved set,
for future reference.</li> for future reference.</li>
</ul> </ul>
</returns> </returns>
</xml> </xml>

File diff suppressed because it is too large Load Diff

View File

@@ -13,109 +13,109 @@ use okapi\BadRequest;
class WebService class WebService
{ {
public static function options() public static function options()
{ {
return array( return array(
'min_auth_level' => 1 'min_auth_level' => 1
); );
} }
public static function call(OkapiRequest $request) public static function call(OkapiRequest $request)
{ {
# Check search method # Check search method
$search_method = $request->get_parameter('search_method'); $search_method = $request->get_parameter('search_method');
if (!$search_method) if (!$search_method)
throw new ParamMissing('search_method'); throw new ParamMissing('search_method');
if (strpos($search_method, "services/caches/search/") !== 0) if (strpos($search_method, "services/caches/search/") !== 0)
throw new InvalidParam('search_method', "Should begin with 'services/caches/search/'."); throw new InvalidParam('search_method', "Should begin with 'services/caches/search/'.");
if (!OkapiServiceRunner::exists($search_method)) if (!OkapiServiceRunner::exists($search_method))
throw new InvalidParam('search_method', "Method does not exist: '$search_method'"); throw new InvalidParam('search_method', "Method does not exist: '$search_method'");
$search_params = $request->get_parameter('search_params'); $search_params = $request->get_parameter('search_params');
if (!$search_params) if (!$search_params)
throw new ParamMissing('search_params'); throw new ParamMissing('search_params');
$search_params = json_decode($search_params, true); $search_params = json_decode($search_params, true);
if (!is_array($search_params)) if (!is_array($search_params))
throw new InvalidParam('search_params', "Should be a JSON-encoded dictionary"); throw new InvalidParam('search_params', "Should be a JSON-encoded dictionary");
# Check retrieval method # Check retrieval method
$retr_method = $request->get_parameter('retr_method'); $retr_method = $request->get_parameter('retr_method');
if (!$retr_method) if (!$retr_method)
throw new ParamMissing('retr_method'); throw new ParamMissing('retr_method');
if (!OkapiServiceRunner::exists($retr_method)) if (!OkapiServiceRunner::exists($retr_method))
throw new InvalidParam('retr_method', "Method does not exist: '$retr_method'"); throw new InvalidParam('retr_method', "Method does not exist: '$retr_method'");
$retr_params = $request->get_parameter('retr_params'); $retr_params = $request->get_parameter('retr_params');
if (!$retr_params) if (!$retr_params)
throw new ParamMissing('retr_params'); throw new ParamMissing('retr_params');
$retr_params = json_decode($retr_params, true); $retr_params = json_decode($retr_params, true);
if (!is_array($retr_params)) if (!is_array($retr_params))
throw new InvalidParam('retr_params', "Should be a JSON-encoded dictionary"); throw new InvalidParam('retr_params', "Should be a JSON-encoded dictionary");
self::map_values_to_strings($search_params); self::map_values_to_strings($search_params);
self::map_values_to_strings($retr_params); self::map_values_to_strings($retr_params);
# Wrapped? # Wrapped?
$wrap = $request->get_parameter('wrap'); $wrap = $request->get_parameter('wrap');
if ($wrap == null) throw new ParamMissing('wrap'); if ($wrap == null) throw new ParamMissing('wrap');
if (!in_array($wrap, array('true', 'false'))) if (!in_array($wrap, array('true', 'false')))
throw new InvalidParam('wrap'); throw new InvalidParam('wrap');
$wrap = ($wrap == 'true'); $wrap = ($wrap == 'true');
# Run search method # Run search method
try try
{ {
$search_result = OkapiServiceRunner::call($search_method, new OkapiInternalRequest( $search_result = OkapiServiceRunner::call($search_method, new OkapiInternalRequest(
$request->consumer, $request->token, $search_params)); $request->consumer, $request->token, $search_params));
} }
catch (BadRequest $e) catch (BadRequest $e)
{ {
throw new InvalidParam('search_params', "Search method responded with the ". throw new InvalidParam('search_params', "Search method responded with the ".
"following error message: ".$e->getMessage()); "following error message: ".$e->getMessage());
} }
# Run retrieval method # Run retrieval method
try try
{ {
$retr_result = OkapiServiceRunner::call($retr_method, new OkapiInternalRequest( $retr_result = OkapiServiceRunner::call($retr_method, new OkapiInternalRequest(
$request->consumer, $request->token, array_merge($retr_params, $request->consumer, $request->token, array_merge($retr_params,
array('cache_codes' => implode("|", $search_result['results']))))); array('cache_codes' => implode("|", $search_result['results'])))));
} }
catch (BadRequest $e) catch (BadRequest $e)
{ {
throw new InvalidParam('retr_params', "Retrieval method responded with the ". throw new InvalidParam('retr_params', "Retrieval method responded with the ".
"following error message: ".$e->getMessage()); "following error message: ".$e->getMessage());
} }
if ($wrap) if ($wrap)
{ {
# $retr_result might be a PHP object, but also might be a binary response # $retr_result might be a PHP object, but also might be a binary response
# (e.g. a GPX file). # (e.g. a GPX file).
if ($retr_result instanceof OkapiHttpResponse) if ($retr_result instanceof OkapiHttpResponse)
$result = array('results' => $retr_result->get_body()); $result = array('results' => $retr_result->get_body());
else else
$result = array('results' => $retr_result); $result = array('results' => $retr_result);
foreach ($search_result as $key => &$value_ref) foreach ($search_result as $key => &$value_ref)
if ($key != 'results') if ($key != 'results')
$result[$key] = $value_ref; $result[$key] = $value_ref;
return Okapi::formatted_response($request, $result); return Okapi::formatted_response($request, $result);
} }
else else
{ {
if ($retr_result instanceof OkapiHttpResponse) if ($retr_result instanceof OkapiHttpResponse)
return $retr_result; return $retr_result;
else else
return Okapi::formatted_response($request, $retr_result); return Okapi::formatted_response($request, $retr_result);
} }
} }
private static function map_values_to_strings(&$dict) private static function map_values_to_strings(&$dict)
{ {
foreach (array_keys($dict) as $key) foreach (array_keys($dict) as $key)
{ {
$val = $dict[$key]; $val = $dict[$key];
if (is_numeric($val) || is_string($val)) if (is_numeric($val) || is_string($val))
$dict[$key] = (string)$val; $dict[$key] = (string)$val;
else else
throw new BadRequest("Invalid value format for key: ".$key); throw new BadRequest("Invalid value format for key: ".$key);
} }
} }
} }

View File

@@ -1,65 +1,65 @@
<xml> <xml>
<brief>Search for caches and retrieve formatted results</brief> <brief>Search for caches and retrieve formatted results</brief>
<issue-id>18</issue-id> <issue-id>18</issue-id>
<desc> <desc>
<p>We think that searching and retrieving data are two different things <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 and that's why they were cleanly separated in the documentation (put in
separate methods). This was done primarily to keep the docs clean. 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> 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. <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 Then you have to use other method (like services/caches/geocaches) to
retrieve the names and locations, based on those cache codes. The 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 <b>search_and_retrieve</b> method allows you to do these two steps in one
method call.</p> method call.</p>
<p>First, you have to choose both methods and their parameters - one method <p>First, you have to choose both methods and their parameters - one method
which returns the cache codes, and the other one, that responds which returns the cache codes, and the other one, that responds
with additional data for the given caches.</p> with additional data for the given caches.</p>
</desc> </desc>
<req name='search_method'> <req name='search_method'>
<p>Name of the search method (begin with "services/").</p> <p>Name of the search method (begin with "services/").</p>
<p>E.g. <i>services/caches/search/nearest</i>.</p> <p>E.g. <i>services/caches/search/nearest</i>.</p>
</req> </req>
<req name='search_params'> <req name='search_params'>
<p>JSON-formatted dictionary of parameters to be passed on <p>JSON-formatted dictionary of parameters to be passed on
to the search method.</p> to the search method.</p>
<p>E.g. <i>{"center": "49|19", "status": "Available"}</i>.</p> <p>E.g. <i>{"center": "49|19", "status": "Available"}</i>.</p>
</req> </req>
<req name='retr_method'> <req name='retr_method'>
<p>Name of the retrieval method (begin with "services/").</p> <p>Name of the retrieval method (begin with "services/").</p>
<p>E.g. <i>services/caches/geocaches</i>.</p> <p>E.g. <i>services/caches/geocaches</i>.</p>
</req> </req>
<req name='retr_params'> <req name='retr_params'>
<p>JSON-formatted dictionary of parameters to be passed on <p>JSON-formatted dictionary of parameters to be passed on
to the retrieval method.</p> to the retrieval method.</p>
<p>E.g. <i>{"fields": "name|location|type"}</i></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>. <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> These will be the cache codes collected from the results of the search method.</p>
</req> </req>
<req name='wrap'> <req name='wrap'>
<p>Boolean.</p> <p>Boolean.</p>
<ul> <ul>
<li>If <b>true</b>, then results will be wrapped in an additional <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 object, in order to include any additional data received along with the
search_method response (i.e. the <b>more</b> value).</li> search_method response (i.e. the <b>more</b> value).</li>
<li>If <b>false</b>, then this method will return exactly what the <li>If <b>false</b>, then this method will return exactly what the
<b>retr_method</b> will respond with.</li> <b>retr_method</b> will respond with.</li>
</ul> </ul>
</req> </req>
<common-format-params/> <common-format-params/>
<returns> <returns>
<p>If <b>wrap</b> is <b>true</b>, then the method will return a <p>If <b>wrap</b> is <b>true</b>, then the method will return a
dictionary of the following structure:</p> dictionary of the following structure:</p>
<ul> <ul>
<li><b>results</b> - anything the retrival method <li><b>results</b> - anything the retrival method
responds with (as long as it's not an error).</li> responds with (as long as it's not an error).</li>
<li>any extra keys and values received as a response of <li>any extra keys and values received as a response of
the search_method (i.e. the <b>more</b> variable).</li> the search_method (i.e. the <b>more</b> variable).</li>
</ul> </ul>
<p>If <b>wrap</b> is <b>false</b>, then the method will return <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> anything the retrieval methods responds with (along with its HTTP headers).</p>
<p><b>Example:</b></p> <p><b>Example:</b></p>
<pre>search_and_retrieve<br/>?search_method=services/caches/search/bbox<br/>&amp;search_params={"bbox":"49|19|50|20","limit":"1"}<br/>&amp;retr_method=services/caches/geocaches<br/>&amp;retr_params={"fields":"location"}<br/>&amp;wrap=false</pre> <pre>search_and_retrieve<br/>?search_method=services/caches/search/bbox<br/>&amp;search_params={"bbox":"49|19|50|20","limit":"1"}<br/>&amp;retr_method=services/caches/geocaches<br/>&amp;retr_params={"fields":"location"}<br/>&amp;wrap=false</pre>
<p>Possible output:<br/><code>{"OP205A": {"location": "49.572417|19.525867"}}</code></p> <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> <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> </returns>
</xml> </xml>

View File

@@ -15,149 +15,149 @@ use okapi\services\caches\search\SearchAssistant;
class WebService class WebService
{ {
public static function options() public static function options()
{ {
return array( return array(
'min_auth_level' => 1 'min_auth_level' => 1
); );
} }
private static $valid_field_names = array( private static $valid_field_names = array(
'uuid', 'cache_code', 'date', 'user', 'type', 'was_recommended', 'comment', 'uuid', 'cache_code', 'date', 'user', 'type', 'was_recommended', 'comment',
'images', 'internal_id', 'oc_team_entry', 'images', 'internal_id', 'oc_team_entry',
); );
public static function call(OkapiRequest $request) public static function call(OkapiRequest $request)
{ {
$log_uuids = $request->get_parameter('log_uuids'); $log_uuids = $request->get_parameter('log_uuids');
if ($log_uuids === null) throw new ParamMissing('log_uuids'); if ($log_uuids === null) throw new ParamMissing('log_uuids');
if ($log_uuids === "") if ($log_uuids === "")
{ {
$log_uuids = array(); $log_uuids = array();
} }
else else
$log_uuids = explode("|", $log_uuids); $log_uuids = explode("|", $log_uuids);
if ((count($log_uuids) > 500) && (!$request->skip_limits)) if ((count($log_uuids) > 500) && (!$request->skip_limits))
throw new InvalidParam('log_uuids', "Maximum allowed number of referenced ". throw new InvalidParam('log_uuids', "Maximum allowed number of referenced ".
"log entries is 500. You provided ".count($log_uuids)." UUIDs."); "log entries is 500. You provided ".count($log_uuids)." UUIDs.");
if (count($log_uuids) != count(array_unique($log_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)."); throw new InvalidParam('log_uuids', "Duplicate UUIDs detected (make sure each UUID is referenced only once).");
$fields = $request->get_parameter('fields'); $fields = $request->get_parameter('fields');
if (!$fields) $fields = "date|user|type|comment"; if (!$fields) $fields = "date|user|type|comment";
$fields = explode("|", $fields); $fields = explode("|", $fields);
foreach ($fields as $field) foreach ($fields as $field)
if (!in_array($field, self::$valid_field_names)) if (!in_array($field, self::$valid_field_names))
throw new InvalidParam('fields', "'$field' is not a valid field code."); throw new InvalidParam('fields', "'$field' is not a valid field code.");
if (Settings::get('OC_BRANCH') == 'oc.de') if (Settings::get('OC_BRANCH') == 'oc.de')
{ {
$teamentry_field = 'cl.oc_team_comment'; $teamentry_field = 'cl.oc_team_comment';
$ratingdate_condition = 'and cr.rating_date=cl.date'; $ratingdate_condition = 'and cr.rating_date=cl.date';
} }
else else
{ {
$teamentry_field = '(cl.type=12)'; $teamentry_field = '(cl.type=12)';
$ratingdate_condition = ''; $ratingdate_condition = '';
} }
$rs = Db::query(" $rs = Db::query("
select select
cl.id, c.wp_oc as cache_code, cl.uuid, cl.type, cl.id, c.wp_oc as cache_code, cl.uuid, cl.type,
".$teamentry_field." as oc_team_entry, ".$teamentry_field." as oc_team_entry,
unix_timestamp(cl.date) as date, cl.text, unix_timestamp(cl.date) as date, cl.text,
u.uuid as user_uuid, u.username, u.user_id, u.uuid as user_uuid, u.username, u.user_id,
if(cr.user_id is null, 0, 1) as was_recommended if(cr.user_id is null, 0, 1) as was_recommended
from from
(cache_logs cl, (cache_logs cl,
user u, user u,
caches c) caches c)
left join cache_rating cr left join cache_rating cr
on cr.user_id = u.user_id on cr.user_id = u.user_id
and cr.cache_id = c.cache_id and cr.cache_id = c.cache_id
".$ratingdate_condition." ".$ratingdate_condition."
and cl.type in ( and cl.type in (
".Okapi::logtypename2id("Found it").", ".Okapi::logtypename2id("Found it").",
".Okapi::logtypename2id("Attended")." ".Okapi::logtypename2id("Attended")."
) )
where where
cl.uuid in ('".implode("','", array_map('mysql_real_escape_string', $log_uuids))."') cl.uuid in ('".implode("','", array_map('mysql_real_escape_string', $log_uuids))."')
and ".((Settings::get('OC_BRANCH') == 'oc.pl') ? "cl.deleted = 0" : "true")." and ".((Settings::get('OC_BRANCH') == 'oc.pl') ? "cl.deleted = 0" : "true")."
and cl.user_id = u.user_id and cl.user_id = u.user_id
and c.cache_id = cl.cache_id and c.cache_id = cl.cache_id
and c.status in (1,2,3) and c.status in (1,2,3)
"); ");
$results = array(); $results = array();
$log_id2uuid = array(); /* Maps logs' internal_ids to uuids */ $log_id2uuid = array(); /* Maps logs' internal_ids to uuids */
while ($row = mysql_fetch_assoc($rs)) while ($row = mysql_fetch_assoc($rs))
{ {
$results[$row['uuid']] = array( $results[$row['uuid']] = array(
'uuid' => $row['uuid'], 'uuid' => $row['uuid'],
'cache_code' => $row['cache_code'], 'cache_code' => $row['cache_code'],
'date' => date('c', $row['date']), 'date' => date('c', $row['date']),
'user' => array( 'user' => array(
'uuid' => $row['user_uuid'], 'uuid' => $row['user_uuid'],
'username' => $row['username'], 'username' => $row['username'],
'profile_url' => Settings::get('SITE_URL')."viewprofile.php?userid=".$row['user_id'], 'profile_url' => Settings::get('SITE_URL')."viewprofile.php?userid=".$row['user_id'],
), ),
'type' => Okapi::logtypeid2name($row['type']), 'type' => Okapi::logtypeid2name($row['type']),
'was_recommended' => $row['was_recommended'] ? true : false, 'was_recommended' => $row['was_recommended'] ? true : false,
'comment' => Okapi::fix_oc_html($row['text']), 'comment' => Okapi::fix_oc_html($row['text']),
'images' => array(), 'images' => array(),
'internal_id' => $row['id'], 'internal_id' => $row['id'],
); );
if (in_array('oc_team_entry',$fields)) if (in_array('oc_team_entry',$fields))
$results[$row['uuid']]['oc_team_entry'] = $row['oc_team_entry'] ? true : false; $results[$row['uuid']]['oc_team_entry'] = $row['oc_team_entry'] ? true : false;
$log_id2uuid[$row['id']] = $row['uuid']; $log_id2uuid[$row['id']] = $row['uuid'];
} }
mysql_free_result($rs); mysql_free_result($rs);
# fetch images # fetch images
if (in_array('images', $fields)) if (in_array('images', $fields))
{ {
$rs = Db::query(" $rs = Db::query("
select object_id, uuid, url, title, spoiler select object_id, uuid, url, title, spoiler
from pictures from pictures
where where
object_type = 1 object_type = 1
and object_id in ('".implode("','", array_map('mysql_real_escape_string', array_keys($log_id2uuid)))."') and object_id in ('".implode("','", array_map('mysql_real_escape_string', array_keys($log_id2uuid)))."')
and display = 1 /* currently is always 1 for logpix */ and display = 1 /* currently is always 1 for logpix */
and unknown_format = 0 and unknown_format = 0
order by date_created order by date_created
"); ");
while ($row = mysql_fetch_assoc($rs)) while ($row = mysql_fetch_assoc($rs))
{ {
$results[$log_id2uuid[$row['object_id']]]['images'][] = $results[$log_id2uuid[$row['object_id']]]['images'][] =
array( array(
'uuid' => $row['uuid'], 'uuid' => $row['uuid'],
'url' => $row['url'], 'url' => $row['url'],
'thumb_url' => Settings::get('SITE_URL') . 'thumbs.php?uuid=' . $row['uuid'], 'thumb_url' => Settings::get('SITE_URL') . 'thumbs.php?uuid=' . $row['uuid'],
'caption' => $row['title'], 'caption' => $row['title'],
'is_spoiler' => ($row['spoiler'] ? true : false), 'is_spoiler' => ($row['spoiler'] ? true : false),
); );
} }
mysql_free_result($rs); mysql_free_result($rs);
} }
# Check which UUIDs were not found and mark them with null. # Check which UUIDs were not found and mark them with null.
foreach ($log_uuids as $log_uuid) foreach ($log_uuids as $log_uuid)
if (!isset($results[$log_uuid])) if (!isset($results[$log_uuid]))
$results[$log_uuid] = null; $results[$log_uuid] = null;
# Remove unwanted fields. # Remove unwanted fields.
foreach (self::$valid_field_names as $field) foreach (self::$valid_field_names as $field)
if (!in_array($field, $fields)) if (!in_array($field, $fields))
foreach ($results as &$result_ref) foreach ($results as &$result_ref)
unset($result_ref[$field]); unset($result_ref[$field]);
# Order the results in the same order as the input codes were given. # Order the results in the same order as the input codes were given.
$ordered_results = array(); $ordered_results = array();
foreach ($log_uuids as $log_uuid) foreach ($log_uuids as $log_uuid)
$ordered_results[$log_uuid] = $results[$log_uuid]; $ordered_results[$log_uuid] = $results[$log_uuid];
return Okapi::formatted_response($request, $ordered_results); return Okapi::formatted_response($request, $ordered_results);
} }
} }

View File

@@ -1,27 +1,27 @@
<xml> <xml>
<brief>Retrieve information on multiple log entries</brief> <brief>Retrieve information on multiple log entries</brief>
<issue-id>107</issue-id> <issue-id>107</issue-id>
<desc> <desc>
<p>This method works like the services/logs/entry method, but works <p>This method works like the services/logs/entry method, but works
with multiple log entries (instead of only one).</p> with multiple log entries (instead of only one).</p>
</desc> </desc>
<req name='log_uuids'> <req name='log_uuids'>
<p>Pipe-separated list of UUIDs. These represent the log entries you <p>Pipe-separated list of UUIDs. These represent the log entries you
are interested in. No more than 500 codes are allowed. are interested in. No more than 500 codes are allowed.
Unlike in the "entry" method, this CAN be an empty string (it will Unlike in the "entry" method, this CAN be an empty string (it will
result in an empty, but valid, response).</p> result in an empty, but valid, response).</p>
</req> </req>
<opt name='fields' default='date|user|type|comment'> <opt name='fields' default='date|user|type|comment'>
<p>Same as in the services/logs/entry method. Pipe-separated list <p>Same as in the services/logs/entry method. Pipe-separated list
of field names which you are interested with. of field names which you are interested with.
See services/logs/entry method for a list of available values.</p> See services/logs/entry method for a list of available values.</p>
</opt> </opt>
<common-format-params/> <common-format-params/>
<returns> <returns>
<p>A dictionary. UUIDs you provide will be mapped to dictionary keys, <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> 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. <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 (This behavior is different than in the services/logs/entry method, which
responds with an HTTP 400 error in such case.)</p> responds with an HTTP 400 error in such case.)</p>
</returns> </returns>
</xml> </xml>

View File

@@ -11,26 +11,26 @@ use okapi\InvalidParam;
class WebService class WebService
{ {
public static function options() public static function options()
{ {
return array( return array(
'min_auth_level' => 1 'min_auth_level' => 1
); );
} }
public static function call(OkapiRequest $request) public static function call(OkapiRequest $request)
{ {
$log_uuid = $request->get_parameter('log_uuid'); $log_uuid = $request->get_parameter('log_uuid');
if (!$log_uuid) throw new ParamMissing('log_uuid'); if (!$log_uuid) throw new ParamMissing('log_uuid');
$fields = $request->get_parameter('fields'); $fields = $request->get_parameter('fields');
if (!$fields) $fields = "date|user|type|comment"; if (!$fields) $fields = "date|user|type|comment";
$results = OkapiServiceRunner::call('services/logs/entries', new OkapiInternalRequest( $results = OkapiServiceRunner::call('services/logs/entries', new OkapiInternalRequest(
$request->consumer, $request->token, array('log_uuids' => $log_uuid, $request->consumer, $request->token, array('log_uuids' => $log_uuid,
'fields' => $fields))); 'fields' => $fields)));
$result = $results[$log_uuid]; $result = $results[$log_uuid];
if ($result == null) if ($result == null)
throw new InvalidParam('log_uuid', "This log entry does not exist."); throw new InvalidParam('log_uuid', "This log entry does not exist.");
return Okapi::formatted_response($request, $result); return Okapi::formatted_response($request, $result);
} }
} }

View File

@@ -1,117 +1,117 @@
<xml> <xml>
<brief>Retrieve information on a single log entry</brief> <brief>Retrieve information on a single log entry</brief>
<issue-id>108</issue-id> <issue-id>108</issue-id>
<desc> <desc>
<p>Retrieve information on a single log entry.</p> <p>Retrieve information on a single log entry.</p>
</desc> </desc>
<req name='log_uuid'>UUID of the log entry</req> <req name='log_uuid'>UUID of the log entry</req>
<opt name='fields' default='date|user|type|comment'> <opt name='fields' default='date|user|type|comment'>
<p>Pipe-separated list of field names which you are interested with. <p>Pipe-separated list of field names which you are interested with.
Selected fields will be included in the response. See below fot the list Selected fields will be included in the response. See below fot the list
of available fields.</p> of available fields.</p>
</opt> </opt>
<common-format-params/> <common-format-params/>
<returns> <returns>
<p>A dictionary of fields you have selected. Currently available fields:</p> <p>A dictionary of fields you have selected. Currently available fields:</p>
<ul> <ul>
<li><b>uuid</b> - unique ID of the log entry,</li> <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><b>cache_code</b> - code of the cache which the log entry refers to,</li>
<li> <li>
<p><b>date</b> - date and time (ISO 8601) when the log entry was submitted.</p> <p><b>date</b> - date and time (ISO 8601) when the log entry was submitted.</p>
<p>Please note that log entries often contain dates only (with the times <p>Please note that log entries often contain dates only (with the times
truncated to midnight, as in the local timezone). In such cases, you may truncated to midnight, as in the local timezone). In such cases, you may
want to avoid displaying the time. You may assume that if the <b>date</b> want to avoid displaying the time. You may assume that if the <b>date</b>
value contains the "00:00:00" string, then it is date-only.</p> value contains the "00:00:00" string, then it is date-only.</p>
</li> </li>
<li> <li>
<p><b>user</b> - a dictionary:</p> <p><b>user</b> - a dictionary:</p>
<ul> <ul>
<li><b>uuid</b> - ID of the user (author of the log entry),</li> <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>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> <li><b>profile_url</b> - URL of the profile page of the user,</li>
</ul> </ul>
</li> </li>
<li> <li>
<p><b>type</b> - string; log type. One of the values documented <p><b>type</b> - string; log type. One of the values documented
below.</p> below.</p>
<p>Primary types, commonly used by all Opencaching installations:</p> <p>Primary types, commonly used by all Opencaching installations:</p>
<ul> <ul>
<li>"Found it" - a user found the cache (Non-Event caches).</li> <li>"Found it" - a user found the cache (Non-Event caches).</li>
<li>"Didn't find it" - a user searched for, but couldn't find the cache (Non-Event caches).</li> <li>"Didn't find it" - a user searched for, but couldn't find the cache (Non-Event caches).</li>
<li>"Comment".</li> <li>"Comment".</li>
<li>"Will attend" - a user is planning to attend the event (for Event caches only).</li> <li>"Will attend" - a user is planning to attend the event (for Event caches only).</li>
<li>"Attended" - a user has attended the event (for Event caches only).</li> <li>"Attended" - a user has attended the event (for Event caches only).</li>
</ul> </ul>
<p>Types which indicate a change of state of the geocache or confirm the <p>Types which indicate a change of state of the geocache or confirm the
current state (used only by some Opencaching installations):</p> current state (used only by some Opencaching installations):</p>
<ul> <ul>
<li>"Temporarily unavailable" - probably the cache cannot be found, <li>"Temporarily unavailable" - probably the cache cannot be found,
but it may be repaired (then, "Ready to search" will be submitted).</li> but it may be repaired (then, "Ready to search" will be submitted).</li>
<li>"Ready to search" - the cache can be found again.</li> <li>"Ready to search" - the cache can be found again.</li>
<li>"Archived" - the cache cannot be found and probably won't be <li>"Archived" - the cache cannot be found and probably won't be
repaired.</li> repaired.</li>
<li>"Locked" - the cache has been archived and can no longer be logged.</li> <li>"Locked" - the cache has been archived and can no longer be logged.</li>
</ul> </ul>
<p>Other types (used only by some Opencaching installations):</p> <p>Other types (used only by some Opencaching installations):</p>
<ul> <ul>
<li>"Needs maintenance" - a user states that the cache is <li>"Needs maintenance" - a user states that the cache is
in need of maintenance.</li> in need of maintenance.</li>
<li>"Maintenance performed" - the cache owner states that he <li>"Maintenance performed" - the cache owner states that he
had performed the maintenance.</li> had performed the maintenance.</li>
<li>"Moved" - the cache has been moved to a different location.</li> <li>"Moved" - the cache has been moved to a different location.</li>
<li>"OC Team comment" - a comment made by the official OC Team <li>"OC Team comment" - a comment made by the official OC Team
member.</li> member.</li>
<li><i>(to be continued)</i> - this list MAY expand in time! <li><i>(to be continued)</i> - this list MAY expand in time!
Your application should accept unknown log types (you may Your application should accept unknown log types (you may
treat them as "Comment"s).</li> treat them as "Comment"s).</li>
</ul> </ul>
</li> </li>
<li> <li>
<p><b>oc_team_entry</b> - <b>true</b> if the log entry has been made by an <p><b>oc_team_entry</b> - <b>true</b> if the log entry has been made by an
official OC team member and marked as administrative log; <b>false</b> if it has official OC team member and marked as administrative log; <b>false</b> if it has
not been marked.</p> not been marked.</p>
<p>Note: <b>false</b> does NOT mean that it is no administrative log, <p>Note: <b>false</b> does NOT mean that it is no administrative log,
because this flag can be missing for (mostly old) admin logs.</p> because this flag can be missing for (mostly old) admin logs.</p>
</li> </li>
<li> <li>
<p><b>was_recommended</b> - <b>true</b>, if the author included his recommendation <p><b>was_recommended</b> - <b>true</b>, if the author included his recommendation
in this log entry,</p> in this log entry,</p>
</li> </li>
<li><b>comment</b> - <a href='%OKAPI:docurl:html%'>HTML string</a>, text entered <li><b>comment</b> - <a href='%OKAPI:docurl:html%'>HTML string</a>, text entered
with the log entry,</li> with the log entry,</li>
<li> <li>
<p><b>images</b> - list of dictionaries, each dictionary represents one <p><b>images</b> - list of dictionaries, each dictionary represents one
image saved along with the log; each dictionary has the following image saved along with the log; each dictionary has the following
structure:</p> structure:</p>
<ul> <ul>
<li><b>uuid</b> - UUID of the image,</li> <li><b>uuid</b> - UUID of the image,</li>
<li><b>url</b> - URL of the image,</li> <li><b>url</b> - URL of the image,</li>
<li><b>thumb_url</b> - URL of a small (thumb) version of the image,</li> <li><b>thumb_url</b> - URL of a small (thumb) version of the image,</li>
<li><b>caption</b> - plain-text string, caption of the image,</li> <li><b>caption</b> - plain-text string, caption of the image,</li>
<li><b>is_spoiler</b> - boolean, if <b>true</b> then the image is <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 a spoiler image and should not be displayed to the user unless
the user explicitly asks for it.</li> the user explicitly asks for it.</li>
</ul> </ul>
</li> </li>
<li> <li>
<p><b>internal_id</b> - undocumented, you <u>should not</u> use <p><b>internal_id</b> - undocumented, you <u>should not</u> use
this unless you really know you need to. Internal IDs are this unless you really know you need to. Internal IDs are
<b>not</b> unique across various OKAPI installations. <b>not</b> unique across various OKAPI installations.
Try to use UUIDs instead.</p> Try to use UUIDs instead.</p>
</li> </li>
</ul> </ul>
<p>Note, that some fields can change in time (users can edit/delete <p>Note, that some fields can change in time (users can edit/delete
their log entries).</p> their log entries).</p>
<p>If given log entry does not exist, the method will <p>If given log entry does not exist, the method will
respond with an HTTP 400 error.</p> respond with an HTTP 400 error.</p>
</returns> </returns>
</xml> </xml>

View File

@@ -15,58 +15,69 @@ use okapi\services\caches\search\SearchAssistant;
class WebService class WebService
{ {
public static function options() public static function options()
{ {
return array( return array(
'min_auth_level' => 1 'min_auth_level' => 1
); );
} }
public static function call(OkapiRequest $request) public static function call(OkapiRequest $request)
{ {
$cache_code = $request->get_parameter('cache_code'); $cache_code = $request->get_parameter('cache_code');
if (!$cache_code) throw new ParamMissing('cache_code'); if (!$cache_code) throw new ParamMissing('cache_code');
$fields = $request->get_parameter('fields'); $fields = $request->get_parameter('fields');
if (!$fields) $fields = "uuid|date|user|type|comment"; if (!$fields) $fields = "uuid|date|user|type|comment";
$offset = $request->get_parameter('offset'); $offset = $request->get_parameter('offset');
if (!$offset) $offset = "0"; if (!$offset) $offset = "0";
if ((((int)$offset) != $offset) || ((int)$offset) < 0) if ((((int)$offset) != $offset) || ((int)$offset) < 0)
throw new InvalidParam('offset', "Expecting non-negative integer."); throw new InvalidParam('offset', "Expecting non-negative integer.");
$limit = $request->get_parameter('limit'); $limit = $request->get_parameter('limit');
if (!$limit) $limit = "none"; if (!$limit) $limit = "none";
if ($limit == "none") $limit = "999999999"; if ($limit == "none") $limit = "999999999";
if ((((int)$limit) != $limit) || ((int)$limit) < 0) if ((((int)$limit) != $limit) || ((int)$limit) < 0)
throw new InvalidParam('limit', "Expecting non-negative integer or 'none'."); throw new InvalidParam('limit', "Expecting non-negative integer or 'none'.");
# Check if code exists and retrieve cache ID (this will throw # Check if code exists and retrieve cache ID (this will throw
# a proper exception on invalid code). # a proper exception on invalid code).
$cache = OkapiServiceRunner::call('services/caches/geocache', new OkapiInternalRequest( $cache = OkapiServiceRunner::call('services/caches/geocache', new OkapiInternalRequest(
$request->consumer, null, array('cache_code' => $cache_code, 'fields' => 'internal_id'))); $request->consumer, null, array('cache_code' => $cache_code, 'fields' => 'internal_id')));
# Cache exists. Getting the uuids of its logs. # Cache exists. Getting the uuids of its logs.
$log_uuids = Db::select_column(" $log_uuids = Db::select_column("
select uuid select uuid
from cache_logs from cache_logs
where where
cache_id = '".mysql_real_escape_string($cache['internal_id'])."' cache_id = '".mysql_real_escape_string($cache['internal_id'])."'
and ".((Settings::get('OC_BRANCH') == 'oc.pl') ? "deleted = 0" : "true")." and ".((Settings::get('OC_BRANCH') == 'oc.pl') ? "deleted = 0" : "true")."
order by date desc order by date desc
limit $offset, $limit limit $offset, $limit
"); ");
# Getting the logs themselves. Formatting as an ordered list. # Getting the logs themselves. Formatting as an ordered list.
$internal_request = new OkapiInternalRequest( $internal_request = new OkapiInternalRequest(
$request->consumer, $request->token, array('log_uuids' => implode("|", $log_uuids), $request->consumer, $request->token, array('log_uuids' => implode("|", $log_uuids),
'fields' => $fields)); 'fields' => $fields));
$internal_request->skip_limits = true; $internal_request->skip_limits = true;
$logs = OkapiServiceRunner::call('services/logs/entries', $internal_request); $logs = OkapiServiceRunner::call('services/logs/entries', $internal_request);
$results = array(); $results = array();
foreach ($log_uuids as $log_uuid) foreach ($log_uuids as $log_uuid)
$results[] = $logs[$log_uuid]; $results[] = $logs[$log_uuid];
return Okapi::formatted_response($request, $results); /* Handle OCPL's "access logs" feature. */
}
if (
(Settings::get('OC_BRANCH') == 'oc.pl')
&& Settings::get('OCPL_ENABLE_GEOCACHE_ACCESS_LOGS')
&& (count($log_uuids) > 0)
) {
require_once($GLOBALS['rootpath'].'okapi/lib/ocpl_access_logs.php');
\okapi\OCPLAccessLogs::log_geocache_access($request, $cache['internal_id']);
}
return Okapi::formatted_response($request, $results);
}
} }

View File

@@ -1,31 +1,31 @@
<xml> <xml>
<brief>Retrieve all log entries for the specified geocache</brief> <brief>Retrieve all log entries for the specified geocache</brief>
<issue-id>41</issue-id> <issue-id>41</issue-id>
<desc> <desc>
<p>Retrieve the log entries for the specified geocache. Use the offset and <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 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> 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> <p>Log entries are ordered by a descending date of the entry.</p>
</desc> </desc>
<req name='cache_code'> <req name='cache_code'>
<p>Code of the geocache.</p> <p>Code of the geocache.</p>
</req> </req>
<opt name='fields' default='uuid|date|user|type|comment'> <opt name='fields' default='uuid|date|user|type|comment'>
<p>Same as in the services/logs/entry method. Pipe-separated list <p>Same as in the services/logs/entry method. Pipe-separated list
of field names which you are interested with. of field names which you are interested with.
See services/logs/entry method for a list of available values.</p> See services/logs/entry method for a list of available values.</p>
</opt> </opt>
<opt name='offset' default='0'> <opt name='offset' default='0'>
<p>Number of entries to skip at the beginning. Use this along the <b>limit</b> parameter <p>Number of entries to skip at the beginning. Use this along the <b>limit</b> parameter
for pagination.</p> for pagination.</p>
</opt> </opt>
<opt name='limit' default='none'> <opt name='limit' default='none'>
<p>Maximum number of entries to return or <b>none</b> <p>Maximum number of entries to return or <b>none</b>
if you want all the entries.</p> if you want all the entries.</p>
</opt> </opt>
<common-format-params/> <common-format-params/>
<returns> <returns>
<p>A list of log entries, ordered by date. Each log entry is a dictionary of a format <p>A list of log entries, ordered by date. Each log entry is a dictionary of a format
described in the "entry" method.</p> described in the "entry" method.</p>
</returns> </returns>
</xml> </xml>

File diff suppressed because it is too large Load Diff

View File

@@ -1,110 +1,110 @@
<xml> <xml>
<brief>Submit a log entry</brief> <brief>Submit a log entry</brief>
<issue-id>42</issue-id> <issue-id>42</issue-id>
<desc> <desc>
<p>Submit a log entry for the geocache. Please note, that you won't be <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> able to use this method until you learn to handle OAuth.</p>
</desc> </desc>
<req name='cache_code'> <req name='cache_code'>
<p>Code of the geocache.</p> <p>Code of the geocache.</p>
</req> </req>
<req name='logtype'> <req name='logtype'>
<p>Type of an entry. This should be one of:</p> <p>Type of an entry. This should be one of:</p>
<ul> <ul>
<li> <li>
<i>Will attend</i>, <i>Attended</i> or <i>Comment</i> for Event caches; <i>Will attend</i>, <i>Attended</i> or <i>Comment</i> for Event caches;
</li> </li>
<li> <li>
<i>Found it</i>, <i>Didn't find it</i> or <i>Comment</i> for all other <i>Found it</i>, <i>Didn't find it</i> or <i>Comment</i> for all other
cache types. cache types.
</li> </li>
</ul> </ul>
</req> </req>
<opt name='comment'> <opt name='comment'>
<p>Text to be submitted with the log entry.</p> <p>Text to be submitted with the log entry.</p>
</opt> </opt>
<opt name='comment_format' default='auto'> <opt name='comment_format' default='auto'>
<p>Indicates the format of your <b>comment</b>. Three values allowed: <p>Indicates the format of your <b>comment</b>. Three values allowed:
<b>auto</b>, <b>html</b> or <b>plaintext</b>. Usually, you should <b>not</b> <b>auto</b>, <b>html</b> or <b>plaintext</b>. Usually, you should <b>not</b>
use the <b>auto</b> option, because its exact behavior is unspecified use the <b>auto</b> option, because its exact behavior is unspecified
and may depend on the installation and may depend on the installation
(<a href='https://code.google.com/p/opencaching-api/issues/detail?id=124'>more info</a>).</p> (<a href='https://code.google.com/p/opencaching-api/issues/detail?id=124'>more info</a>).</p>
<p><b>Important note:</b> The subset of allowed HTML elements is left undefined <p><b>Important note:</b> The subset of allowed HTML elements is left undefined
and may change in the future. For future-compatibility, you should use only and may change in the future. For future-compatibility, you should use only
basic formatting tags.</p> basic formatting tags.</p>
</opt> </opt>
<opt name='when'> <opt name='when'>
<p>A date and time string. This should be in ISO 8601 format (currently any <p>A date and time string. This should be in ISO 8601 format (currently any
format acceptable by PHP's <a href='http://pl2.php.net/strtotime'>strtotime</a> format acceptable by PHP's <a href='http://pl2.php.net/strtotime'>strtotime</a>
function also will do, but most of them don't handle time zones properly, function also will do, but most of them don't handle time zones properly,
try to use ISO 8601!).</p> try to use ISO 8601!).</p>
<p>Indicates when the cache was found. If given, the log will be published <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 with the given date and time. If not, log will be published using the current
date and time.</p> date and time.</p>
</opt> </opt>
<opt name='password'> <opt name='password'>
<p>Some caches require a password in order to submit a "Found it" log entry. <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 You may check if this cache requires password with <b>req_passwd</b> field
of the services/caches/geocache method.</p> of the services/caches/geocache method.</p>
</opt> </opt>
<opt name='langpref' default='en'> <opt name='langpref' default='en'>
<p>Pipe-separated list of ISO 639-1 language codes. This indicates the <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> order of preference in which language will be chosen for error messages.</p>
</opt> </opt>
<opt name='on_duplicate' default='silent_success'> <opt name='on_duplicate' default='silent_success'>
<p>How should OKAPI react when you are trying to submit a duplicate entry? <p>How should OKAPI react when you are trying to submit a duplicate entry?
One of the following values:</p> One of the following values:</p>
<ul> <ul>
<li><b>silent_success</b> - try to respond with success=true, but don't <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> 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>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 <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 fail in some cases, i.e. when you're trying to submit a "Found it" entry for an
already found cache).</li> already found cache).</li>
</ul> </ul>
<p>Note, that duplicate detection may take the <b>when</b> parameter into account. <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 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> have to supply the "when" parameter if you want duplicate detection to work.</p>
</opt> </opt>
<opt name='rating'> <opt name='rating'>
<p>An integer in range between 1 and 5 - user's optional rating of a found cache.</p> <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>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: 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 <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 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> successfully, but rating will be ignored).</p>
</opt> </opt>
<opt name='recommend' default='false'> <opt name='recommend' default='false'>
<p>Set to <b>true</b> if the user wants to recommend this cache.</p> <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" or "Attended" in order to <p>Important: <b>logtype</b> has to be "Found it" or "Attended" in order to
use this argument. However, some installations do not support recommending use this argument. However, some installations do not support recommending
event caches (if you include a recommendation, the log entry will be posted event caches (if you include a recommendation, the log entry will be posted
successfully, but the recommendation will be <b>ignored</b>).</p> successfully, but the recommendation will be <b>ignored</b>).</p>
<p>Recommending may only succeed when the user meets certain criteria <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 set on him by the OC site. If criteria are not met, the request will
end with user error (HTTP 200, success=false).</p> end with user error (HTTP 200, success=false).</p>
</opt> </opt>
<opt name='needs_maintenance' default='false'> <opt name='needs_maintenance' default='false'>
<p>Set to <b>true</b> if the user thinks that the cache needs some special attension <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> 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 <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 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 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 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> <b>completely ignored</b> (not all OC servers support this feature).</p>
<p>Note: Currently, this is not allowed for Event Caches (you will get a HTTP 200 <p>Note: Currently, this is not allowed for Event Caches (you will get a HTTP 200
"user friendly" response).</p> "user friendly" response).</p>
</opt> </opt>
<common-format-params/> <common-format-params/>
<returns> <returns>
<p>A dictionary of the following structure:</p> <p>A dictionary of the following structure:</p>
<ul> <ul>
<li><b>success</b> - true, if the log entry was submitted successfully,</li> <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 <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> 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> <li><b>log_uuid</b> - ID of the newly created log entry, <b>or null</b>
in case of an error.</li> in case of an error.</li>
</ul> </ul>
</returns> </returns>
</xml> </xml>

View File

@@ -15,62 +15,62 @@ use okapi\services\caches\search\SearchAssistant;
class WebService class WebService
{ {
public static function options() public static function options()
{ {
return array( return array(
'min_auth_level' => 1 'min_auth_level' => 1
); );
} }
public static function call(OkapiRequest $request) public static function call(OkapiRequest $request)
{ {
$user_uuid = $request->get_parameter('user_uuid'); $user_uuid = $request->get_parameter('user_uuid');
if (!$user_uuid) throw new ParamMissing('user_uuid'); if (!$user_uuid) throw new ParamMissing('user_uuid');
$limit = $request->get_parameter('limit'); $limit = $request->get_parameter('limit');
if (!$limit) $limit = "20"; if (!$limit) $limit = "20";
if (!is_numeric($limit)) if (!is_numeric($limit))
throw new InvalidParam('limit', "'$limit'"); throw new InvalidParam('limit', "'$limit'");
$limit = intval($limit); $limit = intval($limit);
if (($limit < 1) || ($limit > 1000)) if (($limit < 1) || ($limit > 1000))
throw new InvalidParam('limit', "Has to be in range 1..1000."); throw new InvalidParam('limit', "Has to be in range 1..1000.");
$offset = $request->get_parameter('offset'); $offset = $request->get_parameter('offset');
if (!$offset) $offset = "0"; if (!$offset) $offset = "0";
if (!is_numeric($offset)) if (!is_numeric($offset))
throw new InvalidParam('offset', "'$offset'"); throw new InvalidParam('offset', "'$offset'");
$offset = intval($offset); $offset = intval($offset);
if ($offset < 0) if ($offset < 0)
throw new InvalidParam('offset', "'$offset'"); throw new InvalidParam('offset', "'$offset'");
# Check if user exists and retrieve user's ID (this will throw # Check if user exists and retrieve user's ID (this will throw
# a proper exception on invalid UUID). # a proper exception on invalid UUID).
$user = OkapiServiceRunner::call('services/users/user', new OkapiInternalRequest( $user = OkapiServiceRunner::call('services/users/user', new OkapiInternalRequest(
$request->consumer, null, array('user_uuid' => $user_uuid, 'fields' => 'internal_id'))); $request->consumer, null, array('user_uuid' => $user_uuid, 'fields' => 'internal_id')));
# User exists. Retrieving logs. # User exists. Retrieving logs.
$rs = Db::query(" $rs = Db::query("
select cl.id, cl.uuid, cl.type, unix_timestamp(cl.date) as date, cl.text, select cl.id, cl.uuid, cl.type, unix_timestamp(cl.date) as date, cl.text,
c.wp_oc as cache_code c.wp_oc as cache_code
from cache_logs cl, caches c from cache_logs cl, caches c
where where
cl.user_id = '".mysql_real_escape_string($user['internal_id'])."' cl.user_id = '".mysql_real_escape_string($user['internal_id'])."'
and ".((Settings::get('OC_BRANCH') == 'oc.pl') ? "cl.deleted = 0" : "true")." and ".((Settings::get('OC_BRANCH') == 'oc.pl') ? "cl.deleted = 0" : "true")."
and c.status in (1,2,3) and c.status in (1,2,3)
and cl.cache_id = c.cache_id and cl.cache_id = c.cache_id
order by cl.date desc order by cl.date desc
limit $offset, $limit limit $offset, $limit
"); ");
$results = array(); $results = array();
while ($row = mysql_fetch_assoc($rs)) while ($row = mysql_fetch_assoc($rs))
{ {
$results[] = array( $results[] = array(
'uuid' => $row['uuid'], 'uuid' => $row['uuid'],
'date' => date('c', $row['date']), 'date' => date('c', $row['date']),
'cache_code' => $row['cache_code'], 'cache_code' => $row['cache_code'],
'type' => Okapi::logtypeid2name($row['type']), 'type' => Okapi::logtypeid2name($row['type']),
'comment' => $row['text'] 'comment' => $row['text']
); );
} }
return Okapi::formatted_response($request, $results); return Okapi::formatted_response($request, $results);
} }
} }

View File

@@ -1,41 +1,45 @@
<xml> <xml>
<brief>Retrieve log entries of a specified user</brief> <brief>Retrieve log entries of a specified user</brief>
<issue-id>80</issue-id> <issue-id>80</issue-id>
<desc> <desc>
<p>Retrieve log entries of a specified user.</p> <p>Retrieve log entries of a specified user.</p>
</desc> </desc>
<req name='user_uuid'> <req name='user_uuid'>
<p>ID of the user. (Use services/users/by_username to get it.)</p> <p>ID of the user. (Use services/users/by_username to get it.)</p>
</req> </req>
<opt name='limit' default='20'> <opt name='limit' default='20'>
<p>Integer N. If given, no more than N logs will be returned (the most recent ones). <p>Integer N. If given, no more than N logs will be returned (the most recent ones).
Maximum allowed value is 1000.</p> Maximum allowed value is 1000.</p>
<p>Note: Some users have thousands of log entries!</p> <p>Note: Some users have thousands of log entries!</p>
</opt> </opt>
<opt name='offset' default='0'> <opt name='offset' default='0'>
<p>Combined with the <b>limit</b> argument, this gives you an abbility to get <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> <b>all</b> log entries of the user (with multiple requests).</p>
</opt> </opt>
<common-format-params/> <common-format-params/>
<returns> <returns>
<p>Log entries. A dictionary of the following format:</p> <p>A list of log entries, ordered by descending date. Each entry is a
<ul> dictionary of the following format:</p>
<li><b>uuid</b> - ID of the log entry,</li>
<li> <ul>
<p><b>date</b> - date and time (ISO 8601) when the log entry was submitted.</p> <li><b>uuid</b> - ID of the log entry,</li>
<p>Please note that log entries often contain dates only (with the times <li>
truncated to midnight, as in the local timezone). In such cases, you may <p><b>date</b> - date and time (ISO 8601) when the log entry was submitted.</p>
want to avoid displaying the time. You may assume that if the <b>date</b> <p>Please note that log entries often contain dates only (with the times
value contains the "00:00:00" string, then it is date-only.</p> truncated to midnight, as in the local timezone). In such cases, you may
</li> want to avoid displaying the time. You may assume that if the <b>date</b>
<li><b>cache_code</b> - code of the geocache,</li> value contains the "00:00:00" string, then it is date-only.</p>
<li> </li>
<p><b>type</b> - string; log type. This could be <b>pretty much <li><b>cache_code</b> - code of the geocache,</li>
everything</b>, but there are some primary types (see logs/entry <li>
method for more info).</p> <p><b>type</b> - string; log type. This could be <b>pretty much
</li> everything</b>, but there are some primary types (see logs/entry
<li><b>comment</b> - <a href='%OKAPI:docurl:html%'>HTML string</a>, text entered method for more info).</p>
with the log entry.</li> </li>
</ul> <li>
</returns> <b>comment</b> - <a href='%OKAPI:docurl:html%'>HTML string</a>, text entered
with the log entry.
</li>
</ul>
</returns>
</xml> </xml>

View File

@@ -10,28 +10,28 @@ use okapi\InvalidParam;
class WebService class WebService
{ {
public static function options() public static function options()
{ {
return array( return array(
'min_auth_level' => 3, 'min_auth_level' => 3,
'token_type' => 'request' 'token_type' => 'request'
); );
} }
public static function call(OkapiRequest $request) public static function call(OkapiRequest $request)
{ {
$verifier = $request->get_parameter('oauth_verifier'); $verifier = $request->get_parameter('oauth_verifier');
if (!$verifier) if (!$verifier)
{ {
# We require the 1.0a flow (throw an error when there is no oauth_verifier). # We require the 1.0a flow (throw an error when there is no oauth_verifier).
throw new ParamMissing("oauth_verifier"); throw new ParamMissing("oauth_verifier");
} }
$new_token = Okapi::$data_store->new_access_token($request->token, $request->consumer, $verifier); $new_token = Okapi::$data_store->new_access_token($request->token, $request->consumer, $verifier);
$response = new OkapiHttpResponse(); $response = new OkapiHttpResponse();
$response->content_type = "text/plain; charset=utf-8"; $response->content_type = "text/plain; charset=utf-8";
$response->body = $new_token; $response->body = $new_token;
return $response; return $response;
} }
} }

View File

@@ -1,20 +1,20 @@
<xml> <xml>
<brief>Exchange authorized Request Token for an Access Token</brief> <brief>Exchange authorized Request Token for an Access Token</brief>
<issue-id>21</issue-id> <issue-id>21</issue-id>
<desc> <desc>
<p>Exchange authorized Request Token for an Access Token. <p>Exchange authorized Request Token for an Access Token.
Access Token can be used to access resources of a user who Access Token can be used to access resources of a user who
authorized the Request Token.</p> authorized the Request Token.</p>
<p>Standard OAuth "Consumer &amp; Request Token" Authorization arguments <p>Standard OAuth "Consumer &amp; Request Token" Authorization arguments
are required.</p> are required.</p>
</desc> </desc>
<req name='oauth_verifier'> <req name='oauth_verifier'>
Consult <a href='http://oauth.net/documentation/spec/'>OAuth documentation</a> for details. Consult <a href='http://oauth.net/documentation/spec/'>OAuth documentation</a> for details.
</req> </req>
<returns> <returns>
<p>Standard OAuth 1.0a Token response - a string in a form-encoded format:</p> <p>Standard OAuth 1.0a Token response - a string in a form-encoded format:</p>
<pre>oauth_token=...&amp;oauth_token_secret=...</pre> <pre>oauth_token=...&amp;oauth_token_secret=...</pre>
<p>You <b>must</b> be prepared that there might be more parameters returned <p>You <b>must</b> be prepared that there might be more parameters returned
in the future (you should ignore them gracefully).</p> in the future (you should ignore them gracefully).</p>
</returns> </returns>
</xml> </xml>

View File

@@ -11,31 +11,31 @@ use okapi\InvalidParam;
class WebService class WebService
{ {
public static function options() public static function options()
{ {
return array( return array(
'min_auth_level' => 0 'min_auth_level' => 0
); );
} }
public static function call(OkapiRequest $request) public static function call(OkapiRequest $request)
{ {
$token_key = $request->get_parameter('oauth_token'); $token_key = $request->get_parameter('oauth_token');
if (!$token_key) if (!$token_key)
throw new ParamMissing("oauth_token"); throw new ParamMissing("oauth_token");
$langpref = $request->get_parameter('langpref'); $langpref = $request->get_parameter('langpref');
$interactivity = $request->get_parameter('interactivity'); $interactivity = $request->get_parameter('interactivity');
if (!$interactivity) $interactivity = 'minimal'; if (!$interactivity) $interactivity = 'minimal';
if (!in_array($interactivity, array('minimal', 'confirm_user'))) if (!in_array($interactivity, array('minimal', 'confirm_user')))
throw new InvalidParam('interactivity', $interactivity); throw new InvalidParam('interactivity', $interactivity);
# Redirect to the "apps" folder. This is done there (not here) # Redirect to the "apps" folder. This is done there (not here)
# because: 1) we don't want any cookie or session-handling # because: 1) we don't want any cookie or session-handling
# done in the "services" folder. 2) "services" don't display # done in the "services" folder. 2) "services" don't display
# any interactive webpages, they just return the result. # any interactive webpages, they just return the result.
return new OkapiRedirectResponse(Settings::get('SITE_URL')."okapi/apps/authorize". return new OkapiRedirectResponse(Settings::get('SITE_URL')."okapi/apps/authorize".
"?oauth_token=".$token_key.(($langpref != null) ? "&langpref=".$langpref : ""). "?oauth_token=".$token_key.(($langpref != null) ? "&langpref=".$langpref : "").
"&interactivity=".$interactivity); "&interactivity=".$interactivity);
} }
} }

View File

@@ -1,66 +1,66 @@
<xml> <xml>
<brief>Authorize the Request Token</brief> <brief>Authorize the Request Token</brief>
<issue-id>22</issue-id> <issue-id>22</issue-id>
<desc> <desc>
<p>Unlike other methods, the <b>authorize</b> method is to be executed <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 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> 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> <p>Once the User is redirected to this URL, several things will happen:</p>
<ul> <ul>
<li>If he's not already logged in, he will be asked to do so.</li> <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 <li>OKAPI will check if the User haven't previously granted your
application access to his Opencaching account.</li> application access to his Opencaching account.</li>
<li>If User did not previously authorize your application, OKAPI <li>If User did not previously authorize your application, OKAPI
will display an "Authorization Request" form to the User. User will display an "Authorization Request" form to the User. User
will be presented with a choice to allow or not to allow your will be presented with a choice to allow or not to allow your
application access to his account.</li> application access to his account.</li>
<li>If the User clicks one of these two options ("allow" or "don't allow"), <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 he's browser will be redirected to the <b>callback_url</b> you defined
while getting your Request Token. while getting your Request Token.
If you did not provide a callback (in other word, provided "oob"), If you did not provide a callback (in other word, provided "oob"),
user will be redirected to a default "authorized" page, where he user will be redirected to a default "authorized" page, where he
will be presented with an oauth_verifier (user will know it 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> by name of a <b>PIN code</b>) and asked to type it into your application.</li>
</ul> </ul>
</desc> </desc>
<req name='oauth_token'> <req name='oauth_token'>
Consult <a href='http://oauth.net/documentation/spec/'>OAuth documentation</a> for details. Consult <a href='http://oauth.net/documentation/spec/'>OAuth documentation</a> for details.
</req> </req>
<opt name='interactivity' default='minimal'> <opt name='interactivity' default='minimal'>
<p>Currently, one of the following values:</p> <p>Currently, one of the following values:</p>
<ul> <ul>
<li><b>minimal</b> - OKAPI will use as little interactivity as it can. <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 It will assume that currently logged in user is the user which you
want to authorize. If the user has already authorized your application, want to authorize. If the user has already authorized your application,
he will not be asked to do this again.</li> 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 <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 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 offer to authorize a different user (e.g. by automatically logging out
the user who is currently logged in).</li> the user who is currently logged in).</li>
</ul> </ul>
</opt> </opt>
<opt name='langpref'> <opt name='langpref'>
<p>Pipe-separated list of ISO 639-1 language codes. This indicates the <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> 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 <p>By default, OKAPI will display the page in the primary native language of local
Opencaching installation.</p> Opencaching installation.</p>
</opt> </opt>
<returns> <returns>
<p>Technically, an HTTP 302 Redirect - it will direct user's browser to the OKAPI apps <p>Technically, an HTTP 302 Redirect - it will direct user's browser to the OKAPI apps
authorization page.</p> authorization page.</p>
<p>Whether with callback_url or with a manual user entry - you will get <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 your <b>oauth_verifier</b>, which allows you to continue the 3-legged
authentication dance.</p> authentication dance.</p>
<p>If you used <b>callback_url</b>, you should wait for an HTTP GET request, <p>If you used <b>callback_url</b>, you should wait for an HTTP GET request,
with one additional GET parameter appended:</p> with one additional GET parameter appended:</p>
<ul> <ul>
<li><b>oauth_token</b> - the Request Token that has been just authorized,</li> <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> <li><b>oauth_verifier</b> - the PIN code required to get an Access Token.</li>
</ul> </ul>
<p>OR, in case when user denied the request:</p> <p>OR, in case when user denied the request:</p>
<ul> <ul>
<li><b>oauth_token</b> - the Request Token,</li> <li><b>oauth_token</b> - the Request Token,</li>
<li><b>error</b> - codename of an error - <b>access_denied</b>.</li> <li><b>error</b> - codename of an error - <b>access_denied</b>.</li>
</ul> </ul>
</returns> </returns>
</xml> </xml>

View File

@@ -10,27 +10,27 @@ use okapi\InvalidParam;
class WebService class WebService
{ {
public static function options() public static function options()
{ {
return array( return array(
'min_auth_level' => 2 'min_auth_level' => 2
); );
} }
public static function call(OkapiRequest $request) public static function call(OkapiRequest $request)
{ {
$callback = $request->get_parameter('oauth_callback'); $callback = $request->get_parameter('oauth_callback');
if (!$callback) if (!$callback)
{ {
# We require the 1.0a flow (throw an error when there is no oauth_callback). # We require the 1.0a flow (throw an error when there is no oauth_callback).
throw new ParamMissing("oauth_callback"); throw new ParamMissing("oauth_callback");
} }
$new_token = Okapi::$data_store->new_request_token($request->consumer, $callback); $new_token = Okapi::$data_store->new_request_token($request->consumer, $callback);
$response = new OkapiHttpResponse(); $response = new OkapiHttpResponse();
$response->content_type = "text/plain; charset=utf-8"; $response->content_type = "text/plain; charset=utf-8";
$response->body = $new_token."&oauth_callback_confirmed=true"; $response->body = $new_token."&oauth_callback_confirmed=true";
return $response; return $response;
} }
} }

View File

@@ -1,24 +1,24 @@
<xml> <xml>
<brief>Get a new unauthorized OAuth Request Token</brief> <brief>Get a new unauthorized OAuth Request Token</brief>
<issue-id>23</issue-id> <issue-id>23</issue-id>
<desc> <desc>
<p>Get a new unauthorized OAuth Request Token. This token is bound to <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 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 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> the user, then it has to be exchanged for an Access Token.</p>
</desc> </desc>
<req name='oauth_callback'> <req name='oauth_callback'>
<p>URL which you want a User to be redirected to after a <p>URL which you want a User to be redirected to after a
successful Request Token Authorization (see "authorize" method). successful Request Token Authorization (see "authorize" method).
If the client is unable to receive callbacks, the parameter If the client is unable to receive callbacks, the parameter
must be set to "oob", OKAPI will provide a user with a must be set to "oob", OKAPI will provide a user with a
PIN code (oauth_verifier) in this case.</p> PIN code (oauth_verifier) in this case.</p>
Consult <a href='http://oauth.net/documentation/spec/'>OAuth documentation</a> for details. Consult <a href='http://oauth.net/documentation/spec/'>OAuth documentation</a> for details.
</req> </req>
<returns> <returns>
<p>Standard OAuth 1.0a Token response - a string in a form-encoded format:</p> <p>Standard OAuth 1.0a Token response - a string in a form-encoded format:</p>
<pre>oauth_token=...&amp;oauth_token_secret=...&amp;oauth_callback_confirmed=true</pre> <pre>oauth_token=...&amp;oauth_token_secret=...&amp;oauth_callback_confirmed=true</pre>
<p>You <b>must</b> be prepared that there might be more parameters returned <p>You <b>must</b> be prepared that there might be more parameters returned
in the future (you should ignore them gracefully).</p> in the future (you should ignore them gracefully).</p>
</returns> </returns>
</xml> </xml>

View File

@@ -17,37 +17,37 @@ use okapi\services\replicate\ReplicateCommon;
class WebService class WebService
{ {
public static function options() public static function options()
{ {
return array( return array(
'min_auth_level' => 1 'min_auth_level' => 1
); );
} }
public static function call(OkapiRequest $request) public static function call(OkapiRequest $request)
{ {
require_once('replicate_common.inc.php'); require_once('replicate_common.inc.php');
$since = $request->get_parameter('since'); $since = $request->get_parameter('since');
if ($since === null) throw new ParamMissing('since'); if ($since === null) throw new ParamMissing('since');
if ((int)$since != $since) throw new InvalidParam('since'); if ((int)$since != $since) throw new InvalidParam('since');
# Let's check the $since parameter. # Let's check the $since parameter.
if (!ReplicateCommon::check_since_param($since)) if (!ReplicateCommon::check_since_param($since))
throw new BadRequest("The 'since' parameter is too old. You must update your database more frequently."); 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). # Select a best chunk for the given $since, get the chunk from the database (or cache).
list($from, $to) = ReplicateCommon::select_best_chunk($since); list($from, $to) = ReplicateCommon::select_best_chunk($since);
$clog_entries = ReplicateCommon::get_chunk($from, $to); $clog_entries = ReplicateCommon::get_chunk($from, $to);
$result = array( $result = array(
'changelog' => &$clog_entries, 'changelog' => &$clog_entries,
'revision' => $to + 0, 'revision' => $to + 0,
'more' => $to < ReplicateCommon::get_revision(), 'more' => $to < ReplicateCommon::get_revision(),
); );
return Okapi::formatted_response($request, $result); return Okapi::formatted_response($request, $result);
} }
} }

View File

@@ -1,133 +1,140 @@
<xml> <xml>
<brief>Get the list of changes for your database</brief> <brief>Get the list of changes for your database</brief>
<issue-id>109</issue-id> <issue-id>109</issue-id>
<desc> <desc>
<p><b>Beta status.</b> Get the list of changes to be replayed on your own copy <p>Get the list of changes to be <u>replayed on your own copy
of Opencaching database. Use this method periodically (ex. once per hour) to of Opencaching database</u>. Use this method periodically (e.g. every 5 minutes) to
keep your database in sync with ours.</p> keep your database in sync with ours.</p>
<p>For some applications it might be desireable to have a quick access to the entire <p>For some applications it might be desireable to have a quick access to the entire
Opencaching database (instead of querying for specific portions of it). You may use Opencaching database (instead of querying for specific portions of it). You may use
OKAPI's <b>replicate</b> module to achive this effect. The <b>changelog</b> method 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 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> 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> <p>A couple of things for you to remember:</p>
<ul> <ul>
<li>You <b>must</b> update your database frequently for this method to work. <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> We don't keep the changelog indefinitelly. You must update at least once a 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 <li>You <b>should not</b> update your database more frequently than once per
every 5 minutes anyway.</li> 5 minutes. This could kill our servers, and it wouldn't do you any good, since
</ul> 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 <p>Let's assume that you already have a copy of OKAPI database, but it's
your copy to the most current state.</p> 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 <p>Changelog is the list of all changes which appeared in the OKAPI database
list of changes and to replay them on your copy of our database. After you do since the last time you downloaded it. What you have to do is to download this
this, your database is up-to-date.</p> 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> <p>We use <b>revision</b> numbers to keep track of all the versions of the
number along with it. You must keep this number carefully, because you need database. Every time we update a database, the <b>revision</b> is increased.
it in order for us to generate a proper changelog for you next time you ask You will also receive this number every time you fetch a changelog or a fulldump
for it.</p> of our database. 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
<p>Example. This is a valid list of requests you should issue and their for it!</p>
responses:</p>
<ul> <p><b>Example.</b> This is a valid list of requests you should issue and their
<li><b>fulldump</b> - you receive a fulldump of our database with the responses:</p>
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 <ul>
recorded since revision 238004. It responds with the list of changes and the <li><b>fulldump</b> - you receive a fulldump of our database with the
new revision number 238017. You replay the changes on your database.</li> revision number 238004. <b>You will call this method only once</b> (to
<li>You wait for some time between requesting the changelog again.</li> initiate your copy of the database).</li>
<li><b>changelog</b>?since=238017 - etc.</li>
</ul> <li><b>changelog</b>?since=238004 - OKAPI checks if there were any changes
</desc> recorded since revision 238004. It responds with the list of changes and the
<req name='since'> new revision number 238017. You replay the changes on your database.</li>
<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> <li>You wait for some time between requesting the changelog again.</li>
<p>Old revisions are deleted, the <b>since</b> argument MUST referer to a revision no older <li>Upon your next update, you'll ask for <b>changelog</b>?since=238017, etc.</li>
than 10 days. You will have to download a fulldump if you have an older copy </ul>
(shame on you!).</p> </desc>
</req> <req name='since'>
<common-format-params/> <p>Current revision of your database. This should be the same as the value
<returns> of <b>revision</b> attribute, which you received with your previous update.</p>
<p>A dictionary of the following structure:</p>
<ul> <p>Old revisions are deleted, the <b>since</b> argument MUST referer to a revision no older
<li> than 10 days. You will have to download a fulldump if you have an older copy
<p><b>changelog</b> - a <b>list</b> of dictionaries. Each dictionary has the following structure:</p> (shame on you!).</p>
<ul> </req>
<li> <common-format-params/>
<p><b>object_type</b> - string, object type to which the change refers to. One of the following values:</p> <returns>
<ul> <p>A dictionary of the following structure:</p>
<li><b>geocache</b> - this change refers to a geocache object,</li> <ul>
<li><b>log_entry</b> - this change refers to a log entry.</li> <li>
</ul> <p><b>changelog</b> - a <b>list</b> of dictionaries. Each dictionary has the following structure:</p>
<p>More object types will come in the future. You should ignore all changelog <ul>
entries with an unknown object_type.</p> <li>
</li> <p><b>object_type</b> - string, object type to which the change refers to. One of the following values:</p>
<li> <ul>
<p><b>object_key</b> - a dictionary of fields which compose the primary key for the object. <li><b>geocache</b> - this change refers to a geocache object,</li>
This will be the <b>code</b> field for the <b>geocache</b> object, and <b>uuid</b> field <li><b>log_entry</b> - this change refers to a log entry.</li>
for the <b>log_entry</b> object.</p> </ul>
</li> <p>More object types will come in the future. You should ignore all changelog
<li> entries with an unknown object_type.</p>
<p><b>change_type</b> - string, the type of the change. One of the following values:</p> </li>
<ul> <li>
<li><b>replace</b> - the object was inserted or updated. You should check if you <p><b>object_key</b> - a dictionary of fields which compose the primary key for the object.
already have the object in your database. If you have it, you should update its This will be the <b>code</b> field for the <b>geocache</b> object, and <b>uuid</b> field
fields accordingly. If you don't, you should create it.</li> for the <b>log_entry</b> object.</p>
<li><b>delete</b> - the object was deleted. You should check if you already have </li>
the object in your database. If you do, you should delete it.</li> <li>
</ul> <p><b>change_type</b> - string, the type of the change. One of the following values:</p>
</li> <ul>
<li> <li><b>replace</b> - the object was inserted or updated. You should check if you
<p><b>fields</b> - a dictionary of fields associated with the object (present only already have the object in your database. If you have it, you should update its
if <b>change_type</b> equals <b>replace</b>).</p> fields accordingly. If you don't, you should create it.</li>
<ul> <li><b>delete</b> - the object was deleted. You should check if you already have
<li>For <b>geocache</b> objects, this might be any subset of fields described the object in your database. If you do, you should delete it.</li>
in the services/caches/geocache method. Note that not all of these fields will </ul>
be included here (i.e. latest_logs will not).</li> </li>
<li>For <b>log_entry</b> objects, this might be any subset of fields described <li>
in the services/logs/logs method, <b>plus</b> additional <b>cache_code</b> field, <p><b>data</b> - a dictionary of fields associated with the object (present only
the code of the geocache to which the log entry refers to.</li> if <b>change_type</b> equals <b>replace</b>).</p>
</ul> <ul>
<p>In theory this dictionary should contain only these fields which actually changed. <li>For <b>geocache</b> objects, this might be any subset of fields described
In truth, it MAY contain all the other fields too. This behavior may change in future.</p> in the services/caches/geocache method. Note that not all of these fields will
</li> be included here (i.e. latest_logs will not).</li>
</ul> <li>For <b>log_entry</b> objects, this might be any subset of fields described
<p>You should iterate through this changelog sequentially. All the changes should be applied in the services/logs/logs method, <b>plus</b> additional <b>cache_code</b> field,
in the same order as they were listed in the changelog. Single object may appear multiple times the code of the geocache to which the log entry refers to.</li>
inside the changelog. Changelog will contain all the changes which occured since the time you </ul>
specified in the <b>since</b> parameter and it MAY contain some more, which were submitted <p>In theory this dictionary should contain only these fields which actually changed.
<b>before</b> this date (see below). The changes which are unnecessary MAY be skipped (ex. when In truth, it MAY contain all the other fields too. This behavior may change in future.</p>
cache description changes multiple times, we may want to include only the last change).</p> </li>
</li> </ul>
<li> <p>You should iterate through this changelog sequentially. All the changes should be applied
<p><b>revision</b> - this is the revision number which you should use in the <b>since</b> parameter in the same order as they were listed in the changelog. Single object may appear multiple times
when you call this method next time.</p> inside the changelog. Changelog will contain all the changes which occured since the time you
</li> specified in the <b>since</b> parameter and it MAY contain some more, which were submitted
<li> <b>before</b> this date (see below). The changes which are unnecessary MAY be skipped (ex. when
<p><b>more</b> - boolean. If <b>false</b>, then it means that the entire changelog had been cache description changes multiple times, we may want to include only the last change).</p>
pulled. If <b>true</b>, then there are more items waiting to be pulled - you should rerun this </li>
method again (with the value of <b>revision</b> inserted in the <b>since</b> parameter).</p> <li>
<p>The changelog is usually tiny, but it also might be huge at times. It may even <p><b>revision</b> - this is the revision number which you should use in the <b>since</b> parameter
contain all the caches in the database (i.e. if we decide to do some changes on all when you call this method next time.</p>
caches in a bulk). This is the reason why we introduced the <b>more</b> attribute. It makes it </li>
easier to parse the changelog sequentially.</p> <li>
</li> <p><b>more</b> - boolean. If <b>false</b>, then it means that the entire changelog had been
</ul> pulled. If <b>true</b>, then there are more items waiting to be pulled - you should rerun this
<p>The response MAY contain changes recorded before the revision which you stated in your <b>since</b> method again (with the value of <b>revision</b> inserted in the <b>since</b> parameter).</p>
parameter. This is due to caching. If we already have a response ready, that was produced for a <p>The changelog is usually tiny, but it also might be huge at times. It may even
"since" parameter which was a little more in the past, we may want to return this cached copy, contain all the caches in the database (i.e. if we decide to do some changes on all
instead of producing a new one. It will be faster for us, and you shouldn't even notice. We will caches in a bulk). This is the reason why we introduced the <b>more</b> attribute. It makes it
still make sure to set the <b>revision</b> and <b>more</b> attributes in a correct way, which easier to parse the changelog sequentially.</p>
in turn will make you query us again, if you need to.</p> </li>
</returns> </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> </xml>

View File

@@ -14,67 +14,67 @@ use okapi\BadRequest;
class WebService class WebService
{ {
public static function options() public static function options()
{ {
return array( return array(
'min_auth_level' => 1 'min_auth_level' => 1
); );
} }
private static function count_calls($consumer_key, $days) private static function count_calls($consumer_key, $days)
{ {
return ( return (
Db::select_value(" Db::select_value("
select count(*) select count(*)
from okapi_stats_temp from okapi_stats_temp
where where
consumer_key = '".mysql_real_escape_string($consumer_key)."' consumer_key = '".mysql_real_escape_string($consumer_key)."'
and service_name='services/replicate/fulldump' and service_name='services/replicate/fulldump'
") ")
+ +
Db::select_value(" Db::select_value("
select sum(total_calls) select sum(total_calls)
from okapi_stats_hourly from okapi_stats_hourly
where where
consumer_key = '".mysql_real_escape_string($consumer_key)."' consumer_key = '".mysql_real_escape_string($consumer_key)."'
and service_name='services/replicate/fulldump' and service_name='services/replicate/fulldump'
and period_start > date_add(now(), interval -$days day) and period_start > date_add(now(), interval -$days day)
limit 1 limit 1
") ")
); );
} }
public static function call(OkapiRequest $request) public static function call(OkapiRequest $request)
{ {
require_once('replicate_common.inc.php'); require_once('replicate_common.inc.php');
$data = Cache::get("last_fulldump"); $data = Cache::get("last_fulldump");
if ($data == null) if ($data == null)
throw new BadRequest("No fulldump found. Try again later. If this doesn't help ". throw new BadRequest("No fulldump found. Try again later. If this doesn't help ".
"contact site administrator and/or OKAPI developers."); "contact site administrator and/or OKAPI developers.");
# Check consumer's quota # Check consumer's quota
$please = $request->get_parameter('pleeaase'); $please = $request->get_parameter('pleeaase');
if ($please != 'true') if ($please != 'true')
{ {
$not_good = 3 < self::count_calls($request->consumer->key, 30); $not_good = 3 < self::count_calls($request->consumer->key, 30);
if ($not_good) if ($not_good)
throw new BadRequest("Consumer's monthly quota exceeded. Try later or call with '&pleeaase=true'."); throw new BadRequest("Consumer's monthly quota exceeded. Try later or call with '&pleeaase=true'.");
} }
else else
{ {
$not_good = 5 < self::count_calls($request->consumer->key, 1); $not_good = 5 < self::count_calls($request->consumer->key, 1);
if ($not_good) if ($not_good)
throw new BadRequest("No more please. Seriously, dude..."); throw new BadRequest("No more please. Seriously, dude...");
} }
$response = new OkapiHttpResponse(); $response = new OkapiHttpResponse();
$response->content_type = $data['meta']['content_type']; $response->content_type = $data['meta']['content_type'];
$response->content_disposition = 'attachment; filename="'.$data['meta']['public_filename'].'"'; $response->content_disposition = 'attachment; filename="'.$data['meta']['public_filename'].'"';
$response->stream_length = $data['meta']['compressed_size']; $response->stream_length = $data['meta']['compressed_size'];
$response->body = fopen($data['meta']['filepath'], "rb"); $response->body = fopen($data['meta']['filepath'], "rb");
$response->allow_gzip = false; $response->allow_gzip = false;
return $response; return $response;
} }
} }

View File

@@ -1,63 +1,68 @@
<xml> <xml>
<brief>Download OKAPI database snapshot</brief> <brief>Download OKAPI database snapshot</brief>
<issue-id>110</issue-id> <issue-id>110</issue-id>
<desc> <desc>
<p>Download the latest snapshot of OKAPI database. You should call this method <p>Download the latest snapshot of OKAPI database. You should call this method
only once.</p> only once.</p>
<p>For some applications it might be desireable to have a quick access to the entire
Opencaching database (instead of querying for specific portions of it). You may use
OKAPI's <b>replicate</b> module to achive this condition. 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> There is a remote possibility that this method MAY change in <p>For some applications it might be desireable to have a quick access to the entire
a non-backward-compatible way or it might even get removed. We don't plan on doing Opencaching database (instead of querying for specific portions of it). You may use
this, but we might be forced to (i.e. to prevent abuse).</p> OKAPI's <b>replicate</b> module to achive this state. The <b>changelog</b> method
is the primary replication service which you will use. However, to correctly set up
<p><b>Note:</b> The cache descriptions will be generated using the <b>attribution_append=static</b> your first database copy, you will need to use the <b>fulldump</b> method.</p>
parameter (see the geocache method). Full attributions are not always suitable for replication,
since they may contain dates on some installations <p>A couple of things for you to remember:</p>
(<a href='https://code.google.com/p/opencaching-api/issues/detail?id=178'>why?</a>).
To make sure that static attributions are enough, consult the local Data <ul>
Licence (the Sign Up page).</p> <li>Currently, this method is available <b>for developers only</b>,
not for the individual users. We don't want users to download a fresh snapshot of
<p>A couple of things for you to remember:</p> 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
<ul> must use <b>your own server</b> for serving fulldump requests. (You don't
<li>Currently, this functionality is available <b>for developers only</b>, have to use your server to relay changelog requests.)</li>
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. <li>Fulldump is a copy of the entire database. We generate such copy once every couple of
If you want to enable such features for your users, you can do it, but you days. This copy if intended for you to make a fresh start only. Later, you
must use <b>your own server</b> for data traffic (especially, fulldump requests).</li> must use the <b>changelog</b> method to keep your data up-to-date.</li>
<li>Fulldump is a copy of the entire database. We generate such copy once every couple of <li>Every time our database is extended (new fields or new object types), and you want to
days. This copy if intended for you to start only, later you must use the changelog to make use of these new fields, you are of course allowed to download a fulldump copy again.</li>
keep it up-to-date.</li>
<li>There is no XMLMAP version of this file. JSON only.</li>
<li>Every time our database is extended (new fields or new object types), and you want to </ul>
make use of these new fields, you are of course allowed to download a fulldump copy again.</li>
<p><b>Additional notes on data attribution:</b> Cache descriptions will be
<li>There is no XMLMAP version of this file.</li> generated using the <b>attribution_append=static</b> parameter (see the
</ul> geocache method). This is because the full attributions are not always suitable
</desc> for replication, since they may contain dynamically changing dates on some
<returns> installations (<a href='https://code.google.com/p/opencaching-api/issues/detail?id=178'>why?</a>).
<p>Archive with JSON-encoded files. File named <b>index.json</b> will contain a dictionary To make sure that static attributions are enough, consult the local Data
of the following structure:</p> Licence (the Sign Up page).</p>
<ul> </desc>
<li><b>revision</b> - revision of the database snapshot contained in the archive,</li> <returns>
<li><b>data_files</b> - list of filenames which contain the changelog entries for <p>Compressed archive with JSON-encoded files. File named <b>index.json</b> will
you to parse. Each file contains a JSON-encoded list of changelog entries, in format contain a dictionary of the following structure:</p>
described in the <b>changelog</b> method ("replace" only).</li>
<li><b>meta</b> - a dictionary of other meta data, not important.</li> <ul>
</ul> <li><b>revision</b> - revision of the database snapshot contained in the archive,</li>
<p>Note: We use TGZ or TBZ2 format to encode this archive:</p>
<ul> <li><b>data_files</b> - list of filenames which contain the changelog entries for
<li><b>TGZ</b> archive (more commonly known as <b>.tar.gz</b> archive) is a TAR you to parse. Each file contains a JSON-encoded list of changelog entries, in format
archive compressed with GZIP.</li> described in the <b>changelog</b> method ("replace" only).</li>
<li><b>TBZ2</b> archive (more commonly known as <b>.tar.bz2</b> archive) is a TAR
archive compressed with BZIP2.</li> <li><b>meta</b> - a dictionary of other meta data, not important.</li>
</ul> </ul>
<p>There are many tools available for handling these archives. In Linux, try "tar -xf filename".</p>
</returns> <p>Note: We use TGZ or TBZ2 format to encode this archive:</p>
<ul>
<li><b>TGZ</b> archive (also known as <b>.tar.gz</b>) is a TAR
archive compressed with GZIP.</li>
<li><b>TBZ2</b> archive (also known as <b>.tar.bz2</b>) 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> </xml>

View File

@@ -18,35 +18,35 @@ use okapi\services\replicate\ReplicateCommon;
class WebService class WebService
{ {
public static function options() public static function options()
{ {
return array( return array(
'min_auth_level' => 1 'min_auth_level' => 1
); );
} }
public static function call(OkapiRequest $request) public static function call(OkapiRequest $request)
{ {
require_once('replicate_common.inc.php'); require_once('replicate_common.inc.php');
$result = array(); $result = array();
$result['changelog'] = array( $result['changelog'] = array(
'min_since' => ReplicateCommon::get_min_since(), 'min_since' => ReplicateCommon::get_min_since(),
'revision' => ReplicateCommon::get_revision(), 'revision' => ReplicateCommon::get_revision(),
); );
$dump = Cache::get("last_fulldump"); $dump = Cache::get("last_fulldump");
if ($dump) if ($dump)
{ {
$result['latest_fulldump'] = array( $result['latest_fulldump'] = array(
'revision' => $dump['revision'], 'revision' => $dump['revision'],
'generated_at' => $dump['meta']['generated_at'], 'generated_at' => $dump['meta']['generated_at'],
'size' => $dump['meta']['compressed_size'], 'size' => $dump['meta']['compressed_size'],
'size_uncompressed' => $dump['meta']['uncompressed_size'], 'size_uncompressed' => $dump['meta']['uncompressed_size'],
); );
} else { } else {
$result['latest_fulldump'] = null; $result['latest_fulldump'] = null;
} }
return Okapi::formatted_response($request, $result); return Okapi::formatted_response($request, $result);
} }
} }

View File

@@ -1,31 +1,31 @@
<xml> <xml>
<brief>Get information on current changelog and fulldump state</brief> <brief>Get information on current changelog and fulldump state</brief>
<issue-id>111</issue-id> <issue-id>111</issue-id>
<desc> <desc>
<p><b>Beta status.</b> Get information on current changelog and fulldump state.</p> <p><b>Beta status.</b> Get information on current changelog and fulldump state.</p>
</desc> </desc>
<common-format-params/> <common-format-params/>
<returns> <returns>
<p>A dictionary of the following structure:</p> <p>A dictionary of the following structure:</p>
<ul> <ul>
<li> <li>
<p><b>changelog</b> - a dictionary of the following structure:</p> <p><b>changelog</b> - a dictionary of the following structure:</p>
<ul> <ul>
<li><b>min_since</b> - the lowest allowed value for the <b>since</b> attribute <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> (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 <li><b>revision</b> - the current revision of the database (the ID of the
newest changelog entry kept in our database),</li> newest changelog entry kept in our database),</li>
</ul> </ul>
</li> </li>
<li> <li>
<p><b>latest_fulldump</b> - a dictionary of the following structure:</p> <p><b>latest_fulldump</b> - a dictionary of the following structure:</p>
<ul> <ul>
<li><b>revision</b> - revision of the database saved in the latest fulldump,</li> <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>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</b> - size of the file, in bytes,</li>
<li><b>size_uncompressed</b> - approximate size of the uncompressed contents, in bytes.</li> <li><b>size_uncompressed</b> - approximate size of the uncompressed contents, in bytes.</li>
</ul> </ul>
</li> </li>
</ul> </ul>
</returns> </returns>
</xml> </xml>

File diff suppressed because it is too large Load Diff

View File

@@ -13,27 +13,27 @@ use okapi\services\caches\search\SearchAssistant;
class WebService class WebService
{ {
public static function options() public static function options()
{ {
return array( return array(
'min_auth_level' => 1 'min_auth_level' => 1
); );
} }
public static function call(OkapiRequest $request) public static function call(OkapiRequest $request)
{ {
$internal_id = $request->get_parameter('internal_id'); $internal_id = $request->get_parameter('internal_id');
if (!$internal_id) throw new ParamMissing('internal_id'); if (!$internal_id) throw new ParamMissing('internal_id');
$fields = $request->get_parameter('fields'); $fields = $request->get_parameter('fields');
# There's no need to validate the fields parameter. # There's no need to validate the fields parameter.
$results = OkapiServiceRunner::call('services/users/by_internal_ids', new OkapiInternalRequest( $results = OkapiServiceRunner::call('services/users/by_internal_ids', new OkapiInternalRequest(
$request->consumer, $request->token, array('internal_ids' => $internal_id, $request->consumer, $request->token, array('internal_ids' => $internal_id,
'fields' => $fields))); 'fields' => $fields)));
$result = $results[$internal_id]; $result = $results[$internal_id];
if ($result == null) if ($result == null)
throw new InvalidParam('internal_id', "There is no user by this internal_id."); throw new InvalidParam('internal_id', "There is no user by this internal_id.");
return Okapi::formatted_response($request, $result); return Okapi::formatted_response($request, $result);
} }
} }

View File

@@ -1,18 +1,18 @@
<xml> <xml>
<brief>Find single user, by his internal_id</brief> <brief>Find single user, by his internal_id</brief>
<issue-id>44</issue-id> <issue-id>44</issue-id>
<desc> <desc>
<p>Retrieve information on a single user, referencing him by his <b>internal</b> ID.</p> <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 <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 normal ID - <b>user_uuid</b>. Also, internal IDs are not universally unique and are
a poor choice for a key.</p> a poor choice for a key.</p>
</desc> </desc>
<req name='internal_id'>Internal ID</req> <req name='internal_id'>Internal ID</req>
<req name='fields'> <req name='fields'>
<p>See services/users/user method.</p> <p>See services/users/user method.</p>
</req> </req>
<common-format-params/> <common-format-params/>
<returns> <returns>
<p>See services/users/user method.</p> <p>See services/users/user method.</p>
</returns> </returns>
</xml> </xml>

View File

@@ -13,56 +13,56 @@ use okapi\services\caches\search\SearchAssistant;
class WebService class WebService
{ {
public static function options() public static function options()
{ {
return array( return array(
'min_auth_level' => 1 'min_auth_level' => 1
); );
} }
public static function call(OkapiRequest $request) public static function call(OkapiRequest $request)
{ {
$internal_ids = $request->get_parameter('internal_ids'); $internal_ids = $request->get_parameter('internal_ids');
if (!$internal_ids) throw new ParamMissing('internal_ids'); if (!$internal_ids) throw new ParamMissing('internal_ids');
$internal_ids = explode("|", $internal_ids); $internal_ids = explode("|", $internal_ids);
if (count($internal_ids) > 500) if (count($internal_ids) > 500)
throw new InvalidParam('internal_ids', "Maximum allowed number of referenced users ". throw new InvalidParam('internal_ids', "Maximum allowed number of referenced users ".
"is 500. You provided ".count($internal_ids)." references."); "is 500. You provided ".count($internal_ids)." references.");
$fields = $request->get_parameter('fields'); $fields = $request->get_parameter('fields');
if (!$fields) if (!$fields)
throw new ParamMissing('fields'); throw new ParamMissing('fields');
# There's no need to validate the fields parameter as the 'users' # There's no need to validate the fields parameter as the 'users'
# method does this (it will raise a proper exception on invalid values). # method does this (it will raise a proper exception on invalid values).
$rs = Db::query(" $rs = Db::query("
select user_id, uuid select user_id, uuid
from user from user
where user_id in ('".implode("','", array_map('mysql_real_escape_string', $internal_ids))."') where user_id in ('".implode("','", array_map('mysql_real_escape_string', $internal_ids))."')
"); ");
$internalid2useruuid = array(); $internalid2useruuid = array();
while ($row = mysql_fetch_assoc($rs)) while ($row = mysql_fetch_assoc($rs))
{ {
$internalid2useruuid[$row['user_id']] = $row['uuid']; $internalid2useruuid[$row['user_id']] = $row['uuid'];
} }
mysql_free_result($rs); mysql_free_result($rs);
# Retrieve data on given user_uuids. # Retrieve data on given user_uuids.
$id_results = OkapiServiceRunner::call('services/users/users', new OkapiInternalRequest( $id_results = OkapiServiceRunner::call('services/users/users', new OkapiInternalRequest(
$request->consumer, $request->token, array('user_uuids' => implode("|", array_values($internalid2useruuid)), $request->consumer, $request->token, array('user_uuids' => implode("|", array_values($internalid2useruuid)),
'fields' => $fields))); 'fields' => $fields)));
# Map user_uuids to internal_ids. Also check which internal_ids were not found # Map user_uuids to internal_ids. Also check which internal_ids were not found
# and mark them with null. # and mark them with null.
$results = array(); $results = array();
foreach ($internal_ids as $internal_id) foreach ($internal_ids as $internal_id)
{ {
if (!isset($internalid2useruuid[$internal_id])) if (!isset($internalid2useruuid[$internal_id]))
$results[$internal_id] = null; $results[$internal_id] = null;
else else
$results[$internal_id] = $id_results[$internalid2useruuid[$internal_id]]; $results[$internal_id] = $id_results[$internalid2useruuid[$internal_id]];
} }
return Okapi::formatted_response($request, $results); return Okapi::formatted_response($request, $results);
} }
} }

View File

@@ -1,20 +1,20 @@
<xml> <xml>
<brief>Find multiple users, by their internal IDs</brief> <brief>Find multiple users, by their internal IDs</brief>
<issue-id>45</issue-id> <issue-id>45</issue-id>
<desc> <desc>
<p>This method works like the services/users/by_internal_id method, but works <p>This method works like the services/users/by_internal_id method, but works
with multiple users (instead of only one).</p> with multiple users (instead of only one).</p>
</desc> </desc>
<req name='internal_ids'> <req name='internal_ids'>
<p>Pipe-separated list of internal user IDs. No more than 500 are allowed.</p> <p>Pipe-separated list of internal user IDs. No more than 500 are allowed.</p>
</req> </req>
<req name='fields'> <req name='fields'>
<p>See services/users/user method.</p> <p>See services/users/user method.</p>
</req> </req>
<common-format-params/> <common-format-params/>
<returns> <returns>
<p>A dictionary. Internal IDs you provide will be mapped to dictionary keys, <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> 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> <p>Value of <b>null</b> means that the given internal ID haven't been found.</p>
</returns> </returns>
</xml> </xml>

View File

@@ -16,27 +16,27 @@ use okapi\services\caches\search\SearchAssistant;
class WebService class WebService
{ {
public static function options() public static function options()
{ {
return array( return array(
'min_auth_level' => 1 'min_auth_level' => 1
); );
} }
public static function call(OkapiRequest $request) public static function call(OkapiRequest $request)
{ {
$username = $request->get_parameter('username'); $username = $request->get_parameter('username');
if (!$username) throw new ParamMissing('username'); if (!$username) throw new ParamMissing('username');
$fields = $request->get_parameter('fields'); $fields = $request->get_parameter('fields');
# There's no need to validate the fields parameter. # There's no need to validate the fields parameter.
$results = OkapiServiceRunner::call('services/users/by_usernames', new OkapiInternalRequest( $results = OkapiServiceRunner::call('services/users/by_usernames', new OkapiInternalRequest(
$request->consumer, $request->token, array('usernames' => $username, $request->consumer, $request->token, array('usernames' => $username,
'fields' => $fields))); 'fields' => $fields)));
$result = $results[$username]; $result = $results[$username];
if ($result == null) if ($result == null)
throw new InvalidParam('username', "There is no user by this username."); throw new InvalidParam('username', "There is no user by this username.");
return Okapi::formatted_response($request, $result); return Okapi::formatted_response($request, $result);
} }
} }

View File

@@ -1,22 +1,22 @@
<xml> <xml>
<brief>Find single user, by his/her username</brief> <brief>Find single user, by his/her username</brief>
<issue-id>24</issue-id> <issue-id>24</issue-id>
<desc> <desc>
<p>Retrieve information on a single user, referencing him by his username.</p> <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><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 <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 of referenced users. You should not store usernames in your database, it is
much safer to reference them by their IDs.</p> much safer to reference them by their IDs.</p>
</desc> </desc>
<req name='username'>Name of the user (case insensitive).</req> <req name='username'>Name of the user (case insensitive).</req>
<req name='fields'> <req name='fields'>
<p>Same as in the services/users/user method. Pipe-separated list <p>Same as in the services/users/user method. Pipe-separated list
of field names which you are interested with. of field names which you are interested with.
See services/users/user method for a list available values.</p> See services/users/user method for a list available values.</p>
</req> </req>
<common-format-params/> <common-format-params/>
<returns> <returns>
<p>A dictionary of fields you have selected.</p> <p>A dictionary of fields you have selected.</p>
<p>If given user does not exist, the method will respond with an HTTP 400 error.</p> <p>If given user does not exist, the method will respond with an HTTP 400 error.</p>
</returns> </returns>
</xml> </xml>

View File

@@ -13,63 +13,63 @@ use okapi\OkapiInternalRequest;
class WebService class WebService
{ {
public static function options() public static function options()
{ {
return array( return array(
'min_auth_level' => 1 'min_auth_level' => 1
); );
} }
public static function call(OkapiRequest $request) public static function call(OkapiRequest $request)
{ {
$usernames = $request->get_parameter('usernames'); $usernames = $request->get_parameter('usernames');
if (!$usernames) throw new ParamMissing('usernames'); if (!$usernames) throw new ParamMissing('usernames');
$usernames = explode("|", $usernames); $usernames = explode("|", $usernames);
if (count($usernames) > 500) if (count($usernames) > 500)
throw new InvalidParam('usernames', "Maximum allowed number of referenced users ". throw new InvalidParam('usernames', "Maximum allowed number of referenced users ".
"is 500. You provided ".count($usernames)." usernames."); "is 500. You provided ".count($usernames)." usernames.");
$fields = $request->get_parameter('fields'); $fields = $request->get_parameter('fields');
if (!$fields) if (!$fields)
throw new ParamMissing('fields'); throw new ParamMissing('fields');
# There's no need to validate the fields parameter as the 'users' # There's no need to validate the fields parameter as the 'users'
# method does this (it will raise a proper exception on invalid values). # method does this (it will raise a proper exception on invalid values).
$rs = Db::query(" $rs = Db::query("
select username, uuid select username, uuid
from user from user
where username collate utf8_general_ci in ('".implode("','", array_map('mysql_real_escape_string', $usernames))."') where username collate utf8_general_ci in ('".implode("','", array_map('mysql_real_escape_string', $usernames))."')
"); ");
$lower_username2useruuid = array(); $lower_username2useruuid = array();
while ($row = mysql_fetch_assoc($rs)) while ($row = mysql_fetch_assoc($rs))
{ {
$lower_username2useruuid[mb_strtolower($row['username'], 'utf-8')] = $row['uuid']; $lower_username2useruuid[mb_strtolower($row['username'], 'utf-8')] = $row['uuid'];
} }
mysql_free_result($rs); mysql_free_result($rs);
# Retrieve data for the found user_uuids. # Retrieve data for the found user_uuids.
if (count($lower_username2useruuid) > 0) if (count($lower_username2useruuid) > 0)
{ {
$id_results = OkapiServiceRunner::call('services/users/users', new OkapiInternalRequest( $id_results = OkapiServiceRunner::call('services/users/users', new OkapiInternalRequest(
$request->consumer, $request->token, array('user_uuids' => implode("|", array_values($lower_username2useruuid)), $request->consumer, $request->token, array('user_uuids' => implode("|", array_values($lower_username2useruuid)),
'fields' => $fields))); 'fields' => $fields)));
} else { } else {
$id_results = array(); $id_results = array();
} }
# Map user_uuids back to usernames. Also check which usernames were not found # Map user_uuids back to usernames. Also check which usernames were not found
# and mark them with null. # and mark them with null.
$results = array(); $results = array();
foreach ($usernames as $username) foreach ($usernames as $username)
{ {
if (!isset($lower_username2useruuid[mb_strtolower($username, 'utf-8')])) if (!isset($lower_username2useruuid[mb_strtolower($username, 'utf-8')]))
$results[$username] = null; $results[$username] = null;
else else
$results[$username] = $id_results[$lower_username2useruuid[mb_strtolower($username, 'utf-8')]]; $results[$username] = $id_results[$lower_username2useruuid[mb_strtolower($username, 'utf-8')]];
} }
return Okapi::formatted_response($request, $results); return Okapi::formatted_response($request, $results);
} }
} }

View File

@@ -1,24 +1,24 @@
<xml> <xml>
<brief>Find multiple users, by their usernames</brief> <brief>Find multiple users, by their usernames</brief>
<issue-id>25</issue-id> <issue-id>25</issue-id>
<desc> <desc>
<p>This method works like the services/users/by_username method, but works <p>This method works like the services/users/by_username method, but works
with multiple users (instead of only one).</p> with multiple users (instead of only one).</p>
</desc> </desc>
<req name='usernames'> <req name='usernames'>
<p>Pipe-separated list of usernames. No more than 500 are allowed.</p> <p>Pipe-separated list of usernames. No more than 500 are allowed.</p>
</req> </req>
<req name='fields'> <req name='fields'>
<p>Same as in the services/users/user method. Pipe-separated list <p>Same as in the services/users/user method. Pipe-separated list
of field names which you are interested with. of field names which you are interested with.
See services/users/user method for a list available values.</p> See services/users/user method for a list available values.</p>
</req> </req>
<common-format-params/> <common-format-params/>
<returns> <returns>
<p>A dictionary. Usernames you provide will be mapped to dictionary keys, <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> 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. <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 (This behavior is different than in the services/users/by_username method, which
responds with an HTTP 400 error in such case.)</p> responds with an HTTP 400 error in such case.)</p>
</returns> </returns>
</xml> </xml>

View File

@@ -16,41 +16,41 @@ use okapi\services\caches\search\SearchAssistant;
class WebService class WebService
{ {
public static function options() public static function options()
{ {
return array( return array(
'min_auth_level' => 1 'min_auth_level' => 1
); );
} }
public static function call(OkapiRequest $request) public static function call(OkapiRequest $request)
{ {
$user_uuid = $request->get_parameter('user_uuid'); $user_uuid = $request->get_parameter('user_uuid');
if (!$user_uuid) if (!$user_uuid)
{ {
if ($request->token) if ($request->token)
{ {
$tmp = OkapiServiceRunner::call('services/users/by_internal_id', new OkapiInternalRequest( $tmp = OkapiServiceRunner::call('services/users/by_internal_id', new OkapiInternalRequest(
$request->consumer, null, array('internal_id' => $request->token->user_id, 'fields' => 'uuid'))); $request->consumer, null, array('internal_id' => $request->token->user_id, 'fields' => 'uuid')));
$user_uuid = $tmp['uuid']; $user_uuid = $tmp['uuid'];
} }
else else
{ {
throw new BadRequest("You must either: 1. supply the user_uuid argument, or " throw new BadRequest("You must either: 1. supply the user_uuid argument, or "
."2. sign your request with an Access Token."); ."2. sign your request with an Access Token.");
} }
} }
$fields = $request->get_parameter('fields'); $fields = $request->get_parameter('fields');
# There's no need to validate the fields parameter as the 'users' # There's no need to validate the fields parameter as the 'users'
# method does this (it will raise a proper exception on invalid values). # method does this (it will raise a proper exception on invalid values).
$results = OkapiServiceRunner::call('services/users/users', new OkapiInternalRequest( $results = OkapiServiceRunner::call('services/users/users', new OkapiInternalRequest(
$request->consumer, $request->token, array('user_uuids' => $user_uuid, $request->consumer, $request->token, array('user_uuids' => $user_uuid,
'fields' => $fields))); 'fields' => $fields)));
$result = $results[$user_uuid]; $result = $results[$user_uuid];
if ($result == null) if ($result == null)
throw new InvalidParam('user_uuid', "There is no user by this ID."); throw new InvalidParam('user_uuid', "There is no user by this ID.");
return Okapi::formatted_response($request, $result); return Okapi::formatted_response($request, $result);
} }
} }

View File

@@ -1,45 +1,45 @@
<xml> <xml>
<brief>Retrieve information on a single user</brief> <brief>Retrieve information on a single user</brief>
<issue-id>26</issue-id> <issue-id>26</issue-id>
<desc> <desc>
Retrieve information on a single user. This method might be also used 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 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> issued. To do this, include the Access Token in your request, and <b>don't</b>
include the user_uuid argument. include the user_uuid argument.
</desc> </desc>
<req name='fields'> <req name='fields'>
<p>Pipe-separated list of field names which you are interested with. <p>Pipe-separated list of field names which you are interested with.
Selected fields will be included in the response. See below for the Selected fields will be included in the response. See below for the
list of available fields.</p> list of available fields.</p>
</req> </req>
<opt name='user_uuid'> <opt name='user_uuid'>
<p>ID of the user.</p> <p>ID of the user.</p>
<p>This parameter is optional only when you sign your <p>This parameter is optional only when you sign your
request with an Access Token (Level 3 Authentication). Otherwise, request with an Access Token (Level 3 Authentication). Otherwise,
it is <b>required</b>.</p> it is <b>required</b>.</p>
</opt> </opt>
<common-format-params/> <common-format-params/>
<returns> <returns>
<p>A dictionary of fields you have selected. Currently available fields:</p> <p>A dictionary of fields you have selected. Currently available fields:</p>
<ul> <ul>
<li><b>uuid</b> - ID of the user,</li> <li><b>uuid</b> - ID of the user,</li>
<li><b>username</b> - username (login) 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><b>profile_url</b> - URL of the user's Opencaching profile page,</li>
<li> <li>
<p><b>is_admin</b> - boolean; true is user has admin privileges.</p> <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 <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 and only for the user of your Access Token. For all other reads, is_admin
will equal <b>null</b>.</p> will equal <b>null</b>.</p>
</li> </li>
<li><b>internal_id</b> - internal ID of the user (<b>DO NOT</b> use this! <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> use the <b>uuid</b> as the user identifier),</li>
<li><b>caches_found</b> - number of "Found it" and "Attended" log entries,</li> <li><b>caches_found</b> - number of "Found it" and "Attended" log entries,</li>
<li><b>caches_notfound</b> - number of "Didn't find 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>caches_hidden</b> - number of caches owned,</li>
<li><b>rcmds_given</b> - number of recommendations given.</li> <li><b>rcmds_given</b> - number of recommendations given.</li>
</ul> </ul>
<p>If given user does not exist, the method will respond with an HTTP 400 error.</p> <p>If given user does not exist, the method will respond with an HTTP 400 error.</p>
</returns> </returns>
</xml> </xml>

View File

@@ -13,154 +13,154 @@ use okapi\services\caches\search\SearchAssistant;
class WebService class WebService
{ {
public static function options() public static function options()
{ {
return array( return array(
'min_auth_level' => 1 'min_auth_level' => 1
); );
} }
private static $valid_field_names = array('uuid', 'username', 'profile_url', 'internal_id', 'is_admin', private static $valid_field_names = array('uuid', 'username', 'profile_url', 'internal_id', 'is_admin',
'caches_found', 'caches_notfound', 'caches_hidden', 'rcmds_given'); 'caches_found', 'caches_notfound', 'caches_hidden', 'rcmds_given');
public static function call(OkapiRequest $request) public static function call(OkapiRequest $request)
{ {
$user_uuids = $request->get_parameter('user_uuids'); $user_uuids = $request->get_parameter('user_uuids');
if (!$user_uuids) throw new ParamMissing('user_uuids'); if (!$user_uuids) throw new ParamMissing('user_uuids');
$user_uuids = explode("|", $user_uuids); $user_uuids = explode("|", $user_uuids);
if (count($user_uuids) > 500) if (count($user_uuids) > 500)
throw new InvalidParam('user_uuids', "Maximum allowed number of referenced users ". throw new InvalidParam('user_uuids', "Maximum allowed number of referenced users ".
"is 500. You provided ".count($user_uuids)." user IDs."); "is 500. You provided ".count($user_uuids)." user IDs.");
$fields = $request->get_parameter('fields'); $fields = $request->get_parameter('fields');
if (!$fields) if (!$fields)
throw new ParamMissing('fields'); throw new ParamMissing('fields');
$fields = explode("|", $fields); $fields = explode("|", $fields);
foreach ($fields as $field) foreach ($fields as $field)
if (!in_array($field, self::$valid_field_names)) if (!in_array($field, self::$valid_field_names))
throw new InvalidParam('fields', "'$field' is not a valid field code."); throw new InvalidParam('fields', "'$field' is not a valid field code.");
$rs = Db::query(" $rs = Db::query("
select user_id, uuid, username, admin select user_id, uuid, username, admin
from user from user
where uuid in ('".implode("','", array_map('mysql_real_escape_string', $user_uuids))."') where uuid in ('".implode("','", array_map('mysql_real_escape_string', $user_uuids))."')
"); ");
$results = array(); $results = array();
$id2uuid = array(); $id2uuid = array();
$uuid2id = array(); $uuid2id = array();
while ($row = mysql_fetch_assoc($rs)) while ($row = mysql_fetch_assoc($rs))
{ {
$id2uuid[$row['user_id']] = $row['uuid']; $id2uuid[$row['user_id']] = $row['uuid'];
$uuid2id[$row['uuid']] = $row['user_id']; $uuid2id[$row['uuid']] = $row['user_id'];
$entry = array(); $entry = array();
foreach ($fields as $field) foreach ($fields as $field)
{ {
switch ($field) switch ($field)
{ {
case 'uuid': $entry['uuid'] = $row['uuid']; break; case 'uuid': $entry['uuid'] = $row['uuid']; break;
case 'username': $entry['username'] = $row['username']; break; case 'username': $entry['username'] = $row['username']; break;
case 'profile_url': $entry['profile_url'] = Settings::get('SITE_URL')."viewprofile.php?userid=".$row['user_id']; break; case 'profile_url': $entry['profile_url'] = Settings::get('SITE_URL')."viewprofile.php?userid=".$row['user_id']; break;
case 'is_admin': case 'is_admin':
if (!$request->token) { if (!$request->token) {
$entry['is_admin'] = null; $entry['is_admin'] = null;
} elseif ($request->token->user_id != $row['user_id']) { } elseif ($request->token->user_id != $row['user_id']) {
$entry['is_admin'] = null; $entry['is_admin'] = null;
} else { } else {
$entry['is_admin'] = $row['admin'] ? true : false; $entry['is_admin'] = $row['admin'] ? true : false;
} }
break; break;
case 'internal_id': $entry['internal_id'] = $row['user_id']; break; case 'internal_id': $entry['internal_id'] = $row['user_id']; break;
case 'caches_found': /* handled separately */ break; case 'caches_found': /* handled separately */ break;
case 'caches_notfound': /* handled separately */ break; case 'caches_notfound': /* handled separately */ break;
case 'caches_hidden': /* handled separately */ break; case 'caches_hidden': /* handled separately */ break;
case 'rcmds_given': /* handled separately */ break; case 'rcmds_given': /* handled separately */ break;
default: throw new Exception("Missing field case: ".$field); default: throw new Exception("Missing field case: ".$field);
} }
} }
$results[$row['uuid']] = $entry; $results[$row['uuid']] = $entry;
} }
mysql_free_result($rs); mysql_free_result($rs);
# caches_found, caches_notfound, caches_hidden # caches_found, caches_notfound, caches_hidden
if (in_array('caches_found', $fields) || in_array('caches_notfound', $fields) || in_array('caches_hidden', $fields) if (in_array('caches_found', $fields) || in_array('caches_notfound', $fields) || in_array('caches_hidden', $fields)
|| in_array('rcmds_given', $fields)) || in_array('rcmds_given', $fields))
{ {
# We will load all these stats together. Then we may remove these which # We will load all these stats together. Then we may remove these which
# the user doesn't need. # the user doesn't need.
$extras = array(); $extras = array();
if (Settings::get('OC_BRANCH') == 'oc.pl') if (Settings::get('OC_BRANCH') == 'oc.pl')
{ {
# OCPL stores user stats in 'user' table. # OCPL stores user stats in 'user' table.
$rs = Db::query(" $rs = Db::query("
select user_id, founds_count, notfounds_count, hidden_count select user_id, founds_count, notfounds_count, hidden_count
from user from user
where user_id in ('".implode("','", array_map('mysql_real_escape_string', array_keys($id2uuid)))."') where user_id in ('".implode("','", array_map('mysql_real_escape_string', array_keys($id2uuid)))."')
"); ");
} }
else else
{ {
# OCDE stores user stats in 'stat_user' table. # OCDE stores user stats in 'stat_user' table.
$rs = Db::query(" $rs = Db::query("
select select
u.user_id, u.user_id,
ifnull(su.found, 0) as founds_count, ifnull(su.found, 0) as founds_count,
ifnull(su.notfound, 0) as notfounds_count, ifnull(su.notfound, 0) as notfounds_count,
ifnull(su.hidden, 0) as hidden_count ifnull(su.hidden, 0) as hidden_count
from from
user u user u
left join stat_user su left join stat_user su
on su.user_id = u.user_id on su.user_id = u.user_id
where u.user_id in ('".implode("','", array_map('mysql_real_escape_string', array_keys($id2uuid)))."') where u.user_id in ('".implode("','", array_map('mysql_real_escape_string', array_keys($id2uuid)))."')
"); ");
} }
while ($row = mysql_fetch_assoc($rs)) while ($row = mysql_fetch_assoc($rs))
{ {
$extras[$row['user_id']] = array();; $extras[$row['user_id']] = array();;
$extra_ref = &$extras[$row['user_id']]; $extra_ref = &$extras[$row['user_id']];
$extra_ref['caches_found'] = 0 + $row['founds_count']; $extra_ref['caches_found'] = 0 + $row['founds_count'];
$extra_ref['caches_notfound'] = 0 + $row['notfounds_count']; $extra_ref['caches_notfound'] = 0 + $row['notfounds_count'];
$extra_ref['caches_hidden'] = 0 + $row['hidden_count']; $extra_ref['caches_hidden'] = 0 + $row['hidden_count'];
} }
mysql_free_result($rs); mysql_free_result($rs);
if (in_array('rcmds_given', $fields)) if (in_array('rcmds_given', $fields))
{ {
$rs = Db::query(" $rs = Db::query("
select user_id, count(*) as rcmds_given select user_id, count(*) as rcmds_given
from cache_rating from cache_rating
where user_id in ('".implode("','", array_map('mysql_real_escape_string', array_keys($id2uuid)))."') where user_id in ('".implode("','", array_map('mysql_real_escape_string', array_keys($id2uuid)))."')
group by user_id group by user_id
"); ");
$rcmds_counts = array(); $rcmds_counts = array();
while ($row = mysql_fetch_assoc($rs)) while ($row = mysql_fetch_assoc($rs))
$rcmds_counts[$row['user_id']] = $row['rcmds_given']; $rcmds_counts[$row['user_id']] = $row['rcmds_given'];
foreach ($extras as $user_id => &$extra_ref) foreach ($extras as $user_id => &$extra_ref)
{ {
$extra_ref['rcmds_given'] = isset($rcmds_counts[$user_id]) ? 0 + $rcmds_counts[$user_id] : 0; $extra_ref['rcmds_given'] = isset($rcmds_counts[$user_id]) ? 0 + $rcmds_counts[$user_id] : 0;
} }
} }
# "Apply" only those fields which the consumer wanted. # "Apply" only those fields which the consumer wanted.
foreach (array('caches_found', 'caches_notfound', 'caches_hidden', 'rcmds_given') as $field) foreach (array('caches_found', 'caches_notfound', 'caches_hidden', 'rcmds_given') as $field)
{ {
if (!in_array($field, $fields)) if (!in_array($field, $fields))
continue; continue;
foreach ($results as $uuid => &$result_ref) foreach ($results as $uuid => &$result_ref)
$result_ref[$field] = $extras[$uuid2id[$uuid]][$field]; $result_ref[$field] = $extras[$uuid2id[$uuid]][$field];
} }
} }
# Check which user IDs were not found and mark them with null. # Check which user IDs were not found and mark them with null.
foreach ($user_uuids as $user_uuid) foreach ($user_uuids as $user_uuid)
if (!isset($results[$user_uuid])) if (!isset($results[$user_uuid]))
$results[$user_uuid] = null; $results[$user_uuid] = null;
return Okapi::formatted_response($request, $results); return Okapi::formatted_response($request, $results);
} }
} }

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