From ccf5cf33a32a50d2b84d346dea274b506840e620 Mon Sep 17 00:00:00 2001 From: Wojciech Rygielski Date: Wed, 8 Oct 2014 10:06:07 +0200 Subject: [PATCH] OKAPI Project update (r1031) --- htdocs/okapi/controller.php | 102 +- htdocs/okapi/core.php | 3661 ++++++------ htdocs/okapi/cronjobs.php | 1194 ++-- htdocs/okapi/datastore.php | 324 +- htdocs/okapi/facade.php | 324 +- htdocs/okapi/lib/oc_session.php | 34 +- .../de_DE/LC_MESSAGES/okapi_messages.mo | Bin 8979 -> 9251 bytes .../de_DE/LC_MESSAGES/okapi_messages.po | 203 +- .../it_IT/LC_MESSAGES/okapi_messages.mo | Bin 8246 -> 8423 bytes .../it_IT/LC_MESSAGES/okapi_messages.po | 258 +- htdocs/okapi/locale/locales.php | 90 +- .../nl_NL/LC_MESSAGES/okapi_messages.mo | Bin 6212 -> 8949 bytes .../nl_NL/LC_MESSAGES/okapi_messages.po | 271 +- .../pl_PL/LC_MESSAGES/okapi_messages.mo | Bin 9123 -> 10407 bytes .../pl_PL/LC_MESSAGES/okapi_messages.po | 179 +- htdocs/okapi/oauth.php | 3 + htdocs/okapi/service_runner.php | 339 +- htdocs/okapi/services/apiref/issue.php | 52 +- htdocs/okapi/services/apiref/issue.xml | 62 +- htdocs/okapi/services/apiref/method.php | 346 +- htdocs/okapi/services/apiref/method.xml | 106 +- htdocs/okapi/services/apiref/method_index.php | 56 +- htdocs/okapi/services/apiref/method_index.xml | 30 +- htdocs/okapi/services/apisrv/installation.php | 30 +- htdocs/okapi/services/apisrv/installation.xml | 58 +- .../okapi/services/apisrv/installations.php | 176 +- .../okapi/services/apisrv/installations.xml | 62 +- htdocs/okapi/services/apisrv/stats.php | 86 +- htdocs/okapi/services/apisrv/stats.xml | 36 +- .../okapi/services/attrs/attr_helper.inc.php | 486 +- .../services/attrs/attribute-definitions.xml | 5229 +++++++++-------- htdocs/okapi/services/attrs/attribute.php | 68 +- htdocs/okapi/services/attrs/attribute.xml | 264 +- .../okapi/services/attrs/attribute_index.php | 86 +- .../okapi/services/attrs/attribute_index.xml | 66 +- htdocs/okapi/services/attrs/attributes.php | 188 +- htdocs/okapi/services/attrs/attributes.xml | 56 +- .../services/caches/formatters/garmin.php | 346 +- .../services/caches/formatters/garmin.xml | 98 +- .../okapi/services/caches/formatters/gpx.php | 610 +- .../okapi/services/caches/formatters/gpx.xml | 451 +- .../caches/formatters/gpxfile.tpl.php | 369 +- htdocs/okapi/services/caches/geocache.php | 88 +- htdocs/okapi/services/caches/geocache.xml | 845 +-- htdocs/okapi/services/caches/geocaches.php | 2641 +++++---- htdocs/okapi/services/caches/geocaches.xml | 101 +- .../caches/map/replicate_listener.inc.php | 406 +- htdocs/okapi/services/caches/map/tile.php | 314 +- htdocs/okapi/services/caches/map/tile.xml | 32 +- .../services/caches/map/tilerenderer.inc.php | 1214 ++-- .../services/caches/map/tiletree.inc.php | 489 +- htdocs/okapi/services/caches/mark.php | 126 +- htdocs/okapi/services/caches/mark.xml | 62 +- htdocs/okapi/services/caches/search/all.php | 25 +- htdocs/okapi/services/caches/search/all.xml | 414 +- htdocs/okapi/services/caches/search/bbox.php | 133 +- htdocs/okapi/services/caches/search/bbox.xml | 68 +- .../okapi/services/caches/search/by_urls.php | 290 +- .../okapi/services/caches/search/by_urls.xml | 60 +- .../okapi/services/caches/search/nearest.php | 131 +- .../okapi/services/caches/search/nearest.xml | 70 +- htdocs/okapi/services/caches/search/save.php | 286 +- htdocs/okapi/services/caches/search/save.xml | 68 +- .../services/caches/search/searching.inc.php | 1294 ++-- .../caches/shortcuts/search_and_retrieve.php | 194 +- .../caches/shortcuts/search_and_retrieve.xml | 126 +- htdocs/okapi/services/logs/entries.php | 264 +- htdocs/okapi/services/logs/entries.xml | 50 +- htdocs/okapi/services/logs/entry.php | 40 +- htdocs/okapi/services/logs/entry.xml | 230 +- htdocs/okapi/services/logs/logs.php | 103 +- htdocs/okapi/services/logs/logs.xml | 58 +- htdocs/okapi/services/logs/submit.php | 1280 ++-- htdocs/okapi/services/logs/submit.xml | 214 +- htdocs/okapi/services/logs/userlogs.php | 108 +- htdocs/okapi/services/logs/userlogs.xml | 82 +- htdocs/okapi/services/oauth/access_token.php | 42 +- htdocs/okapi/services/oauth/access_token.xml | 36 +- htdocs/okapi/services/oauth/authorize.php | 48 +- htdocs/okapi/services/oauth/authorize.xml | 128 +- htdocs/okapi/services/oauth/request_token.php | 40 +- htdocs/okapi/services/oauth/request_token.xml | 44 +- htdocs/okapi/services/replicate/changelog.php | 50 +- htdocs/okapi/services/replicate/changelog.xml | 269 +- htdocs/okapi/services/replicate/fulldump.php | 114 +- htdocs/okapi/services/replicate/fulldump.xml | 125 +- htdocs/okapi/services/replicate/info.php | 56 +- htdocs/okapi/services/replicate/info.xml | 58 +- .../replicate/replicate_common.inc.php | 999 ++-- .../okapi/services/users/by_internal_id.php | 40 +- .../okapi/services/users/by_internal_id.xml | 32 +- .../okapi/services/users/by_internal_ids.php | 92 +- .../okapi/services/users/by_internal_ids.xml | 36 +- htdocs/okapi/services/users/by_username.php | 40 +- htdocs/okapi/services/users/by_username.xml | 40 +- htdocs/okapi/services/users/by_usernames.php | 102 +- htdocs/okapi/services/users/by_usernames.xml | 44 +- htdocs/okapi/services/users/user.php | 68 +- htdocs/okapi/services/users/user.xml | 86 +- htdocs/okapi/services/users/users.php | 270 +- htdocs/okapi/services/users/users.xml | 46 +- htdocs/okapi/settings.php | 456 +- htdocs/okapi/static/common.js | 66 +- .../static/examples/javascript_nearest.html | 158 +- htdocs/okapi/urls.php | 42 +- htdocs/okapi/views/apps/authorize.php | 300 +- htdocs/okapi/views/apps/authorize.tpl.php | 122 +- htdocs/okapi/views/apps/authorized.php | 84 +- htdocs/okapi/views/apps/authorized.tpl.php | 64 +- htdocs/okapi/views/apps/index.php | 84 +- htdocs/okapi/views/apps/index.tpl.php | 106 +- htdocs/okapi/views/apps/revoke_access.php | 58 +- htdocs/okapi/views/cron5.php | 22 +- htdocs/okapi/views/devel/attrlist.php | 336 +- htdocs/okapi/views/devel/clogentry.php | 32 +- htdocs/okapi/views/devel/comparator.inc.php | 1386 ++--- htdocs/okapi/views/devel/cronreport.php | 114 +- htdocs/okapi/views/devel/dbstruct.php | 248 +- htdocs/okapi/views/devel/tilereport.php | 228 +- htdocs/okapi/views/examples.php | 38 +- htdocs/okapi/views/examples.tpl.php | 96 +- htdocs/okapi/views/http404.php | 34 +- htdocs/okapi/views/http404.tpl.php | 54 +- htdocs/okapi/views/index.php | 14 +- htdocs/okapi/views/installations_box.tpl.php | 16 +- htdocs/okapi/views/introduction.php | 40 +- htdocs/okapi/views/introduction.tpl.php | 472 +- htdocs/okapi/views/menu.inc.php | 104 +- htdocs/okapi/views/method_call.php | 20 +- htdocs/okapi/views/method_doc.php | 54 +- htdocs/okapi/views/method_doc.tpl.php | 167 +- htdocs/okapi/views/signup.php | 116 +- htdocs/okapi/views/signup.tpl.php | 236 +- htdocs/okapi/views/tilestress.php | 124 +- htdocs/okapi/views/update.php | 1210 ++-- 135 files changed, 19432 insertions(+), 18479 deletions(-) diff --git a/htdocs/okapi/controller.php b/htdocs/okapi/controller.php index 539ec67e..828f9b19 100644 --- a/htdocs/okapi/controller.php +++ b/htdocs/okapi/controller.php @@ -23,75 +23,75 @@ require_once($GLOBALS['rootpath'].'okapi/urls.php'); if (ob_list_handlers() == array('default output handler')) { - # We will assume that this one comes from "output_buffering" being turned on - # in PHP config. This is very common and probably is good for most other OC - # pages. But we don't need it in OKAPI. We will just turn this off. + # We will assume that this one comes from "output_buffering" being turned on + # in PHP config. This is very common and probably is good for most other OC + # pages. But we don't need it in OKAPI. We will just turn this off. - ob_end_clean(); + ob_end_clean(); } class OkapiScriptEntryPointController { - public static function dispatch_request($uri) - { - # Chop off the ?args=... part. + public static function dispatch_request($uri) + { + # Chop off the ?args=... part. - if (strpos($uri, '?') !== false) - $uri = substr($uri, 0, strpos($uri, '?')); + if (strpos($uri, '?') !== false) + $uri = substr($uri, 0, strpos($uri, '?')); - # Chop off everything before "/okapi/". This should work for okay for most "weird" - # server configurations. It will also address a more subtle issue described here: - # http://stackoverflow.com/questions/8040461/request-uri-unexpectedly-contains-fqdn + # Chop off everything before "/okapi/". This should work for okay for most "weird" + # server configurations. It will also address a more subtle issue described here: + # http://stackoverflow.com/questions/8040461/request-uri-unexpectedly-contains-fqdn - if (strpos($uri, "/okapi/") !== false) - $uri = substr($uri, strpos($uri, "/okapi/")); + if (strpos($uri, "/okapi/") !== false) + $uri = substr($uri, strpos($uri, "/okapi/")); - # Make sure we're in the right directory (.htaccess should make sure of that). + # Make sure we're in the right directory (.htaccess should make sure of that). - if (strpos($uri, "/okapi/") !== 0) - throw new Exception("'$uri' is outside of the /okapi/ path."); - $uri = substr($uri, 7); + if (strpos($uri, "/okapi/") !== 0) + throw new Exception("'$uri' is outside of the /okapi/ path."); + $uri = substr($uri, 7); - # Initializing internals and running pre-request cronjobs (we don't want - # cronjobs to be run before "okapi/update", for example before database - # was installed). + # Initializing internals and running pre-request cronjobs (we don't want + # cronjobs to be run before "okapi/update", for example before database + # was installed). - $allow_cronjobs = ($uri != "update"); - Okapi::init_internals($allow_cronjobs); + $allow_cronjobs = ($uri != "update"); + Okapi::init_internals($allow_cronjobs); - # Checking for allowed patterns... + # Checking for allowed patterns... - try - { - foreach (OkapiUrls::$mapping as $pattern => $namespace) - { - $matches = null; - if (preg_match("#$pattern#", $uri, $matches)) - { - # Pattern matched! Moving on to the proper View... + try + { + foreach (OkapiUrls::$mapping as $pattern => $namespace) + { + $matches = null; + if (preg_match("#$pattern#", $uri, $matches)) + { + # Pattern matched! Moving on to the proper View... - array_shift($matches); - require_once($GLOBALS['rootpath']."okapi/views/$namespace.php"); - $response = call_user_func_array(array('\\okapi\\views\\'. - str_replace('/', '\\', $namespace).'\\View', 'call'), $matches); - if ($response) - $response->display(); - return; - } - } - } - catch (Http404 $e) - { - /* pass */ - } + array_shift($matches); + require_once($GLOBALS['rootpath']."okapi/views/$namespace.php"); + $response = call_user_func_array(array('\\okapi\\views\\'. + str_replace('/', '\\', $namespace).'\\View', 'call'), $matches); + if ($response) + $response->display(); + return; + } + } + } + catch (Http404 $e) + { + /* pass */ + } - # None of the patterns matched OR method threw the Http404 exception. + # None of the patterns matched OR method threw the Http404 exception. - require_once($GLOBALS['rootpath']."okapi/views/http404.php"); - $response = \okapi\views\http404\View::call(); - $response->display(); - } + require_once($GLOBALS['rootpath']."okapi/views/http404.php"); + $response = \okapi\views\http404\View::call(); + $response->display(); + } } Okapi::gettext_domain_init(); diff --git a/htdocs/okapi/core.php b/htdocs/okapi/core.php index 3ae8a015..e3914f91 100644 --- a/htdocs/okapi/core.php +++ b/htdocs/okapi/core.php @@ -9,6 +9,7 @@ namespace okapi; use Exception; use ErrorException; +use ArrayObject; use OAuthServerException; use OAuthServer400Exception; use OAuthServer401Exception; @@ -23,20 +24,20 @@ use okapi\cronjobs\CronJobController; /** Return an array of email addresses which always get notified on OKAPI errors. */ function get_admin_emails() { - $emails = array(); - if (class_exists("okapi\\Settings")) - { - try - { - foreach (Settings::get('ADMINS') as $email) - if (!in_array($email, $emails)) - $emails[] = $email; - } - catch (Exception $e) { /* pass */ } - } - if (count($emails) == 0) - $emails[] = 'root@localhost'; - return $emails; + $emails = array(); + if (class_exists("okapi\\Settings")) + { + try + { + foreach (Settings::get('ADMINS') as $email) + if (!in_array($email, $emails)) + $emails[] = $email; + } + catch (Exception $e) { /* pass */ } + } + if (count($emails) == 0) + $emails[] = 'root@localhost'; + return $emails; } # @@ -45,19 +46,19 @@ function get_admin_emails() /** A base class for all bad request exceptions. */ class BadRequest extends Exception { - protected function provideExtras(&$extras) { - $extras['reason_stack'][] = 'bad_request'; - $extras['status'] = 400; - } - public function getOkapiJSON() { - $extras = array( - 'developer_message' => $this->getMessage(), - 'reason_stack' => array(), - ); - $this->provideExtras($extras); - $extras['more_info'] = Settings::get('SITE_URL')."okapi/introduction.html#errors"; - return json_encode(array("error" => $extras)); - } + protected function provideExtras(&$extras) { + $extras['reason_stack'][] = 'bad_request'; + $extras['status'] = 400; + } + public function getOkapiJSON() { + $extras = array( + 'developer_message' => $this->getMessage(), + 'reason_stack' => array(), + ); + $this->provideExtras($extras); + $extras['more_info'] = Settings::get('SITE_URL')."okapi/introduction.html#errors"; + return json_encode(array("error" => $extras)); + } } /** Thrown on PHP's FATAL errors (detected in a shutdown function). */ @@ -71,198 +72,218 @@ class FatalError extends ErrorException {} /** Container for exception-handling functions. */ class OkapiExceptionHandler { - /** Handle exception thrown while executing OKAPI request. */ - public static function handle($e) - { - if ($e instanceof OAuthServerException) - { - # This is thrown on invalid OAuth requests. There are many subclasses - # of this exception. All of them result in HTTP 400 or HTTP 401 error - # code. See also: http://oauth.net/core/1.0a/#http_codes + /** Handle exception thrown while executing OKAPI request. */ + public static function handle($e) + { + if ($e instanceof OAuthServerException) + { + # This is thrown on invalid OAuth requests. There are many subclasses + # of this exception. All of them result in HTTP 400 or HTTP 401 error + # code. See also: http://oauth.net/core/1.0a/#http_codes - if ($e instanceof OAuthServer400Exception) - header("HTTP/1.0 400 Bad Request"); - else - header("HTTP/1.0 401 Unauthorized"); - header("Access-Control-Allow-Origin: *"); - header("Content-Type: application/json; charset=utf-8"); + if ($e instanceof OAuthServer400Exception) + header("HTTP/1.0 400 Bad Request"); + else + header("HTTP/1.0 401 Unauthorized"); + header("Access-Control-Allow-Origin: *"); + header("Content-Type: application/json; charset=utf-8"); - print $e->getOkapiJSON(); - } - elseif ($e instanceof BadRequest) - { - # Intentionally thrown from within the OKAPI method code. - # Consumer (aka external developer) had something wrong with his - # request and we want him to know that. + print $e->getOkapiJSON(); + } + elseif ($e instanceof BadRequest) + { + # Intentionally thrown from within the OKAPI method code. + # Consumer (aka external developer) had something wrong with his + # request and we want him to know that. - header("HTTP/1.0 400 Bad Request"); - header("Access-Control-Allow-Origin: *"); - header("Content-Type: application/json; charset=utf-8"); + header("HTTP/1.0 400 Bad Request"); + header("Access-Control-Allow-Origin: *"); + header("Content-Type: application/json; charset=utf-8"); - print $e->getOkapiJSON(); - } - else # (ErrorException, MySQL exception etc.) - { - # This one is thrown on PHP notices and warnings - usually this - # indicates an error in OKAPI method. If thrown, then something - # must be fixed on OUR part. + print $e->getOkapiJSON(); + } + else # (ErrorException, MySQL exception etc.) + { + # This one is thrown on PHP notices and warnings - usually this + # indicates an error in OKAPI method. If thrown, then something + # must be fixed on OUR part. - if (!headers_sent()) - { - header("HTTP/1.0 500 Internal Server Error"); - header("Access-Control-Allow-Origin: *"); - header("Content-Type: text/plain; charset=utf-8"); - } + if (!headers_sent()) + { + header("HTTP/1.0 500 Internal Server Error"); + header("Access-Control-Allow-Origin: *"); + header("Content-Type: text/plain; charset=utf-8"); + } - print "Oops... Something went wrong on *our* part.\n\n"; - print "Message was passed on to the site administrators. We'll try to fix it.\n"; - print "Contact the developers if you think you can help!"; + print "Oops... Something went wrong on *our* part.\n\n"; + print "Message was passed on to the site administrators. We'll try to fix it.\n"; + print "Contact the developers if you think you can help!"; - error_log($e->getMessage()); + error_log($e->getMessage()); - $exception_info = self::get_exception_info($e); + $exception_info = self::get_exception_info($e); - if (class_exists("okapi\\Settings") && (Settings::get('DEBUG'))) - { - print "\n\nBUT! Since the DEBUG flag is on, then you probably ARE a developer yourself.\n"; - print "Let's cut to the chase then:"; - print "\n\n".$exception_info; - } - if (class_exists("okapi\\Settings") && (Settings::get('DEBUG_PREVENT_EMAILS'))) - { - # Sending emails was blocked on admin's demand. - # This is possible only on development environment. - } - else - { - $subject = "OKAPI Method Error - ".substr( - $_SERVER['REQUEST_URI'], 0, strpos( - $_SERVER['REQUEST_URI'].'?', '?')); + if (class_exists("okapi\\Settings") && (Settings::get('DEBUG'))) + { + print "\n\nBUT! Since the DEBUG flag is on, then you probably ARE a developer yourself.\n"; + print "Let's cut to the chase then:"; + print "\n\n".$exception_info; + } + if (class_exists("okapi\\Settings") && (Settings::get('DEBUG_PREVENT_EMAILS'))) + { + # Sending emails was blocked on admin's demand. + # This is possible only on development environment. + } + else + { + $subject = "OKAPI Method Error - ".substr( + $_SERVER['REQUEST_URI'], 0, strpos( + $_SERVER['REQUEST_URI'].'?', '?')); - $message = ( - "OKAPI caught the following exception while executing API method request.\n". - "This is an error in OUR code and should be fixed. Please contact the\n". - "developer of the module that threw this error. Thanks!\n\n". - $exception_info - ); - try - { - Okapi::mail_admins($subject, $message); - } - catch (Exception $e) - { - # Unable to use full-featured mail_admins version. We'll use a backup. - # We need to make sure we're not spamming. + $message = ( + "OKAPI caught the following exception while executing API method request.\n". + "This is an error in OUR code and should be fixed. Please contact the\n". + "developer of the module that threw this error. Thanks!\n\n". + $exception_info + ); + try + { + Okapi::mail_admins($subject, $message); + } + catch (Exception $e) + { + # Unable to use full-featured mail_admins version. We'll use a backup. + # We need to make sure we're not spamming. - $lock_file = "/tmp/okapi-fatal-error-mode"; - $last_email = false; - if (file_exists($lock_file)) - $last_email = filemtime($lock_file); - if ($last_email === false) { - # Assume this is the first email. - $last_email = 0; - } - if (time() - $last_email < 60) { - # Send no more than one per minute. - return; - } - @touch($lock_file); + $lock_file = "/tmp/okapi-fatal-error-mode"; + $last_email = false; + if (file_exists($lock_file)) + $last_email = filemtime($lock_file); + if ($last_email === false) { + # Assume this is the first email. + $last_email = 0; + } + if (time() - $last_email < 60) { + # Send no more than one per minute. + return; + } + @touch($lock_file); - $admin_email = implode(", ", get_admin_emails()); - $sender_email = class_exists("okapi\\Settings") ? Settings::get('FROM_FIELD') : 'root@localhost'; - $subject = "Fatal error mode: ".$subject; - $message = "Fatal error mode: OKAPI will send at most ONE message per minute.\n\n".$message; - $headers = ( - "Content-Type: text/plain; charset=utf-8\n". - "From: OKAPI <$sender_email>\n". - "Reply-To: $sender_email\n" - ); - mail($admin_email, $subject, $message, $headers); - } - } - } - } + $admin_email = implode(", ", get_admin_emails()); + $sender_email = class_exists("okapi\\Settings") ? Settings::get('FROM_FIELD') : 'root@localhost'; + $subject = "Fatal error mode: ".$subject; + $message = "Fatal error mode: OKAPI will send at most ONE message per minute.\n\n".$message; + $headers = ( + "Content-Type: text/plain; charset=utf-8\n". + "From: OKAPI <$sender_email>\n". + "Reply-To: $sender_email\n" + ); + mail($admin_email, $subject, $message, $headers); + } + } + } + } - public static function get_exception_info($e) - { - $exception_info = "===== ERROR MESSAGE =====\n".trim($e->getMessage())."\n=========================\n\n"; - if ($e instanceof FatalError) - { - # This one doesn't have a stack trace. It is fed directly to OkapiExceptionHandler::handle - # by OkapiErrorHandler::handle_shutdown. Instead of printing trace, we will just print - # the file and line. + public static function removeSensitiveData($message) + { + return str_replace( + array( + Settings::get('DB_PASSWORD'), + "'".Settings::get('DB_USERNAME')."'", + Settings::get('DB_SERVER'), + "'".Settings::get('DB_NAME')."'" + ), + array( + "******", + "'******'", + "******", + "'******'" + ), + $message + ); + } - $exception_info .= "File: ".$e->getFile()."\nLine: ".$e->getLine()."\n\n"; - } - else - { - $exception_info .= "--- Stack trace ---\n".$e->getTraceAsString()."\n\n"; - } + public static function get_exception_info($e) + { + $exception_info = "===== ERROR MESSAGE =====\n" + .trim(self::removeSensitiveData($e->getMessage())) + ."\n=========================\n\n"; + if ($e instanceof FatalError) + { + # This one doesn't have a stack trace. It is fed directly to OkapiExceptionHandler::handle + # by OkapiErrorHandler::handle_shutdown. Instead of printing trace, we will just print + # the file and line. - $exception_info .= (isset($_SERVER['REQUEST_URI']) ? "--- OKAPI method called ---\n". - preg_replace("/([?&])/", "\n$1", $_SERVER['REQUEST_URI'])."\n\n" : ""); - $exception_info .= "--- OKAPI revision ---\n".Okapi::$revision."\n\n"; + $exception_info .= "File: ".$e->getFile()."\nLine: ".$e->getLine()."\n\n"; + } + else + { + $exception_info .= "--- Stack trace ---\n". + self::removeSensitiveData($e->getTraceAsString())."\n\n"; + } - # This if-condition will solve some (but not all) problems when trying to execute - # OKAPI code from command line; - # see http://code.google.com/p/opencaching-api/issues/detail?id=243. - if (function_exists('getallheaders')) - { - $exception_info .= "--- Request headers ---\n".implode("\n", array_map( - function($k, $v) { return "$k: $v"; }, - array_keys(getallheaders()), array_values(getallheaders()) - )); - } + $exception_info .= (isset($_SERVER['REQUEST_URI']) ? "--- OKAPI method called ---\n". + preg_replace("/([?&])/", "\n$1", $_SERVER['REQUEST_URI'])."\n\n" : ""); + $exception_info .= "--- OKAPI revision ---\n".Okapi::$revision."\n\n"; - return $exception_info; - } + # This if-condition will solve some (but not all) problems when trying to execute + # OKAPI code from command line; + # see http://code.google.com/p/opencaching-api/issues/detail?id=243. + if (function_exists('getallheaders')) + { + $exception_info .= "--- Request headers ---\n".implode("\n", array_map( + function($k, $v) { return "$k: $v"; }, + array_keys(getallheaders()), array_values(getallheaders()) + )); + } + + return $exception_info; + } } /** Container for error-handling functions. */ class OkapiErrorHandler { - public static $treat_notices_as_errors = false; + public static $treat_notices_as_errors = false; - /** Handle error encountered while executing OKAPI request. */ - public static function handle($severity, $message, $filename, $lineno) - { - if ($severity == E_STRICT) return false; - if (($severity == E_NOTICE || $severity == E_DEPRECATED) && - !self::$treat_notices_as_errors) - { - return false; - } - throw new ErrorException($message, 0, $severity, $filename, $lineno); - } + /** Handle error encountered while executing OKAPI request. */ + public static function handle($severity, $message, $filename, $lineno) + { + if ($severity == E_STRICT || $severity == E_DEPRECATED) return false; + if (($severity == E_NOTICE) && !self::$treat_notices_as_errors) { + return false; + } + throw new ErrorException($message, 0, $severity, $filename, $lineno); + } - /** Use this BEFORE calling a piece of buggy code. */ - public static function disable() - { - restore_error_handler(); - } + /** Use this BEFORE calling a piece of buggy code. */ + public static function disable() + { + restore_error_handler(); + } - /** Use this AFTER calling a piece of buggy code. */ - public static function reenable() - { - set_error_handler(array('\okapi\OkapiErrorHandler', 'handle')); - } + /** Use this AFTER calling a piece of buggy code. */ + public static function reenable() + { + set_error_handler(array('\okapi\OkapiErrorHandler', 'handle')); + } - /** Handle FATAL errors (not catchable, report only). */ - public static function handle_shutdown() - { - $error = error_get_last(); + /** Handle FATAL errors (not catchable, report only). */ + public static function handle_shutdown() + { + $error = error_get_last(); - # We don't know whether this error has been already handled. The error_get_last - # function will return E_NOTICE or E_STRICT errors if the stript has shut down - # correctly. The only error which cannot be recovered from is E_ERROR, we have - # to check the type then. + # We don't know whether this error has been already handled. The error_get_last + # function will return E_NOTICE or E_STRICT errors if the stript has shut down + # correctly. The only error which cannot be recovered from is E_ERROR, we have + # to check the type then. - if (($error !== null) && ($error['type'] == E_ERROR)) - { - $e = new FatalError($error['message'], 0, $error['type'], $error['file'], $error['line']); - OkapiExceptionHandler::handle($e); - } - } + if (($error !== null) && ($error['type'] == E_ERROR)) + { + $e = new FatalError($error['message'], 0, $error['type'], $error['file'], $error['line']); + OkapiExceptionHandler::handle($e); + } + } } # Setting handlers. Errors will now throw exceptions, and all exceptions @@ -283,42 +304,42 @@ class Http404 extends BadRequest {} /** Common type of BadRequest: Required parameter is missing. */ class ParamMissing extends BadRequest { - private $paramName; - protected function provideExtras(&$extras) { - parent::provideExtras($extras); - $extras['reason_stack'][] = 'missing_parameter'; - $extras['parameter'] = $this->paramName; - } - public function __construct($paramName) - { - parent::__construct("Required parameter '$paramName' is missing."); - $this->paramName = $paramName; - } + private $paramName; + protected function provideExtras(&$extras) { + parent::provideExtras($extras); + $extras['reason_stack'][] = 'missing_parameter'; + $extras['parameter'] = $this->paramName; + } + public function __construct($paramName) + { + parent::__construct("Required parameter '$paramName' is missing."); + $this->paramName = $paramName; + } } /** Common type of BadRequest: Parameter has invalid value. */ class InvalidParam extends BadRequest { - public $paramName; + public $paramName; - /** What was wrong about the param? */ - public $whats_wrong_about_it; + /** What was wrong about the param? */ + public $whats_wrong_about_it; - protected function provideExtras(&$extras) { - parent::provideExtras($extras); - $extras['reason_stack'][] = 'invalid_parameter'; - $extras['parameter'] = $this->paramName; - $extras['whats_wrong_about_it'] = $this->whats_wrong_about_it; - } - public function __construct($paramName, $whats_wrong_about_it = "", $code = 0) - { - $this->paramName = $paramName; - $this->whats_wrong_about_it = $whats_wrong_about_it; - if ($whats_wrong_about_it) - parent::__construct("Parameter '$paramName' has invalid value: ".$whats_wrong_about_it, $code); - else - parent::__construct("Parameter '$paramName' has invalid value.", $code); - } + protected function provideExtras(&$extras) { + parent::provideExtras($extras); + $extras['reason_stack'][] = 'invalid_parameter'; + $extras['parameter'] = $this->paramName; + $extras['whats_wrong_about_it'] = $this->whats_wrong_about_it; + } + public function __construct($paramName, $whats_wrong_about_it = "", $code = 0) + { + $this->paramName = $paramName; + $this->whats_wrong_about_it = $whats_wrong_about_it; + if ($whats_wrong_about_it) + parent::__construct("Parameter '$paramName' has invalid value: ".$whats_wrong_about_it, $code); + else + parent::__construct("Parameter '$paramName' has invalid value.", $code); + } } /** Thrown on invalid SQL queries. */ @@ -331,142 +352,152 @@ class DbException extends Exception {} /** Database access class. Use this instead of mysql_query, sql or sqlValue. */ class Db { - private static $connected = false; + private static $connected = false; - public static function connect() - { - if (mysql_connect(Settings::get('DB_SERVER'), Settings::get('DB_USERNAME'), Settings::get('DB_PASSWORD'))) - { - mysql_select_db(Settings::get('DB_NAME')); - mysql_query("set names 'utf8'"); - self::$connected = true; - } - else - throw new Exception("Could not connect to MySQL: ".mysql_error()); - } + public static function connect() + { + if (mysql_connect(Settings::get('DB_SERVER'), Settings::get('DB_USERNAME'), Settings::get('DB_PASSWORD'))) + { + mysql_select_db(Settings::get('DB_NAME')); + mysql_query("set names 'utf8'"); + self::$connected = true; + } + else + throw new Exception("Could not connect to MySQL: ".mysql_error()); + } - /** Fetch [{row}], return {row}. */ - public static function select_row($query) - { - $rows = self::select_all($query); - switch (count($rows)) - { - case 0: return null; - case 1: return $rows[0]; - default: - throw new DbException("Invalid query. Db::select_row returned more than one row for:\n\n".$query."\n"); - } - } + /** Fetch [{row}], return {row}. */ + public static function select_row($query) + { + $rows = self::select_all($query); + switch (count($rows)) + { + case 0: return null; + case 1: return $rows[0]; + default: + throw new DbException("Invalid query. Db::select_row returned more than one row for:\n\n".$query."\n"); + } + } - /** Fetch all [{row}, {row}], return [{row}, {row}]. */ - public static function select_all($query) - { - $rows = array(); - self::select_and_push($query, $rows); - return $rows; - } + /** Fetch all [{row}, {row}], return [{row}, {row}]. */ + public static function select_all($query) + { + $rows = array(); + self::select_and_push($query, $rows); + return $rows; + } - /** Private. */ - private static function select_and_push($query, & $arr, $keyField = null) - { - $rs = self::query($query); - while (true) - { - $row = mysql_fetch_assoc($rs); - if ($row === false) - break; - if ($keyField == null) - $arr[] = $row; - else - $arr[$row[$keyField]] = $row; - } - mysql_free_result($rs); - } + /** Private. */ + private static function select_and_push($query, & $arr, $keyField = null) + { + $rs = self::query($query); + while (true) + { + $row = mysql_fetch_assoc($rs); + if ($row === false) + break; + if ($keyField == null) + $arr[] = $row; + else + $arr[$row[$keyField]] = $row; + } + mysql_free_result($rs); + } - /** Fetch all [(A,A), (A,B), (B,A)], return {A: [{row}, {row}], B: [{row}]}. */ - public static function select_group_by($keyField, $query) - { - $groups = array(); - $rs = self::query($query); - while (true) - { - $row = mysql_fetch_assoc($rs); - if ($row === false) - break; - $groups[$row[$keyField]][] = $row; - } - mysql_free_result($rs); - return $groups; - } + /** Fetch all [(A,A), (A,B), (B,A)], return {A: [{row}, {row}], B: [{row}]}. */ + public static function select_group_by($keyField, $query) + { + $groups = array(); + $rs = self::query($query); + while (true) + { + $row = mysql_fetch_assoc($rs); + if ($row === false) + break; + $groups[$row[$keyField]][] = $row; + } + mysql_free_result($rs); + return $groups; + } - /** Fetch [(A)], return A. */ - public static function select_value($query) - { - $column = self::select_column($query); - if ($column == null) - return null; - if (count($column) == 1) - return $column[0]; - throw new DbException("Invalid query. Db::select_value returned more than one row for:\n\n".$query."\n"); - } + /** Fetch [(A)], return A. */ + public static function select_value($query) + { + $column = self::select_column($query); + if ($column == null) + return null; + if (count($column) == 1) + return $column[0]; + throw new DbException("Invalid query. Db::select_value returned more than one row for:\n\n".$query."\n"); + } - /** Fetch all [(A), (B), (C)], return [A, B, C]. */ - public static function select_column($query) - { - $column = array(); - $rs = self::query($query); - while (true) - { - $values = mysql_fetch_array($rs); - if ($values === false) - break; - array_push($column, $values[0]); - } - mysql_free_result($rs); - return $column; - } + /** Fetch all [(A), (B), (C)], return [A, B, C]. */ + public static function select_column($query) + { + $column = array(); + $rs = self::query($query); + while (true) + { + $values = mysql_fetch_array($rs); + if ($values === false) + break; + array_push($column, $values[0]); + } + mysql_free_result($rs); + return $column; + } - public static function last_insert_id() - { - return mysql_insert_id(); - } + public static function last_insert_id() + { + return mysql_insert_id(); + } - public static function execute($query) - { - $rs = self::query($query); - if ($rs !== true) - throw new DbException("Db::execute returned a result set for your query. ". - "You should use Db::select_* or Db::query for SELECT queries!"); - } + public static function execute($query) + { + $rs = self::query($query); + if ($rs !== true) + throw new DbException("Db::execute returned a result set for your query. ". + "You should use Db::select_* or Db::query for SELECT queries!"); + } - public static function query($query) - { - if (!self::$connected) - self::connect(); - $rs = mysql_query($query); - if (!$rs) - { - throw new DbException("SQL Error ".mysql_errno().": ".mysql_error()."\n\nThe query was:\n".$query."\n"); - } - return $rs; - } + public static function query($query) + { + if (!self::$connected) + self::connect(); + $rs = mysql_query($query); + if (!$rs) + { + throw new DbException("SQL Error ".mysql_errno().": ".mysql_error()."\n\nThe query was:\n".$query."\n"); + } + return $rs; + } - public static function field_exists($table, $field) - { - if (!preg_match("/[a-z0-9_]+/", $table.$field)) - return false; - try { - $spec = self::select_all("desc ".$table.";"); - } catch (Exception $e) { - /* Table doesn't exist, probably. */ - return false; - } - foreach ($spec as &$row_ref) { - if (strtoupper($row_ref['Field']) == strtoupper($field)) - return true; - } - return false; - } + /** + * Return number of rows actually updated, inserted or deleted by the last + * statement executed with execute(). It DOES NOT return number of rows + * returned by the last select statement. + */ + public static function get_affected_row_count() + { + return mysql_affected_rows(); + } + + public static function field_exists($table, $field) + { + if (!preg_match("/[a-z0-9_]+/", $table.$field)) + return false; + try { + $spec = self::select_all("desc ".$table.";"); + } catch (Exception $e) { + /* Table doesn't exist, probably. */ + return false; + } + foreach ($spec as &$row_ref) { + if (strtoupper($row_ref['Field']) == strtoupper($field)) + return true; + } + return false; + } } # @@ -477,25 +508,25 @@ require_once($GLOBALS['rootpath']."okapi/oauth.php"); class OkapiConsumer extends OAuthConsumer { - public $name; - public $url; - public $email; - public $admin; + public $name; + public $url; + public $email; + public $admin; - public function __construct($key, $secret, $name, $url, $email, $admin=false) - { - $this->key = $key; - $this->secret = $secret; - $this->name = $name; - $this->url = $url; - $this->email = $email; - $this->admin = $admin; - } + public function __construct($key, $secret, $name, $url, $email, $admin=false) + { + $this->key = $key; + $this->secret = $secret; + $this->name = $name; + $this->url = $url; + $this->email = $email; + $this->admin = $admin; + } - public function __toString() - { - return "OkapiConsumer[key=$this->key,name=$this->name]"; - } + public function __toString() + { + return "OkapiConsumer[key=$this->key,name=$this->name]"; + } } /** @@ -504,11 +535,11 @@ class OkapiConsumer extends OAuthConsumer */ class OkapiInternalConsumer extends OkapiConsumer { - public function __construct() - { - $admins = get_admin_emails(); - parent::__construct('internal', null, "Internal OKAPI jobs", null, $admins[0]); - } + public function __construct() + { + $admins = get_admin_emails(); + parent::__construct('internal', null, "Internal OKAPI jobs", null, $admins[0]); + } } /** @@ -516,11 +547,11 @@ class OkapiInternalConsumer extends OkapiConsumer */ class OkapiDebugConsumer extends OkapiConsumer { - public function __construct() - { - $admins = get_admin_emails(); - parent::__construct('debug', null, "DEBUG_AS_USERNAME Debugger", null, $admins[0]); - } + public function __construct() + { + $admins = get_admin_emails(); + parent::__construct('debug', null, "DEBUG_AS_USERNAME Debugger", null, $admins[0]); + } } /** @@ -529,112 +560,112 @@ class OkapiDebugConsumer extends OkapiConsumer */ class OkapiFacadeConsumer extends OkapiConsumer { - public function __construct() - { - $admins = get_admin_emails(); - parent::__construct('facade', null, "Internal usage via Facade", null, $admins[0]); - } + public function __construct() + { + $admins = get_admin_emails(); + parent::__construct('facade', null, "Internal usage via Facade", null, $admins[0]); + } } class OkapiToken extends OAuthToken { - public $consumer_key; - public $token_type; + public $consumer_key; + public $token_type; - public function __construct($key, $secret, $consumer_key, $token_type) - { - parent::__construct($key, $secret); - $this->consumer_key = $consumer_key; - $this->token_type = $token_type; - } + public function __construct($key, $secret, $consumer_key, $token_type) + { + parent::__construct($key, $secret); + $this->consumer_key = $consumer_key; + $this->token_type = $token_type; + } } class OkapiRequestToken extends OkapiToken { - public $callback_url; - public $authorized_by_user_id; - public $verifier; + public $callback_url; + public $authorized_by_user_id; + public $verifier; - public function __construct($key, $secret, $consumer_key, $callback_url, - $authorized_by_user_id, $verifier) - { - parent::__construct($key, $secret, $consumer_key, 'request'); - $this->callback_url = $callback_url; - $this->authorized_by_user_id = $authorized_by_user_id; - $this->verifier = $verifier; - } + public function __construct($key, $secret, $consumer_key, $callback_url, + $authorized_by_user_id, $verifier) + { + parent::__construct($key, $secret, $consumer_key, 'request'); + $this->callback_url = $callback_url; + $this->authorized_by_user_id = $authorized_by_user_id; + $this->verifier = $verifier; + } } class OkapiAccessToken extends OkapiToken { - public $user_id; + public $user_id; - public function __construct($key, $secret, $consumer_key, $user_id) - { - parent::__construct($key, $secret, $consumer_key, 'access'); - $this->user_id = $user_id; - } + public function __construct($key, $secret, $consumer_key, $user_id) + { + parent::__construct($key, $secret, $consumer_key, 'access'); + $this->user_id = $user_id; + } } /** Use this in conjunction with OkapiInternalConsumer. */ class OkapiInternalAccessToken extends OkapiAccessToken { - public function __construct($user_id) - { - parent::__construct('internal-'.$user_id, null, 'internal', $user_id); - } + public function __construct($user_id) + { + parent::__construct('internal-'.$user_id, null, 'internal', $user_id); + } } /** Use this in conjunction with OkapiFacadeConsumer. */ class OkapiFacadeAccessToken extends OkapiAccessToken { - public function __construct($user_id) - { - parent::__construct('facade-'.$user_id, null, 'facade', $user_id); - } + public function __construct($user_id) + { + parent::__construct('facade-'.$user_id, null, 'facade', $user_id); + } } /** Used when debugging with DEBUG_AS_USERNAME. */ class OkapiDebugAccessToken extends OkapiAccessToken { - public function __construct($user_id) - { - parent::__construct('debug-'.$user_id, null, 'debug', $user_id); - } + public function __construct($user_id) + { + parent::__construct('debug-'.$user_id, null, 'debug', $user_id); + } } /** Default OAuthServer with some OKAPI-specific methods added. */ class OkapiOAuthServer extends OAuthServer { - public function __construct($data_store) - { - parent::__construct($data_store); - # We want HMAC_SHA1 authorization method only. - $this->add_signature_method(new OAuthSignatureMethod_HMAC_SHA1()); - } + public function __construct($data_store) + { + parent::__construct($data_store); + # We want HMAC_SHA1 authorization method only. + $this->add_signature_method(new OAuthSignatureMethod_HMAC_SHA1()); + } - /** - * By default, works like verify_request, but it does support some additional - * options. If $token_required == false, it doesn't throw an exception when - * there is no token specified. You may also change the token_type required - * for this request. - */ - public function verify_request2(&$request, $token_type = 'access', $token_required = true) - { - $this->get_version($request); - $consumer = $this->get_consumer($request); - try { - $token = $this->get_token($request, $consumer, $token_type); - } catch (OAuthMissingParameterException $e) { - # Note, that exception will be different if token is supplied - # and is invalid. We catch only a completely MISSING token parameter. - if (($e->getParamName() == 'oauth_token') && (!$token_required)) - $token = null; - else - throw $e; - } - $this->check_signature($request, $consumer, $token); - return array($consumer, $token); - } + /** + * By default, works like verify_request, but it does support some additional + * options. If $token_required == false, it doesn't throw an exception when + * there is no token specified. You may also change the token_type required + * for this request. + */ + public function verify_request2(&$request, $token_type = 'access', $token_required = true) + { + $this->get_version($request); + $consumer = $this->get_consumer($request); + try { + $token = $this->get_token($request, $consumer, $token_type); + } catch (OAuthMissingParameterException $e) { + # Note, that exception will be different if token is supplied + # and is invalid. We catch only a completely MISSING token parameter. + if (($e->getParamName() == 'oauth_token') && (!$token_required)) + $token = null; + else + throw $e; + } + $this->check_signature($request, $consumer, $token); + return array($consumer, $token); + } } # Including local datastore and settings (connecting SQL database etc.). @@ -644,1180 +675,1192 @@ require_once($GLOBALS['rootpath']."okapi/datastore.php"); class OkapiHttpResponse { - public $status = "200 OK"; - public $cache_control = "no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0"; - public $content_type = "text/plain; charset=utf-8"; - public $content_disposition = null; - public $allow_gzip = true; - public $connection_close = false; - public $etag = null; + public $status = "200 OK"; + public $cache_control = "no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0"; + public $content_type = "text/plain; charset=utf-8"; + public $content_disposition = null; + public $allow_gzip = true; + public $connection_close = false; + public $etag = null; - /** Use this only as a setter, use get_body or print_body for reading! */ - public $body; + /** Use this only as a setter, use get_body or print_body for reading! */ + public $body; - /** This could be set in case when body is a stream of known length. */ - public $stream_length = null; + /** This could be set in case when body is a stream of known length. */ + public $stream_length = null; - public function get_length() - { - if (is_resource($this->body)) - return $this->stream_length; - return strlen($this->body); - } + public function get_length() + { + if (is_resource($this->body)) + return $this->stream_length; + return strlen($this->body); + } - /** Note: You can call this only once! */ - public function print_body() - { - if (is_resource($this->body)) - { - while (!feof($this->body)) - print fread($this->body, 1024*1024); - } - else - print $this->body; - } + /** Note: You can call this only once! */ + public function print_body() + { + if (is_resource($this->body)) + { + while (!feof($this->body)) + print fread($this->body, 1024*1024); + } + else + print $this->body; + } - /** - * Note: You can call this only once! The result might be huge (a stream), - * it is usually better to print it directly with ->print_body(). - */ - public function get_body() - { - if (is_resource($this->body)) - { - ob_start(); - fpassthru($this->body); - return ob_get_clean(); - } - else - return $this->body; - } + /** + * Note: You can call this only once! The result might be huge (a stream), + * it is usually better to print it directly with ->print_body(). + */ + public function get_body() + { + if (is_resource($this->body)) + { + ob_start(); + fpassthru($this->body); + return ob_get_clean(); + } + else + return $this->body; + } - /** - * Print the headers and the body. This should be the last thing your script does. - */ - public function display() - { - header("HTTP/1.1 ".$this->status); - header("Access-Control-Allow-Origin: *"); - header("Content-Type: ".$this->content_type); - header("Cache-Control: ".$this->cache_control); - if ($this->connection_close) - header("Connection: close"); - if ($this->content_disposition) - header("Content-Disposition: ".$this->content_disposition); - if ($this->etag) - header("ETag: $this->etag"); + /** + * Print the headers and the body. This should be the last thing your script does. + */ + public function display() + { + header("HTTP/1.1 ".$this->status); + header("Access-Control-Allow-Origin: *"); + header("Content-Type: ".$this->content_type); + header("Cache-Control: ".$this->cache_control); + if ($this->connection_close) + header("Connection: close"); + if ($this->content_disposition) + header("Content-Disposition: ".$this->content_disposition); + if ($this->etag) + header("ETag: $this->etag"); - # Make sure that gzip is supported by the client. - $try_gzip = $this->allow_gzip; - if (empty($_SERVER["HTTP_ACCEPT_ENCODING"]) || (strpos($_SERVER["HTTP_ACCEPT_ENCODING"], "gzip") === false)) - $try_gzip = false; + # Make sure that gzip is supported by the client. + $try_gzip = $this->allow_gzip; + if (empty($_SERVER["HTTP_ACCEPT_ENCODING"]) || (strpos($_SERVER["HTTP_ACCEPT_ENCODING"], "gzip") === false)) + $try_gzip = false; - # We will gzip the data ourselves, while disabling gziping by Apache. This way, we can - # set the Content-Length correctly which is handy in some scenarios. + # We will gzip the data ourselves, while disabling gziping by Apache. This way, we can + # set the Content-Length correctly which is handy in some scenarios. - if ($try_gzip && is_string($this->body)) - { - header("Content-Encoding: gzip"); - $gzipped = gzencode($this->body, 5); - header("Content-Length: ".strlen($gzipped)); - print $gzipped; - } - else - { - $length = $this->get_length(); - if ($length) - header("Content-Length: ".$length); - $this->print_body(); - } - } + if ($try_gzip && is_string($this->body)) + { + header("Content-Encoding: gzip"); + $gzipped = gzencode($this->body, 5); + header("Content-Length: ".strlen($gzipped)); + print $gzipped; + } + else + { + $length = $this->get_length(); + if ($length) + header("Content-Length: ".$length); + $this->print_body(); + } + } } class OkapiRedirectResponse extends OkapiHttpResponse { - public $url; - public function __construct($url) { $this->url = $url; } - public function display() - { - header("HTTP/1.1 303 See Other"); - header("Location: ".$this->url); - } + public $url; + public function __construct($url) { $this->url = $url; } + public function display() + { + header("HTTP/1.1 303 See Other"); + header("Location: ".$this->url); + } } class OkapiLock { - private $lockfile; - private $lock; + private $lockfile; + private $lock; - /** Note: This does NOT tell you if someone currently locked it! */ - public static function exists($name) - { - $lockfile = Okapi::get_var_dir()."/okapi-lock-".$name; - return file_exists($lockfile); - } + /** Note: This does NOT tell you if someone currently locked it! */ + public static function exists($name) + { + $lockfile = Okapi::get_var_dir()."/okapi-lock-".$name; + return file_exists($lockfile); + } - public static function get($name) - { - return new OkapiLock($name); - } + public static function get($name) + { + return new OkapiLock($name); + } - private function __construct($name) - { - if (Settings::get('DEBUG_PREVENT_SEMAPHORES')) - { - # Using semaphores is forbidden on this server by its admin. - # This is possible only on development environment. - $this->lock = null; - } - else - { - $this->lockfile = Okapi::get_var_dir()."/okapi-lock-".$name; - $this->lock = fopen($this->lockfile, "wb"); - } - } + private function __construct($name) + { + if (Settings::get('DEBUG_PREVENT_SEMAPHORES')) + { + # Using semaphores is forbidden on this server by its admin. + # This is possible only on development environment. + $this->lock = null; + } + else + { + $this->lockfile = Okapi::get_var_dir()."/okapi-lock-".$name; + $this->lock = fopen($this->lockfile, "wb"); + } + } - public function acquire() - { - if ($this->lock !== null) - flock($this->lock, LOCK_EX); - } + public function acquire() + { + if ($this->lock !== null) + flock($this->lock, LOCK_EX); + } - public function try_acquire() - { - if ($this->lock !== null) - return flock($this->lock, LOCK_EX | LOCK_NB); - else - return true; # $lock can be null only when debugging - } + public function try_acquire() + { + if ($this->lock !== null) + return flock($this->lock, LOCK_EX | LOCK_NB); + else + return true; # $lock can be null only when debugging + } - public function release() - { - if ($this->lock !== null) - flock($this->lock, LOCK_UN); - } + public function release() + { + if ($this->lock !== null) + flock($this->lock, LOCK_UN); + } - /** - * Use this method clean up obsolete and *unused* lock names (usually there - * is no point in removing locks that can be reused. - */ - public function remove() - { - if ($this->lock !== null) - { - fclose($this->lock); - unlink($this->lockfile); - } - } + /** + * Use this method clean up obsolete and *unused* lock names (usually there + * is no point in removing locks that can be reused. + */ + public function remove() + { + if ($this->lock !== null) + { + fclose($this->lock); + unlink($this->lockfile); + } + } } /** Container for various OKAPI functions. */ class Okapi { - public static $data_store; - public static $server; - public static $revision = 938; # This gets replaced in automatically deployed packages - private static $okapi_vars = null; - - /** Get a variable stored in okapi_vars. If variable not found, return $default. */ - public static function get_var($varname, $default = null) - { - if (self::$okapi_vars === null) - { - $rs = Db::query(" - select var, value - from okapi_vars - "); - self::$okapi_vars = array(); - while ($row = mysql_fetch_assoc($rs)) - self::$okapi_vars[$row['var']] = $row['value']; - } - if (isset(self::$okapi_vars[$varname])) - return self::$okapi_vars[$varname]; - return $default; - } - - /** - * Save a variable to okapi_vars. WARNING: The entire content of okapi_vars table - * is loaded on EVERY execution. Do not store data in this table, unless it's - * frequently needed. - */ - public static function set_var($varname, $value) - { - Okapi::get_var($varname); - Db::execute(" - replace into okapi_vars (var, value) - values ( - '".mysql_real_escape_string($varname)."', - '".mysql_real_escape_string($value)."'); - "); - self::$okapi_vars[$varname] = $value; - } - - /** Send an email message to local OKAPI administrators. */ - public static function mail_admins($subject, $message) - { - # Make sure we're not sending HUGE emails. - - if (strlen($message) > 10000) { - $message = substr($message, 0, 10000)."\n\n...(message clipped at 10k chars)\n"; - } - - # Make sure we're not spamming. - - $cache_key = 'mail_admins_counter/'.(floor(time() / 3600) * 3600).'/'.md5($subject); - try { - $counter = Cache::get($cache_key); - } catch (DbException $e) { - # Why catching exceptions here? See bug#156. - $counter = null; - } - if ($counter === null) - $counter = 0; - $counter++; - try { - Cache::set($cache_key, $counter, 3600); - } catch (DbException $e) { - # Why catching exceptions here? See bug#156. - } - if ($counter <= 5) - { - # We're not spamming yet. - - self::mail_from_okapi(get_admin_emails(), $subject, $message); - } - else - { - # We are spamming. Prevent sending more emails. - - $content_cache_key_prefix = 'mail_admins_spam/'.(floor(time() / 3600) * 3600).'/'; - $timeout = 86400; - if ($counter == 6) - { - self::mail_from_okapi(get_admin_emails(), "Anti-spam mode activated for '$subject'", - "OKAPI has activated an \"anti-spam\" mode for the following subject:\n\n". - "\"$subject\"\n\n". - "Anti-spam mode is activiated when more than 5 messages with\n". - "the same subject are sent within one hour.\n\n". - "Additional debug information:\n". - "- counter cache key: $cache_key\n". - "- content prefix: $content_cache_key_prefix\n". - "- content timeout: $timeout\n" - ); - } - $content_cache_key = $content_cache_key_prefix.$counter; - Cache::set($content_cache_key, $message, $timeout); - } - } - - /** Send an email message from OKAPI to the given recipients. */ - public static function mail_from_okapi($email_addresses, $subject, $message) - { - if (class_exists("okapi\\Settings") && (Settings::get('DEBUG_PREVENT_EMAILS'))) - { - # Sending emails was blocked on admin's demand. - # This is possible only on development environment. - return; - } - if (!is_array($email_addresses)) - $email_addresses = array($email_addresses); - $sender_email = class_exists("okapi\\Settings") ? Settings::get('FROM_FIELD') : 'root@localhost'; - mail(implode(", ", $email_addresses), $subject, $message, - "Content-Type: text/plain; charset=utf-8\n". - "From: OKAPI <$sender_email>\n". - "Reply-To: $sender_email\n" - ); - } - - /** Get directory to store dynamic (cache or temporary) files. No trailing slash included. */ - public static function get_var_dir() - { - $dir = Settings::get('VAR_DIR'); - if ($dir != null) - return rtrim($dir, "/"); - throw new Exception("You need to set a valid VAR_DIR."); - } - - /** Returns something like "Opencaching.PL" or "Opencaching.DE". */ - public static function get_normalized_site_name($site_url = null) - { - if ($site_url == null) - $site_url = Settings::get('SITE_URL'); - $matches = null; - if (preg_match("#^https?://(www.)?opencaching.([a-z.]+)/$#", $site_url, $matches)) { - return "Opencaching.".strtoupper($matches[2]); - } else { - return "DEVELSITE"; - } - } - - /** - * Pick text from $langdict based on language preference $langpref. - * - * Example: - * pick_best_language( - * array('pl' => 'X', 'de' => 'Y', 'en' => 'Z'), - * array('sp', 'de', 'en') - * ) == 'Y'. - * - * @param array $langdict - assoc array of lang-code => text. - * @param array $langprefs - list of lang codes, in order of preference. - */ - public static function pick_best_language($langdict, $langprefs) - { - foreach ($langprefs as $pref) - if (isset($langdict[$pref])) - return $langdict[$pref]; - foreach ($langdict as &$text_ref) - return $text_ref; - return ""; - } - - /** - * Split the array into groups of max. $size items. - */ - public static function make_groups($array, $size) - { - $i = 0; - $groups = array(); - while ($i < count($array)) - { - $groups[] = array_slice($array, $i, $size); - $i += $size; - } - return $groups; - } - - /** - * Check if any pre-request cronjobs are scheduled to execute and execute - * them if needed. Reschedule for new executions. - */ - public static function execute_prerequest_cronjobs() - { - $nearest_event = Okapi::get_var("cron_nearest_event"); - if ($nearest_event + 0 <= time()) - { - require_once($GLOBALS['rootpath']."okapi/cronjobs.php"); - $nearest_event = CronJobController::run_jobs('pre-request'); - Okapi::set_var("cron_nearest_event", $nearest_event); - } - } - - /** - * Check if any cron-5 cronjobs are scheduled to execute and execute - * them if needed. Reschedule for new executions. - */ - public static function execute_cron5_cronjobs() - { - $nearest_event = Okapi::get_var("cron_nearest_event"); - if ($nearest_event + 0 <= time()) - { - set_time_limit(0); - ignore_user_abort(true); - require_once($GLOBALS['rootpath']."okapi/cronjobs.php"); - $nearest_event = CronJobController::run_jobs('cron-5'); - Okapi::set_var("cron_nearest_event", $nearest_event); - } - } - - private static function gettext_set_lang($langprefs) - { - static $gettext_last_used_langprefs = null; - static $gettext_last_set_locale = null; - - # We remember the last $langprefs argument which we've been called with. - # This way, we don't need to call the actual locale-switching code most - # of the times. - - if ($gettext_last_used_langprefs != $langprefs) - { - $gettext_last_set_locale = call_user_func(Settings::get("GETTEXT_INIT"), $langprefs); - $gettext_last_used_langprefs = $langprefs; - textdomain(Settings::get("GETTEXT_DOMAIN")); - } - return $gettext_last_set_locale; - } - - private static $gettext_original_domain = null; - private static $gettext_langprefs_stack = array(); - - /** - * Attempt to switch the language based on the preference array given. - * Previous language settings will be remembered (in a stack). You SHOULD - * restore them later by calling gettext_domain_restore. - */ - public static function gettext_domain_init($langprefs = null) - { - # Put the langprefs on the stack. - - if ($langprefs == null) - $langprefs = array(Settings::get('SITELANG')); - self::$gettext_langprefs_stack[] = $langprefs; - - if (count(self::$gettext_langprefs_stack) == 1) - { - # This is the first time gettext_domain_init is called. In order to - # properly reinitialize the original settings after gettext_domain_restore - # is called for the last time, we need to save current textdomain (which - # should be different than the one which we use - Settings::get("GETTEXT_DOMAIN")). - - self::$gettext_original_domain = textdomain(null); - } - - # Attempt to change the language. Acquire the actual locale code used - # (might differ from expected when language was not found). - - $locale_code = self::gettext_set_lang($langprefs); - return $locale_code; - } - public static function gettext_domain_restore() - { - # Dismiss the last element on the langpref stack. This is the language - # which we've been actualy using until now. We want it replaced with - # the language below it. - - array_pop(self::$gettext_langprefs_stack); - - $size = count(self::$gettext_langprefs_stack); - if ($size > 0) - { - $langprefs = self::$gettext_langprefs_stack[$size - 1]; - self::gettext_set_lang($langprefs); - } - else - { - # The stack is empty. This means we're going out of OKAPI code and - # we want the original textdomain reestablished. - - textdomain(self::$gettext_original_domain); - self::$gettext_original_domain = null; - } - } - - /** - * Internal. This is called always when OKAPI core is included. - */ - public static function init_internals($allow_cronjobs = true) - { - static $init_made = false; - if ($init_made) - return; - ini_set('memory_limit', '256M'); - Db::connect(); - if (Settings::get('TIMEZONE') !== null) - date_default_timezone_set(Settings::get('TIMEZONE')); - if (!self::$data_store) - self::$data_store = new OkapiDataStore(); - if (!self::$server) - self::$server = new OkapiOAuthServer(self::$data_store); - if ($allow_cronjobs) - self::execute_prerequest_cronjobs(); - $init_made = true; - } - - /** - * Generate a string of random characters, suitable for keys as passwords. - * Troublesome characters like '0', 'O', '1', 'l' will not be used. - * If $user_friendly=true, then it will consist from numbers only. - */ - public static function generate_key($length, $user_friendly = false) - { - if ($user_friendly) - $chars = "0123456789"; - else - $chars = "abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789"; - $max = strlen($chars); - $key = ""; - for ($i=0; $i<$length; $i++) - { - $key .= $chars[rand(0, $max-1)]; - } - return $key; - } - - /** - * Register new OKAPI Consumer, send him an email with his key-pair, etc. - * This method does not verify parameter values, check if they are in - * a correct format prior the execution. - */ - public static function register_new_consumer($appname, $appurl, $email) - { - require_once($GLOBALS['rootpath']."okapi/service_runner.php"); - $consumer = new OkapiConsumer(Okapi::generate_key(20), Okapi::generate_key(40), - $appname, $appurl, $email); - $sample_cache = OkapiServiceRunner::call("services/caches/search/all", - new OkapiInternalRequest($consumer, null, array('limit', 1))); - if (count($sample_cache['results']) > 0) - $sample_cache_code = $sample_cache['results'][0]; - else - $sample_cache_code = "CACHECODE"; - - # Message for the Consumer. - ob_start(); - print "This is the key-pair we have created for your application:\n\n"; - print "Consumer Key: $consumer->key\n"; - print "Consumer Secret: $consumer->secret\n\n"; - print "Note: Consumer Secret is needed only when you intend to use OAuth.\n"; - print "You don't need Consumer Secret for Level 1 Authentication.\n\n"; - print "Now you can easily access Level 1 OKAPI methods. E.g.:\n"; - print Settings::get('SITE_URL')."okapi/services/caches/geocache?cache_code=$sample_cache_code&consumer_key=$consumer->key\n\n"; - print "If you plan on using OKAPI for a longer time, then you may want to\n"; - print "subscribe to the OKAPI News blog to stay up-to-date:\n"; - print "http://opencaching-api.blogspot.com/\n\n"; - print "Have fun!\n\n"; - print "-- \n"; - print "OKAPI Team\n"; - Okapi::mail_from_okapi($email, "Your OKAPI Consumer Key", ob_get_clean()); - - # Message for the Admins. - - ob_start(); - print "Name: $consumer->name\n"; - print "Developer: $consumer->email\n"; - print ($consumer->url ? "URL: $consumer->url\n" : ""); - print "Consumer Key: $consumer->key\n"; - Okapi::mail_admins("New OKAPI app registered!", ob_get_clean()); - - Db::execute(" - insert into okapi_consumers (`key`, name, secret, url, email, date_created) - values ( - '".mysql_real_escape_string($consumer->key)."', - '".mysql_real_escape_string($consumer->name)."', - '".mysql_real_escape_string($consumer->secret)."', - '".mysql_real_escape_string($consumer->url)."', - '".mysql_real_escape_string($consumer->email)."', - now() - ); - "); - } - - /** Return the distance between two geopoints, in meters. */ - public static function get_distance($lat1, $lon1, $lat2, $lon2) - { - $x1 = (90-$lat1) * 3.14159 / 180; - $x2 = (90-$lat2) * 3.14159 / 180; - $d = acos(cos($x1) * cos($x2) + sin($x1) * sin($x2) * cos(($lon1-$lon2) * 3.14159 / 180)) * 6371000; - if ($d < 0) $d = 0; - return $d; - } - - /** - * Return an SQL formula for calculating distance between two geopoints. - * Parameters should be either numberals or strings (SQL field references). - */ - public static function get_distance_sql($lat1, $lon1, $lat2, $lon2) - { - $x1 = "(90-$lat1) * 3.14159 / 180"; - $x2 = "(90-$lat2) * 3.14159 / 180"; - $d = "acos(cos($x1) * cos($x2) + sin($x1) * sin($x2) * cos(($lon1-$lon2) * 3.14159 / 180)) * 6371000"; - return $d; - } - - /** Return bearing (float 0..360) from geopoint 1 to 2. */ - public static function get_bearing($lat1, $lon1, $lat2, $lon2) - { - if ($lat1 == $lat2 && $lon1 == $lon2) - return null; - if ($lat1 == $lat2) $lat1 += 0.0000166; - if ($lon1 == $lon2) $lon1 += 0.0000166; - - $rad_lat1 = $lat1 / 180.0 * 3.14159; - $rad_lon1 = $lon1 / 180.0 * 3.14159; - $rad_lat2 = $lat2 / 180.0 * 3.14159; - $rad_lon2 = $lon2 / 180.0 * 3.14159; - - $delta_lon = $rad_lon2 - $rad_lon1; - $bearing = atan2(sin($delta_lon) * cos($rad_lat2), - cos($rad_lat1) * sin($rad_lat2) - sin($rad_lat1) * cos($rad_lat2) * cos($delta_lon)); - $bearing = 180.0 * $bearing / 3.14159; - if ( $bearing < 0.0 ) $bearing = $bearing + 360.0; - - return $bearing; - } - - /** Transform bearing (float 0..360) to simple 2-letter string (N, NE, E, SE, etc.) */ - public static function bearing_as_two_letters($b) - { - static $names = array('N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW'); - if ($b === null) return 'n/a'; - return $names[round(($b / 360.0) * 8.0) % 8]; - } - - /** Transform bearing (float 0..360) to simple 3-letter string (N, NNE, NE, ESE, etc.) */ - public static function bearing_as_three_letters($b) - { - static $names = array('N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', - 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW'); - if ($b === null) return 'n/a'; - return $names[round(($b / 360.0) * 16.0) % 16]; - } - - /** Escape string for use with XML. See issue 169. */ - public static function xmlescape($string) - { - static $pattern = '/[^\x{0009}\x{000a}\x{000d}\x{0020}-\x{D7FF}\x{E000}-\x{FFFD}]+/u'; - $string = preg_replace($pattern, '', $string); - return strtr($string, array("<" => "<", ">" => ">", "\"" => """, "'" => "'", "&" => "&")); - } - - /** - * Return object as a standard OKAPI response. The $object will be formatted - * using one of the default formatters (JSON, JSONP, XML, etc.). Formatter is - * auto-detected by peeking on the $request's 'format' parameter. In some - * specific cases, this method can also return the $object itself, instead - * of OkapiResponse - this allows nesting methods within other methods. - */ - public static function formatted_response(OkapiRequest $request, &$object) - { - if ($request instanceof OkapiInternalRequest && ($request->i_want_okapi_response == false)) - { - # If you call a method internally, then you probably expect to get - # the actual object instead of it's formatted representation. - return $object; - } - $format = $request->get_parameter('format'); - if ($format == null) $format = 'json'; - if (!in_array($format, array('json', 'jsonp', 'xmlmap', 'xmlmap2'))) - throw new InvalidParam('format', "'$format'"); - $callback = $request->get_parameter('callback'); - if ($callback && $format != 'jsonp') - throw new BadRequest("The 'callback' parameter is reserved to be used with the JSONP output format."); - if ($format == 'json') - { - $response = new OkapiHttpResponse(); - $response->content_type = "application/json; charset=utf-8"; - $response->body = json_encode($object); - return $response; - } - elseif ($format == 'jsonp') - { - if (!$callback) - throw new BadRequest("'callback' parameter is required for JSONP calls"); - if (!preg_match("/^[a-zA-Z_][a-zA-Z0-9_]*$/", $callback)) - throw new InvalidParam('callback', "'$callback' doesn't seem to be a valid JavaScript function name (should match /^[a-zA-Z_][a-zA-Z0-9_]*\$/)."); - $response = new OkapiHttpResponse(); - $response->content_type = "application/javascript; charset=utf-8"; - $response->body = $callback."(".json_encode($object).");"; - return $response; - } - elseif ($format == 'xmlmap') - { - # Deprecated (see issue 128). Keeping this for backward-compatibility. - $response = new OkapiHttpResponse(); - $response->content_type = "text/xml; charset=utf-8"; - $response->body = self::xmlmap_dumps($object); - return $response; - } - elseif ($format == 'xmlmap2') - { - $response = new OkapiHttpResponse(); - $response->content_type = "text/xml; charset=utf-8"; - $response->body = self::xmlmap2_dumps($object); - return $response; - } - else - { - # Should not happen (as we do a proper check above). - throw new Exception(); - } - } - - private static function _xmlmap_add(&$chunks, &$obj) - { - if (is_string($obj)) - { - $chunks[] = ""; - $chunks[] = self::xmlescape($obj); - $chunks[] = ""; - } - elseif (is_int($obj)) - { - $chunks[] = "$obj"; - } - elseif (is_float($obj)) - { - $chunks[] = "$obj"; - } - elseif (is_bool($obj)) - { - $chunks[] = $obj ? "true" : "false"; - } - elseif (is_null($obj)) - { - $chunks[] = ""; - } - elseif (is_array($obj)) - { - # Have to check if this is associative or not! Shit. I hate PHP. - if (array_keys($obj) === range(0, count($obj) - 1)) - { - # Not assoc. - $chunks[] = ""; - foreach ($obj as &$item_ref) - { - $chunks[] = ""; - self::_xmlmap_add($chunks, $item_ref); - $chunks[] = ""; - } - $chunks[] = ""; - } - else - { - # Assoc. - $chunks[] = ""; - foreach ($obj as $key => &$item_ref) - { - $chunks[] = ""; - self::_xmlmap_add($chunks, $item_ref); - $chunks[] = ""; - } - $chunks[] = ""; - } - } - else - { - # That's a bug. - throw new Exception("Cannot encode as xmlmap: " + print_r($obj, true)); - } - } - - private static function _xmlmap2_add(&$chunks, &$obj, $key) - { - $attrs = ($key !== null) ? " key=\"".self::xmlescape($key)."\"" : ""; - if (is_string($obj)) - { - $chunks[] = ""; - $chunks[] = self::xmlescape($obj); - $chunks[] = ""; - } - elseif (is_int($obj)) - { - $chunks[] = "$obj"; - } - elseif (is_float($obj)) - { - $chunks[] = "$obj"; - } - elseif (is_bool($obj)) - { - $chunks[] = $obj ? "true" : "false"; - } - elseif (is_null($obj)) - { - $chunks[] = ""; - } - elseif (is_array($obj)) - { - # Have to check if this is associative or not! Shit. I hate PHP. - if (array_keys($obj) === range(0, count($obj) - 1)) - { - # Not assoc. - $chunks[] = ""; - foreach ($obj as &$item_ref) - { - self::_xmlmap2_add($chunks, $item_ref, null); - } - $chunks[] = ""; - } - else - { - # Assoc. - $chunks[] = ""; - foreach ($obj as $key => &$item_ref) - { - self::_xmlmap2_add($chunks, $item_ref, $key); - } - $chunks[] = ""; - } - } - else - { - # That's a bug. - throw new Exception("Cannot encode as xmlmap2: " + print_r($obj, true)); - } - } - - /** Return the object in a serialized version, in the (deprecated) "xmlmap" format. */ - public static function xmlmap_dumps(&$obj) - { - $chunks = array(); - self::_xmlmap_add($chunks, $obj); - return implode('', $chunks); - } - - /** Return the object in a serialized version, in the "xmlmap2" format. */ - public static function xmlmap2_dumps(&$obj) - { - $chunks = array(); - self::_xmlmap2_add($chunks, $obj, null); - return implode('', $chunks); - } - - private static $cache_types = array( - # - # OKAPI does not expose type IDs. Instead, it uses the following - # "code words". Only the "primary" cache types are documented. - # This means that all other types may (in theory) be altered. - # Cache type may become "primary" ONLY when *all* OC servers recognize - # that type. - # - # Changing this may introduce nasty bugs (e.g. in the replicate module). - # CONTACT ME BEFORE YOU MODIFY THIS! - # - 'oc.pl' => array( - # Primary types (documented, cannot change) - 'Traditional' => 2, 'Multi' => 3, 'Quiz' => 7, 'Virtual' => 4, - 'Event' => 6, - # Additional types (may get changed) - 'Other' => 1, 'Webcam' => 5, - 'Moving' => 8, 'Podcast' => 9, 'Own' => 10, - ), - 'oc.de' => array( - # Primary types (documented, cannot change) - 'Traditional' => 2, 'Multi' => 3, 'Quiz' => 7, 'Virtual' => 4, - 'Event' => 6, - # Additional types (might get changed) - 'Other' => 1, 'Webcam' => 5, - 'Math/Physics' => 8, 'Moving' => 9, 'Drive-In' => 10, - ) - ); - - /** E.g. 'Traditional' => 2. For unknown names throw an Exception. */ - public static function cache_type_name2id($name) - { - $ref = &self::$cache_types[Settings::get('OC_BRANCH')]; - if (isset($ref[$name])) - return $ref[$name]; - throw new Exception("Method cache_type_name2id called with unsupported cache ". - "type name '$name'. You should not allow users to submit caches ". - "of non-primary type."); - } - - /** E.g. 2 => 'Traditional'. For unknown ids returns "Other". */ - public static function cache_type_id2name($id) - { - static $reversed = null; - if ($reversed == null) - { - $reversed = array(); - foreach (self::$cache_types[Settings::get('OC_BRANCH')] as $key => $value) - $reversed[$value] = $key; - } - if (isset($reversed[$id])) - return $reversed[$id]; - return "Other"; - } - - private static $cache_statuses = array( - 'Available' => 1, 'Temporarily unavailable' => 2, 'Archived' => 3 - ); - - /** E.g. 'Available' => 1. For unknown names throws an Exception. */ - public static function cache_status_name2id($name) - { - if (isset(self::$cache_statuses[$name])) - return self::$cache_statuses[$name]; - throw new Exception("Method cache_status_name2id called with invalid name '$name'."); - } - - /** E.g. 1 => 'Available'. For unknown ids returns 'Archived'. */ - public static function cache_status_id2name($id) - { - static $reversed = null; - if ($reversed == null) - { - $reversed = array(); - foreach (self::$cache_statuses as $key => $value) - $reversed[$value] = $key; - } - if (isset($reversed[$id])) - return $reversed[$id]; - return 'Archived'; - } - - private static $cache_sizes = array( - 'none' => 7, - 'nano' => 8, - 'micro' => 2, - 'small' => 3, - 'regular' => 4, - 'large' => 5, - 'xlarge' => 6, - 'other' => 1, - ); - - /** E.g. 'micro' => 2. For unknown names throw an Exception. */ - public static function cache_size2_to_sizeid($size2) - { - if (isset(self::$cache_sizes[$size2])) - return self::$cache_sizes[$size2]; - throw new Exception("Method cache_size2_to_sizeid called with invalid size2 '$size2'."); - } - - /** E.g. 2 => 'micro'. For unknown ids returns "other". */ - public static function cache_sizeid_to_size2($id) - { - static $reversed = null; - if ($reversed == null) - { - $reversed = array(); - foreach (self::$cache_sizes as $key => $value) - $reversed[$value] = $key; - } - if (isset($reversed[$id])) - return $reversed[$id]; - return "other"; - } - - /** Maps OKAPI's 'size2' values to opencaching.com (OX) size codes. */ - private static $cache_OX_sizes = array( - 'none' => null, - 'nano' => 1.3, - 'micro' => 2.0, - 'small' => 3.0, - 'regular' => 3.8, - 'large' => 4.6, - 'xlarge' => 4.9, - 'other' => null, - ); - - /** - * E.g. 'micro' => 2.0, 'other' => null. For unknown names throw an - * Exception. Note, that this is not a bijection ('none' are 'other' are - * both null). - */ - public static function cache_size2_to_oxsize($size2) - { - if (array_key_exists($size2, self::$cache_OX_sizes)) - return self::$cache_OX_sizes[$size2]; - throw new Exception("Method cache_size2_to_oxsize called with invalid size2 '$size2'."); - } - - /** - * E.g. 'Found it' => 1. For unsupported names throws Exception. - */ - public static function logtypename2id($name) - { - if ($name == 'Found it') return 1; - if ($name == "Didn't find it") return 2; - if ($name == 'Comment') return 3; - if ($name == 'Attended') return 7; - if ($name == 'Will attend') return 8; - if (($name == 'Needs maintenance') && (Settings::get('SUPPORTS_LOGTYPE_NEEDS_MAINTENANCE'))) - return 5; - throw new Exception("logtype2id called with invalid log type argument: $name"); - } - - /** E.g. 1 => 'Found it'. For unknown ids returns 'Comment'. */ - public static function logtypeid2name($id) - { - # Various OC nodes use different English names, even for primary - # log types. OKAPI needs to have them the same across *all* OKAPI - # installations. That's why all known types are hardcoded here. - # These names are officially documented and may never change! - - # Primary. - if ($id == 1) return "Found it"; - if ($id == 2) return "Didn't find it"; - if ($id == 3) return "Comment"; - if ($id == 7) return "Attended"; - if ($id == 8) return "Will attend"; - - # Other. - if ($id == 4) return "Moved"; - if ($id == 5) return "Needs maintenance"; - if ($id == 6) return "Maintenance performed"; - if ($id == 9) return "Archived"; - if ($id == 10) return "Ready to search"; - if ($id == 11) return "Temporarily unavailable"; - if ($id == 12) return "OC Team comment"; - if ($id == 13 || $id == 14) return "Locked"; - - # Important: This set is not closed. Other types may be introduced - # in the future. This has to be documented in the public method - # description. - - return "Comment"; - } - - /** - * "Fix" user-supplied HTML fetched from the OC database. - */ - public static function fix_oc_html($html) - { - /* There are thousands of relative URLs in cache descriptions. We will - * attempt to find them and fix them. In theory, the "proper" way to do this - * would be to parse the description into a DOM tree, but that would simply - * be very hard (and inefficient) to do, since most of the descriptions are - * not even valid HTML. - */ - - $html = preg_replace( - "~\b(src|href)=([\"'])(?![a-z0-9_-]+:)~", - "$1=$2".Settings::get("SITE_URL"), - $html - ); - - /* Other things to do in the future: - * - * 1. Check for XSS vulerabilities? - * 2. Transform to a valid (X)HTML? - */ - - return $html; - } + public static $data_store; + public static $server; + public static $revision = 1031; # This gets replaced in automatically deployed packages + private static $okapi_vars = null; + + /** Get a variable stored in okapi_vars. If variable not found, return $default. */ + public static function get_var($varname, $default = null) + { + if (self::$okapi_vars === null) + { + $rs = Db::query(" + select var, value + from okapi_vars + "); + self::$okapi_vars = array(); + while ($row = mysql_fetch_assoc($rs)) + self::$okapi_vars[$row['var']] = $row['value']; + } + if (isset(self::$okapi_vars[$varname])) + return self::$okapi_vars[$varname]; + return $default; + } + + /** + * Save a variable to okapi_vars. WARNING: The entire content of okapi_vars table + * is loaded on EVERY execution. Do not store data in this table, unless it's + * frequently needed. + */ + public static function set_var($varname, $value) + { + Okapi::get_var($varname); + Db::execute(" + replace into okapi_vars (var, value) + values ( + '".mysql_real_escape_string($varname)."', + '".mysql_real_escape_string($value)."'); + "); + self::$okapi_vars[$varname] = $value; + } + + /** + * Remove database passwords and other sensitive data from the given + * message (which is usually a trace, var_dump or print_r output). + */ + public static function removeSensitiveData($message) + { + # This method is initially defined in the OkapiExceptionHandler class, + # so that it is accessible even before the Okapi class is initialized. + + return OkapiExceptionHandler::removeSensitiveData($message); + } + + /** Send an email message to local OKAPI administrators. */ + public static function mail_admins($subject, $message) + { + # Make sure we're not sending HUGE emails. + + if (strlen($message) > 10000) { + $message = substr($message, 0, 10000)."\n\n...(message clipped at 10k chars)\n"; + } + + # Make sure we're not spamming. + + $cache_key = 'mail_admins_counter/'.(floor(time() / 3600) * 3600).'/'.md5($subject); + try { + $counter = Cache::get($cache_key); + } catch (DbException $e) { + # Why catching exceptions here? See bug#156. + $counter = null; + } + if ($counter === null) + $counter = 0; + $counter++; + try { + Cache::set($cache_key, $counter, 3600); + } catch (DbException $e) { + # Why catching exceptions here? See bug#156. + } + if ($counter <= 5) + { + # We're not spamming yet. + + self::mail_from_okapi(get_admin_emails(), $subject, $message); + } + else + { + # We are spamming. Prevent sending more emails. + + $content_cache_key_prefix = 'mail_admins_spam/'.(floor(time() / 3600) * 3600).'/'; + $timeout = 86400; + if ($counter == 6) + { + self::mail_from_okapi(get_admin_emails(), "Anti-spam mode activated for '$subject'", + "OKAPI has activated an \"anti-spam\" mode for the following subject:\n\n". + "\"$subject\"\n\n". + "Anti-spam mode is activiated when more than 5 messages with\n". + "the same subject are sent within one hour.\n\n". + "Additional debug information:\n". + "- counter cache key: $cache_key\n". + "- content prefix: $content_cache_key_prefix\n". + "- content timeout: $timeout\n" + ); + } + $content_cache_key = $content_cache_key_prefix.$counter; + Cache::set($content_cache_key, $message, $timeout); + } + } + + /** Send an email message from OKAPI to the given recipients. */ + public static function mail_from_okapi($email_addresses, $subject, $message) + { + if (class_exists("okapi\\Settings") && (Settings::get('DEBUG_PREVENT_EMAILS'))) + { + # Sending emails was blocked on admin's demand. + # This is possible only on development environment. + return; + } + if (!is_array($email_addresses)) + $email_addresses = array($email_addresses); + $sender_email = class_exists("okapi\\Settings") ? Settings::get('FROM_FIELD') : 'root@localhost'; + mail(implode(", ", $email_addresses), $subject, $message, + "Content-Type: text/plain; charset=utf-8\n". + "From: OKAPI <$sender_email>\n". + "Reply-To: $sender_email\n" + ); + } + + /** Get directory to store dynamic (cache or temporary) files. No trailing slash included. */ + public static function get_var_dir() + { + $dir = Settings::get('VAR_DIR'); + if ($dir != null) + return rtrim($dir, "/"); + throw new Exception("You need to set a valid VAR_DIR."); + } + + /** Returns something like "Opencaching.PL" or "Opencaching.DE". */ + public static function get_normalized_site_name($site_url = null) + { + if ($site_url == null) + $site_url = Settings::get('SITE_URL'); + $matches = null; + if (preg_match("#^https?://(www.)?opencaching.([a-z.]+)/$#", $site_url, $matches)) { + return "Opencaching.".strtoupper($matches[2]); + } else { + return "DEVELSITE"; + } + } + + /** + * Pick text from $langdict based on language preference $langpref. + * + * Example: + * pick_best_language( + * array('pl' => 'X', 'de' => 'Y', 'en' => 'Z'), + * array('sp', 'de', 'en') + * ) == 'Y'. + * + * @param array $langdict - assoc array of lang-code => text. + * @param array $langprefs - list of lang codes, in order of preference. + */ + public static function pick_best_language($langdict, $langprefs) + { + foreach ($langprefs as $pref) + if (isset($langdict[$pref])) + return $langdict[$pref]; + foreach ($langdict as &$text_ref) + return $text_ref; + return ""; + } + + /** + * Split the array into groups of max. $size items. + */ + public static function make_groups($array, $size) + { + $i = 0; + $groups = array(); + while ($i < count($array)) + { + $groups[] = array_slice($array, $i, $size); + $i += $size; + } + return $groups; + } + + /** + * Check if any pre-request cronjobs are scheduled to execute and execute + * them if needed. Reschedule for new executions. + */ + public static function execute_prerequest_cronjobs() + { + $nearest_event = Okapi::get_var("cron_nearest_event"); + if ($nearest_event + 0 <= time()) + { + require_once($GLOBALS['rootpath']."okapi/cronjobs.php"); + $nearest_event = CronJobController::run_jobs('pre-request'); + Okapi::set_var("cron_nearest_event", $nearest_event); + } + } + + /** + * Check if any cron-5 cronjobs are scheduled to execute and execute + * them if needed. Reschedule for new executions. + */ + public static function execute_cron5_cronjobs() + { + $nearest_event = Okapi::get_var("cron_nearest_event"); + if ($nearest_event + 0 <= time()) + { + set_time_limit(0); + ignore_user_abort(true); + require_once($GLOBALS['rootpath']."okapi/cronjobs.php"); + $nearest_event = CronJobController::run_jobs('cron-5'); + Okapi::set_var("cron_nearest_event", $nearest_event); + } + } + + private static function gettext_set_lang($langprefs) + { + static $gettext_last_used_langprefs = null; + static $gettext_last_set_locale = null; + + # We remember the last $langprefs argument which we've been called with. + # This way, we don't need to call the actual locale-switching code most + # of the times. + + if ($gettext_last_used_langprefs != $langprefs) + { + $gettext_last_set_locale = call_user_func(Settings::get("GETTEXT_INIT"), $langprefs); + $gettext_last_used_langprefs = $langprefs; + textdomain(Settings::get("GETTEXT_DOMAIN")); + } + return $gettext_last_set_locale; + } + + private static $gettext_original_domain = null; + private static $gettext_langprefs_stack = array(); + + /** + * Attempt to switch the language based on the preference array given. + * Previous language settings will be remembered (in a stack). You SHOULD + * restore them later by calling gettext_domain_restore. + */ + public static function gettext_domain_init($langprefs = null) + { + # Put the langprefs on the stack. + + if ($langprefs == null) + $langprefs = array(Settings::get('SITELANG')); + self::$gettext_langprefs_stack[] = $langprefs; + + if (count(self::$gettext_langprefs_stack) == 1) + { + # This is the first time gettext_domain_init is called. In order to + # properly reinitialize the original settings after gettext_domain_restore + # is called for the last time, we need to save current textdomain (which + # should be different than the one which we use - Settings::get("GETTEXT_DOMAIN")). + + self::$gettext_original_domain = textdomain(null); + } + + # Attempt to change the language. Acquire the actual locale code used + # (might differ from expected when language was not found). + + $locale_code = self::gettext_set_lang($langprefs); + return $locale_code; + } + public static function gettext_domain_restore() + { + # Dismiss the last element on the langpref stack. This is the language + # which we've been actualy using until now. We want it replaced with + # the language below it. + + array_pop(self::$gettext_langprefs_stack); + + $size = count(self::$gettext_langprefs_stack); + if ($size > 0) + { + $langprefs = self::$gettext_langprefs_stack[$size - 1]; + self::gettext_set_lang($langprefs); + } + else + { + # The stack is empty. This means we're going out of OKAPI code and + # we want the original textdomain reestablished. + + textdomain(self::$gettext_original_domain); + self::$gettext_original_domain = null; + } + } + + /** + * Internal. This is called always when OKAPI core is included. + */ + public static function init_internals($allow_cronjobs = true) + { + static $init_made = false; + if ($init_made) + return; + ini_set('memory_limit', '256M'); + Db::connect(); + if (Settings::get('TIMEZONE') !== null) + date_default_timezone_set(Settings::get('TIMEZONE')); + if (!self::$data_store) + self::$data_store = new OkapiDataStore(); + if (!self::$server) + self::$server = new OkapiOAuthServer(self::$data_store); + if ($allow_cronjobs) + self::execute_prerequest_cronjobs(); + $init_made = true; + } + + /** + * Generate a string of random characters, suitable for keys as passwords. + * Troublesome characters like '0', 'O', '1', 'l' will not be used. + * If $user_friendly=true, then it will consist from numbers only. + */ + public static function generate_key($length, $user_friendly = false) + { + if ($user_friendly) + $chars = "0123456789"; + else + $chars = "abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789"; + $max = strlen($chars); + $key = ""; + for ($i=0; $i<$length; $i++) + { + $key .= $chars[rand(0, $max-1)]; + } + return $key; + } + + /** + * Register new OKAPI Consumer, send him an email with his key-pair, etc. + * This method does not verify parameter values, check if they are in + * a correct format prior the execution. + */ + public static function register_new_consumer($appname, $appurl, $email) + { + require_once($GLOBALS['rootpath']."okapi/service_runner.php"); + $consumer = new OkapiConsumer(Okapi::generate_key(20), Okapi::generate_key(40), + $appname, $appurl, $email); + $sample_cache = OkapiServiceRunner::call("services/caches/search/all", + new OkapiInternalRequest($consumer, null, array('limit', 1))); + if (count($sample_cache['results']) > 0) + $sample_cache_code = $sample_cache['results'][0]; + else + $sample_cache_code = "CACHECODE"; + + # Message for the Consumer. + ob_start(); + print "This is the key-pair we have created for your application:\n\n"; + print "Consumer Key: $consumer->key\n"; + print "Consumer Secret: $consumer->secret\n\n"; + print "Note: Consumer Secret is needed only when you intend to use OAuth.\n"; + print "You don't need Consumer Secret for Level 1 Authentication.\n\n"; + print "Now you can easily access Level 1 OKAPI methods. E.g.:\n"; + print Settings::get('SITE_URL')."okapi/services/caches/geocache?cache_code=$sample_cache_code&consumer_key=$consumer->key\n\n"; + print "If you plan on using OKAPI for a longer time, then you may want to\n"; + print "subscribe to the OKAPI News blog to stay up-to-date:\n"; + print "http://opencaching-api.blogspot.com/\n\n"; + print "Have fun!\n\n"; + print "-- \n"; + print "OKAPI Team\n"; + Okapi::mail_from_okapi($email, "Your OKAPI Consumer Key", ob_get_clean()); + + # Message for the Admins. + + ob_start(); + print "Name: $consumer->name\n"; + print "Developer: $consumer->email\n"; + print ($consumer->url ? "URL: $consumer->url\n" : ""); + print "Consumer Key: $consumer->key\n"; + Okapi::mail_admins("New OKAPI app registered!", ob_get_clean()); + + Db::execute(" + insert into okapi_consumers (`key`, name, secret, url, email, date_created) + values ( + '".mysql_real_escape_string($consumer->key)."', + '".mysql_real_escape_string($consumer->name)."', + '".mysql_real_escape_string($consumer->secret)."', + '".mysql_real_escape_string($consumer->url)."', + '".mysql_real_escape_string($consumer->email)."', + now() + ); + "); + } + + /** Return the distance between two geopoints, in meters. */ + public static function get_distance($lat1, $lon1, $lat2, $lon2) + { + $x1 = (90-$lat1) * 3.14159 / 180; + $x2 = (90-$lat2) * 3.14159 / 180; + $d = acos(cos($x1) * cos($x2) + sin($x1) * sin($x2) * cos(($lon1-$lon2) * 3.14159 / 180)) * 6371000; + if ($d < 0) $d = 0; + return $d; + } + + /** + * Return an SQL formula for calculating distance between two geopoints. + * Parameters should be either numberals or strings (SQL field references). + */ + public static function get_distance_sql($lat1, $lon1, $lat2, $lon2) + { + $x1 = "(90-$lat1) * 3.14159 / 180"; + $x2 = "(90-$lat2) * 3.14159 / 180"; + $d = "acos(cos($x1) * cos($x2) + sin($x1) * sin($x2) * cos(($lon1-$lon2) * 3.14159 / 180)) * 6371000"; + return $d; + } + + /** Return bearing (float 0..360) from geopoint 1 to 2. */ + public static function get_bearing($lat1, $lon1, $lat2, $lon2) + { + if ($lat1 == $lat2 && $lon1 == $lon2) + return null; + if ($lat1 == $lat2) $lat1 += 0.0000166; + if ($lon1 == $lon2) $lon1 += 0.0000166; + + $rad_lat1 = $lat1 / 180.0 * 3.14159; + $rad_lon1 = $lon1 / 180.0 * 3.14159; + $rad_lat2 = $lat2 / 180.0 * 3.14159; + $rad_lon2 = $lon2 / 180.0 * 3.14159; + + $delta_lon = $rad_lon2 - $rad_lon1; + $bearing = atan2(sin($delta_lon) * cos($rad_lat2), + cos($rad_lat1) * sin($rad_lat2) - sin($rad_lat1) * cos($rad_lat2) * cos($delta_lon)); + $bearing = 180.0 * $bearing / 3.14159; + if ( $bearing < 0.0 ) $bearing = $bearing + 360.0; + + return $bearing; + } + + /** Transform bearing (float 0..360) to simple 2-letter string (N, NE, E, SE, etc.) */ + public static function bearing_as_two_letters($b) + { + static $names = array('N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW'); + if ($b === null) return 'n/a'; + return $names[round(($b / 360.0) * 8.0) % 8]; + } + + /** Transform bearing (float 0..360) to simple 3-letter string (N, NNE, NE, ESE, etc.) */ + public static function bearing_as_three_letters($b) + { + static $names = array('N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', + 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW'); + if ($b === null) return 'n/a'; + return $names[round(($b / 360.0) * 16.0) % 16]; + } + + /** Escape string for use with XML. See issue 169. */ + public static function xmlescape($string) + { + static $pattern = '/[^\x{0009}\x{000a}\x{000d}\x{0020}-\x{D7FF}\x{E000}-\x{FFFD}]+/u'; + $string = preg_replace($pattern, '', $string); + return strtr($string, array("<" => "<", ">" => ">", "\"" => """, "'" => "'", "&" => "&")); + } + + /** + * Return object as a standard OKAPI response. The $object will be formatted + * using one of the default formatters (JSON, JSONP, XML, etc.). Formatter is + * auto-detected by peeking on the $request's 'format' parameter. In some + * specific cases, this method can also return the $object itself, instead + * of OkapiResponse - this allows nesting methods within other methods. + */ + public static function formatted_response(OkapiRequest $request, &$object) + { + if ($request instanceof OkapiInternalRequest && ($request->i_want_okapi_response == false)) + { + # If you call a method internally, then you probably expect to get + # the actual object instead of it's formatted representation. + return $object; + } + $format = $request->get_parameter('format'); + if ($format == null) $format = 'json'; + if (!in_array($format, array('json', 'jsonp', 'xmlmap', 'xmlmap2'))) + throw new InvalidParam('format', "'$format'"); + $callback = $request->get_parameter('callback'); + if ($callback && $format != 'jsonp') + throw new BadRequest("The 'callback' parameter is reserved to be used with the JSONP output format."); + if ($format == 'json') + { + $response = new OkapiHttpResponse(); + $response->content_type = "application/json; charset=utf-8"; + $response->body = json_encode($object); + return $response; + } + elseif ($format == 'jsonp') + { + if (!$callback) + throw new BadRequest("'callback' parameter is required for JSONP calls"); + if (!preg_match("/^[a-zA-Z_][a-zA-Z0-9_]*$/", $callback)) + throw new InvalidParam('callback', "'$callback' doesn't seem to be a valid JavaScript function name (should match /^[a-zA-Z_][a-zA-Z0-9_]*\$/)."); + $response = new OkapiHttpResponse(); + $response->content_type = "application/javascript; charset=utf-8"; + $response->body = $callback."(".json_encode($object).");"; + return $response; + } + elseif ($format == 'xmlmap') + { + # Deprecated (see issue 128). Keeping this for backward-compatibility. + $response = new OkapiHttpResponse(); + $response->content_type = "text/xml; charset=utf-8"; + $response->body = self::xmlmap_dumps($object); + return $response; + } + elseif ($format == 'xmlmap2') + { + $response = new OkapiHttpResponse(); + $response->content_type = "text/xml; charset=utf-8"; + $response->body = self::xmlmap2_dumps($object); + return $response; + } + else + { + # Should not happen (as we do a proper check above). + throw new Exception(); + } + } + + private static function _xmlmap_add(&$chunks, &$obj) + { + if (is_string($obj)) + { + $chunks[] = ""; + $chunks[] = self::xmlescape($obj); + $chunks[] = ""; + } + elseif (is_int($obj)) + { + $chunks[] = "$obj"; + } + elseif (is_float($obj)) + { + $chunks[] = "$obj"; + } + elseif (is_bool($obj)) + { + $chunks[] = $obj ? "true" : "false"; + } + elseif (is_null($obj)) + { + $chunks[] = ""; + } + elseif (is_array($obj)) + { + # Have to check if this is associative or not! Shit. I hate PHP. + if (array_keys($obj) === range(0, count($obj) - 1)) + { + # Not assoc. + $chunks[] = ""; + foreach ($obj as &$item_ref) + { + $chunks[] = ""; + self::_xmlmap_add($chunks, $item_ref); + $chunks[] = ""; + } + $chunks[] = ""; + } + else + { + # Assoc. + $chunks[] = ""; + foreach ($obj as $key => &$item_ref) + { + $chunks[] = ""; + self::_xmlmap_add($chunks, $item_ref); + $chunks[] = ""; + } + $chunks[] = ""; + } + } + else + { + # That's a bug. + throw new Exception("Cannot encode as xmlmap: " + print_r($obj, true)); + } + } + + private static function _xmlmap2_add(&$chunks, &$obj, $key) + { + $attrs = ($key !== null) ? " key=\"".self::xmlescape($key)."\"" : ""; + if (is_string($obj)) + { + $chunks[] = ""; + $chunks[] = self::xmlescape($obj); + $chunks[] = ""; + } + elseif (is_int($obj)) + { + $chunks[] = "$obj"; + } + elseif (is_float($obj)) + { + $chunks[] = "$obj"; + } + elseif (is_bool($obj)) + { + $chunks[] = $obj ? "true" : "false"; + } + elseif (is_null($obj)) + { + $chunks[] = ""; + } + elseif (is_array($obj) || ($obj instanceof ArrayObject)) + { + # Have to check if this is associative or not! Shit. I hate PHP. + if (is_array($obj) && (array_keys($obj) === range(0, count($obj) - 1))) + { + # Not assoc. + $chunks[] = ""; + foreach ($obj as &$item_ref) + { + self::_xmlmap2_add($chunks, $item_ref, null); + } + $chunks[] = ""; + } + else + { + # Assoc. + $chunks[] = ""; + foreach ($obj as $key => &$item_ref) + { + self::_xmlmap2_add($chunks, $item_ref, $key); + } + $chunks[] = ""; + } + } + else + { + # That's a bug. + throw new Exception("Cannot encode as xmlmap2: " . print_r($obj, true)); + } + } + + /** Return the object in a serialized version, in the (deprecated) "xmlmap" format. */ + public static function xmlmap_dumps(&$obj) + { + $chunks = array(); + self::_xmlmap_add($chunks, $obj); + return implode('', $chunks); + } + + /** Return the object in a serialized version, in the "xmlmap2" format. */ + public static function xmlmap2_dumps(&$obj) + { + $chunks = array(); + self::_xmlmap2_add($chunks, $obj, null); + return implode('', $chunks); + } + + private static $cache_types = array( + # + # OKAPI does not expose type IDs. Instead, it uses the following + # "code words". Only the "primary" cache types are documented. + # This means that all other types may (in theory) be altered. + # Cache type may become "primary" ONLY when *all* OC servers recognize + # that type. + # + # Changing this may introduce nasty bugs (e.g. in the replicate module). + # CONTACT ME BEFORE YOU MODIFY THIS! + # + 'oc.pl' => array( + # Primary types (documented, cannot change) + 'Traditional' => 2, 'Multi' => 3, 'Quiz' => 7, 'Virtual' => 4, + 'Event' => 6, + # Additional types (may get changed) + 'Other' => 1, 'Webcam' => 5, + 'Moving' => 8, 'Podcast' => 9, 'Own' => 10, + ), + 'oc.de' => array( + # Primary types (documented, cannot change) + 'Traditional' => 2, 'Multi' => 3, 'Quiz' => 7, 'Virtual' => 4, + 'Event' => 6, + # Additional types (might get changed) + 'Other' => 1, 'Webcam' => 5, + 'Math/Physics' => 8, 'Moving' => 9, 'Drive-In' => 10, + ) + ); + + /** E.g. 'Traditional' => 2. For unknown names throw an Exception. */ + public static function cache_type_name2id($name) + { + $ref = &self::$cache_types[Settings::get('OC_BRANCH')]; + if (isset($ref[$name])) + return $ref[$name]; + throw new Exception("Method cache_type_name2id called with unsupported cache ". + "type name '$name'. You should not allow users to submit caches ". + "of non-primary type."); + } + + /** E.g. 2 => 'Traditional'. For unknown ids returns "Other". */ + public static function cache_type_id2name($id) + { + static $reversed = null; + if ($reversed == null) + { + $reversed = array(); + foreach (self::$cache_types[Settings::get('OC_BRANCH')] as $key => $value) + $reversed[$value] = $key; + } + if (isset($reversed[$id])) + return $reversed[$id]; + return "Other"; + } + + private static $cache_statuses = array( + 'Available' => 1, 'Temporarily unavailable' => 2, 'Archived' => 3 + ); + + /** E.g. 'Available' => 1. For unknown names throws an Exception. */ + public static function cache_status_name2id($name) + { + if (isset(self::$cache_statuses[$name])) + return self::$cache_statuses[$name]; + throw new Exception("Method cache_status_name2id called with invalid name '$name'."); + } + + /** E.g. 1 => 'Available'. For unknown ids returns 'Archived'. */ + public static function cache_status_id2name($id) + { + static $reversed = null; + if ($reversed == null) + { + $reversed = array(); + foreach (self::$cache_statuses as $key => $value) + $reversed[$value] = $key; + } + if (isset($reversed[$id])) + return $reversed[$id]; + return 'Archived'; + } + + private static $cache_sizes = array( + 'none' => 7, + 'nano' => 8, + 'micro' => 2, + 'small' => 3, + 'regular' => 4, + 'large' => 5, + 'xlarge' => 6, + 'other' => 1, + ); + + /** E.g. 'micro' => 2. For unknown names throw an Exception. */ + public static function cache_size2_to_sizeid($size2) + { + if (isset(self::$cache_sizes[$size2])) + return self::$cache_sizes[$size2]; + throw new Exception("Method cache_size2_to_sizeid called with invalid size2 '$size2'."); + } + + /** E.g. 2 => 'micro'. For unknown ids returns "other". */ + public static function cache_sizeid_to_size2($id) + { + static $reversed = null; + if ($reversed == null) + { + $reversed = array(); + foreach (self::$cache_sizes as $key => $value) + $reversed[$value] = $key; + } + if (isset($reversed[$id])) + return $reversed[$id]; + return "other"; + } + + /** Maps OKAPI's 'size2' values to opencaching.com (OX) size codes. */ + private static $cache_OX_sizes = array( + 'none' => null, + 'nano' => 1.3, + 'micro' => 2.0, + 'small' => 3.0, + 'regular' => 3.8, + 'large' => 4.6, + 'xlarge' => 4.9, + 'other' => null, + ); + + /** + * E.g. 'micro' => 2.0, 'other' => null. For unknown names throw an + * Exception. Note, that this is not a bijection ('none' are 'other' are + * both null). + */ + public static function cache_size2_to_oxsize($size2) + { + if (array_key_exists($size2, self::$cache_OX_sizes)) + return self::$cache_OX_sizes[$size2]; + throw new Exception("Method cache_size2_to_oxsize called with invalid size2 '$size2'."); + } + + /** + * E.g. 'Found it' => 1. For unsupported names throws Exception. + */ + public static function logtypename2id($name) + { + if ($name == 'Found it') return 1; + if ($name == "Didn't find it") return 2; + if ($name == 'Comment') return 3; + if ($name == 'Attended') return 7; + if ($name == 'Will attend') return 8; + if (($name == 'Needs maintenance') && (Settings::get('SUPPORTS_LOGTYPE_NEEDS_MAINTENANCE'))) + return 5; + throw new Exception("logtype2id called with invalid log type argument: $name"); + } + + /** E.g. 1 => 'Found it'. For unknown ids returns 'Comment'. */ + public static function logtypeid2name($id) + { + # Various OC nodes use different English names, even for primary + # log types. OKAPI needs to have them the same across *all* OKAPI + # installations. That's why all known types are hardcoded here. + # These names are officially documented and may never change! + + # Primary. + if ($id == 1) return "Found it"; + if ($id == 2) return "Didn't find it"; + if ($id == 3) return "Comment"; + if ($id == 7) return "Attended"; + if ($id == 8) return "Will attend"; + + # Other. + if ($id == 4) return "Moved"; + if ($id == 5) return "Needs maintenance"; + if ($id == 6) return "Maintenance performed"; + if ($id == 9) return "Archived"; + if ($id == 10) return "Ready to search"; + if ($id == 11) return "Temporarily unavailable"; + if ($id == 12) return "OC Team comment"; + if ($id == 13 || $id == 14) return "Locked"; + + # Important: This set is not closed. Other types may be introduced + # in the future. This has to be documented in the public method + # description. + + return "Comment"; + } + + /** + * "Fix" user-supplied HTML fetched from the OC database. + */ + public static function fix_oc_html($html) + { + /* There are thousands of relative URLs in cache descriptions. We will + * attempt to find them and fix them. In theory, the "proper" way to do this + * would be to parse the description into a DOM tree, but that would simply + * be very hard (and inefficient) to do, since most of the descriptions are + * not even valid HTML. + */ + + $html = preg_replace( + "~\b(src|href)=([\"'])(?![a-z0-9_-]+:)~", + "$1=$2".Settings::get("SITE_URL"), + $html + ); + + /* Other things to do in the future: + * + * 1. Check for XSS vulerabilities? + * 2. Transform to a valid (X)HTML? + */ + + return $html; + } } /** A data caching layer. For slow SQL queries etc. */ class Cache { - /** - * Save object $value under the key $key. Store this object for - * $timeout seconds. $key must be a string of max 64 characters in length. - * $value might be any serializable PHP object. - * - * If $timeout is null, then the object will be treated as persistent - * (the Cache will do its best to NEVER remove it). - */ - public static function set($key, $value, $timeout) - { - if ($timeout == null) - { - # The current cache implementation is ALWAYS persistent, so we will - # just replace it with a big value. - $timeout = 100*365*86400; - } - Db::execute(" - replace into okapi_cache (`key`, value, expires) - values ( - '".mysql_real_escape_string($key)."', - '".mysql_real_escape_string(gzdeflate(serialize($value)))."', - date_add(now(), interval '".mysql_real_escape_string($timeout)."' second) - ); - "); - } + /** + * Save object $value under the key $key. Store this object for + * $timeout seconds. $key must be a string of max 64 characters in length. + * $value might be any serializable PHP object. + * + * If $timeout is null, then the object will be treated as persistent + * (the Cache will do its best to NEVER remove it). + */ + public static function set($key, $value, $timeout) + { + if ($timeout == null) + { + # The current cache implementation is ALWAYS persistent, so we will + # just replace it with a big value. + $timeout = 100*365*86400; + } + Db::execute(" + replace into okapi_cache (`key`, value, expires) + values ( + '".mysql_real_escape_string($key)."', + '".mysql_real_escape_string(gzdeflate(serialize($value)))."', + date_add(now(), interval '".mysql_real_escape_string($timeout)."' second) + ); + "); + } - /** - * Scored version of set. Elements set up this way will expire when they're - * not used. - */ - public static function set_scored($key, $value) - { - Db::execute(" - replace into okapi_cache (`key`, value, expires, score) - values ( - '".mysql_real_escape_string($key)."', - '".mysql_real_escape_string(gzdeflate(serialize($value)))."', - date_add(now(), interval 120 day), - 1.0 - ); - "); - } + /** + * Scored version of set. Elements set up this way will expire when they're + * not used. + */ + public static function set_scored($key, $value) + { + Db::execute(" + replace into okapi_cache (`key`, value, expires, score) + values ( + '".mysql_real_escape_string($key)."', + '".mysql_real_escape_string(gzdeflate(serialize($value)))."', + date_add(now(), interval 120 day), + 1.0 + ); + "); + } - /** Do 'set' on many keys at once. */ - public static function set_many($dict, $timeout) - { - if (count($dict) == 0) - return; - if ($timeout == null) - { - # The current cache implementation is ALWAYS persistent, so we will - # just replace it with a big value. - $timeout = 100*365*86400; - } - $entries_escaped = array(); - foreach ($dict as $key => $value) - { - $entries_escaped[] = "( - '".mysql_real_escape_string($key)."', - '".mysql_real_escape_string(gzdeflate(serialize($value)))."', - date_add(now(), interval '".mysql_real_escape_string($timeout)."' second) - )"; - } - Db::execute(" - replace into okapi_cache (`key`, value, expires) - values ".implode(", ", $entries_escaped)." - "); - } + /** Do 'set' on many keys at once. */ + public static function set_many($dict, $timeout) + { + if (count($dict) == 0) + return; + if ($timeout == null) + { + # The current cache implementation is ALWAYS persistent, so we will + # just replace it with a big value. + $timeout = 100*365*86400; + } + $entries_escaped = array(); + foreach ($dict as $key => $value) + { + $entries_escaped[] = "( + '".mysql_real_escape_string($key)."', + '".mysql_real_escape_string(gzdeflate(serialize($value)))."', + date_add(now(), interval '".mysql_real_escape_string($timeout)."' second) + )"; + } + Db::execute(" + replace into okapi_cache (`key`, value, expires) + values ".implode(", ", $entries_escaped)." + "); + } - /** - * Retrieve object stored under the key $key. If object does not - * exist or timeout expired, return null. - */ - public static function get($key) - { - $rs = Db::query(" - select value, score - from okapi_cache - where - `key` = '".mysql_real_escape_string($key)."' - and expires > now() - "); - list($blob, $score) = mysql_fetch_array($rs); - if (!$blob) - return null; - if ($score != null) # Only non-null entries are scored. - { - Db::execute(" - insert into okapi_cache_reads (`cache_key`) - values ('".mysql_real_escape_string($key)."') - "); - } - return unserialize(gzinflate($blob)); - } + /** + * Retrieve object stored under the key $key. If object does not + * exist or timeout expired, return null. + */ + public static function get($key) + { + $rs = Db::query(" + select value, score + from okapi_cache + where + `key` = '".mysql_real_escape_string($key)."' + and expires > now() + "); + list($blob, $score) = mysql_fetch_array($rs); + if (!$blob) + return null; + if ($score != null) # Only non-null entries are scored. + { + Db::execute(" + insert into okapi_cache_reads (`cache_key`) + values ('".mysql_real_escape_string($key)."') + "); + } + return unserialize(gzinflate($blob)); + } - /** Do 'get' on many keys at once. */ - public static function get_many($keys) - { - $dict = array(); - $rs = Db::query(" - select `key`, value - from okapi_cache - where - `key` in ('".implode("','", array_map('mysql_real_escape_string', $keys))."') - and expires > now() - "); - while ($row = mysql_fetch_assoc($rs)) - { - try - { - $dict[$row['key']] = unserialize(gzinflate($row['value'])); - } - catch (ErrorException $e) - { - unset($dict[$row['key']]); - Okapi::mail_admins("Debug: Unserialize error", - "Could not unserialize key '".$row['key']."' from Cache.\n". - "Probably something REALLY big was put there and data has been truncated.\n". - "Consider upgrading cache table to LONGBLOB.\n\n". - "Length of data, compressed: ".strlen($row['value'])); - } - } - if (count($dict) < count($keys)) - foreach ($keys as $key) - if (!isset($dict[$key])) - $dict[$key] = null; - return $dict; - } + /** Do 'get' on many keys at once. */ + public static function get_many($keys) + { + $dict = array(); + $rs = Db::query(" + select `key`, value + from okapi_cache + where + `key` in ('".implode("','", array_map('mysql_real_escape_string', $keys))."') + and expires > now() + "); + while ($row = mysql_fetch_assoc($rs)) + { + try + { + $dict[$row['key']] = unserialize(gzinflate($row['value'])); + } + catch (ErrorException $e) + { + unset($dict[$row['key']]); + Okapi::mail_admins("Debug: Unserialize error", + "Could not unserialize key '".$row['key']."' from Cache.\n". + "Probably something REALLY big was put there and data has been truncated.\n". + "Consider upgrading cache table to LONGBLOB.\n\n". + "Length of data, compressed: ".strlen($row['value'])); + } + } + if (count($dict) < count($keys)) + foreach ($keys as $key) + if (!isset($dict[$key])) + $dict[$key] = null; + return $dict; + } - /** - * Delete key $key from the cache. - */ - public static function delete($key) - { - self::delete_many(array($key)); - } + /** + * Delete key $key from the cache. + */ + public static function delete($key) + { + self::delete_many(array($key)); + } - /** Do 'delete' on many keys at once. */ - public static function delete_many($keys) - { - if (count($keys) == 0) - return; - Db::execute(" - delete from okapi_cache - where `key` in ('".implode("','", array_map('mysql_real_escape_string', $keys))."') - "); - } + /** Do 'delete' on many keys at once. */ + public static function delete_many($keys) + { + if (count($keys) == 0) + return; + Db::execute(" + delete from okapi_cache + where `key` in ('".implode("','", array_map('mysql_real_escape_string', $keys))."') + "); + } } /** @@ -1827,25 +1870,25 @@ class Cache */ class FileCache { - public static function get_file_path($key) - { - $filename = Okapi::get_var_dir()."/okapi_filecache_".md5($key); - if (!file_exists($filename)) - return null; - return $filename; - } + public static function get_file_path($key) + { + $filename = Okapi::get_var_dir()."/okapi_filecache_".md5($key); + if (!file_exists($filename)) + return null; + return $filename; + } - /** - * Note, there is no $timeout (time to live) parameter. Currently, - * OKAPI will delete every old file after certain amount of time. - * See CacheCleanupCronJob for details. - */ - public static function set($key, $value) - { - $filename = Okapi::get_var_dir()."/okapi_filecache_".md5($key); - file_put_contents($filename, $value); - return $filename; - } + /** + * Note, there is no $timeout (time to live) parameter. Currently, + * OKAPI will delete every old file after certain amount of time. + * See CacheCleanupCronJob for details. + */ + public static function set($key, $value) + { + $filename = Okapi::get_var_dir()."/okapi_filecache_".md5($key); + file_put_contents($filename, $value); + return $filename; + } } /** @@ -1861,255 +1904,263 @@ class FileCache */ abstract class OkapiRequest { - public $consumer; - public $token; - public $etag; # see: http://en.wikipedia.org/wiki/HTTP_ETag + public $consumer; + public $token; + public $etag; # see: http://en.wikipedia.org/wiki/HTTP_ETag - /** - * Set this to true, for some method to allow you to set higher "limit" - * parameter than usually allowed. This should be used ONLY by trusted, - * fast and *cacheable* code! - */ - public $skip_limits = false; + /** + * Set this to true, for some method to allow you to set higher "limit" + * parameter than usually allowed. This should be used ONLY by trusted, + * fast and *cacheable* code! + */ + public $skip_limits = false; - /** - * Return request parameter, or NULL when not found. Use this instead of - * $_GET or $_POST or $_REQUEST. - */ - public abstract function get_parameter($name); + /** + * Return request parameter, or NULL when not found. Use this instead of + * $_GET or $_POST or $_REQUEST. + */ + public abstract function get_parameter($name); - /** - * Return the list of all request parameters. You should use this method - * ONLY when you use in your documentation and you want - * to pass all unknown parameters onto the other method. - */ - public abstract function get_all_parameters_including_unknown(); + /** + * Return the list of all request parameters. You should use this method + * ONLY when you use in your documentation and you want + * to pass all unknown parameters onto the other method. + */ + public abstract function get_all_parameters_including_unknown(); - /** Return true, if this requests is to be logged as HTTP request in okapi_stats. */ - public abstract function is_http_request(); + /** Return true, if this requests is to be logged as HTTP request in okapi_stats. */ + public abstract function is_http_request(); } class OkapiInternalRequest extends OkapiRequest { - private $parameters; + private $parameters; - /** - * Set this to true, if you want this request to be considered as HTTP request - * in okapi_stats tables. This is useful when running requests through Facade - * (we want them logged and displayed in weekly report). - */ - public $perceive_as_http_request = false; + /** + * Set this to true, if you want this request to be considered as HTTP request + * in okapi_stats tables. This is useful when running requests through Facade + * (we want them logged and displayed in weekly report). + */ + public $perceive_as_http_request = false; - /** - * Set this to true, if you want to receive OkapiResponse instead of - * the actual object. - */ - public $i_want_okapi_response = false; + /** + * Set this to true, if you want to receive OkapiResponse instead of + * the actual object. + */ + public $i_want_okapi_response = false; - /** - * You may use "null" values in parameters if you want them skipped - * (null-ized keys will be removed from parameters). - */ - public function __construct($consumer, $token, $parameters) - { - $this->consumer = $consumer; - $this->token = $token; - $this->parameters = array(); - foreach ($parameters as $key => $value) - if ($value !== null) - $this->parameters[$key] = $value; - } + /** + * You may use "null" values in parameters if you want them skipped + * (null-ized keys will be removed from parameters). + */ + public function __construct($consumer, $token, $parameters) + { + $this->consumer = $consumer; + $this->token = $token; + $this->parameters = array(); + foreach ($parameters as $key => $value) + if ($value !== null) + $this->parameters[$key] = $value; + } - public function get_parameter($name) - { - if (isset($this->parameters[$name])) - return $this->parameters[$name]; - else - return null; - } + public function get_parameter($name) + { + if (isset($this->parameters[$name])) + return $this->parameters[$name]; + else + return null; + } - public function get_all_parameters_including_unknown() - { - return $this->parameters; - } + public function get_all_parameters_including_unknown() + { + return $this->parameters; + } - public function is_http_request() { return $this->perceive_as_http_request; } + public function is_http_request() { return $this->perceive_as_http_request; } } class OkapiHttpRequest extends OkapiRequest { - private $request; /* @var OAuthRequest */ - private $opt_min_auth_level; # 0..3 - private $opt_token_type = 'access'; # "access" or "request" + private $request; /* @var OAuthRequest */ + private $opt_min_auth_level; # 0..3 + private $opt_token_type = 'access'; # "access" or "request" - public function __construct($options) - { - Okapi::init_internals(); - $this->init_request(); - # - # Parsing options. - # - $DEBUG_AS_USERNAME = null; - foreach ($options as $key => $value) - { - switch ($key) - { - case 'min_auth_level': - if (!in_array($value, array(0, 1, 2, 3))) - { - throw new Exception("'min_auth_level' option has invalid value: $value"); - } - $this->opt_min_auth_level = $value; - break; - case 'token_type': - if (!in_array($value, array("request", "access"))) - { - throw new Exception("'token_type' option has invalid value: $value"); - } - $this->opt_token_type = $value; - break; - case 'DEBUG_AS_USERNAME': - $DEBUG_AS_USERNAME = $value; - break; - default: - throw new Exception("Unknown option: $key"); - break; - } - } - if ($this->opt_min_auth_level === null) throw new Exception("Required 'min_auth_level' option is missing."); + public function __construct($options) + { + Okapi::init_internals(); + $this->init_request(); + # + # Parsing options. + # + $DEBUG_AS_USERNAME = null; + foreach ($options as $key => $value) + { + switch ($key) + { + case 'min_auth_level': + if (!in_array($value, array(0, 1, 2, 3))) + { + throw new Exception("'min_auth_level' option has invalid value: $value"); + } + $this->opt_min_auth_level = $value; + break; + case 'token_type': + if (!in_array($value, array("request", "access"))) + { + throw new Exception("'token_type' option has invalid value: $value"); + } + $this->opt_token_type = $value; + break; + case 'DEBUG_AS_USERNAME': + $DEBUG_AS_USERNAME = $value; + break; + default: + throw new Exception("Unknown option: $key"); + break; + } + } + if ($this->opt_min_auth_level === null) throw new Exception("Required 'min_auth_level' option is missing."); - if ($DEBUG_AS_USERNAME != null) - { - # Enables debugging Level 2 and Level 3 methods. Should not be committed - # at any time! If run on production server, make it an error. + if ($DEBUG_AS_USERNAME != null) + { + # Enables debugging Level 2 and Level 3 methods. Should not be committed + # at any time! If run on production server, make it an error. - if (!Settings::get('DEBUG')) - { - throw new Exception("Attempted to use DEBUG_AS_USERNAME in ". - "non-debug environment. Accidental commit?"); - } + if (!Settings::get('DEBUG')) + { + throw new Exception("Attempted to use DEBUG_AS_USERNAME in ". + "non-debug environment. Accidental commit?"); + } - # Lower required authentication to Level 0, to pass the checks. + # Lower required authentication to Level 0, to pass the checks. - $this->opt_min_auth_level = 0; - } + $this->opt_min_auth_level = 0; + } - # - # Let's see if the request is signed. If it is, verify the signature. - # It it's not, check if it isn't against the rules defined in the $options. - # + # + # Let's see if the request is signed. If it is, verify the signature. + # It it's not, check if it isn't against the rules defined in the $options. + # - if ($this->get_parameter('oauth_signature')) - { - # User is using OAuth. There is a cronjob scheduled to run every 5 minutes and - # delete old Request Tokens and Nonces. We may assume that cleanup was executed - # not more than 5 minutes ago. + if ($this->get_parameter('oauth_signature')) + { + # User is using OAuth. - list($this->consumer, $this->token) = Okapi::$server-> - verify_request2($this->request, $this->opt_token_type, $this->opt_min_auth_level == 3); - if ($this->get_parameter('consumer_key') && $this->get_parameter('consumer_key') != $this->get_parameter('oauth_consumer_key')) - throw new BadRequest("Inproper mixing of authentication types. You used both 'consumer_key' ". - "and 'oauth_consumer_key' parameters (Level 1 and Level 2), but they do not match with ". - "each other. Were you trying to hack me? ;)"); - if ($this->opt_min_auth_level == 3 && !$this->token) - { - throw new BadRequest("This method requires a valid Token to be included (Level 3 ". - "Authentication). You didn't provide one."); - } - } - else - { - if ($this->opt_min_auth_level >= 2) - { - throw new BadRequest("This method requires OAuth signature (Level ". - $this->opt_min_auth_level." Authentication). You didn't sign your request."); - } - else - { - $consumer_key = $this->get_parameter('consumer_key'); - if ($consumer_key) - { - $this->consumer = Okapi::$data_store->lookup_consumer($consumer_key); - if (!$this->consumer) - throw new InvalidParam('consumer_key', "Consumer does not exist."); - } - if (($this->opt_min_auth_level == 1) && (!$this->consumer)) - throw new BadRequest("This method requires the 'consumer_key' argument (Level 1 ". - "Authentication). You didn't provide one."); - } - } + # Check for duplicate keys in the parameters. (Datastore doesn't + # do that on its own, it caused vague server errors - issue #307.) - if (is_object($this->consumer) && $this->consumer->admin) - { - /* Some chosen Consumers gain special permissions within OKAPI. - * Currently, there's only a single "admin" flag in the okapi_consumers - * table, and there's just a single extra permission to gain, but - * the this set of permissions may grow in time. */ + $this->get_parameter('oauth_consumer'); + $this->get_parameter('oauth_version'); + $this->get_parameter('oauth_token'); + $this->get_parameter('oauth_nonce'); - $this->skip_limits = true; - } + # Verify OAuth request. - # - # Prevent developers from accessing request parameters with PHP globals. - # Remember, that OKAPI requests can be nested within other OKAPI requests! - # Search the code for "new OkapiInternalRequest" to see examples. - # + list($this->consumer, $this->token) = Okapi::$server-> + verify_request2($this->request, $this->opt_token_type, $this->opt_min_auth_level == 3); + if ($this->get_parameter('consumer_key') && $this->get_parameter('consumer_key') != $this->get_parameter('oauth_consumer_key')) + throw new BadRequest("Inproper mixing of authentication types. You used both 'consumer_key' ". + "and 'oauth_consumer_key' parameters (Level 1 and Level 2), but they do not match with ". + "each other. Were you trying to hack me? ;)"); + if ($this->opt_min_auth_level == 3 && !$this->token) + { + throw new BadRequest("This method requires a valid Token to be included (Level 3 ". + "Authentication). You didn't provide one."); + } + } + else + { + if ($this->opt_min_auth_level >= 2) + { + throw new BadRequest("This method requires OAuth signature (Level ". + $this->opt_min_auth_level." Authentication). You didn't sign your request."); + } + else + { + $consumer_key = $this->get_parameter('consumer_key'); + if ($consumer_key) + { + $this->consumer = Okapi::$data_store->lookup_consumer($consumer_key); + if (!$this->consumer) + throw new InvalidParam('consumer_key', "Consumer does not exist."); + } + if (($this->opt_min_auth_level == 1) && (!$this->consumer)) + throw new BadRequest("This method requires the 'consumer_key' argument (Level 1 ". + "Authentication). You didn't provide one."); + } + } - $_GET = $_POST = $_REQUEST = null; + if (is_object($this->consumer) && $this->consumer->admin) + { + /* Some chosen Consumers gain special permissions within OKAPI. + * Currently, there's only a single "admin" flag in the okapi_consumers + * table, and there's just a single extra permission to gain, but + * the this set of permissions may grow in time. */ - # When debugging, simulate as if been run using a proper Level 3 Authentication. + $this->skip_limits = true; + } - if ($DEBUG_AS_USERNAME != null) - { - # Note, that this will override any other valid authentication the - # developer might have issued. + # + # Prevent developers from accessing request parameters with PHP globals. + # Remember, that OKAPI requests can be nested within other OKAPI requests! + # Search the code for "new OkapiInternalRequest" to see examples. + # - $debug_user_id = Db::select_value("select user_id from user where username='". - mysql_real_escape_string($options['DEBUG_AS_USERNAME'])."'"); - if ($debug_user_id == null) - throw new Exception("Invalid user name in DEBUG_AS_USERNAME: '".$options['DEBUG_AS_USERNAME']."'"); - $this->consumer = new OkapiDebugConsumer(); - $this->token = new OkapiDebugAccessToken($debug_user_id); - } + $_GET = $_POST = $_REQUEST = null; - # Read the ETag. + # When debugging, simulate as if been run using a proper Level 3 Authentication. - if (isset($_SERVER['HTTP_IF_NONE_MATCH'])) - $this->etag = $_SERVER['HTTP_IF_NONE_MATCH']; - } + if ($DEBUG_AS_USERNAME != null) + { + # Note, that this will override any other valid authentication the + # developer might have issued. - private function init_request() - { - $this->request = OAuthRequest::from_request(); - if (!in_array($this->request->get_normalized_http_method(), - array('GET', 'POST'))) - { - throw new BadRequest("Use GET and POST methods only."); - } - } + $debug_user_id = Db::select_value("select user_id from user where username='". + mysql_real_escape_string($options['DEBUG_AS_USERNAME'])."'"); + if ($debug_user_id == null) + throw new Exception("Invalid user name in DEBUG_AS_USERNAME: '".$options['DEBUG_AS_USERNAME']."'"); + $this->consumer = new OkapiDebugConsumer(); + $this->token = new OkapiDebugAccessToken($debug_user_id); + } - /** - * Return request parameter, or NULL when not found. Use this instead of - * $_GET or $_POST or $_REQUEST. - */ - public function get_parameter($name) - { - $value = $this->request->get_parameter($name); + # Read the ETag. - # Default implementation of OAuthRequest allows arrays to be passed with - # multiple references to the same variable ("a=1&a=2&a=3"). This is invalid - # in OKAPI and should be reported back. See issue 85: - # http://code.google.com/p/opencaching-api/issues/detail?id=85 + if (isset($_SERVER['HTTP_IF_NONE_MATCH'])) + $this->etag = $_SERVER['HTTP_IF_NONE_MATCH']; + } - if (is_array($value)) - throw new InvalidParam($name, "Make sure you are using '$name' no more than ONCE in your URL."); - return $value; - } + private function init_request() + { + $this->request = OAuthRequest::from_request(); + if (!in_array($this->request->get_normalized_http_method(), + array('GET', 'POST'))) + { + throw new BadRequest("Use GET and POST methods only."); + } + } - public function get_all_parameters_including_unknown() - { - return $this->request->get_parameters(); - } + /** + * Return request parameter, or NULL when not found. Use this instead of + * $_GET or $_POST or $_REQUEST. + */ + public function get_parameter($name) + { + $value = $this->request->get_parameter($name); - public function is_http_request() { return true; } + # Default implementation of OAuthRequest allows arrays to be passed with + # multiple references to the same variable ("a=1&a=2&a=3"). This is invalid + # in OKAPI and should be reported back. See issue 85: + # http://code.google.com/p/opencaching-api/issues/detail?id=85 + + if (is_array($value)) + throw new InvalidParam($name, "Make sure you are using '$name' no more than ONCE in your URL."); + return $value; + } + + public function get_all_parameters_including_unknown() + { + return $this->request->get_parameters(); + } + + public function is_http_request() { return true; } } diff --git a/htdocs/okapi/cronjobs.php b/htdocs/okapi/cronjobs.php index 8d4d3287..b12772ce 100644 --- a/htdocs/okapi/cronjobs.php +++ b/htdocs/okapi/cronjobs.php @@ -31,173 +31,173 @@ use okapi\services\replicate\ReplicateCommon; class CronJobController { - /** Return the list of all currently enabled cronjobs. */ - public static function get_enabled_cronjobs() - { - static $cache = null; - if ($cache == null) - { - $cache = array( - new OAuthCleanupCronJob(), - new CacheCleanupCronJob(), - new StatsWriterCronJob(), - new CheckCronTab1(), - new CheckCronTab2(), - new ChangeLogWriterJob(), - new ChangeLogCleanerJob(), - new ChangeLogCheckerJob(), - new AdminStatsSender(), - new LocaleChecker(), - new FulldumpGeneratorJob(), - new TileTreeUpdater(), - new SearchSetsCleanerJob(), - new TableOptimizerJob(), - ); - foreach ($cache as $cronjob) - if (!in_array($cronjob->get_type(), array('pre-request', 'cron-5'))) - throw new Exception("Cronjob '".$cronjob->get_name()."' has an invalid (unsupported) type."); - } - return $cache; - } + /** Return the list of all currently enabled cronjobs. */ + public static function get_enabled_cronjobs() + { + static $cache = null; + if ($cache == null) + { + $cache = array( + new OAuthCleanupCronJob(), + new CacheCleanupCronJob(), + new StatsWriterCronJob(), + new CheckCronTab1(), + new CheckCronTab2(), + new ChangeLogWriterJob(), + new ChangeLogCleanerJob(), + new ChangeLogCheckerJob(), + new AdminStatsSender(), + new LocaleChecker(), + new FulldumpGeneratorJob(), + new TileTreeUpdater(), + new SearchSetsCleanerJob(), + new TableOptimizerJob(), + ); + foreach ($cache as $cronjob) + if (!in_array($cronjob->get_type(), array('pre-request', 'cron-5'))) + throw new Exception("Cronjob '".$cronjob->get_name()."' has an invalid (unsupported) type."); + } + return $cache; + } - /** - * Execute all scheduled cronjobs of given type, reschedule, and return - * UNIX timestamp of the nearest scheduled event. - */ - public static function run_jobs($type) - { - require_once($GLOBALS['rootpath'].'okapi/service_runner.php'); + /** + * Execute all scheduled cronjobs of given type, reschedule, and return + * UNIX timestamp of the nearest scheduled event. + */ + public static function run_jobs($type) + { + require_once($GLOBALS['rootpath'].'okapi/service_runner.php'); - # We don't want other cronjobs of the same time to run simultanously. - $lock = OkapiLock::get('cronjobs-'.$type); - $lock->acquire(); + # We don't want other cronjobs of the same time to run simultanously. + $lock = OkapiLock::get('cronjobs-'.$type); + $lock->acquire(); - $schedule = Cache::get("cron_schedule"); - if ($schedule == null) - $schedule = array(); - foreach (self::get_enabled_cronjobs() as $cronjob) - { - $name = $cronjob->get_name(); - if ((!isset($schedule[$name])) || ($schedule[$name] <= time())) - { - if ($cronjob->get_type() != $type) - { - $next_run = isset($schedule[$name]) ? $schedule[$name] : (time() - 1); - } - else - { - try - { - $cronjob->execute(); - } - catch (Exception $e) - { - Okapi::mail_admins("Cronjob error: ".$cronjob->get_name(), - OkapiExceptionHandler::get_exception_info($e)); - } - $next_run = $cronjob->get_next_scheduled_run(isset($schedule[$name]) ? $schedule[$name] : time()); - } - $schedule[$name] = $next_run; - Cache::set("cron_schedule", $schedule, 30*86400); - } - } + $schedule = Cache::get("cron_schedule"); + if ($schedule == null) + $schedule = array(); + foreach (self::get_enabled_cronjobs() as $cronjob) + { + $name = $cronjob->get_name(); + if ((!isset($schedule[$name])) || ($schedule[$name] <= time())) + { + if ($cronjob->get_type() != $type) + { + $next_run = isset($schedule[$name]) ? $schedule[$name] : (time() - 1); + } + else + { + try + { + $cronjob->execute(); + } + catch (Exception $e) + { + Okapi::mail_admins("Cronjob error: ".$cronjob->get_name(), + OkapiExceptionHandler::get_exception_info($e)); + } + $next_run = $cronjob->get_next_scheduled_run(isset($schedule[$name]) ? $schedule[$name] : time()); + } + $schedule[$name] = $next_run; + Cache::set("cron_schedule", $schedule, 30*86400); + } + } - # Remove "stale" schedule keys (those which are no longer declared). + # Remove "stale" schedule keys (those which are no longer declared). - $fixed_schedule = array(); - foreach (self::get_enabled_cronjobs() as $cronjob) - { - $name = $cronjob->get_name(); - $fixed_schedule[$name] = $schedule[$name]; - } - unset($schedule); + $fixed_schedule = array(); + foreach (self::get_enabled_cronjobs() as $cronjob) + { + $name = $cronjob->get_name(); + $fixed_schedule[$name] = $schedule[$name]; + } + unset($schedule); - # Return the nearest scheduled event time. + # Return the nearest scheduled event time. - $nearest = time() + 3600; - foreach ($fixed_schedule as $name => $time) - if ($time < $nearest) - $nearest = $time; - Cache::set("cron_schedule", $fixed_schedule, 30*86400); - $lock->release(); - return $nearest; - } + $nearest = time() + 3600; + foreach ($fixed_schedule as $name => $time) + if ($time < $nearest) + $nearest = $time; + Cache::set("cron_schedule", $fixed_schedule, 30*86400); + $lock->release(); + return $nearest; + } - /** - * Force a specified cronjob to run. Throw an exception if cronjob not found. - * $job_name mast equal one of the names returned by ->get_name() method. - */ - public static function force_run($job_name) - { - require_once($GLOBALS['rootpath'].'okapi/service_runner.php'); + /** + * Force a specified cronjob to run. Throw an exception if cronjob not found. + * $job_name mast equal one of the names returned by ->get_name() method. + */ + public static function force_run($job_name) + { + require_once($GLOBALS['rootpath'].'okapi/service_runner.php'); - foreach (self::get_enabled_cronjobs() as $cronjob) - { - if (($cronjob->get_name() == $job_name) || ($cronjob->get_name() == "okapi\\cronjobs\\".$job_name)) - { - $cronjob->execute(); - return; - } - } - throw new Exception("CronJob $job_name not found."); - } + foreach (self::get_enabled_cronjobs() as $cronjob) + { + if (($cronjob->get_name() == $job_name) || ($cronjob->get_name() == "okapi\\cronjobs\\".$job_name)) + { + $cronjob->execute(); + return; + } + } + throw new Exception("CronJob $job_name not found."); + } - /** - * Reset the schedule of a specified cronjob. This will force the job to - * run on nearest occasion (but not NOW). - */ - public static function reset_job_schedule($job_name) - { - $thejob = null; - foreach (self::get_enabled_cronjobs() as $tmp) - if (($tmp->get_name() == $job_name) || ($tmp->get_name() == "okapi\\cronjobs\\".$job_name)) - $thejob = $tmp; - if ($thejob == null) - throw new Exception("Could not reset schedule for job $job_name. $jon_name not found."); + /** + * Reset the schedule of a specified cronjob. This will force the job to + * run on nearest occasion (but not NOW). + */ + public static function reset_job_schedule($job_name) + { + $thejob = null; + foreach (self::get_enabled_cronjobs() as $tmp) + if (($tmp->get_name() == $job_name) || ($tmp->get_name() == "okapi\\cronjobs\\".$job_name)) + $thejob = $tmp; + if ($thejob == null) + throw new Exception("Could not reset schedule for job $job_name. $jon_name not found."); - # We have to acquire lock on the schedule. This might take some time if cron-5 jobs are - # currently being run. + # We have to acquire lock on the schedule. This might take some time if cron-5 jobs are + # currently being run. - $type = $thejob->get_type(); - $lock = OkapiLock::get('cronjobs-'.$type); - $lock->acquire(); + $type = $thejob->get_type(); + $lock = OkapiLock::get('cronjobs-'.$type); + $lock->acquire(); - $schedule = Cache::get("cron_schedule"); - if ($schedule != null) - { - if (isset($schedule[$thejob->get_name()])) - unset($schedule[$thejob->get_name()]); - Cache::set("cron_schedule", $schedule, 30*86400); - } + $schedule = Cache::get("cron_schedule"); + if ($schedule != null) + { + if (isset($schedule[$thejob->get_name()])) + unset($schedule[$thejob->get_name()]); + Cache::set("cron_schedule", $schedule, 30*86400); + } - $lock->release(); - } + $lock->release(); + } } abstract class CronJob { - /** Run the job. */ - public abstract function execute(); + /** Run the job. */ + public abstract function execute(); - /** Get unique name for this cronjob. */ - public function get_name() { return get_class($this); } + /** Get unique name for this cronjob. */ + public function get_name() { return get_class($this); } - /** - * Get the type of this cronjob. Currently there are two: 'pre-request' - * and 'cron-5'. The first can be executed before every request, the second - * is executed from system's crontab, as a separate process. 'cron-5' can be - * executed every 5 minutes, or every 10, 15 etc. minutes. 'pre-request' - * can be executed before each HTTP request, AND additionally every 5 minutes - * (before 'cron-5' runs). - */ - public abstract function get_type(); + /** + * Get the type of this cronjob. Currently there are two: 'pre-request' + * and 'cron-5'. The first can be executed before every request, the second + * is executed from system's crontab, as a separate process. 'cron-5' can be + * executed every 5 minutes, or every 10, 15 etc. minutes. 'pre-request' + * can be executed before each HTTP request, AND additionally every 5 minutes + * (before 'cron-5' runs). + */ + public abstract function get_type(); - /** - * Get the next scheduled run (unix timestamp). You may assume this function - * will be called ONLY directly after the job was run. You may use this to say, - * for example, "run the job before first request made after midnight". - */ - public abstract function get_next_scheduled_run($previously_scheduled_run); + /** + * Get the next scheduled run (unix timestamp). You may assume this function + * will be called ONLY directly after the job was run. You may use this to say, + * for example, "run the job before first request made after midnight". + */ + public abstract function get_next_scheduled_run($previously_scheduled_run); } /** @@ -208,21 +208,21 @@ abstract class CronJob */ abstract class PrerequestCronJob extends CronJob { - /** - * Always returns 'pre-request'. - */ - public final function get_type() { return 'pre-request'; } + /** + * Always returns 'pre-request'. + */ + public final function get_type() { return 'pre-request'; } - /** - * Return number of seconds - a *minimum* time period that should pass between - * running the job. - */ - public abstract function get_period(); + /** + * Return number of seconds - a *minimum* time period that should pass between + * running the job. + */ + public abstract function get_period(); - public function get_next_scheduled_run($previously_scheduled_run) - { - return time() + $this->get_period(); - } + public function get_next_scheduled_run($previously_scheduled_run) + { + return time() + $this->get_period(); + } } /** @@ -231,22 +231,22 @@ abstract class PrerequestCronJob extends CronJob */ abstract class Cron5Job extends CronJob { - /** - * Always returns 'cron-5'. - */ - public final function get_type() { return 'cron-5'; } + /** + * Always returns 'cron-5'. + */ + public final function get_type() { return 'cron-5'; } - /** - * Return number of seconds - period of time after which cronjob execution - * should be repeated. This should be dividable be 300 (5 minutes). - */ - public abstract function get_period(); + /** + * Return number of seconds - period of time after which cronjob execution + * should be repeated. This should be dividable be 300 (5 minutes). + */ + public abstract function get_period(); - public function get_next_scheduled_run($previously_scheduled_run) - { - $t = time() + $this->get_period(); - return ($t - ($t % 300)); - } + public function get_next_scheduled_run($previously_scheduled_run) + { + $t = time() + $this->get_period(); + return ($t - ($t % 300)); + } } @@ -256,152 +256,152 @@ abstract class Cron5Job extends CronJob */ class OAuthCleanupCronJob extends PrerequestCronJob { - public function get_period() { return 300; } # 5 minutes - public function execute() - { - if (Okapi::$data_store) - Okapi::$data_store->cleanup(); - } + public function get_period() { return 300; } # 5 minutes + public function execute() + { + if (Okapi::$data_store) + Okapi::$data_store->cleanup(); + } } /** Clean up the saved search tables, every 10 minutes. */ class SearchSetsCleanerJob extends Cron5Job { - public function get_period() { return 600; } - public function execute() - { - Db::execute(" - delete oss, osr - from - okapi_search_sets oss - left join okapi_search_results osr - on oss.id = osr.set_id - where - date_add(oss.expires, interval 60 second) < now() - "); - } + public function get_period() { return 600; } + public function execute() + { + Db::execute(" + delete oss, osr + from + okapi_search_sets oss + left join okapi_search_results osr + on oss.id = osr.set_id + where + date_add(oss.expires, interval 60 second) < now() + "); + } } /** Clean up the cache, once per hour. */ class CacheCleanupCronJob extends Cron5Job { - public function get_period() { return 3600; } - public function execute() - { - # Delete all expired elements. + public function get_period() { return 3600; } + public function execute() + { + # Delete all expired elements. - Db::execute(" - delete from okapi_cache - where expires < now() - "); + Db::execute(" + delete from okapi_cache + where expires < now() + "); - # Update the "score" stats. + # Update the "score" stats. - $multiplier = 0.9; # Every hour, all scores are multiplied by this. - $limit = 0.01; # When a score reaches this limit, the entry is deleted. + $multiplier = 0.9; # Every hour, all scores are multiplied by this. + $limit = 0.01; # When a score reaches this limit, the entry is deleted. - # Every time the entry is read, its score is incread by 1. If an entry - # is saved, but never read, it will be deleted after log(L,M) hours - # (log(0.01, 0.9) = 43h). If an entry is read 1000000 times and then - # never read anymore, it will be deleted after log(1000000/L, 1/M) - # hours (log(1000000/0.01, 1/0.9) = 174h = 7 days). + # Every time the entry is read, its score is incread by 1. If an entry + # is saved, but never read, it will be deleted after log(L,M) hours + # (log(0.01, 0.9) = 43h). If an entry is read 1000000 times and then + # never read anymore, it will be deleted after log(1000000/L, 1/M) + # hours (log(1000000/0.01, 1/0.9) = 174h = 7 days). - Db::execute(" - update okapi_cache - set score = score * '".mysql_real_escape_string($multiplier)."' - where score is not null - "); - Db::execute(" - update - okapi_cache c, - ( - select cache_key, count(*) as count - from okapi_cache_reads - group by cache_key - ) cr - set c.score = c.score + cr.count - where - c.`key` = cr.cache_key - and c.score is not null - "); - Db::execute("truncate okapi_cache_reads"); + Db::execute(" + update okapi_cache + set score = score * '".mysql_real_escape_string($multiplier)."' + where score is not null + "); + Db::execute(" + update + okapi_cache c, + ( + select cache_key, count(*) as count + from okapi_cache_reads + group by cache_key + ) cr + set c.score = c.score + cr.count + where + c.`key` = cr.cache_key + and c.score is not null + "); + Db::execute("truncate okapi_cache_reads"); - # Delete elements with the lowest score. Entries which have been set - # but never read will be removed after 36 hours (0.9^36 < 0.02 < 0.9^35). + # Delete elements with the lowest score. Entries which have been set + # but never read will be removed after 36 hours (0.9^36 < 0.02 < 0.9^35). - Db::execute(" - delete from okapi_cache - where - score is not null - and score < '".mysql_real_escape_string($limit)."' - "); - Db::query("optimize table okapi_cache"); + Db::execute(" + delete from okapi_cache + where + score is not null + and score < '".mysql_real_escape_string($limit)."' + "); + Db::query("optimize table okapi_cache"); - # FileCache does not have an expiry date. We will delete all files older - # than 24 hours. + # FileCache does not have an expiry date. We will delete all files older + # than 24 hours. - $dir = Okapi::get_var_dir(); - if ($dh = opendir($dir)) { - while (($file = readdir($dh)) !== false) { - if (strpos($file, "okapi_filecache_") === 0) { - if (filemtime("$dir/$file") < time() - 86400) { - unlink("$dir/$file"); - } - } - } - closedir($dh); - } - } + $dir = Okapi::get_var_dir(); + if ($dh = opendir($dir)) { + while (($file = readdir($dh)) !== false) { + if (strpos($file, "okapi_filecache_") === 0) { + if (filemtime("$dir/$file") < time() - 86400) { + unlink("$dir/$file"); + } + } + } + closedir($dh); + } + } } /** Reads temporary (fast) stats-tables and reformats them into more permanent structures. */ class StatsWriterCronJob extends PrerequestCronJob { - public function get_period() { return 60; } # 1 minute - public function execute() - { - if (Okapi::get_var('db_version', 0) + 0 < 32) - return; - Db::query("lock tables okapi_stats_hourly write, okapi_stats_temp write;"); - $rs = Db::query(" - select - consumer_key, - user_id, - concat(substr(`datetime`, 1, 13), ':00:00') as period_start, - service_name, - calltype, - count(*) as calls, - sum(runtime) as runtime - from okapi_stats_temp - group by substr(`datetime`, 1, 13), consumer_key, user_id, service_name, calltype - "); - while ($row = mysql_fetch_assoc($rs)) - { - Db::execute(" - insert into okapi_stats_hourly (consumer_key, user_id, period_start, service_name, - total_calls, http_calls, total_runtime, http_runtime) - values ( - '".mysql_real_escape_string($row['consumer_key'])."', - '".mysql_real_escape_string($row['user_id'])."', - '".mysql_real_escape_string($row['period_start'])."', - '".mysql_real_escape_string($row['service_name'])."', - '".mysql_real_escape_string($row['calls'])."', - '".mysql_real_escape_string(($row['calltype'] == 'http') ? $row['calls'] : 0)."', - '".mysql_real_escape_string($row['runtime'])."', - '".mysql_real_escape_string(($row['calltype'] == 'http') ? $row['runtime'] : 0)."' - ) - on duplicate key update - ".(($row['calltype'] == 'http') ? " - http_calls = http_calls + '".mysql_real_escape_string($row['calls'])."', - http_runtime = http_runtime + '".mysql_real_escape_string($row['runtime'])."', - " : "")." - total_calls = total_calls + '".mysql_real_escape_string($row['calls'])."', - total_runtime = total_runtime + '".mysql_real_escape_string($row['runtime'])."' - "); - } - Db::execute("delete from okapi_stats_temp;"); - Db::execute("unlock tables;"); - } + public function get_period() { return 60; } # 1 minute + public function execute() + { + if (Okapi::get_var('db_version', 0) + 0 < 32) + return; + Db::query("lock tables okapi_stats_hourly write, okapi_stats_temp write;"); + $rs = Db::query(" + select + consumer_key, + user_id, + concat(substr(`datetime`, 1, 13), ':00:00') as period_start, + service_name, + calltype, + count(*) as calls, + sum(runtime) as runtime + from okapi_stats_temp + group by substr(`datetime`, 1, 13), consumer_key, user_id, service_name, calltype + "); + while ($row = mysql_fetch_assoc($rs)) + { + Db::execute(" + insert into okapi_stats_hourly (consumer_key, user_id, period_start, service_name, + total_calls, http_calls, total_runtime, http_runtime) + values ( + '".mysql_real_escape_string($row['consumer_key'])."', + '".mysql_real_escape_string($row['user_id'])."', + '".mysql_real_escape_string($row['period_start'])."', + '".mysql_real_escape_string($row['service_name'])."', + '".mysql_real_escape_string($row['calls'])."', + '".mysql_real_escape_string(($row['calltype'] == 'http') ? $row['calls'] : 0)."', + '".mysql_real_escape_string($row['runtime'])."', + '".mysql_real_escape_string(($row['calltype'] == 'http') ? $row['runtime'] : 0)."' + ) + on duplicate key update + ".(($row['calltype'] == 'http') ? " + http_calls = http_calls + '".mysql_real_escape_string($row['calls'])."', + http_runtime = http_runtime + '".mysql_real_escape_string($row['runtime'])."', + " : "")." + total_calls = total_calls + '".mysql_real_escape_string($row['calls'])."', + total_runtime = total_runtime + '".mysql_real_escape_string($row['runtime'])."' + "); + } + Db::execute("delete from okapi_stats_temp;"); + Db::execute("unlock tables;"); + } } /** @@ -410,11 +410,11 @@ class StatsWriterCronJob extends PrerequestCronJob */ class CheckCronTab1 extends Cron5Job { - public function get_period() { return 3600; } - public function execute() - { - Cache::set('crontab_last_ping', time(), 86400); - } + public function get_period() { return 3600; } + public function execute() + { + Cache::set('crontab_last_ping', time(), 86400); + } } /** @@ -423,51 +423,51 @@ class CheckCronTab1 extends Cron5Job */ class CheckCronTab2 extends PrerequestCronJob { - public function get_period() { return 30 * 60; } - public function execute() - { - $last_ping = Cache::get('crontab_last_ping'); - if ($last_ping === null) - $last_ping = time() - 86400; # if not set, assume 1 day ago. - if ($last_ping > time() - 3600) - { - # There was a ping during the last hour. Everything is okay. - # Reset the counter and return. + public function get_period() { return 30 * 60; } + public function execute() + { + $last_ping = Cache::get('crontab_last_ping'); + if ($last_ping === null) + $last_ping = time() - 86400; # if not set, assume 1 day ago. + if ($last_ping > time() - 3600) + { + # There was a ping during the last hour. Everything is okay. + # Reset the counter and return. - Cache::set('crontab_check_counter', 5, 86400); - return; - } + Cache::set('crontab_check_counter', 5, 86400); + return; + } - # There was no ping. Decrement the counter. When reached zero, alert. + # There was no ping. Decrement the counter. When reached zero, alert. - $counter = Cache::get('crontab_check_counter'); - if ($counter === null) - $counter = 5; - $counter--; - if ($counter > 0) - { - Cache::set('crontab_check_counter', $counter, 86400); - } - elseif ($counter == 0) - { - Okapi::mail_admins( - "Crontab not working.", - "Hello. OKAPI detected, that it's crontab is not working properly.\n". - "Please check your configuration or contact OKAPI developers.\n\n". - "This line should be present among your crontab entries:\n\n". - "*/5 * * * * wget -O - -q -t 1 ".Settings::get('SITE_URL')."okapi/cron5\n\n". - "If you're receiving this in Virtual Machine development environment, then\n". - "ignore it. Probably you just paused (or switched off) your VM for some time\n". - "(which would be considered an error in production environment)." - ); + $counter = Cache::get('crontab_check_counter'); + if ($counter === null) + $counter = 5; + $counter--; + if ($counter > 0) + { + Cache::set('crontab_check_counter', $counter, 86400); + } + elseif ($counter == 0) + { + Okapi::mail_admins( + "Crontab not working.", + "Hello. OKAPI detected, that it's crontab is not working properly.\n". + "Please check your configuration or contact OKAPI developers.\n\n". + "This line should be present among your crontab entries:\n\n". + "*/5 * * * * wget -O - -q -t 1 ".Settings::get('SITE_URL')."okapi/cron5\n\n". + "If you're receiving this in Virtual Machine development environment, then\n". + "ignore it. Probably you just paused (or switched off) your VM for some time\n". + "(which would be considered an error in production environment)." + ); - # Schedule the next admin-nagging. Each subsequent notification will be sent - # with a greater delay. + # Schedule the next admin-nagging. Each subsequent notification will be sent + # with a greater delay. - $since_last = time() - $last_ping; - Cache::set('crontab_check_counter', (int)($since_last / $this->get_period()), 86400); - } - } + $since_last = time() - $last_ping; + Cache::set('crontab_check_counter', (int)($since_last / $this->get_period()), 86400); + } + } } /** @@ -475,12 +475,12 @@ class CheckCronTab2 extends PrerequestCronJob */ class ChangeLogWriterJob extends Cron5Job { - public function get_period() { return 300; } - public function execute() - { - require_once($GLOBALS['rootpath']."okapi/services/replicate/replicate_common.inc.php"); - ReplicateCommon::update_clog_table(); - } + public function get_period() { return 300; } + public function execute() + { + require_once($GLOBALS['rootpath']."okapi/services/replicate/replicate_common.inc.php"); + ReplicateCommon::update_clog_table(); + } } /** @@ -491,12 +491,12 @@ class ChangeLogWriterJob extends Cron5Job */ class ChangeLogCheckerJob extends Cron5Job { - public function get_period() { return 86400; } - public function execute() - { - require_once($GLOBALS['rootpath']."okapi/services/replicate/replicate_common.inc.php"); - ReplicateCommon::verify_clog_consistency(); - } + public function get_period() { return 86400; } + public function execute() + { + require_once($GLOBALS['rootpath']."okapi/services/replicate/replicate_common.inc.php"); + ReplicateCommon::verify_clog_consistency(); + } } /** @@ -504,12 +504,12 @@ class ChangeLogCheckerJob extends Cron5Job */ class FulldumpGeneratorJob extends Cron5Job { - public function get_period() { return 7*86400; } - public function execute() - { - require_once($GLOBALS['rootpath']."okapi/services/replicate/replicate_common.inc.php"); - ReplicateCommon::generate_fulldump(); - } + public function get_period() { return 7*86400; } + public function execute() + { + require_once($GLOBALS['rootpath']."okapi/services/replicate/replicate_common.inc.php"); + ReplicateCommon::generate_fulldump(); + } } /** @@ -517,77 +517,77 @@ class FulldumpGeneratorJob extends Cron5Job */ class TileTreeUpdater extends Cron5Job { - public function get_period() { return 5*60; } - public function execute() - { - $current_clog_revision = Okapi::get_var('clog_revision', 0); - $tiletree_revision = Okapi::get_var('clog_followup_revision', 0); - if ($tiletree_revision === $current_clog_revision) { - # No update necessary. - } elseif ($tiletree_revision < $current_clog_revision) { - require_once($GLOBALS['rootpath']."okapi/services/caches/map/replicate_listener.inc.php"); - if ($current_clog_revision - $tiletree_revision < 30000) # In the middle of 2012, OCPL generated 30000 entries per week - { - for ($timeout = time() + 240; time() < $timeout; ) # Try to stop after 4 minutes. - { - try { - $response = OkapiServiceRunner::call('services/replicate/changelog', new OkapiInternalRequest( - new OkapiInternalConsumer(), null, array('since' => $tiletree_revision))); - \okapi\services\caches\map\ReplicateListener::receive($response['changelog']); - $tiletree_revision = $response['revision']; - Okapi::set_var('clog_followup_revision', $tiletree_revision); - if (!$response['more']) - break; - } catch (BadRequest $e) { - # Invalid 'since' parameter? May happen when crontab was - # not working for more than 10 days. Or, just after OKAPI - # is installed (and this is the first time this cronjob - # if being run). + public function get_period() { return 5*60; } + public function execute() + { + $current_clog_revision = Okapi::get_var('clog_revision', 0); + $tiletree_revision = Okapi::get_var('clog_followup_revision', 0); + if ($tiletree_revision === $current_clog_revision) { + # No update necessary. + } elseif ($tiletree_revision < $current_clog_revision) { + require_once($GLOBALS['rootpath']."okapi/services/caches/map/replicate_listener.inc.php"); + if ($current_clog_revision - $tiletree_revision < 30000) # In the middle of 2012, OCPL generated 30000 entries per week + { + for ($timeout = time() + 240; time() < $timeout; ) # Try to stop after 4 minutes. + { + try { + $response = OkapiServiceRunner::call('services/replicate/changelog', new OkapiInternalRequest( + new OkapiInternalConsumer(), null, array('since' => $tiletree_revision))); + \okapi\services\caches\map\ReplicateListener::receive($response['changelog']); + $tiletree_revision = $response['revision']; + Okapi::set_var('clog_followup_revision', $tiletree_revision); + if (!$response['more']) + break; + } catch (BadRequest $e) { + # Invalid 'since' parameter? May happen when crontab was + # not working for more than 10 days. Or, just after OKAPI + # is installed (and this is the first time this cronjob + # if being run). - $mail_admins = ($tiletree_revision > 0); - \okapi\services\caches\map\ReplicateListener::reset(); - Okapi::set_var('clog_followup_revision', $current_clog_revision); - break; - } - } - } else { - # Some kind of bigger update. Resetting TileTree might be a better option. - \okapi\services\caches\map\ReplicateListener::reset(); - Okapi::set_var('clog_followup_revision', $current_clog_revision); - } - } - } + $mail_admins = ($tiletree_revision > 0); + \okapi\services\caches\map\ReplicateListener::reset(); + Okapi::set_var('clog_followup_revision', $current_clog_revision); + break; + } + } + } else { + # Some kind of bigger update. Resetting TileTree might be a better option. + \okapi\services\caches\map\ReplicateListener::reset(); + Okapi::set_var('clog_followup_revision', $current_clog_revision); + } + } + } } /** Once per day, removes all revisions older than 10 days from okapi_clog table. */ class ChangeLogCleanerJob extends Cron5Job { - public function get_period() { return 86400; } - public function execute() - { - require_once($GLOBALS['rootpath']."okapi/services/replicate/replicate_common.inc.php"); - $max_revision = ReplicateCommon::get_revision(); - $cache_key = 'clog_revisions_daily'; - $data = Cache::get($cache_key); - if ($data == null) - $data = array(); - $data[time()] = $max_revision; - $new_min_revision = 1; - $new_data = array(); - foreach ($data as $time => $r) - { - if ($time < time() - 10*86400) - $new_min_revision = max($new_min_revision, $r); - else - $new_data[$time] = $r; - } - Db::execute(" - delete from okapi_clog - where id < '".mysql_real_escape_string($new_min_revision)."' - "); - Cache::set($cache_key, $new_data, 10*86400); - Db::query("optimize table okapi_clog"); - } + public function get_period() { return 86400; } + public function execute() + { + require_once($GLOBALS['rootpath']."okapi/services/replicate/replicate_common.inc.php"); + $max_revision = ReplicateCommon::get_revision(); + $cache_key = 'clog_revisions_daily'; + $data = Cache::get($cache_key); + if ($data == null) + $data = array(); + $data[time()] = $max_revision; + $new_min_revision = 1; + $new_data = array(); + foreach ($data as $time => $r) + { + if ($time < time() - 10*86400) + $new_min_revision = max($new_min_revision, $r); + else + $new_data[$time] = $r; + } + Db::execute(" + delete from okapi_clog + where id < '".mysql_real_escape_string($new_min_revision)."' + "); + Cache::set($cache_key, $new_data, 10*86400); + Db::query("optimize table okapi_clog"); + } } /** @@ -595,131 +595,131 @@ class ChangeLogCleanerJob extends Cron5Job */ class AdminStatsSender extends Cron5Job { - public function get_period() { return 7*86400; } - public function execute() - { - ob_start(); - $apisrv_stats = OkapiServiceRunner::call('services/apisrv/stats', new OkapiInternalRequest( - new OkapiInternalConsumer(), null, array())); - $active_apps_count = Db::select_value(" - select count(distinct s.consumer_key) - from - okapi_stats_hourly s, - okapi_consumers c - where - s.consumer_key = c.`key` - and s.period_start > date_add(now(), interval -7 day) - "); - $weekly_stats = Db::select_row(" - select - sum(s.http_calls) as total_http_calls, - sum(s.http_runtime) as total_http_runtime - from okapi_stats_hourly s - where - s.consumer_key != 'internal' -- we don't want to exclude 'anonymous' nor 'facade' - and s.period_start > date_add(now(), interval -7 day) - "); - print "Hello! This is your weekly summary of OKAPI usage.\n\n"; - print "Apps active this week: ".$active_apps_count." out of ".$apisrv_stats['apps_count'].".\n"; - print "Total of ".$weekly_stats['total_http_calls']." requests were made (".sprintf("%01.1f", $weekly_stats['total_http_runtime'])." seconds).\n\n"; - $consumers = Db::select_all(" - select - s.consumer_key, - c.name, - sum(s.http_calls) as http_calls, - sum(s.http_runtime) as http_runtime - from - okapi_stats_hourly s - left join okapi_consumers c - on s.consumer_key = c.`key` - where s.period_start > date_add(now(), interval -7 day) - group by s.consumer_key - having sum(s.http_calls) > 0 - order by sum(s.http_calls) desc - "); - print "== Consumers ==\n\n"; - print "Consumer name Calls Runtime\n"; - print "----------------------------------- ------- -----------\n"; - foreach ($consumers as $row) - { - $name = $row['name']; - if ($row['consumer_key'] == 'anonymous') - $name = "Anonymous (Level 0 Authentication)"; - elseif ($row['consumer_key'] == 'facade') - $name = "Internal usage via Facade"; - if (mb_strlen($name) > 35) - $name = mb_substr($name, 0, 32)."..."; - print self::mb_str_pad($name, 35, " ", STR_PAD_RIGHT); - print str_pad($row['http_calls'], 8, " ", STR_PAD_LEFT); - print str_pad(sprintf("%01.2f", $row['http_runtime']), 11, " ", STR_PAD_LEFT)."s\n"; - } - print "\n"; - $methods = Db::select_all(" - select - s.service_name, - sum(s.http_calls) as http_calls, - sum(s.http_runtime) as http_runtime - from okapi_stats_hourly s - where s.period_start > date_add(now(), interval -7 day) - group by s.service_name - having sum(s.http_calls) > 0 - order by sum(s.http_calls) desc - "); - print "== Methods ==\n\n"; - print "Service name Calls Runtime Avg\n"; - print "----------------------------------- ------- ----------- --------\n"; - foreach ($methods as $row) - { - $name = $row['service_name']; - if (mb_strlen($name) > 35) - $name = mb_substr($name, 0, 32)."..."; - print self::mb_str_pad($name, 35, " ", STR_PAD_RIGHT); - print str_pad($row['http_calls'], 8, " ", STR_PAD_LEFT); - print str_pad(sprintf("%01.2f", $row['http_runtime']), 11, " ", STR_PAD_LEFT)."s"; - print str_pad(sprintf("%01.4f", ( - ($row['http_calls'] > 0) ? ($row['http_runtime'] / $row['http_calls']) : 0 - )), 8, " ", STR_PAD_LEFT)."s\n"; - } - print "\n"; - $oauth_users = Db::select_all(" - select - c.name, - count(*) as users - from - okapi_authorizations a, - okapi_consumers c - where a.consumer_key = c.`key` - group by a.consumer_key - having count(*) >= 5 - order by count(*) desc; - "); - print "== Current OAuth usage by Consumers with at least 5 users ==\n\n"; - print "Consumer name Users\n"; - print "----------------------------------- -------\n"; - foreach ($oauth_users as $row) - { - $name = $row['name']; - if (mb_strlen($name) > 35) - $name = mb_substr($name, 0, 32)."..."; - print self::mb_str_pad($name, 35, " ", STR_PAD_RIGHT); - print str_pad($row['users'], 8, " ", STR_PAD_LEFT)."\n"; - } - print "\n"; + public function get_period() { return 7*86400; } + public function execute() + { + ob_start(); + $apisrv_stats = OkapiServiceRunner::call('services/apisrv/stats', new OkapiInternalRequest( + new OkapiInternalConsumer(), null, array())); + $active_apps_count = Db::select_value(" + select count(distinct s.consumer_key) + from + okapi_stats_hourly s, + okapi_consumers c + where + s.consumer_key = c.`key` + and s.period_start > date_add(now(), interval -7 day) + "); + $weekly_stats = Db::select_row(" + select + sum(s.http_calls) as total_http_calls, + sum(s.http_runtime) as total_http_runtime + from okapi_stats_hourly s + where + s.consumer_key != 'internal' -- we don't want to exclude 'anonymous' nor 'facade' + and s.period_start > date_add(now(), interval -7 day) + "); + print "Hello! This is your weekly summary of OKAPI usage.\n\n"; + print "Apps active this week: ".$active_apps_count." out of ".$apisrv_stats['apps_count'].".\n"; + print "Total of ".$weekly_stats['total_http_calls']." requests were made (".sprintf("%01.1f", $weekly_stats['total_http_runtime'])." seconds).\n\n"; + $consumers = Db::select_all(" + select + s.consumer_key, + c.name, + sum(s.http_calls) as http_calls, + sum(s.http_runtime) as http_runtime + from + okapi_stats_hourly s + left join okapi_consumers c + on s.consumer_key = c.`key` + where s.period_start > date_add(now(), interval -7 day) + group by s.consumer_key + having sum(s.http_calls) > 0 + order by sum(s.http_calls) desc + "); + print "== Consumers ==\n\n"; + print "Consumer name Calls Runtime\n"; + print "----------------------------------- ------- -----------\n"; + foreach ($consumers as $row) + { + $name = $row['name']; + if ($row['consumer_key'] == 'anonymous') + $name = "Anonymous (Level 0 Authentication)"; + elseif ($row['consumer_key'] == 'facade') + $name = "Internal usage via Facade"; + if (mb_strlen($name) > 35) + $name = mb_substr($name, 0, 32)."..."; + print self::mb_str_pad($name, 35, " ", STR_PAD_RIGHT); + print str_pad($row['http_calls'], 8, " ", STR_PAD_LEFT); + print str_pad(sprintf("%01.2f", $row['http_runtime']), 11, " ", STR_PAD_LEFT)."s\n"; + } + print "\n"; + $methods = Db::select_all(" + select + s.service_name, + sum(s.http_calls) as http_calls, + sum(s.http_runtime) as http_runtime + from okapi_stats_hourly s + where s.period_start > date_add(now(), interval -7 day) + group by s.service_name + having sum(s.http_calls) > 0 + order by sum(s.http_calls) desc + "); + print "== Methods ==\n\n"; + print "Service name Calls Runtime Avg\n"; + print "----------------------------------- ------- ----------- --------\n"; + foreach ($methods as $row) + { + $name = $row['service_name']; + if (mb_strlen($name) > 35) + $name = mb_substr($name, 0, 32)."..."; + print self::mb_str_pad($name, 35, " ", STR_PAD_RIGHT); + print str_pad($row['http_calls'], 8, " ", STR_PAD_LEFT); + print str_pad(sprintf("%01.2f", $row['http_runtime']), 11, " ", STR_PAD_LEFT)."s"; + print str_pad(sprintf("%01.4f", ( + ($row['http_calls'] > 0) ? ($row['http_runtime'] / $row['http_calls']) : 0 + )), 8, " ", STR_PAD_LEFT)."s\n"; + } + print "\n"; + $oauth_users = Db::select_all(" + select + c.name, + count(*) as users + from + okapi_authorizations a, + okapi_consumers c + where a.consumer_key = c.`key` + group by a.consumer_key + having count(*) >= 5 + order by count(*) desc; + "); + print "== Current OAuth usage by Consumers with at least 5 users ==\n\n"; + print "Consumer name Users\n"; + print "----------------------------------- -------\n"; + foreach ($oauth_users as $row) + { + $name = $row['name']; + if (mb_strlen($name) > 35) + $name = mb_substr($name, 0, 32)."..."; + print self::mb_str_pad($name, 35, " ", STR_PAD_RIGHT); + print str_pad($row['users'], 8, " ", STR_PAD_LEFT)."\n"; + } + print "\n"; - print "This report includes requests from external consumers and those made via\n"; - print "Facade class (used by OC code). It does not include methods used by OKAPI\n"; - print "internally (i.e. while running cronjobs). Runtimes do not include HTTP\n"; - print "request handling overhead.\n"; + print "This report includes requests from external consumers and those made via\n"; + print "Facade class (used by OC code). It does not include methods used by OKAPI\n"; + print "internally (i.e. while running cronjobs). Runtimes do not include HTTP\n"; + print "request handling overhead.\n"; - $message = ob_get_clean(); - Okapi::mail_admins("Weekly OKAPI usage report", $message); - } + $message = ob_get_clean(); + Okapi::mail_admins("Weekly OKAPI usage report", $message); + } - private static function mb_str_pad($input, $pad_length, $pad_string, $pad_style) - { - return str_pad($input, strlen($input) - mb_strlen($input) + $pad_length, - $pad_string, $pad_style); - } + private static function mb_str_pad($input, $pad_length, $pad_string, $pad_style) + { + return str_pad($input, strlen($input) - mb_strlen($input) + $pad_length, + $pad_string, $pad_style); + } } /** @@ -728,61 +728,61 @@ class AdminStatsSender extends Cron5Job */ class LocaleChecker extends Cron5Job { - public function get_period() { return 7*86400; } - public function execute() - { - require_once($GLOBALS['rootpath']."okapi/locale/locales.php"); - $required = Locales::get_required_locales(); - $installed = Locales::get_installed_locales(); - $missing = array(); - foreach ($required as $locale) - if (!in_array($locale, $installed)) - $missing[] = $locale; - if (count($missing) == 0) - return; # okay! - ob_start(); - print "Hi!\n\n"; - print "Your system is missing some locales required by OKAPI for proper\n"; - print "internationalization support. OKAPI comes with support for different\n"; - print "languages. This number (hopefully) will be growing.\n\n"; - print "Please take a moment to install the following missing locales:\n\n"; - $prefixes = array(); - foreach ($missing as $locale) - { - print " - ".$locale."\n"; - $prefixes[substr($locale, 0, 2)] = true; - } - $prefixes = array_keys($prefixes); - print "\n"; - if ((count($missing) == 1) && ($missing[0] == 'POSIX')) - { - # I don't remember how to install POSIX, probably everyone has it anyway. - } - else - { - print "On Debian, try the following:\n\n"; - foreach ($prefixes as $lang) - { - if ($lang != 'PO') # Two first letters cut from POSIX. - print "sudo apt-get install language-pack-".$lang."-base\n"; - } - print "sudo service apache2 restart\n"; - print "\n"; - } - print "Thanks!\n\n"; - print "-- \n"; - print "OKAPI Team"; - Okapi::mail_admins("Additional setup needed: Missing locales.", ob_get_clean()); - } + public function get_period() { return 7*86400; } + public function execute() + { + require_once($GLOBALS['rootpath']."okapi/locale/locales.php"); + $required = Locales::get_required_locales(); + $installed = Locales::get_installed_locales(); + $missing = array(); + foreach ($required as $locale) + if (!in_array($locale, $installed)) + $missing[] = $locale; + if (count($missing) == 0) + return; # okay! + ob_start(); + print "Hi!\n\n"; + print "Your system is missing some locales required by OKAPI for proper\n"; + print "internationalization support. OKAPI comes with support for different\n"; + print "languages. This number (hopefully) will be growing.\n\n"; + print "Please take a moment to install the following missing locales:\n\n"; + $prefixes = array(); + foreach ($missing as $locale) + { + print " - ".$locale."\n"; + $prefixes[substr($locale, 0, 2)] = true; + } + $prefixes = array_keys($prefixes); + print "\n"; + if ((count($missing) == 1) && ($missing[0] == 'POSIX')) + { + # I don't remember how to install POSIX, probably everyone has it anyway. + } + else + { + print "On Debian, try the following:\n\n"; + foreach ($prefixes as $lang) + { + if ($lang != 'PO') # Two first letters cut from POSIX. + print "sudo apt-get install language-pack-".$lang."-base\n"; + } + print "sudo service apache2 restart\n"; + print "\n"; + } + print "Thanks!\n\n"; + print "-- \n"; + print "OKAPI Team"; + Okapi::mail_admins("Additional setup needed: Missing locales.", ob_get_clean()); + } } /** Once per day, optimize certain MySQL tables. */ class TableOptimizerJob extends Cron5Job { - public function get_period() { return 86400; } - public function execute() - { - Db::query("optimize table okapi_tile_caches"); - Db::query("optimize table okapi_tile_status"); - } + public function get_period() { return 86400; } + public function execute() + { + Db::query("optimize table okapi_tile_caches"); + Db::query("optimize table okapi_tile_status"); + } } diff --git a/htdocs/okapi/datastore.php b/htdocs/okapi/datastore.php index ada6dee5..515a937d 100644 --- a/htdocs/okapi/datastore.php +++ b/htdocs/okapi/datastore.php @@ -6,180 +6,180 @@ use OAuthDataStore; class OkapiDataStore extends OAuthDataStore { - public function lookup_consumer($consumer_key) - { - $row = Db::select_row(" - select `key`, secret, name, url, email, admin - from okapi_consumers - where `key` = '".mysql_real_escape_string($consumer_key)."' - "); - if (!$row) - return null; - return new OkapiConsumer($row['key'], $row['secret'], $row['name'], - $row['url'], $row['email'], $row['admin'] ? true : false); - } + public function lookup_consumer($consumer_key) + { + $row = Db::select_row(" + select `key`, secret, name, url, email, admin + from okapi_consumers + where `key` = '".mysql_real_escape_string($consumer_key)."' + "); + if (!$row) + return null; + return new OkapiConsumer($row['key'], $row['secret'], $row['name'], + $row['url'], $row['email'], $row['admin'] ? true : false); + } - public function lookup_token($consumer, $token_type, $token) - { - $row = Db::select_row(" - select `key`, consumer_key, secret, token_type, user_id, verifier, callback - from okapi_tokens - where - consumer_key = '".mysql_real_escape_string($consumer->key)."' - and token_type = '".mysql_real_escape_string($token_type)."' - and `key` = '".mysql_real_escape_string($token)."' - "); - if (!$row) - return null; - switch ($row['token_type']) - { - case 'request': - return new OkapiRequestToken($row['key'], $row['secret'], - $row['consumer_key'], $row['callback'], $row['user_id'], - $row['verifier']); - case 'access': - return new OkapiAccessToken($row['key'], $row['secret'], - $row['consumer_key'], $row['user_id']); - default: - throw new Exception(); - } - } + public function lookup_token($consumer, $token_type, $token) + { + $row = Db::select_row(" + select `key`, consumer_key, secret, token_type, user_id, verifier, callback + from okapi_tokens + where + consumer_key = '".mysql_real_escape_string($consumer->key)."' + and token_type = '".mysql_real_escape_string($token_type)."' + and `key` = '".mysql_real_escape_string($token)."' + "); + if (!$row) + return null; + switch ($row['token_type']) + { + case 'request': + return new OkapiRequestToken($row['key'], $row['secret'], + $row['consumer_key'], $row['callback'], $row['user_id'], + $row['verifier']); + case 'access': + return new OkapiAccessToken($row['key'], $row['secret'], + $row['consumer_key'], $row['user_id']); + default: + throw new Exception(); + } + } - public function lookup_nonce($consumer, $token, $nonce, $timestamp) - { - # 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 - # key in this hash and drop the column, but we will leave it be for - # now (for a couple of less important reasons). + public function lookup_nonce($consumer, $token, $nonce, $timestamp) + { + # 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 + # key in this hash and drop the column, but we will leave it be for + # now (for a couple of less important reasons). - $nonce_hash = md5(serialize(array( - $token ? $token->key : null, - $timestamp, - $nonce - ))); - try - { - # Time timestamp is saved separately, because we are periodically - # removing older nonces from the database (see cronjobs). + $nonce_hash = md5(serialize(array( + $token ? $token->key : null, + $timestamp, + $nonce + ))); + try + { + # Time timestamp is saved separately, because we are periodically + # removing older nonces from the database (see cronjobs). - Db::execute(" - insert into okapi_nonces (consumer_key, nonce_hash, timestamp) - values ( - '".mysql_real_escape_string($consumer->key)."', - '".mysql_real_escape_string($nonce_hash)."', - '".mysql_real_escape_string($timestamp)."' - ); - "); - return null; - } - catch (\Exception $e) - { - # INSERT failed. This nonce was already used. + Db::execute(" + insert into okapi_nonces (consumer_key, nonce_hash, timestamp) + values ( + '".mysql_real_escape_string($consumer->key)."', + '".mysql_real_escape_string($nonce_hash)."', + '".mysql_real_escape_string($timestamp)."' + ); + "); + return null; + } + catch (\Exception $e) + { + # INSERT failed. This nonce was already used. - return $nonce; - } - } + return $nonce; + } + } - public function new_request_token($consumer, $callback = null) - { - if ((preg_match("#^[a-z][a-z0-9_.-]*://#", $callback) > 0) || - $callback == "oob") - { /* ok */ } - else { throw new BadRequest("oauth_callback should begin with ://, or should equal 'oob'."); } - $token = new OkapiRequestToken(Okapi::generate_key(20), Okapi::generate_key(40), - $consumer->key, $callback, null, Okapi::generate_key(8, true)); - Db::execute(" - insert into okapi_tokens - (`key`, secret, token_type, timestamp, - user_id, consumer_key, verifier, callback) - values ( - '".mysql_real_escape_string($token->key)."', - '".mysql_real_escape_string($token->secret)."', - 'request', - unix_timestamp(), - null, - '".mysql_real_escape_string($consumer->key)."', - '".mysql_real_escape_string($token->verifier)."', - ".(($token->callback_url == 'oob') - ? "null" - : "'".mysql_real_escape_string($token->callback_url)."'" - )." - ); - "); - return $token; - } + public function new_request_token($consumer, $callback = null) + { + if ((preg_match("#^[a-z][a-z0-9_.-]*://#", $callback) > 0) || + $callback == "oob") + { /* ok */ } + else { throw new BadRequest("oauth_callback should begin with lower case ://, or should equal 'oob'."); } + $token = new OkapiRequestToken(Okapi::generate_key(20), Okapi::generate_key(40), + $consumer->key, $callback, null, Okapi::generate_key(8, true)); + Db::execute(" + insert into okapi_tokens + (`key`, secret, token_type, timestamp, + user_id, consumer_key, verifier, callback) + values ( + '".mysql_real_escape_string($token->key)."', + '".mysql_real_escape_string($token->secret)."', + 'request', + unix_timestamp(), + null, + '".mysql_real_escape_string($consumer->key)."', + '".mysql_real_escape_string($token->verifier)."', + ".(($token->callback_url == 'oob') + ? "null" + : "'".mysql_real_escape_string($token->callback_url)."'" + )." + ); + "); + return $token; + } - public function new_access_token($token, $consumer, $verifier = null) - { - if ($token->consumer_key != $consumer->key) - throw new BadRequest("Request Token given is not associated with the Consumer who signed the request."); - if (!$token->authorized_by_user_id) - throw new BadRequest("Request Token given has not been authorized."); - if ($token->verifier != $verifier) - throw new BadRequest("Invalid verifier."); + public function new_access_token($token, $consumer, $verifier = null) + { + if ($token->consumer_key != $consumer->key) + throw new BadRequest("Request Token given is not associated with the Consumer who signed the request."); + if (!$token->authorized_by_user_id) + throw new BadRequest("Request Token given has not been authorized."); + if ($token->verifier != $verifier) + throw new BadRequest("Invalid verifier."); - # Invalidate the Request Token. + # Invalidate the Request Token. - Db::execute(" - delete from okapi_tokens - where `key` = '".mysql_real_escape_string($token->key)."' - "); + Db::execute(" + delete from okapi_tokens + where `key` = '".mysql_real_escape_string($token->key)."' + "); - # In OKAPI, all Access Tokens are long lived. Therefore, we don't want - # to generate a new one every time a Consumer wants it. We will check - # if there is already an Access Token generated for this (Consumer, User) - # pair and return it if there is. + # In OKAPI, all Access Tokens are long lived. Therefore, we don't want + # to generate a new one every time a Consumer wants it. We will check + # if there is already an Access Token generated for this (Consumer, User) + # pair and return it if there is. - $row = Db::select_row(" - select `key`, secret - from okapi_tokens - where - token_type = 'access' - and user_id = '".mysql_real_escape_string($token->authorized_by_user_id)."' - and consumer_key = '".mysql_real_escape_string($consumer->key)."' - "); - if ($row) - { - # Use existing Access Token + $row = Db::select_row(" + select `key`, secret + from okapi_tokens + where + token_type = 'access' + and user_id = '".mysql_real_escape_string($token->authorized_by_user_id)."' + and consumer_key = '".mysql_real_escape_string($consumer->key)."' + "); + if ($row) + { + # Use existing Access Token - $access_token = new OkapiAccessToken($row['key'], $row['secret'], - $consumer->key, $token->authorized_by_user_id); - } - else - { - # Generate a new Access Token. + $access_token = new OkapiAccessToken($row['key'], $row['secret'], + $consumer->key, $token->authorized_by_user_id); + } + else + { + # Generate a new Access Token. - $access_token = new OkapiAccessToken(Okapi::generate_key(20), Okapi::generate_key(40), - $consumer->key, $token->authorized_by_user_id); - Db::execute(" - insert into okapi_tokens - (`key`, secret, token_type, timestamp, user_id, consumer_key) - values ( - '".mysql_real_escape_string($access_token->key)."', - '".mysql_real_escape_string($access_token->secret)."', - 'access', - unix_timestamp(), - '".mysql_real_escape_string($access_token->user_id)."', - '".mysql_real_escape_string($consumer->key)."' - ); - "); - } - return $access_token; - } + $access_token = new OkapiAccessToken(Okapi::generate_key(20), Okapi::generate_key(40), + $consumer->key, $token->authorized_by_user_id); + Db::execute(" + insert into okapi_tokens + (`key`, secret, token_type, timestamp, user_id, consumer_key) + values ( + '".mysql_real_escape_string($access_token->key)."', + '".mysql_real_escape_string($access_token->secret)."', + 'access', + unix_timestamp(), + '".mysql_real_escape_string($access_token->user_id)."', + '".mysql_real_escape_string($consumer->key)."' + ); + "); + } + return $access_token; + } - public function cleanup() - { - Db::execute(" - delete from okapi_nonces - where - timestamp < unix_timestamp(date_add(now(), interval -6 minute)) - or timestamp > unix_timestamp(date_add(now(), interval 6 minute)) - "); - Db::execute(" - delete from okapi_tokens - where - token_type = 'request' - and timestamp < unix_timestamp(date_add(now(), interval -2 hour)) - "); - } + public function cleanup() + { + Db::execute(" + delete from okapi_nonces + where + timestamp < unix_timestamp(date_add(now(), interval -6 minute)) + or timestamp > unix_timestamp(date_add(now(), interval 6 minute)) + "); + Db::execute(" + delete from okapi_tokens + where + token_type = 'request' + and timestamp < unix_timestamp(date_add(now(), interval -2 hour)) + "); + } } diff --git a/htdocs/okapi/facade.php b/htdocs/okapi/facade.php index 40fe1b8d..9abed26c 100644 --- a/htdocs/okapi/facade.php +++ b/htdocs/okapi/facade.php @@ -34,6 +34,7 @@ use okapi\OkapiServiceRunner; use okapi\OkapiInternalRequest; use okapi\OkapiFacadeConsumer; use okapi\OkapiFacadeAccessToken; +use okapi\Cache; require_once($GLOBALS['rootpath']."okapi/core.php"); OkapiErrorHandler::$treat_notices_as_errors = true; @@ -45,131 +46,214 @@ Okapi::init_internals(); */ class Facade { - /** - * Perform OKAPI service call, signed by internal 'facade' consumer key, and return the result - * (this will be PHP object or OkapiHttpResponse, depending on the method). Use this method - * whenever you need to access OKAPI services from within OC code. If you want to simulate - * Level 3 Authentication, you should supply user's internal ID (the second parameter). - */ - public static function service_call($service_name, $user_id_or_null, $parameters) - { - $request = new OkapiInternalRequest( - new OkapiFacadeConsumer(), - ($user_id_or_null !== null) ? new OkapiFacadeAccessToken($user_id_or_null) : null, - $parameters - ); - $request->perceive_as_http_request = true; - return OkapiServiceRunner::call($service_name, $request); - } + /** + * Perform OKAPI service call, signed by internal 'facade' consumer key, and return the result + * (this will be PHP object or OkapiHttpResponse, depending on the method). Use this method + * whenever you need to access OKAPI services from within OC code. If you want to simulate + * Level 3 Authentication, you should supply user's internal ID (the second parameter). + */ + public static function service_call($service_name, $user_id_or_null, $parameters) + { + $request = new OkapiInternalRequest( + new OkapiFacadeConsumer(), + ($user_id_or_null !== null) ? new OkapiFacadeAccessToken($user_id_or_null) : null, + $parameters + ); + $request->perceive_as_http_request = true; + return OkapiServiceRunner::call($service_name, $request); + } - /** - * This works like service_call with two exceptions: 1. It passes all your - * current HTTP request headers to OKAPI (which can make use of them in - * terms of caching), 2. It outputs the service response directly, instead - * of returning it. - */ - public static function service_display($service_name, $user_id_or_null, $parameters) - { - $request = new OkapiInternalRequest( - new OkapiFacadeConsumer(), - ($user_id_or_null !== null) ? new OkapiFacadeAccessToken($user_id_or_null) : null, - $parameters - ); - $request->perceive_as_http_request = true; - if (isset($_SERVER['HTTP_IF_NONE_MATCH'])) - $request->etag = $_SERVER['HTTP_IF_NONE_MATCH']; - $response = OkapiServiceRunner::call($service_name, $request); - $response->display(); - } + /** + * This works like service_call with two exceptions: 1. It passes all your + * current HTTP request headers to OKAPI (which can make use of them in + * terms of caching), 2. It outputs the service response directly, instead + * of returning it. + */ + public static function service_display($service_name, $user_id_or_null, $parameters) + { + $request = new OkapiInternalRequest( + new OkapiFacadeConsumer(), + ($user_id_or_null !== null) ? new OkapiFacadeAccessToken($user_id_or_null) : null, + $parameters + ); + $request->perceive_as_http_request = true; + if (isset($_SERVER['HTTP_IF_NONE_MATCH'])) + $request->etag = $_SERVER['HTTP_IF_NONE_MATCH']; + $response = OkapiServiceRunner::call($service_name, $request); + $response->display(); + } - /** - * Create a search set from a temporary table. This is very similar to - * the "services/caches/search/save" method, but allows OC server to - * 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 - * following (or similar) structure: - * - * create temporary table temp_12345 ( - * cache_id integer primary key - * ) engine=memory; - */ - public static function import_search_set($temp_table, $min_store, $max_ref_age) - { - require_once 'services/caches/search/save.php'; - $tables = array('caches', $temp_table); - $where_conds = array( - $temp_table.".cache_id = caches.cache_id", - 'caches.status in (1,2,3)', - ); - return \okapi\services\caches\search\save\WebService::get_set( - $tables, $where_conds, $min_store, $max_ref_age - ); - } + /** + * Create a search set from a temporary table. This is very similar to + * the "services/caches/search/save" method, but allows OC server to + * 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 + * following (or similar) structure: + * + * create temporary table temp_12345 ( + * cache_id integer primary key + * ) engine=memory; + */ + public static function import_search_set($temp_table, $min_store, $max_ref_age) + { + require_once($GLOBALS['rootpath'].'okapi/services/caches/search/save.php'); + $tables = array('caches', $temp_table); + $where_conds = array( + $temp_table.".cache_id = caches.cache_id", + 'caches.status in (1,2,3)', + ); + return \okapi\services\caches\search\save\WebService::get_set( + $tables, $where_conds, $min_store, $max_ref_age + ); + } - /** - * Mark the specified caches as *possibly* modified. The replicate module - * will scan for changes within these caches on the next changelog update. - * This is useful in some cases, when OKAPI cannot detect the modification - * for itself (grep OCPL code for examples). See issue #179. - * - * $cache_codes - a single cache code OR an array of cache codes. - */ - public static function schedule_geocache_check($cache_codes) - { - if (!is_array($cache_codes)) - $cache_codes = array($cache_codes); - Db::execute(" - update caches - set okapi_syncbase = now() - where wp_oc in ('".implode("','", array_map('mysql_real_escape_string', $cache_codes))."') - "); - } + /** + * Mark the specified caches as *possibly* modified. The replicate module + * will scan for changes within these caches on the next changelog update. + * This is useful in some cases, when OKAPI cannot detect the modification + * for itself (grep OCPL code for examples). See issue #179. + * + * $cache_codes - a single cache code OR an array of cache codes. + */ + public static function schedule_geocache_check($cache_codes) + { + if (!is_array($cache_codes)) + $cache_codes = array($cache_codes); + Db::execute(" + update caches + set okapi_syncbase = now() + 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 - * mark them as *possibly* modified. See issue #265. - * - * $cache_id - internal ID of the geocache, - * $user_id - internal ID of the user. - */ - public static function schedule_user_entries_check($cache_id, $user_id) - { - Db::execute(" - update cache_logs - set okapi_syncbase = now() - where - cache_id = '".mysql_real_escape_string($cache_id)."' - and user_id = '".mysql_real_escape_string($user_id)."' - "); - } + /** + * Find all log entries of the specified user for the specified cache and + * mark them as *possibly* modified. See issue #265. + * + * $cache_id - internal ID of the geocache, + * $user_id - internal ID of the user. + */ + public static function schedule_user_entries_check($cache_id, $user_id) + { + Db::execute(" + update cache_logs + set okapi_syncbase = now() + where + cache_id = '".mysql_real_escape_string($cache_id)."' + and user_id = '".mysql_real_escape_string($user_id)."' + "); + } - /** - * Run OKAPI database update. - * Will output messages to stdout. - */ - public static function database_update() - { - require_once($GLOBALS['rootpath']."okapi/views/update.php"); - $update = new views\update\View; - $update->call(); - } + /** + * Run OKAPI database update. + * Will output messages to stdout. + */ + public static function database_update() + { + require_once($GLOBALS['rootpath']."okapi/views/update.php"); + $update = new views\update\View; + $update->call(); + } - /** - * You will probably want to call that with FALSE when using Facade - * in buggy, legacy OC code. This will disable OKAPI's default behavior - * of treating NOTICEs as errors. - */ - public static function disable_error_handling() - { - OkapiErrorHandler::disable(); - } + /** + * You will probably want to call that with FALSE when using Facade + * in buggy, legacy OC code. This will disable OKAPI's default behavior + * of treating NOTICEs as errors. + */ + public static function disable_error_handling() + { + OkapiErrorHandler::disable(); + } - /** - * If you disabled OKAPI's error handling with disable_error_handling, - * you may reenable it with this method. - */ - public static function reenable_error_handling() - { - OkapiErrorHandler::reenable(); - } + /** + * If you disabled OKAPI's error handling with disable_error_handling, + * you may reenable it with this method. + */ + public static function reenable_error_handling() + { + 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.....) diff --git a/htdocs/okapi/lib/oc_session.php b/htdocs/okapi/lib/oc_session.php index 0c0f2e95..3848d1fa 100644 --- a/htdocs/okapi/lib/oc_session.php +++ b/htdocs/okapi/lib/oc_session.php @@ -8,23 +8,23 @@ namespace okapi; */ class OCSession { - /** Return ID of currently logged in user or NULL if no user is logged in. */ - public static function get_user_id() - { - static $cached_result = false; - if ($cached_result !== false) - return $cached_result; + /** Return ID of currently logged in user or NULL if no user is logged in. */ + public static function get_user_id() + { + static $cached_result = false; + if ($cached_result !== false) + return $cached_result; - $cookie_name = Settings::get('OC_COOKIE_NAME'); - if (!isset($_COOKIE[$cookie_name])) - return null; - $OC_data = unserialize(base64_decode($_COOKIE[$cookie_name])); - if (!isset($OC_data['sessionid'])) - return null; - $OC_sessionid = $OC_data['sessionid']; - if (!$OC_sessionid) - return null; + $cookie_name = Settings::get('OC_COOKIE_NAME'); + if (!isset($_COOKIE[$cookie_name])) + return null; + $OC_data = unserialize(base64_decode($_COOKIE[$cookie_name])); + if (!isset($OC_data['sessionid'])) + return null; + $OC_sessionid = $OC_data['sessionid']; + if (!$OC_sessionid) + return null; - return Db::select_value("select user_id from sys_sessions where uuid='".mysql_real_escape_string($OC_sessionid)."'"); - } + return Db::select_value("select user_id from sys_sessions where uuid='".mysql_real_escape_string($OC_sessionid)."'"); + } } \ No newline at end of file diff --git a/htdocs/okapi/locale/de_DE/LC_MESSAGES/okapi_messages.mo b/htdocs/okapi/locale/de_DE/LC_MESSAGES/okapi_messages.mo index 7262bf7d7d11666ac011c071b5aed88f66efc7af..7d8dcdbddf206708c554eaf938b809a80fb48384 100644 GIT binary patch delta 1575 zcma*lSx8h-7{Ku_$64IcY*Dl2T9ZN3j5AYHWZP6=S*f{HM)WX_Q;n%pE=5#Kg%nam za6#z7r-T-w7bzklD583f4LZ!dzk9zk_dDl&=a>~YcU*TT zm!*kRri(Pr5J@;hPT*^&$O{oE&k%9rFPx9=Op%AU8SmjJM)78rNHIFIMF`1K%ts#< z;1(pM9Khum!CdUkmZYrY;Rb<2_!4W;og>nWCs99qkDBl^GDjK3SvWS)pF2~ei1z~I zCjo9Q45J+@F^n}>kJmARUuGu92d-c`YY7aXuHXZ%!x0Q)NuJ0xY{G5$11m7ZEWCIY z58_?axP#ehyb85)XRr+WZ~?wViYlLzJm`wP<0ZD@Cl0d(XXc8O;z8=S2QL{6QCF?xAg&Kc~cd?6oXvf8jLJGNtI?w=;LhhqZ=oPZ-lKjMjpNA1-Y$+tW2R+z? z`>_fiqn6CINQ8Mx3+n&dP#16>b->H073@ddlG`{3pQCQ&y9tMJC*#Wq50!KT7n2On z;z|69wHPtR_i_*$cz=SrhqFpVdN770lCQW6LrcaF*#*49`yJe_@ugG+&!JYJAA|bE zqlu1hm_tVvM_x--gfmcME4cKdEG|7*oFDOVO*Sp9*NoddRa#VSlrz#2KTaB1xV=0{tBUFVW!j@gR!V5OG8N`B-#~Ew09bbcH?xSyTfRT9gVae_H8ITzUi;}|GiSP z(X}IQdRlY9Rg?W^aB$SMF(YTS*B|f(R~dm&c_?6p^NXwhj08RYfZw0FJJQ+ZZRm)! zceX~l5*_8n{zOYO9*Z6|>P{bt$67m&#f=RelaJxHcw6@=U#z*?cf8f?&tIR`95aXV P$82kSs|JT=hjM=dbhOdw delta 1241 zcmYk)O-NKx6u|ML^Px{uD=o8`ax$|_$7U3n6bk!NCovsAf(!#wAt@(QG1@o;K|v_R z3o42hxe1dN*~cO4WBxl)+ zE#O z+(zwxf|J<8Y*EZ&KQ`bf>O{AYSLFd}LNAeBm)LuP0Kpt;iBn0h1U+~f>#z|gQA_p< z$x}MWLZ9zMUBCe9gcngOIELgTcaXKnQ`D_|XFH2WnP27z!VDB_73s%wID+#S!AO2$ zFDI~#`zh2tO!A3b!FJ?BGLH?oV_V{oUBK(y-^DuZFW``4KWYWWuw37GWDhJL=R$s? zmdx1BB&cQ6L!>XIbLqiyaFubbHmzWZ+2<%}vs%`c8z7j=*fbIIWeaTu*7up+B*$gG-@s8#diM7Q;?r%lofZRRQGL02Mr z<_y`KDSP$Lz7kf1?b; zYt5d>!E7+\n" +"POT-Creation-Date: 2014-01-23 15:51+0100\n" +"PO-Revision-Date: 2014-01-23 15:52+0100\n" +"Last-Translator: Wojciech Rygielski \n" "Language-Team: following \n" -"Language: German\n" +"Language: de\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -14,20 +14,29 @@ msgstr "" "X-Poedit-Basepath: .\n" "Plural-Forms: nplurals=2; plural=(n != 1);\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-1: D:\\PRIV\\Projekty\\EclipseWorkspace\\opencaching-api" "\\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" 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" 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 msgid "" "This geocache description comes from the %sCache-Beschreibung stammt von %s." -#: 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 msgid "" "© %s, %s, CC-BY-NC-ND, Stand: %s; " "alle Logeinträge © 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 msgid "" "© %s, %s, CC-BY-NC-ND; alle " "Logeinträge © jeweiliger Autor" -#: c:\source\oc\server-3.0\htdocs\okapi/services/caches/formatters/gpxfile.tpl.php:31 -#: c:\source\oc\server-3.0\htdocs\okapi/services/caches/formatters/gpxfile.tpl.php:60 +#: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/caches/formatters/gpx.php:360 +msgid "" +"Geocache coordinates have been changed. 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 "" +"Geocache coordinates have been changed. 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" 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 msgid "%d recommendation" msgid_plural "%d recommendations" msgstr[0] "%d Empfehlung" 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 msgid "found %d time" msgid_plural "found %d times" msgstr[0] "%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 msgid "%d trackable" msgid_plural "%d trackables" msgstr[0] "%d Geokret" 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" 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" 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" msgstr "Geokrets" -#: c:\source\oc\server-3.0\htdocs\okapi/services/caches/formatters/gpxfile.tpl.php:88 -#: 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:90 +#: D:\PRIV\Projekty\EclipseWorkspace\opencaching-api\okapi/services/caches/formatters/gpxfile.tpl.php:106 msgid "Images" 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" 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" 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:" msgstr "" "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 "" "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." @@ -121,7 +151,7 @@ msgstr "" "Das Datum deines Logeintrags liegt in der Zukunft. Cache-Logs können nur für " "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 msgid "" "However, your cache rating was ignored, because %s does not have a rating " @@ -129,7 +159,7 @@ msgid "" msgstr "" "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 msgid "" "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 " "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 msgid "" "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 " "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 "" "This cache is an Event cache. You cannot \"Find\" it (but you can attend it, " "or comment on it)!" @@ -155,7 +185,7 @@ msgstr "" "Dies ist ein Event-Cache. Du kannst ihn nicht \"finden\" (aber du kannst am " "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 "" "This cache is NOT an Event cache. You cannot \"Attend\" it (but you can find " "it, or comment on it)!" @@ -163,26 +193,26 @@ msgstr "" "Dies ist KEIN Event-Cache. Du kannst an ihm nicht \"teilnehmen\" (aber du " "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." 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!" msgstr "" "Dieser Cache kann nur mit Kennwort geloggt werden, aber du hast keines " "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!" 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." msgstr "" "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 "" "You have already submitted a \"Found it\" log entry once. Now you may submit " "\"Comments\" only!" @@ -190,53 +220,53 @@ msgstr "" "Du hast diesen Cache bereits als gefunden geloggt. Ein zweites Fundlog ist " "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!" msgstr "" "Als Besitzer des Caches kannst du nur Hinweise loggen, keine Funde oder " "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." msgstr "" "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." 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!" msgstr "" "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\"." 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." 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" 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" 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." msgstr "" "Die Anfrage ist wegen Zeitüberschreitung abgelaufen. Bitte versuche es noch " "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..." 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 msgid "" "%s wants to access your %s account. Do you agree to grant " @@ -245,28 +275,29 @@ msgstr "" "%s möchte auf dein %s-Benutzerkonto zugreifen. Möchtest du " "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" 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" 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 msgid "" "\n" -"\t\t\t\t\t

Once permission is granted it is valid until its withdrawal on\n" -"\t\t\t\t\tthe applications management page.

\n" -"\t\t\t\t\t

The application will access your acount via the " -"OKAPI Framework.\n" -"\t\t\t\t\tIf you allow this request application will be able to access all " -"methods delivered\n" -"\t\t\t\t\tby the OKAPI Framework, i.e. post log entries on geocaches in your " -"name.\n" -"\t\t\t\t\tYou can revoke this permission at any moment.

\n" -"\t\t\t\t" +"

Once permission is granted it is valid until its " +"withdrawal on\n" +" the applications management page.

\n" +"

The application will access your acount via the OKAPI Framework.\n" +" If you allow this request application will be able to " +"access all methods delivered\n" +" by the OKAPI Framework, i.e. post log entries on " +"geocaches in your name.\n" +" You can revoke this permission at any moment.

\n" +" " msgstr "" "\n" "\t\t\t\t\t

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.

\n" "\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" 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" 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 msgid "" "\n" -"\t\t\t\t

You've just granted %s application access to your %s account.\n" -"\t\t\t\tTo complete the operation, go back to %s and enter the following PIN " -"code:

\n" -"\t\t\t" +"

You've just granted %s application access to your %s " +"account.\n" +" To complete the operation, go back to %s and enter the " +"following PIN code:

\n" +" " msgstr "" "\n" "\t\t\t\t

Du hast der Anwendung \"%s\" Zugriff auf dein %s-Benutzerkonto " @@ -306,26 +337,26 @@ msgstr "" "PIN-Code ein:

\n" "\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" 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" 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 msgid "" "\n" -"\t\t\t\t\t

This is the list of applications which you granted access to " -"your %s account.\n" -"\t\t\t\t\tThis page gives you the abbility to revoke all previously granted " -"privileges.\n" -"\t\t\t\t\tOnce you click \"remove\" the application will no longer be able " -"to perform any\n" -"\t\t\t\t\tactions on your behalf.

\n" -"\t\t\t\t" +"

This is the list of applications which you granted " +"access to your %s account.\n" +" This page gives you the abbility to revoke all " +"previously granted privileges.\n" +" Once you click \"remove\" the application will no longer " +"be able to perform any\n" +" actions on your behalf.

\n" +" " msgstr "" "\n" "\t\t\t\t\t

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.

\n" "\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" 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 msgid "" "\n" -"\t\t\t\t\t

Thanks to the OKAPI Framework you can grant " -"external applications\n" -"\t\t\t\t\taccess to your %s account. Currently no applications are " -"authorized to act\n" -"\t\t\t\t\ton your behalf. Once you start using external Opencaching " -"applications, they will appear here.

\n" -"\t\t\t\t" +"

Thanks to the OKAPI Framework you " +"can grant external applications\n" +" access to your %s account. Currently no " +"applications are authorized to act\n" +" on your behalf. Once you start using external " +"Opencaching applications, they will appear here.

\n" +" " msgstr "" "\n" "\t\t\t\t\t

Die OKAPI-Schnittstelle ermöglichst es dir, " diff --git a/htdocs/okapi/locale/it_IT/LC_MESSAGES/okapi_messages.mo b/htdocs/okapi/locale/it_IT/LC_MESSAGES/okapi_messages.mo index 1b0716478cc41ef9cca6107f6935273321768d51..7125d20c1d5a6e88e04911df17a97c21d55a2c8b 100644 GIT binary patch delta 1597 zcma*lUrbY19Ki7(w51hL3$zrhj0-`;AZ-DK4c)?+sB8!k5ZRW6b)`bJP;AQ<3?Pf$DP*>`2^kX4eug5l|h%$(JK@q${<;PQW<|uiSixuJnw@3~CTP$*l z2Wv`1HsFt_C;AIx=w$f@evEqJX=F&wqTYf9WPZ7g0lbTRN}OcZh?N+}R6m_Cookpt zKO4)U;P`&ehMq_JnHZK zjJje=m?gEDU=0CXK0Tl>&Q1E|^bHNWA+@T*QoG0cI7GS9mxY$ol-|{QeP zUmObY@nKY-dgaHTk>_u=e)BnA&*?EAn9d13@8oOk1Lku??Oo;rm+W0VD>K*k z!B8?18%r3mF=H||5jWloj|8I!eB1rwuRK-%r&p?8bnJGnoK}76=+1vKID5l!%f5O` zU7)_Mq0y+{+OoA#%{#qa%aNv*z?RxTeIStA8B8SW`r^T{L^PO;#aoR1v4f#VI5cAP zOb$oF(Zr#Mu|58Ow{Z{_n)E~wBRHvs@ RKRE8`QH!3vv)&S~<3AYuNq`W(UMYkJAUvoKJ{U=W#y1HN{-<--RBv|Ycjn$Zcjlfm z^XEHf2fo@+xl|+4=@)6ozXHS<4vG9EBI#O@4R{w{!Y7!+SXiVL=P`ocVW$B12ICDCZwLDtMEdABj05;Beu;PRU39q1c07eiT*AY+ zj%{iGr>Ol`P&4%iU&Y93kv7aCo0mhV8?y203nCv?2=0;cM3YDd2BIQ^xVc5-XC{73 zJ~v`p+*@c6i=0oOg*QMcjtR@O`|1d5n?{eTOPYW-Ias0cDa+ zFN?f|X&k~?oX4}c7kk!;?7}aw0~b;Imr$>JyWOklXUM%|0rh3Oh3oMd>U}KB%h(pI zkpw%VQ=eGVr>WHDCo>(o1Z_mUT!?` za*-156<=#jo%<`tdiNh+drV7KZTdb*B&^Nt_YVfV=o^2=M8EqFnrLU>fQe2AhD}r- z?5Wc$sw*4`zGp^m1V>HO8_NA>&c)EU8R@LuYoZhXMh|M+mY>F3(M^PR`t=6x@$iTb z&-@m?7pkO_sZ6rF&q}?K?d?vax>KphzLDgp^GUvxFBXzRcG<~VJ*n=M^IrGn?Y2wh zw@Vnai(Z6J}r{_JN7c#SR87O\n" +"POT-Creation-Date: 2014-01-23 15:53+0100\n" +"PO-Revision-Date: 2014-01-23 16:04+0100\n" +"Last-Translator: Wojciech Rygielski \n" "Language-Team: following \n" -"Language: Italian\n" +"Language: it\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -14,22 +14,31 @@ msgstr "" "X-Poedit-Basepath: .\n" "Plural-Forms: nplurals=2; plural=(n != 1);\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-1: D:\\PRIV\\Projekty\\EclipseWorkspace\\opencaching-api" "\\okapi\n" "X-Poedit-SearchPath-2: C:\\Users\\stefano.cotterli\\Desktop\\opencaching-" "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" 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" 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 msgid "" "This geocache description comes from the %sgeocacheproviene dal sito %s." -#: 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 msgid "" "© %s, %s, %s, %s, %s wants to access your %s account. Do you agree to grant " @@ -235,122 +265,118 @@ msgstr "" "%s vuole accedere al tuo account %s. Sei d'accordo nel " "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" 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" 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 msgid "" "\n" -"\t\t\t\t\t

Once permission is granted it is valid until its withdrawal on\n" -"\t\t\t\t\tthe applications management page.

\n" -"\t\t\t\t\t

The application will access your acount via the " -"OKAPI Framework.\n" -"\t\t\t\t\tIf you allow this request application will be able to access all " -"methods delivered\n" -"\t\t\t\t\tby the OKAPI Framework, i.e. post log entries on geocaches in your " -"name.\n" -"\t\t\t\t\tYou can revoke this permission at any moment.

\n" -"\t\t\t\t" +"

Once permission is granted it is valid until its " +"withdrawal on\n" +" the applications management page.

\n" +"

The application will access your acount via the OKAPI Framework.\n" +" If you allow this request application will be able to " +"access all methods delivered\n" +" by the OKAPI Framework, i.e. post log entries on " +"geocaches in your name.\n" +" You can revoke this permission at any moment.

\n" +" " msgstr "" "\n" -"\t\t\t\t\t

Una volta concessa, l'autorizzazoine è valida finché non venga\n" -"\t\t\t\t\trevocata nella pagina di gestione applicazioni.\n" -"\t\t\t\t\t

L'applicazione accederà al tuo account tramite il frameword OKAPI.\n" -"\t\t\t\t\tSe permetti questa richiesta, l'applicazione potrà accedere a " -"tutti i metodi forniti\n" -"\t\t\t\t\tdal framework OKAPI, per es. postare i log delle geocache a tuo " -"nome..\n" -"\t\t\t\t\tPuoi revocare questo permesso in qualsiasi momento.

\n" -"\t\t\t\t" +"

Una volta concessa, l'autorizzazoine è valida finché non venga\n" +"revocata nella pagina di gestione applicazioni.

\n" +"

L'applicazione accederà al tuo account tramite il frameword " +"OKAPI.\n" +"Se permetti questa richiesta, l'applicazione potrà accedere a tutti i metodi " +"forniti\n" +"dal framework OKAPI, per es. postare i log delle geocache a tuo nome..\n" +"Puoi revocare questo permesso in qualsiasi momento.

" -#: 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" 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" 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 msgid "" "\n" -"\t\t\t\t

You've just granted %s application access to your %s account.\n" -"\t\t\t\tTo complete the operation, go back to %s and enter the following PIN " -"code:

\n" -"\t\t\t" +"

You've just granted %s application access to your %s " +"account.\n" +" To complete the operation, go back to %s and enter the " +"following PIN code:

\n" +" " msgstr "" "\n" -"\t\t\t\t

Hai appena concesso all'applicazione \"%s\" l'accesso al tuo " -"account %s.\n" -"\t\t\t\tPer completare l'operazione, riorna a %s e inserisci il seguente " -"codice PIN:

\n" -"\t\t\t" +"

Hai appena concesso all'applicazione \"%s\" l'accesso al tuo account " +"%s.\n" +"Per completare l'operazione, riorna a %s e inserisci il seguente codice PIN:" +"

" -#: 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" 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" 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 msgid "" "\n" -"\t\t\t\t\t

This is the list of applications which you granted access to " -"your %s account.\n" -"\t\t\t\t\tThis page gives you the abbility to revoke all previously granted " -"privileges.\n" -"\t\t\t\t\tOnce you click \"remove\" the application will no longer be able " -"to perform any\n" -"\t\t\t\t\tactions on your behalf.

\n" -"\t\t\t\t" +"

This is the list of applications which you granted " +"access to your %s account.\n" +" This page gives you the abbility to revoke all " +"previously granted privileges.\n" +" Once you click \"remove\" the application will no longer " +"be able to perform any\n" +" actions on your behalf.

\n" +" " msgstr "" "\n" -"\t\t\t\t\t

Questa è la lista delle applicazioni a cui hai concesso " -"l'accesso al tuo account %sQuesta è la lista delle applicazioni a cui hai concesso l'accesso al tuo " +"account %s\n" -"\t\t\t\t" +"Qundo clicchi su \"rimuovi\" all'applicazione non sarà più concesso eseguire " +"nessuna\n" +"azione per tuo conto.

" -#: 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" 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 msgid "" "\n" -"\t\t\t\t\t

Thanks to the OKAPI Framework you can grant " -"external applications\n" -"\t\t\t\t\taccess to your %s account. Currently no applications are " -"authorized to act\n" -"\t\t\t\t\ton your behalf. Once you start using external Opencaching " -"applications, they will appear here.

\n" -"\t\t\t\t" +"

Thanks to the OKAPI Framework you " +"can grant external applications\n" +" access to your %s account. Currently no " +"applications are authorized to act\n" +" on your behalf. Once you start using external " +"Opencaching applications, they will appear here.

\n" +" " msgstr "" "\n" -"\t\t\t\t\t

Grazie al framework OKAPIpuoi concedere ad " -"applicazioni esterne\n" -"\t\t\t\t\t l'accesso al tuo account %s. Attualmente non ci son " -"applicazioni autorizzate\n" -"\t\t\t\t\tad agire per tuo conto. Quando userai applicazioni esterne a " -"Opencaching, queste appariranno qui

\n" -"\t\t\t\t" +"

Grazie al framework OKAPIpuoi concedere ad applicazioni " +"esterne\n" +"l'accesso al tuo account %s. Attualmente non ci son applicazioni " +"autorizzate\n" +"ad agire per tuo conto. Quando userai applicazioni esterne a Opencaching, " +"queste appariranno qui

\n" +" " #~ msgid "Recommending is allowed only for 'Found it' logtypes." #~ msgstr "Le raccomandazioni sono ammesse solo per i log di tipo 'Trovata'" diff --git a/htdocs/okapi/locale/locales.php b/htdocs/okapi/locale/locales.php index d02bed55..023b1f31 100644 --- a/htdocs/okapi/locale/locales.php +++ b/htdocs/okapi/locale/locales.php @@ -4,53 +4,53 @@ namespace okapi; class Locales { - public static $languages = array( - 'pl' => array('lang' => 'pl', 'locale' => 'pl_PL.utf8', 'name' => 'Polish'), - 'en' => array('lang' => 'en', 'locale' => 'en_US.utf8', 'name' => 'English'), - 'nl' => array('lang' => 'nl', 'locale' => 'nl_NL.utf8', 'name' => 'Dutch'), - 'de' => array('lang' => 'de', 'locale' => 'de_DE.utf8', 'name' => 'German'), - 'it' => array('lang' => 'it', 'locale' => 'it_IT.utf8', 'name' => 'Italian'), - ); + public static $languages = array( + 'pl' => array('lang' => 'pl', 'locale' => 'pl_PL.utf8', 'name' => 'Polish'), + 'en' => array('lang' => 'en', 'locale' => 'en_US.utf8', 'name' => 'English'), + 'nl' => array('lang' => 'nl', 'locale' => 'nl_NL.utf8', 'name' => 'Dutch'), + 'de' => array('lang' => 'de', 'locale' => 'de_DE.utf8', 'name' => 'German'), + 'it' => array('lang' => 'it', 'locale' => 'it_IT.utf8', 'name' => 'Italian'), + ); - /** - * Get the list of locales that should be installed on the system in order - * for all translations to work properly. - */ - public static function get_required_locales() - { - $arr = array('POSIX'); - foreach (self::$languages as $key => $value) - $arr[] = $value['locale']; - return $arr; - } + /** + * Get the list of locales that should be installed on the system in order + * for all translations to work properly. + */ + public static function get_required_locales() + { + $arr = array('POSIX'); + foreach (self::$languages as $key => $value) + $arr[] = $value['locale']; + return $arr; + } - /** - * Get the list of locales installed on the current system. - */ - public static function get_installed_locales() - { - $arr = array(); - foreach (explode("\n", shell_exec("locale -a")) as $item) - if ($item) - $arr[] = $item; - return $arr; - } + /** + * Get the list of locales installed on the current system. + */ + public static function get_installed_locales() + { + $arr = array(); + foreach (explode("\n", shell_exec("locale -a")) as $item) + if ($item) + $arr[] = $item; + return $arr; + } - private static function get_locale_for_language($lang) - { - if (isset(self::$languages[$lang])) - return self::$languages[$lang]['locale']; - return null; - } + private static function get_locale_for_language($lang) + { + if (isset(self::$languages[$lang])) + return self::$languages[$lang]['locale']; + return null; + } - public static function get_best_locale($langprefs) - { - foreach ($langprefs as $lang) - { - $locale = self::get_locale_for_language($lang); - if ($locale != null) - return $locale; - } - return self::$languages['en']['locale']; - } + public static function get_best_locale($langprefs) + { + foreach ($langprefs as $lang) + { + $locale = self::get_locale_for_language($lang); + if ($locale != null) + return $locale; + } + return self::$languages['en']['locale']; + } } diff --git a/htdocs/okapi/locale/nl_NL/LC_MESSAGES/okapi_messages.mo b/htdocs/okapi/locale/nl_NL/LC_MESSAGES/okapi_messages.mo index 158246f0fd1d7d17893c4819e98027c967314cf2..06a046b8a41123edf1e097fc1e33b228ad698c92 100644 GIT binary patch literal 8949 zcmbuE%a0sa6~=E!cr}Fg8v@rIjBPSq9w!b7V|$3>8QVkr7~+Wo!pN%by4_vV)m24R zb&q=#fk5K50SSSSKtR~AWWfSJ-!P*2EGW&eja&)QXdA_L6%l`z$d^j zf?oxH4PF3mqSQt3+h7O$Blt=1^(ZgrN1&YlF(~@I0GY{}=E!crOQ^1$&_Q?N6ZitILn< z?=zs-^D7{#sPBMcuOETO!Jn4bUjRk_zkyGK8d7}>%)w{DUxFg!Y)^I!(Pj!mBhKL^VC zKLdsHzXfGKN-K34cr*A#@D?cd{|S5w)JK#$1xDZw_&rek@^_FW)PrrO-;M$ z+pRr5076Nt11`6p5ArKEW=X5-3Me+NK7_W7L9wgEhUobqU$KkWR^m}?)%+YS55$%a zH!k1?`#D{raOQ1%F_ZH6iVwwZVw>jY5e~%vxWncXxkkv~EWPA&HeJ6S_l!iW{GFhk zo`(M7s-njAr_OJ0>5G{^HhW1nk`=wB3vKm9nD(P6+0(_4eP`y=6O$K**Y2WEH!`|N zDm8dYk4-U5f?NkC3a2JB!O~^AGlU;fYek2i@pPK7T$Btnw#h=1V_!WmNzd;MIS6C7 z-I%>B-@_-^K+liSe432VPj=Dptj}KJnI0!%`{gFTD@%5Uemu%$%ZHr1#KhK=qKRda zHg^h>#qg!!#(b%HO1H9%_3YX1`qBLC$?m!sTAb|Z3zIBE{b;6R%-`gdpW&AYPRPQ2 z$ZNe*EL{b&TJc>o^rOC~Z3u|>^TN*xJ;}ouA5=|frJLTdY#`Y50Djugv5WfMZWx6Hyi6+M z2%S?%6eg1#Ls#pjSvUq zWMiS}(mnV+mqHFWaB229Ol5Kjgd#|eO^x22VC!mUNAsr5tEjG6C|67R2^SA(wv*^y zGEO5?NX!|Xunn8*R`ejzT_0N@N~FYrgf@GY+xm&*8e!G;)-@yqX6?{M)X@N^Af1p6 ztjxn_ayeCGEF)QI;UHIa1nrU2Z2*__xm$p3*x1wlATu__xe%^7XR}Hp4n87lTq?5^ z+eoq?jQs+J4Snp~H73?Qvd6##ga)BpN3e~bGzm%G7$}J`UEi@KvIzrw?)fu&1Gz8@ z2MCTJWMJ}M7Iq18KVSP_m*0_~tyRlR8u>j$Nm+AgA=yJ?=#h*Uwo?b9eLBsOX&CSz zj74cexJuQYI`67eKC#%Jh|(q3)cJzk-ko3|b-v72*0K5`%s+7Ih6pe$qBaac6&tlV zg+L^ETM@{xaZ>1Z%srG<>}X=2a^CjT=AAU82319jp`{x3#CXTu7*`vE)!k!PKTBcPdv~ zv1^r5KhJ44f+OncOrKBFTwSvsrT}jH*+`$%SI7i;&rglIUb0#rhjk|Fh{>YR1SiOj zu3@TT0>f4WtDmaQ%HYGsfDl%u;{nQCyS_uB z)D5D0nw%|-jU_wrLoRGfb9$UGvXxO@4;_*fxUm!u)N4y~%3ovUYJvPdzJycx}GV^KvWsVb)z zvC-`5?FcWBma`dbBp^NYOinqexT3^R(g%sbEukEHFsrq%(`D?>2+i&o3$`yfOXY1; zie^V@0ohDKfV7-Wy7VPOc2sqy(({ljeN9C&LYn@>Mz+2nX=#3&G_bZOlQIXpkVUC4 z#no)XPB=|GDW0z==pJ_s3R%cu`VHJTGM1zW+};rzE$b6sw|4BGRzWThwpXBM9$zqXh@YPa!*twQWji>XJMO+A?VqxUgh2M zp14u}b0^3ugo|wgLwAB-7u;{lBRiT0Whv?|Mb;HlTSltHqWacaM{!v=Hj78O8iqkY zhtZuW*KVrqEZHSzb+&@eO%fj?=bD=WTTdIQMxCqqAPhQBP6l~rCt1_2?dv<83ot`+ zd1nJ|tm%hWPo3_pp6WdOh(7h9wbe%-T0OP8%8i|;X^KVU!hMf-RzIv)A6Yy7n7i)^ zjP4NJaUS_alC9~>e#SVbpNi-b_1U34dcwg-&Usz4-l8T9CgkYOj`7E+RUNPCIJ$l9 zO6%&@)y;ZGr@YnH1?S4n&MZY|G4@G%nJ_*hq|I_uoO@>HV(0PtK2fPpk=cytHVN-F z{dhMlS|9IhCngAs&QoS4bt}KZ9I~cwpBWhP-kqYt+i{Yl_1#aBiBi89uIY`nTiZ`> z-MnQ@KPqOoHfdJU+&s&~ou_nHw`hs08LH!_;jPwoG|Bv^BRx;f4pV!WpL_U>c7L8b z9_u6L^r;hPSn!geG&)l{P4y}7QSWrE$_<9E-f$abI;(5iYpE8!{<9PeBfWMaBMXfJ z5tnO{*>1m}e(y5~nJ5S)$?=|piQ9}ulJnRj8G4~yAwtqQS94}YgjR1ZXh_|>$9fym zE?4*IeM+#bj92w23wH;QRNheRCX>BtoM5Q~>ky?&#%Zrr?Pb@lA)6VCssm%#%aoqm zZK%0EO(#pTlX1d6!`;!4MlxYlx^fCU3e|G z)wvoYxf+eiHw)Mj;$+Ea#QIL4wUKqo2CotJ!(A(T!Ndo2vIZVaV(oTp%}7himP$Li zRAgH9mbrjEMaz2Vspd9))xPvFCeo~KZ-nr3Kg7Iq!Itd8w#f&i%G7CiO`tCs`E!c@ zF{^IA7g*3@-3Bt4%>*{{Q3DUUZr>6o*1^)ZSw_?F zHrA|cz}d>Fb{M}t7LHpNkpg)#!r-BBc;Is)n+$Y}4lcDif37g~*?u6O=u`qqh z`0V>}7wa%@4GtWdcwxL=IB3q=)6Dk=^}Zy*+{J-%+yzfgq8%lec7h%11q>*kXVA1L?<~033S+Y zq9$9|(6Z0K7kL%RI|0|e?I9Xvr64pPXA-KOx4Esp4aMoRCB54T+Kl;|Lh}DfBtV5K zl`(5g117wZisNXl2}7&5T4LH8NX= z!moO~-YjT|&D1z+L;0SX^>!nRZ-o!OlQ(5(7WrG%VJTLij9H$4VVr7Hw)R z?WRY`n0)DWvhYYbVz=9F$hIhd-xg{Q3!(kxgmi@^4!Qeqp~2%Q&7-B>w5 z9afVHe%BR+yBN)FuuQ)MplDZfsMLV{yI`savM`EYHtZr_v1zi3d7hBmEMd4j3q6n!4mF3lqnjG8mP7LTL@rUd-s+RH<28UCK3z%2&6qIc~M**0fR5s@c`nlRxHBoa}2oRe!Rly9670Lu~x7C+2kUp>4XgsmZmo=8V1y_nMpa(C@ z`M*DD;$-=#FNQNCjDYt0HfF3F%e!3}BNa3+e^e%e!v#eu4edQG4NQu~uTHSIemcSP z@|N7T?Y#9BreV}x!oX>3hz$IVl3O)aRX&jpMl$U@vfT+9wff9FG1(HX*ih9Qs(%CH CTfyxB delta 1565 zcmZXTZERCj7{||C+pTXKdui8MW=}6;Y+>zg2a_?HfhZ!|=mwORWazwf7kan07u#FM z2w{*AV~9bMqhAcsm?;oK_`>T45=b;sTK z{1`+|b{T8}ugmoZU<2O21rdV%0HQ{=0;a*=z)j#5R6&K5@YoEV1}`8#y9?)aOziaH zG~fl0-hT&UV8T~9*%a7^_mdzgbrnQxwgggPk3cU}`w_eYg?>d^^+p*nQ@qErr(mUwfFTDrOebQU!+9kbnu6{3lQH}7E>wsLDt&fUh zbqV=)tuFK2zJdBfa^;Kql=N!dsr7btEn^vGp~TGspEt`zenih|xufx9-|=C)&`L6~ z=zhaN`^8`G@mk^m&sW0ln-m}UM#P~1HSvo-Dc*13;`7GdE^b&#ly`40TRcWld0w06 z%ByBRuNN$ZQW;jfNBr4%S}d&+;?1W}zW9kr(d#dVw7#NmXJq+|q?`dF4dT#|sq<1B_ zLwbJ%2Ykcp-OzTCx@V}PC z+edt~X*|!Tb*{~dGwnMalwT~hUld2e`^8Fln<%Z`D>^#H#Y)GzR>R^(iR*fS>+hly z87-65g%art?$)#-pVM<(%ayp2GA+Y0bVV#g&WgkQyjT<6Bf6sv;%4-bxT6gJ3qKa^ Ac>n+a diff --git a/htdocs/okapi/locale/nl_NL/LC_MESSAGES/okapi_messages.po b/htdocs/okapi/locale/nl_NL/LC_MESSAGES/okapi_messages.po index 006e7e38..64f37b18 100644 --- a/htdocs/okapi/locale/nl_NL/LC_MESSAGES/okapi_messages.po +++ b/htdocs/okapi/locale/nl_NL/LC_MESSAGES/okapi_messages.po @@ -2,9 +2,9 @@ msgid "" msgstr "" "Project-Id-Version: OKAPI\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2013-04-10 01:08+0100\n" -"PO-Revision-Date: 2013-04-10 01:12+0100\n" -"Last-Translator: Wojciech Rygielski \n" +"POT-Creation-Date: 2014-01-23 16:05+0100\n" +"PO-Revision-Date: 2014-08-09 03:47+0100\n" +"Last-Translator: Harrie Klomp \n" "Language-Team: \n" "Language: nl_NL\n" "MIME-Version: 1.0\n" @@ -13,50 +13,96 @@ msgstr "" "X-Poedit-KeywordsList: _;gettext;gettext_noop\n" "X-Poedit-Basepath: D:\\PRIV\\Projekty\\EclipseWorkspace\\opencaching-api\\\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -"X-Generator: Poedit 1.5.5\n" +"X-Generator: Poedit 1.5.4\n" "X-Poedit-SearchPath-0: .\n" # For additional waypoints. As in "Stage 1: Parking". -#: okapi/services/caches/geocaches.php:846 +#: okapi/services/caches/geocaches.php:957 msgid "Stage" 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 msgid "" "This geocache description comes from the %s site." msgstr "" +"Deze geocache beschrijving komt van de %s " +"\"\"site." -#: okapi/services/caches/geocaches.php:1021 -#, php-format +#: okapi/services/caches/geocaches.php:1312 +#, fuzzy, php-format msgid "" "© %s, %s, CC-BY-NC-ND, as of " "%s; all log entries © their authors" msgstr "" +"© %s, %s, CC-BY-NC-ND, as of " +"%s; all log entries © their authors" -#: okapi/services/caches/geocaches.php:1032 -#, php-format +#: okapi/services/caches/geocaches.php:1323 +#, fuzzy, php-format msgid "" "© %s, %s, CC-BY-NC-ND; all " "log entries © their authors" msgstr "" +"© %s, %s, CC-BY-NC-ND; all " +"log entries © their authors" -#: okapi/services/caches/formatters/gpxfile.tpl.php:31 -#: okapi/services/caches/formatters/gpxfile.tpl.php:48 +#: okapi/services/caches/formatters/gpx.php:360 +msgid "" +"Geocache coordinates have been changed. They have been replaced with " +"your own custom coordinates which you have provided for this geocache." +msgstr "" +"Geocache coördinaten zijn veranderd.
Deze zijn aangepast door " +"coördinaten die je zelf hebt gewijzigd voor deze geocache." + +#: okapi/services/caches/formatters/gpx.php:366 +msgid "" +"Geocache coordinates have been changed. Currently they point to one " +"of the alternate waypoints originally described as:" +msgstr "" +"Geocache coördinaten zijn veranderd.
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" msgstr "geplaatst door" -#: okapi/services/caches/formatters/gpxfile.tpl.php:50 +#: okapi/services/caches/formatters/gpxfile.tpl.php:64 #, php-format msgid "%d recommendation" msgid_plural "%d recommendations" msgstr[0] "%d aanbeveling" msgstr[1] "%d aanbevelingen" -#: okapi/services/caches/formatters/gpxfile.tpl.php:51 +#: okapi/services/caches/formatters/gpxfile.tpl.php:65 #, php-format msgid "found %d time" msgid_plural "found %d times" @@ -64,37 +110,42 @@ msgstr[0] "%d keer gevonden" msgstr[1] "%d keren gevonden" # Generic term for trackable items (like geocoins and travelbugs). -#: okapi/services/caches/formatters/gpxfile.tpl.php:54 +#: okapi/services/caches/formatters/gpxfile.tpl.php:68 #, php-format msgid "%d trackable" msgid_plural "%d trackables" msgstr[0] "%d trackable" msgstr[1] "%d trackables" -#: okapi/services/caches/formatters/gpxfile.tpl.php:58 +#: okapi/services/caches/formatters/gpxfile.tpl.php:72 msgid "Personal notes" msgstr "Persoonlijke notities" -#: okapi/services/caches/formatters/gpxfile.tpl.php:62 +#: okapi/services/caches/formatters/gpxfile.tpl.php:76 msgid "Attributes" msgstr "Attributen" -#: okapi/services/caches/formatters/gpxfile.tpl.php:66 +#: okapi/services/caches/formatters/gpxfile.tpl.php:80 msgid "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" msgstr "Afbeeldingen" -#: okapi/services/caches/formatters/gpxfile.tpl.php:91 +#: okapi/services/caches/formatters/gpxfile.tpl.php:113 msgid "Spoilers" msgstr "Spoilers" -#: okapi/services/caches/formatters/gpxfile.tpl.php:99 +#: okapi/services/caches/formatters/gpxfile.tpl.php:122 msgid "Image descriptions" 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 msgid "" "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 " "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 msgid "" "However, your \"needs maintenance\" flag was ignored, because %s does not " "support this feature." 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. -#: okapi/services/logs/submit.php:131 +# Missing "you can attend it" +#: okapi/services/logs/submit.php:145 msgid "" -"This cache is an Event cache. You cannot \"Find it\"! (But - you may " -"\"Comment\" on it.)" +"This cache is an Event cache. You cannot \"Find\" it (but you can attend it, " +"or comment on it)!" msgstr "" -"Dit is een eventcache. Deze kan niet als \"Gevonden\" gelogd worden. Maar " -"wel als \"Notitie\"." +"Dit is een evenement. Deze kan niet als \"Gevonden\" gelogd worden. (maar " +"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." 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!" 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!" msgstr "Verkeerd wachtwoord!" # This error will be shown to the user when he tries to submit a log entry which is EXACTLY the same as one he had submitted before. -#: okapi/services/logs/submit.php:260 +#: okapi/services/logs/submit.php:285 msgid "You have already submitted a log entry with exactly the same contents." msgstr "Deze log is reeds met dezelfde tekst verzonden." -#: okapi/services/logs/submit.php:279 +#: okapi/services/logs/submit.php:308 msgid "" "You have already submitted a \"Found it\" log entry once. Now you may submit " "\"Comments\" only!" @@ -153,25 +223,29 @@ msgstr "" "Je hebt deze cache al als \"Gevonden\" gelogd. Je kunt nu wel een \"Notitie" "\" plaatsen." -# The English text was changed from "you cannot rate it" to "you cannot find it". The translation remained. -#: okapi/services/logs/submit.php:281 +#: okapi/services/logs/submit.php:310 msgid "You are the owner of this cache. You may submit \"Comments\" only!" 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." msgstr "" "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." -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!" -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." msgstr "De log is succesvol verzonden." @@ -208,35 +282,33 @@ msgstr "Toestemmen" msgid "Decline" 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. # Sample: http://i.imgur.com/ZCJNT.png #: okapi/views/apps/authorize.tpl.php:56 #, php-format msgid "" "\n" -"\t\t\t\t\t

Once permission is granted it is valid until its withdrawal on\n" -"\t\t\t\t\tthe applications management page.

\n" -"\t\t\t\t\t

The application will access your acount via the " -"OKAPI Framework.\n" -"\t\t\t\t\tIf you allow this request application will be able to access all " -"methods delivered\n" -"\t\t\t\t\tby the OKAPI Framework, i.e. post log entries on geocaches in your " -"name.\n" -"\t\t\t\t\tYou can revoke this permission at any moment.

\n" -"\t\t\t\t" +"

Once permission is granted it is valid until its " +"withdrawal on\n" +" the applications management page.

\n" +"

The application will access your acount via the OKAPI Framework.\n" +" If you allow this request application will be able to " +"access all methods delivered\n" +" by the OKAPI Framework, i.e. post log entries on " +"geocaches in your name.\n" +" You can revoke this permission at any moment.

\n" +" " msgstr "" "\n" -"\t\t\t\t\t

Wanneer toegestemd is blijft deze geldig tot intrekking op\n" -"\t\t\t\t\tde toepassingsbeheer pagina.

\n" -"\t\t\t\t\t

De toepassing zal toegang krijgen via jouw account op the OKAPI Framework.\n" -"\t\t\t\t\tWanneer je toestemming geeft voor deze toepassing zullen de " -"mogelijkheden\n" -"\t\t\t\t\tvan OKAPI Framework toegepast worden, b.v. het loggen van een " -"cache.\n" -"\t\t\t\t\tDe toestemming kan elk moment ingetrokken worden.

\n" -"\t\t\t\t" +"

Wanneer toegestemd is blijft deze geldig tot intrekking op\n" +"de toepassingsbeheer pagina.

\n" +"

De toepassing zal toegang krijgen via jouw account op the " +"OKAPI Framework.\n" +"Wanneer je toestemming geeft voor deze toepassing zullen de mogelijkheden\n" +"van OKAPI Framework toegepast worden, b.v. het loggen van een cache.\n" +"De toestemming kan elk moment ingetrokken worden.

" #: okapi/views/apps/authorized.tpl.php:5 msgid "Authorization Succeeded" @@ -251,18 +323,16 @@ msgstr "Met succes toegang verleend" #, php-format msgid "" "\n" -"\t\t\t\t

You've just granted %s application access to your %s account.\n" -"\t\t\t\tTo complete the operation, go back to %s and enter the following PIN " -"code:

\n" -"\t\t\t" +"

You've just granted %s application access to your %s " +"account.\n" +" To complete the operation, go back to %s and enter the " +"following PIN code:

\n" +" " msgstr "" "\n" -"\t\t\t\t

Je hebt toegang verleent voor %s toepassing op jouw %s account." -"\n" -"\t\t\t\tOm de aktie te voltooien, ga terug naar %s en gebruik de volgende " -"PIN code:

\n" -"\t\t\t" +"

Je hebt toegang verleent voor %s toepassing op jouw %s account.\n" +"Om de actie te voltooien, ga terug naar %s en gebruik de volgende PIN code:" #: okapi/views/apps/index.tpl.php:5 msgid "My Apps" @@ -273,29 +343,25 @@ msgid "Your external applications" msgstr "Jouw externe toepassingen" # This will be shown when user visits /okapi/apps page. -# Sample: http://i.imgur.com/ZCJNT.png #: okapi/views/apps/index.tpl.php:31 #, php-format msgid "" "\n" -"\t\t\t\t\t

This is the list of applications which you granted access to " -"your %s account.\n" -"\t\t\t\t\tThis page gives you the abbility to revoke all previously granted " -"privileges.\n" -"\t\t\t\t\tOnce you click \"remove\" the application will no longer be able " -"to perform any\n" -"\t\t\t\t\tactions on your behalf.

\n" -"\t\t\t\t" +"

This is the list of applications which you granted " +"access to your %s account.\n" +" This page gives you the abbility to revoke all " +"previously granted privileges.\n" +" Once you click \"remove\" the application will no longer " +"be able to perform any\n" +" actions on your behalf.

\n" +" " msgstr "" "\n" -"\t\t\t\t\t

Dit is een lijst met toegestane toepassingen op jouw %s " -"account.\n" -"\t\t\t\t\tOp deze pagina kun je alle toestemmingen intrekken die gegeven " -"zijn.\n" -"\t\t\t\t\tMet een klik op \"verwijderen\" zal de toepassing verwijderen en " -"is dan ook niet meer beschikbaar\n" -"\t\t\t\t\voor anderen.

\n" -"\t\t\t\t" +"

Dit is een lijst met toegestane toepassingen op jouw %s account.\n" +"Op deze pagina kun je alle toestemmingen intrekken die gegeven zijn.\n" +"Met een klik op \"verwijderen\" zal de toepassing verwijderen en is dan ook " +"niet meer beschikbaar\n" +"oor anderen.

" #: okapi/views/apps/index.tpl.php:45 msgid "remove" @@ -305,22 +371,21 @@ msgstr "verwijderen" #, php-format msgid "" "\n" -"\t\t\t\t\t

Thanks to the OKAPI Framework you can grant " -"external applications\n" -"\t\t\t\t\taccess to your %s account. Currently no applications are " -"authorized to act\n" -"\t\t\t\t\ton your behalf. Once you start using external Opencaching " -"applications, they will appear here.

\n" -"\t\t\t\t" +"

Thanks to the OKAPI Framework you " +"can grant external applications\n" +" access to your %s account. Currently no " +"applications are authorized to act\n" +" on your behalf. Once you start using external " +"Opencaching applications, they will appear here.

\n" +" " msgstr "" "\n" -"\t\t\t\t\t

Dankzij het OKAPI Framework kun je toegang " -"verlenen via externe\n" -"\t\t\t\t\ttoepassingen op een %s account. Momenteel zijn op dit " -"account nog geen externe\n" -"\t\t\t\t\ttoepassingen actief. Geactiveerde Opencaching toepassingen " -"zullen hier getoond worden.

\n" -"\t\t\t\t" +"

Dankzij het OKAPI Framework kun je toegang verlenen via " +"externe\n" +"toepassingen op een %s account. Momenteel zijn op dit account nog " +"geen externe\n" +"toepassingen actief. Geactiveerde Opencaching toepassingen zullen hier " +"getoond worden.

" #~ msgid "" #~ "This cache is archived. Only admins and the owner are allowed to add a " diff --git a/htdocs/okapi/locale/pl_PL/LC_MESSAGES/okapi_messages.mo b/htdocs/okapi/locale/pl_PL/LC_MESSAGES/okapi_messages.mo index d9d3dabc0799d5d9a89e1fde5776871035f94f47..3d96d8e18074b9db9b6d4d68c9ce4674f600d804 100644 GIT binary patch delta 2695 zcmbW1TWnNS6owbm%M_#nr6S11El_Bo7us@bsTC!1Nu-rarKp&k&Kx?YGo3S@bEe}= z#3m@l7?i{jK_xK+uMrbCs_k4Ty zW$m@rzosWPy|!)eyNcq&f~{rDV_cXd(g}|h@qwK$7O4t|d;`1LyId;rAnYy^8Djs_ z$s&v4p(!Fa!S~?%@GLwDAHPl{1e>RdJOYovXW*=Ik+~v+@&*&m$}p^eU&0&V4=@P- zgiB%3^&->Ya##!3!96enx4}3iv13WUqGf+$S2Sg32ydj_Q0@#amRqzs$ zJw;h(!Gm)|{^HyToDCjlavUMF!wq=phAuo0KZjqySLU;(*B6L934ekwKM<@`kPFCX5j(2?G}*~d>hgyQoS&*`6{S2y%SOt z*$TBp1|$|qz;*Bt0c>ZB(e#XLtWXfMbv+Q$uSllf-`Al-3tey zj`S4V3NJv4B+IHqI^afl6ds1VVf_-3cK8Hb2G76*ya+eL5FM<0<0RC6euRxs2B||0 zY=Zj0{ZMEAEG&V?A(HuhQ3jVYwCM)ZM$h`DtDrMq$v_pE#aPSGrX|6RZ{#^~n}ie&wufzXR%*2HW!LuVXd=!CUV zJpX-Z&VTp2SWmSC+O)>n_<{KUWOQk@uJogCRM9srRXsyDF|8x389|0`Q&a(KOSE^! zL#E2^DsHVTu&=Vu6|X70ajv+l@J4OPj=~#{m24@zajv9k$;8K{V7%FBrS}x}(O0^? z@W#ibp?~YVA=q8m$H8Dn;f?Qu9or{HuD=^0&$8pLvg0akCmq#eMvU0r`nHBd`&D^e zooTkaZ1ePqZnH0!bxs|>nESnKa$vBnr+t%Yhm26fR3Y1T!dBeyOjkvW0h4FVxC%v# zc&{1e@t$_IA?Y|~+>51^7ctW+VOw!ec{YKX%HFFNl@aqyM>|m|Bkdm`569}oag2Lm z(+xRRj~P~m+baL{{hj*NE3r9dB4&i}m9o4@zEpOKkdv-w_x~fGRKyDTC-S!wjy+(7 z&G1aM*LDcoa#in08R~PFrYsNK-8mAns>Y+~)Vaw-B4(N48Wp2lTAtB_tFGLYv0J0w z#!ly|%YU0x-sPIkm8IF!UJW$t)&gH6>U?mf8uvu@qZtEJSJtg)tZQmkjjLK$W^b#Q zS3Z8BB^Rl9cT!Q;&W+jmm2-+#*Ei={Dw9E7>RoQ)bZ&URlNowGjACxo$)w{^OJ!`= zGluu4Ri@uE3zLQ6MlWRiVD2YjjHhUzoJG)G2-?XclR?NplbjtYJ{ zm#Rhcw6D3oZ6_>u(1@G8w(2vYcB}l4SneH*(Qv(?0|_0a+L^MgeoNV|-DA04nrUi$ ziY(NssCPQ&m}+SX(Njh^V-4>&hhJ3W;Cha&!}H9(v0BvU-kSSZV9#jqI`hIcLzC(R z$rDAw$(R{sBt6?PYUqKH#IJcb*RbH$z}yL9B!@pt>q5q@sF91RUjj8G*GKL0xn0v4 L&5_-;=&rv3vDPs> delta 1396 zcmY+@OKeP07{Kwbw=9wo{ckMWMQ;}x&8Q>-*%Ln1{c zLP8hSh)5a>H?bkCEm$BzDr}ID8X}R}c>KTH+u)?<{=WO&&OPUR=iHmg{zUhi62~Q> zHPZd`fdY}GxS)^=ZKXrxA-NW;Mo*DQ5Q8p}ex9e?BGYjgC!^mZau;JbfO%}gt6q^Q z_#63?DPu%RM7pJpU=oRW$U7y8GjR)!$5S{5FX9e-fJyX^6=}!AsK0xLyif*_IZ7TU z;CK7E%O_IF{dgRQL3A^|G!r;UEXGBcz!aXr-S`@H;D!7uhSyM6@EqsiTU>;d%(e%& zqE_%DuE0<+9kaL%ucPLFX`bdv?LEVZ6NcCh8bw%%SuOTvozu1Ber6T2+ zDi`_5^8=Kz5BF7!yvNIO)7;1LCFbxk9%Qgi&9fifI`C}*woe|RZpBm7{3ko_WHxc` z$06^K7OclM%-}iPiQn)zCZ~!}b$NsoL0+J4(FdG}d8BA!R8jvbf(cxxhJ;ZkvKTpU zvI-Np7PVxT@fg0r6S%QjB#KW^C*}x9s&Zo7ALTsRqwlKkL`V6{$F%&`S z8Q&^7SOcosv`kt$ZB*52dKPptdKT)e%-@TyMkl6CE2m$Awg6o#qX$qMN5=Zp^$XLs znM#U}K8MbtB}gx$bH;>oW$hEy6)I*m-6q{j7H)k7hLs(;=;0NwM_VrMSmGRsmN{c~ zbl$nqj(#{(cC@Rg#g6V5HIC6=jJ~1NwaI>R(zV`>-nrWT`=3MZwEg6jyVZ{7dq$&P zkKdZj$QyHmo|O(AAlKwwTZm?l*JGHse6tL5dhyo+v$o`hksB`U8ru{K$3l@vAlw{p z%$+Fn`~FEZMa{mlI|bEU**&3VZ#tC79L{v?&K~KA2i6_QwC~RbA`Q`osM#^8%Vqik Urw#K*rF=NQcZLeSb2eh4\n" "Language-Team: \n" "Language: pl_PL\n" @@ -15,26 +15,35 @@ msgstr "" "\\okapi\n" "Plural-Forms: nplurals=3; plural= n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " "|| n%100>=20) ? 1 : 2;\n" -"X-Poedit-SourceCharset: utf-8\n" -"X-Generator: Poedit 1.5.5\n" +"X-Poedit-SourceCharset: UTF-8\n" +"X-Generator: Poedit 1.6.3\n" "X-Poedit-SearchPath-0: .\n" -#: services/caches/geocaches.php:956 +#: services/caches/geocaches.php:957 msgid "Stage" 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" msgstr "Park narodowy lub krajobrazowy" -#: services/caches/geocaches.php:1263 +#: services/caches/geocaches.php:1300 #, php-format msgid "" "This geocache description comes from the %s site." msgstr "Opis skrzynki pochodzi z serwisu %s." -#: services/caches/geocaches.php:1275 +#: services/caches/geocaches.php:1312 #, php-format msgid "" "© %s, %s, CC-BY-NC-ND, 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 msgid "" "© %s, %s, CC-BY-NC-ND. Prawa " "autorskie wpisów do logów należą do ich autorów." -#: services/caches/formatters/gpxfile.tpl.php:31 -#: services/caches/formatters/gpxfile.tpl.php:60 +#: services/caches/formatters/gpx.php:360 +msgid "" +"Geocache coordinates have been changed. They have been replaced with " +"your own custom coordinates which you have provided for this geocache." +msgstr "" +"WspółrzÄ™dne skrzynki zostaÅ‚y zmienione. ZostaÅ‚y zastÄ…pione Twoimi " +"osobistymi współrzÄ™dnymi, które wprowadziÅ‚eÅ› na stronie tej skrzynki." + +#: services/caches/formatters/gpx.php:366 +msgid "" +"Geocache coordinates have been changed. Currently they point to one " +"of the alternate waypoints originally described as:" +msgstr "" +"WspółrzÄ™dne skrzynki zostaÅ‚y zmienione. 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" msgstr "ukryta przez" -#: services/caches/formatters/gpxfile.tpl.php:62 +#: services/caches/formatters/gpxfile.tpl.php:64 #, php-format msgid "%d recommendation" msgid_plural "%d recommendations" @@ -69,7 +103,7 @@ msgstr[0] "%d rekomendacja" msgstr[1] "%d rekomendacje" msgstr[2] "%d rekomendacji" -#: services/caches/formatters/gpxfile.tpl.php:63 +#: services/caches/formatters/gpxfile.tpl.php:65 #, php-format msgid "found %d time" msgid_plural "found %d times" @@ -77,7 +111,7 @@ msgstr[0] "znaleziona %d raz" msgstr[1] "znaleziona %d razy" msgstr[2] "znaleziona %d razy" -#: services/caches/formatters/gpxfile.tpl.php:66 +#: services/caches/formatters/gpxfile.tpl.php:68 #, php-format msgid "%d trackable" msgid_plural "%d trackables" @@ -85,32 +119,32 @@ msgstr[0] "%d GeoKret (lub TravelBug)" msgstr[1] "%d GeoKrety (lub TravelBugi)" msgstr[2] "%d GeoKretów (lub TravelBugów)" -#: services/caches/formatters/gpxfile.tpl.php:70 +#: services/caches/formatters/gpxfile.tpl.php:72 msgid "Personal notes" msgstr "Osobiste notatki" -#: services/caches/formatters/gpxfile.tpl.php:74 +#: services/caches/formatters/gpxfile.tpl.php:76 msgid "Attributes" msgstr "Atrybuty" -#: services/caches/formatters/gpxfile.tpl.php:78 +#: services/caches/formatters/gpxfile.tpl.php:80 msgid "Trackables" msgstr "Geokrety, Travelbugi itp." -#: services/caches/formatters/gpxfile.tpl.php:88 -#: services/caches/formatters/gpxfile.tpl.php:104 +#: services/caches/formatters/gpxfile.tpl.php:90 +#: services/caches/formatters/gpxfile.tpl.php:106 msgid "Images" msgstr "Obrazki" -#: services/caches/formatters/gpxfile.tpl.php:111 +#: services/caches/formatters/gpxfile.tpl.php:113 msgid "Spoilers" msgstr "Spoilery" -#: services/caches/formatters/gpxfile.tpl.php:120 +#: services/caches/formatters/gpxfile.tpl.php:122 msgid "Image descriptions" 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:" 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!" 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." 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 "" "You have already submitted a \"Found it\" log entry once. Now you may submit " "\"Comments\" only!" @@ -189,30 +223,30 @@ msgstr "" "Już opublikowaÅ‚eÅ› jeden wpis typu \"Znaleziona\" dla tej skrzynki. Teraz " "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!" msgstr "" "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." 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." 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!" msgstr "" "Aktualnie nie możesz wystawić kolejnej rekomendacji. Znajdź najpierw wiÄ™cej " "skrzynek!" -#: services/logs/submit.php:395 +#: services/logs/submit.php:398 msgid "Event caches cannot \"need maintenance\"." 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." msgstr "Twój wpis do logbooka zostaÅ‚ opublikowany pomyÅ›lnie." @@ -253,16 +287,17 @@ msgstr "Odmawiam" #, php-format msgid "" "\n" -"\t\t\t\t\t

Once permission is granted it is valid until its withdrawal on\n" -"\t\t\t\t\tthe applications management page.

\n" -"\t\t\t\t\t

The application will access your acount via the " -"OKAPI Framework.\n" -"\t\t\t\t\tIf you allow this request application will be able to access all " -"methods delivered\n" -"\t\t\t\t\tby the OKAPI Framework, i.e. post log entries on geocaches in your " -"name.\n" -"\t\t\t\t\tYou can revoke this permission at any moment.

\n" -"\t\t\t\t" +"

Once permission is granted it is valid until its " +"withdrawal on\n" +" the applications management page.

\n" +"

The application will access your acount via the OKAPI Framework.\n" +" If you allow this request application will be able to " +"access all methods delivered\n" +" by the OKAPI Framework, i.e. post log entries on " +"geocaches in your name.\n" +" You can revoke this permission at any moment.

\n" +" " msgstr "" "\n" "

Raz udzielona zgoda jest ważna aż do momentu jej wycofania na stronie You've just granted %s application access to your %s account.\n" -"\t\t\t\tTo complete the operation, go back to %s and enter the following PIN " -"code:

\n" -"\t\t\t" +"

You've just granted %s application access to your %s " +"account.\n" +" To complete the operation, go back to %s and enter the " +"following PIN code:

\n" +" " msgstr "" "\n" "

Właśnie dałeś dostęp aplikacji %s do Twojego konta %s.\n" @@ -308,14 +343,14 @@ msgstr "Twoje zewnętrzne aplikacje" #, php-format msgid "" "\n" -"\t\t\t\t\t

This is the list of applications which you granted access to " -"your %s account.\n" -"\t\t\t\t\tThis page gives you the abbility to revoke all previously granted " -"privileges.\n" -"\t\t\t\t\tOnce you click \"remove\" the application will no longer be able " -"to perform any\n" -"\t\t\t\t\tactions on your behalf.

\n" -"\t\t\t\t" +"

This is the list of applications which you granted " +"access to your %s account.\n" +" This page gives you the abbility to revoke all " +"previously granted privileges.\n" +" Once you click \"remove\" the application will no longer " +"be able to perform any\n" +" actions on your behalf.

\n" +" " msgstr "" "\n" "

Następującym aplikacjom zezwoliłeś na dostęp do swojego konta %s.\n" @@ -332,13 +367,13 @@ msgstr "usuń" #, php-format msgid "" "\n" -"\t\t\t\t\t

Thanks to the OKAPI Framework you can grant " -"external applications\n" -"\t\t\t\t\taccess to your %s account. Currently no applications are " -"authorized to act\n" -"\t\t\t\t\ton your behalf. Once you start using external Opencaching " -"applications, they will appear here.

\n" -"\t\t\t\t" +"

Thanks to the OKAPI Framework you " +"can grant external applications\n" +" access to your %s account. Currently no " +"applications are authorized to act\n" +" on your behalf. Once you start using external " +"Opencaching applications, they will appear here.

\n" +" " msgstr "" "\n" "

Dzięki platformie OKAPI możesz dawać zewnętrznym " @@ -348,6 +383,30 @@ msgstr "" "w Twoim imieniu. Gdy zaczniesz korzystać z zewnętrznych aplikacji, ich lista " "pojawi się tutaj.

" +#~ msgid "" +#~ "\n" +#~ "\t\t\t\t\t\t\t\tWarning: Changed coordinates. 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" +#~ "Uwaga: Zmienione współrzędne. 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 "" +#~ "Geocache's coordinates has been changed to point to the user " +#~ "supplied value." +#~ msgstr "" +#~ "Współrzędne skrzynki zostały zmienione 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." #~ msgstr "Rekomendacje są dozwolone jedynie z wpisem \"Znaleziona\"." diff --git a/htdocs/okapi/oauth.php b/htdocs/okapi/oauth.php index d499eb7e..33f6dfc0 100644 --- a/htdocs/okapi/oauth.php +++ b/htdocs/okapi/oauth.php @@ -839,6 +839,9 @@ class OAuthServer { if( ! $timestamp ) throw new OAuthMissingParameterException('oauth_timestamp'); + // Cast to integer. See issue #314. + $timestamp = $timestamp + 0; + // verify that timestamp is recentish $now = time(); if (abs($now - $timestamp) > $this->timestamp_threshold) { diff --git a/htdocs/okapi/service_runner.php b/htdocs/okapi/service_runner.php index 9be3b6de..2d31b186 100644 --- a/htdocs/okapi/service_runner.php +++ b/htdocs/okapi/service_runner.php @@ -6,186 +6,187 @@ use Exception; class OkapiServiceRunner { - # - # This the list of all available OKAPI methods. All methods on this list become - # immediately public and all of them have to be documented. It is not possible - # to create an invisible or undocumented OKAPI method. If you want to test your - # methods, you should do it in your local development server. If you want to - # create a private, "internal" method, you still have to document it properly - # (you may describe it as "internal" and accessible to selected consumer keys only). - # - public static $all_names = array( - # Valid format: ^services/[0-9a-z_/]*$ (it means you may use only alphanumeric - # characters and the "_" sign in your method names). - 'services/apisrv/installation', - 'services/apisrv/installations', - 'services/apisrv/stats', - 'services/apiref/method', - 'services/apiref/method_index', - 'services/apiref/issue', - 'services/attrs/attribute_index', - 'services/attrs/attribute', - 'services/attrs/attributes', - 'services/oauth/request_token', - 'services/oauth/authorize', - 'services/oauth/access_token', - 'services/caches/search/all', - 'services/caches/search/bbox', - 'services/caches/search/nearest', - 'services/caches/search/by_urls', - 'services/caches/search/save', - 'services/caches/shortcuts/search_and_retrieve', - 'services/caches/geocache', - 'services/caches/geocaches', - 'services/caches/mark', - 'services/caches/formatters/gpx', - 'services/caches/formatters/garmin', - 'services/caches/map/tile', - 'services/logs/entries', - 'services/logs/entry', - 'services/logs/logs', - 'services/logs/userlogs', - 'services/logs/submit', - 'services/users/user', - 'services/users/users', - 'services/users/by_usernames', - 'services/users/by_username', - 'services/users/by_internal_id', - 'services/users/by_internal_ids', - 'services/replicate/changelog', - 'services/replicate/fulldump', - 'services/replicate/info', - ); + # + # This the list of all available OKAPI methods. All methods on this list become + # immediately public and all of them have to be documented. It is not possible + # to create an invisible or undocumented OKAPI method. If you want to test your + # methods, you should do it in your local development server. If you want to + # create a private, "internal" method, you still have to document it properly + # (you may describe it as "internal" and accessible to selected consumer keys only). + # + public static $all_names = array( + # Valid format: ^services/[0-9a-z_/]*$ (it means you may use only alphanumeric + # characters and the "_" sign in your method names). + 'services/apisrv/installation', + 'services/apisrv/installations', + 'services/apisrv/stats', + 'services/apiref/method', + 'services/apiref/method_index', + 'services/apiref/issue', + 'services/attrs/attribute_index', + 'services/attrs/attribute', + 'services/attrs/attributes', + 'services/oauth/request_token', + 'services/oauth/authorize', + 'services/oauth/access_token', + 'services/caches/search/all', + 'services/caches/search/bbox', + 'services/caches/search/nearest', + 'services/caches/search/by_urls', + 'services/caches/search/save', + 'services/caches/shortcuts/search_and_retrieve', + 'services/caches/geocache', + 'services/caches/geocaches', + 'services/caches/mark', + 'services/caches/save_personal_notes', + 'services/caches/formatters/gpx', + 'services/caches/formatters/garmin', + 'services/caches/map/tile', + 'services/logs/entries', + 'services/logs/entry', + 'services/logs/logs', + 'services/logs/userlogs', + 'services/logs/submit', + 'services/users/user', + 'services/users/users', + 'services/users/by_usernames', + 'services/users/by_username', + 'services/users/by_internal_id', + 'services/users/by_internal_ids', + 'services/replicate/changelog', + 'services/replicate/fulldump', + 'services/replicate/info', + ); - /** Check if method exists. */ - public static function exists($service_name) - { - return in_array($service_name, self::$all_names); - } + /** Check if method exists. */ + public static function exists($service_name) + { + return in_array($service_name, self::$all_names); + } - /** Get method options (is consumer required etc.). */ - public static function options($service_name) - { - if (!self::exists($service_name)) - throw new Exception(); - require_once($GLOBALS['rootpath']."okapi/$service_name.php"); - try - { - return call_user_func(array('\\okapi\\'. - str_replace('/', '\\', $service_name).'\\WebService', 'options')); - } catch (Exception $e) - { - throw new Exception("Make sure you've declared your WebService class ". - "in an valid namespace (".'okapi\\'.str_replace('/', '\\', $service_name)."); ". - $e->getMessage()); - } - } + /** Get method options (is consumer required etc.). */ + public static function options($service_name) + { + if (!self::exists($service_name)) + throw new Exception(); + require_once($GLOBALS['rootpath']."okapi/$service_name.php"); + try + { + return call_user_func(array('\\okapi\\'. + str_replace('/', '\\', $service_name).'\\WebService', 'options')); + } catch (Exception $e) + { + throw new Exception("Make sure you've declared your WebService class ". + "in an valid namespace (".'okapi\\'.str_replace('/', '\\', $service_name)."); ". + $e->getMessage()); + } + } - /** - * Get method documentation file contents (stuff within the XML file). - * If you're looking for a parsed representation, use services/apiref/method. - */ - public static function docs($service_name) - { - if (!self::exists($service_name)) - throw new Exception(); - try { - return file_get_contents("$service_name.xml", true); - } catch (Exception $e) { - throw new Exception("Missing documentation file: $service_name.xml"); - } - } + /** + * Get method documentation file contents (stuff within the XML file). + * If you're looking for a parsed representation, use services/apiref/method. + */ + public static function docs($service_name) + { + if (!self::exists($service_name)) + throw new Exception(); + try { + return file_get_contents("$service_name.xml", true); + } catch (Exception $e) { + throw new Exception("Missing documentation file: $service_name.xml"); + } + } - /** - * Execute the method and return the result. - * - * OKAPI methods return OkapiHttpResponses, but some MAY also return - * PHP objects (see OkapiRequest::construct_inside_request for details). - * - * If $request must be consistent with given method's options (must - * include Consumer and Token, if they are required). - */ - public static function call($service_name, OkapiRequest $request) - { - Okapi::init_internals(); + /** + * Execute the method and return the result. + * + * OKAPI methods return OkapiHttpResponses, but some MAY also return + * PHP objects (see OkapiRequest::construct_inside_request for details). + * + * If $request must be consistent with given method's options (must + * include Consumer and Token, if they are required). + */ + public static function call($service_name, OkapiRequest $request) + { + Okapi::init_internals(); - if (!self::exists($service_name)) - throw new Exception("Method does not exist: '$service_name'"); + if (!self::exists($service_name)) + throw new Exception("Method does not exist: '$service_name'"); - $options = self::options($service_name); - if ($options['min_auth_level'] >= 2 && $request->consumer == null) - { - throw new Exception("Method '$service_name' called with mismatched OkapiRequest: ". - "\$request->consumer MAY NOT be empty for Level 2 and Level 3 methods. Provide ". - "a dummy Consumer if you have to."); - } - if ($options['min_auth_level'] >= 3 && $request->token == null) - { - throw new Exception("Method '$service_name' called with mismatched OkapiRequest: ". - "\$request->token MAY NOT be empty for Level 3 methods."); - } + $options = self::options($service_name); + if ($options['min_auth_level'] >= 2 && $request->consumer == null) + { + throw new Exception("Method '$service_name' called with mismatched OkapiRequest: ". + "\$request->consumer MAY NOT be empty for Level 2 and Level 3 methods. Provide ". + "a dummy Consumer if you have to."); + } + if ($options['min_auth_level'] >= 3 && $request->token == null) + { + throw new Exception("Method '$service_name' called with mismatched OkapiRequest: ". + "\$request->token MAY NOT be empty for Level 3 methods."); + } - $time_started = microtime(true); - Okapi::gettext_domain_init(); - try - { - require_once($GLOBALS['rootpath']."okapi/$service_name.php"); - $response = call_user_func(array('\\okapi\\'. - str_replace('/', '\\', $service_name).'\\WebService', 'call'), $request); - Okapi::gettext_domain_restore(); - } catch (Exception $e) { - Okapi::gettext_domain_restore(); - throw $e; - } - $runtime = microtime(true) - $time_started; + $time_started = microtime(true); + Okapi::gettext_domain_init(); + try + { + require_once($GLOBALS['rootpath']."okapi/$service_name.php"); + $response = call_user_func(array('\\okapi\\'. + str_replace('/', '\\', $service_name).'\\WebService', 'call'), $request); + Okapi::gettext_domain_restore(); + } catch (Exception $e) { + Okapi::gettext_domain_restore(); + throw $e; + } + $runtime = microtime(true) - $time_started; - # Log the request to the stats table. Only valid requests (these which didn't end up - # with an exception) are logged. - self::save_stats($service_name, $request, $runtime); + # Log the request to the stats table. Only valid requests (these which didn't end up + # with an exception) are logged. + self::save_stats($service_name, $request, $runtime); - return $response; - } + return $response; + } - /** - * For internal use only. The stats table can be used to store any kind of - * runtime-stats data, i.e. not only regarding services. This is a special - * version of save_stats which saves runtime stats under the name of $extra_name. - * Note, that $request can be null. - */ - public static function save_stats_extra($extra_name, $request, $runtime) - { - self::save_stats("extra/".$extra_name, $request, $runtime); - } + /** + * For internal use only. The stats table can be used to store any kind of + * runtime-stats data, i.e. not only regarding services. This is a special + * version of save_stats which saves runtime stats under the name of $extra_name. + * Note, that $request can be null. + */ + public static function save_stats_extra($extra_name, $request, $runtime) + { + self::save_stats("extra/".$extra_name, $request, $runtime); + } - private static function save_stats($service_name, $request, $runtime) - { - # Getting rid of nulls. MySQL PRIMARY keys cannot contain nullable columns. - # Temp table doesn't have primary key, but other stats tables (which are - # dependant on stats table) - do. + private static function save_stats($service_name, $request, $runtime) + { + # Getting rid of nulls. MySQL PRIMARY keys cannot contain nullable columns. + # Temp table doesn't have primary key, but other stats tables (which are + # dependant on stats table) - do. - if ($request !== null) { - $consumer_key = ($request->consumer != null) ? $request->consumer->key : 'anonymous'; - $user_id = (($request->token != null) && ($request->token instanceof OkapiAccessToken)) ? $request->token->user_id : -1; - if ($request->is_http_request() && ($service_name[0] == 's')) # 's' for "services/", we don't want "extra/" included - $calltype = 'http'; - else - $calltype = 'internal'; - } else { - $consumer_key = 'internal'; - $user_id = -1; - $calltype = 'internal'; - } + if ($request !== null) { + $consumer_key = ($request->consumer != null) ? $request->consumer->key : 'anonymous'; + $user_id = (($request->token != null) && ($request->token instanceof OkapiAccessToken)) ? $request->token->user_id : -1; + if ($request->is_http_request() && ($service_name[0] == 's')) # 's' for "services/", we don't want "extra/" included + $calltype = 'http'; + else + $calltype = 'internal'; + } else { + $consumer_key = 'internal'; + $user_id = -1; + $calltype = 'internal'; + } - Db::execute(" - insert into okapi_stats_temp (`datetime`, consumer_key, user_id, service_name, calltype, runtime) - values ( - now(), - '".mysql_real_escape_string($consumer_key)."', - '".mysql_real_escape_string($user_id)."', - '".mysql_real_escape_string($service_name)."', - '".mysql_real_escape_string($calltype)."', - '".mysql_real_escape_string($runtime)."' - ); - "); - } + Db::execute(" + insert into okapi_stats_temp (`datetime`, consumer_key, user_id, service_name, calltype, runtime) + values ( + now(), + '".mysql_real_escape_string($consumer_key)."', + '".mysql_real_escape_string($user_id)."', + '".mysql_real_escape_string($service_name)."', + '".mysql_real_escape_string($calltype)."', + '".mysql_real_escape_string($runtime)."' + ); + "); + } } \ No newline at end of file diff --git a/htdocs/okapi/services/apiref/issue.php b/htdocs/okapi/services/apiref/issue.php index 300d8be2..bc3dee6b 100644 --- a/htdocs/okapi/services/apiref/issue.php +++ b/htdocs/okapi/services/apiref/issue.php @@ -15,32 +15,32 @@ use okapi\Cache; class WebService { - public static function options() - { - return array( - 'min_auth_level' => 0 - ); - } + public static function options() + { + return array( + 'min_auth_level' => 0 + ); + } - public static function call(OkapiRequest $request) - { - $issue_id = $request->get_parameter('issue_id'); - if (!$issue_id) - throw new ParamMissing('issue_id'); - if ((!preg_match("/^[0-9]+$/", $issue_id)) || (strlen($issue_id) > 6)) - throw new InvalidParam('issue_id'); + public static function call(OkapiRequest $request) + { + $issue_id = $request->get_parameter('issue_id'); + if (!$issue_id) + throw new ParamMissing('issue_id'); + if ((!preg_match("/^[0-9]+$/", $issue_id)) || (strlen($issue_id) > 6)) + throw new InvalidParam('issue_id'); - # In October 2013, Google Code feed at: - # 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. - - $result = array( - 'id' => $issue_id + 0, - 'last_updated' => null, - 'title' => null, - 'url' => "https://code.google.com/p/opencaching-api/issues/detail?id=".$issue_id, - 'comment_count' => null - ); - return Okapi::formatted_response($request, $result); - } + # In October 2013, Google Code feed at: + # 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. + + $result = array( + 'id' => $issue_id + 0, + 'last_updated' => null, + 'title' => null, + 'url' => "https://code.google.com/p/opencaching-api/issues/detail?id=".$issue_id, + 'comment_count' => null + ); + return Okapi::formatted_response($request, $result); + } } diff --git a/htdocs/okapi/services/apiref/issue.xml b/htdocs/okapi/services/apiref/issue.xml index 268ec6f9..c5ffc02c 100644 --- a/htdocs/okapi/services/apiref/issue.xml +++ b/htdocs/okapi/services/apiref/issue.xml @@ -1,33 +1,33 @@ - Retrieve information on given issue - 11 - -

Important: This method stopped working properly in October 2013. - Now, it returns a simple placeholder. - Read more.

- -

OKAPI is trying to be as integrated with its - Main Project Page as it can. - This method retrieves basic information on a given issue from our project - Issue Tracker. - In future, it might also return some of the latest comments (we're not yet - sure if we want them displayed on our documentation pages).

-
- - ID of an Issue. - - - -

A dictionary of the following structure:

-
    -
  • id - number of the issue created for this method,
  • -
  • last_updated - date and time (ISO 8601) when the issue was last updated - or null if unknown,
  • -
  • title - issue title or null if unknown,
  • -
  • url - URL of the issue page,
  • -
  • comment_count - total number of submitted comments or null if unknown.
  • -
-

Note, that this will respond with HTTP 400 if we fail to retrieve data from - the Google Code site.

-
+ Retrieve information on given issue + 11 + +

Important: This method stopped working properly in October 2013. + Now, it returns a simple placeholder. + Read more.

+ +

OKAPI is trying to be as integrated with its + Main Project Page as it can. + This method retrieves basic information on a given issue from our project + Issue Tracker. + In future, it might also return some of the latest comments (we're not yet + sure if we want them displayed on our documentation pages).

+
+ + ID of an Issue. + + + +

A dictionary of the following structure:

+
    +
  • id - number of the issue created for this method,
  • +
  • last_updated - date and time (ISO 8601) when the issue was last updated + or null if unknown,
  • +
  • title - issue title or null if unknown,
  • +
  • url - URL of the issue page,
  • +
  • comment_count - total number of submitted comments or null if unknown.
  • +
+

Note, that this will respond with HTTP 400 if we fail to retrieve data from + the Google Code site.

+
\ No newline at end of file diff --git a/htdocs/okapi/services/apiref/method.php b/htdocs/okapi/services/apiref/method.php index c351f998..4871cf8c 100644 --- a/htdocs/okapi/services/apiref/method.php +++ b/htdocs/okapi/services/apiref/method.php @@ -14,158 +14,216 @@ use okapi\OkapiInternalConsumer; class WebService { - public static function options() - { - return array( - 'min_auth_level' => 0 - ); - } + public static function options() + { + return array( + 'min_auth_level' => 0 + ); + } - private static function arg_desc($arg_node) - { - $attrs = $arg_node->attributes(); - return array( - 'name' => (string)$attrs['name'], - 'is_required' => $arg_node->getName() == 'req', - 'is_deprecated' => (isset($attrs['class']) && (strpos($attrs['class'], 'deprecated') !== false)), - 'class' => 'public', - 'description' => - (isset($attrs['default']) ? ("

Default value: ".$attrs['default']."

") : ""). - self::get_inner_xml($arg_node), + private static function arg_desc($arg_node) + { + $attrs = $arg_node->attributes(); + return array( + 'name' => (string)$attrs['name'], + 'is_required' => $arg_node->getName() == 'req', + 'is_deprecated' => (isset($attrs['class']) && (strpos($attrs['class'], 'deprecated') !== false)), + 'class' => 'public', + 'description' => + (isset($attrs['default']) ? ("

Default value: ".$attrs['default']."

") : ""). + self::get_inner_xml($arg_node), - ); - } + ); + } - private static function get_inner_xml($node) - { - /* Fetch as content, extract content. */ + private static function get_inner_xml($node) + { + /* Fetch as content, extract content. */ - $s = $node->asXML(); - $start = strpos($s, ">") + 1; - $length = strlen($s) - $start - (3 + strlen($node->getName())); - $s = substr($s, $start, $length); + $s = $node->asXML(); + $start = strpos($s, ">") + 1; + $length = strlen($s) - $start - (3 + strlen($node->getName())); + $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) - { - $input = $matches[1]; - $arr = explode(":", $input); - $plugin_name = $arr[0]; + /** + * You can use the following syntax: + * + * any text - to reference fragment of introducing + * documentation + * + * any text - to reference any other method + * + * any text - to reference + * any HTML anchor in other method + * + * any text - to reference any HTML + * anchor within current document + * + * any text - to + * reference argument of another method + * + * any text - to reference + * argument within current method + * + * any text - to + * reference returned value of another method + * + * any text - 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):
  • alt_wpts - list of alternate/additional + * waypoints and access it with (HTML snippet): any text. + */ + public static function plugin_callback($matches) + { + $input = $matches[1]; + $arr = explode(":", $input); + $plugin_name = $arr[0]; - switch ($plugin_name) { - case 'docurl': - $fragment = $arr[1]; - return Settings::get('SITE_URL')."okapi/introduction.html#".$fragment; - default: - throw new Exception("Unknown plugin: ".$input); - } - } + switch ($plugin_name) { + case 'docurl': + $fragment = $arr[1]; + return Settings::get('SITE_URL')."okapi/introduction.html#".$fragment; + case 'methodref': + 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) - { - $methodname = $request->get_parameter('name'); - if (!$methodname) - throw new ParamMissing('name'); - if (!preg_match("#^services/[0-9a-z_/]*$#", $methodname)) - throw new InvalidParam('name'); - if (!OkapiServiceRunner::exists($methodname)) - throw new InvalidParam('name', "Method does not exist: '$methodname'."); - $options = OkapiServiceRunner::options($methodname); - if (!isset($options['min_auth_level'])) - throw new Exception("Method $methodname is missing a required 'min_auth_level' option!"); - $docs = simplexml_load_string(OkapiServiceRunner::docs($methodname)); - $exploded = explode("/", $methodname); - $result = array( - 'name' => $methodname, - 'short_name' => end($exploded), - 'ref_url' => Settings::get('SITE_URL')."okapi/$methodname.html", - 'auth_options' => array( - 'min_auth_level' => $options['min_auth_level'], - 'oauth_consumer' => $options['min_auth_level'] >= 2, - 'oauth_token' => $options['min_auth_level'] >= 3, - ) - ); - if (!$docs->brief) - throw new Exception("Missing element in the $methodname.xml file."); - if ($docs->brief != self::get_inner_xml($docs->brief)) - throw new Exception("The element may not contain HTML markup ($methodname.xml)."); - if (strlen($docs->brief) > 80) - throw new Exception("The description may not be longer than 80 characters ($methodname.xml)."); - if (strpos($docs->brief, "\n") !== false) - throw new Exception("The element may not contain new-lines ($methodname.xml)."); - if (substr(trim($docs->brief), -1) == '.') - throw new Exception("The element should not end with a dot ($methodname.xml)."); - $result['brief_description'] = self::get_inner_xml($docs->brief); - if ($docs->{'issue-id'}) - $result['issue_id'] = (string)$docs->{'issue-id'}; - else - $result['issue_id'] = null; - if (!$docs->desc) - throw new Exception("Missing element in the $methodname.xml file."); - $result['description'] = self::get_inner_xml($docs->desc); - $result['arguments'] = array(); - foreach ($docs->req as $arg) { $result['arguments'][] = self::arg_desc($arg); } - foreach ($docs->opt as $arg) { $result['arguments'][] = self::arg_desc($arg); } - foreach ($docs->{'import-params'} as $import_desc) - { - $attrs = $import_desc->attributes(); - $referenced_methodname = $attrs['method']; - $referenced_method_info = OkapiServiceRunner::call('services/apiref/method', - new OkapiInternalRequest(new OkapiInternalConsumer(), null, array('name' => $referenced_methodname))); - $include_list = isset($attrs['params']) ? explode("|", $attrs['params']) : null; - $exclude_list = isset($attrs['except']) ? explode("|", $attrs['except']) : array(); - foreach ($referenced_method_info['arguments'] as $arg) - { - if ($arg['class'] == 'common-formatting') - continue; - if (($include_list === null) && (count($exclude_list) == 0)) - { - $arg['description'] = "Inherited from ".$referenced_method_info['name']." method."; - } - elseif ( - (($include_list === null) || in_array($arg['name'], $include_list)) - && (!in_array($arg['name'], $exclude_list)) - ) { - $arg['description'] = "Same as in the ".$referenced_method_info['name']." method."; - } else { - continue; - } - $arg['class'] = 'inherited'; - $result['arguments'][] = $arg; - } - } - if ($docs->{'common-format-params'}) - { - $result['arguments'][] = array( - 'name' => 'format', - 'is_required' => false, - 'is_deprecated' => false, - 'class' => 'common-formatting', - 'description' => "Standard common formatting argument." - ); - $result['arguments'][] = array( - 'name' => 'callback', - 'is_required' => false, - 'is_deprecated' => false, - 'class' => 'common-formatting', - 'description' => "Standard common formatting argument." - ); - } - foreach ($result['arguments'] as &$arg_ref) - if ($arg_ref['is_deprecated']) - $arg_ref['class'] .= " deprecated"; - if (!$docs->returns) - throw new Exception("Missing element in the $methodname.xml file. ". - "If your method does not return anything, you should document in nonetheless."); - $result['returns'] = self::get_inner_xml($docs->returns); - return Okapi::formatted_response($request, $result); - } + public static function call(OkapiRequest $request) + { + $methodname = $request->get_parameter('name'); + if (!$methodname) + throw new ParamMissing('name'); + if (!preg_match("#^services/[0-9a-z_/]*$#", $methodname)) + throw new InvalidParam('name'); + if (!OkapiServiceRunner::exists($methodname)) + throw new InvalidParam('name', "Method does not exist: '$methodname'."); + $options = OkapiServiceRunner::options($methodname); + if (!isset($options['min_auth_level'])) + throw new Exception("Method $methodname is missing a required 'min_auth_level' option!"); + $docs = simplexml_load_string(OkapiServiceRunner::docs($methodname)); + $exploded = explode("/", $methodname); + $result = array( + 'name' => $methodname, + 'short_name' => end($exploded), + 'ref_url' => Settings::get('SITE_URL')."okapi/$methodname.html", + 'auth_options' => array( + 'min_auth_level' => $options['min_auth_level'], + 'oauth_consumer' => $options['min_auth_level'] >= 2, + 'oauth_token' => $options['min_auth_level'] >= 3, + ) + ); + if (!$docs->brief) + throw new Exception("Missing element in the $methodname.xml file."); + if ($docs->brief != self::get_inner_xml($docs->brief)) + throw new Exception("The element may not contain HTML markup ($methodname.xml)."); + if (strlen($docs->brief) > 80) + throw new Exception("The description may not be longer than 80 characters ($methodname.xml)."); + if (strpos($docs->brief, "\n") !== false) + throw new Exception("The element may not contain new-lines ($methodname.xml)."); + if (substr(trim($docs->brief), -1) == '.') + throw new Exception("The element should not end with a dot ($methodname.xml)."); + $result['brief_description'] = self::get_inner_xml($docs->brief); + if ($docs->{'issue-id'}) + $result['issue_id'] = (string)$docs->{'issue-id'}; + else + $result['issue_id'] = null; + if (!$docs->desc) + throw new Exception("Missing element in the $methodname.xml file."); + $result['description'] = self::get_inner_xml($docs->desc); + $result['arguments'] = array(); + foreach ($docs->req as $arg) { $result['arguments'][] = self::arg_desc($arg); } + foreach ($docs->opt as $arg) { $result['arguments'][] = self::arg_desc($arg); } + foreach ($docs->{'import-params'} as $import_desc) + { + $attrs = $import_desc->attributes(); + $referenced_methodname = $attrs['method']; + $referenced_method_info = OkapiServiceRunner::call('services/apiref/method', + new OkapiInternalRequest(new OkapiInternalConsumer(), null, array('name' => $referenced_methodname))); + $include_list = isset($attrs['params']) ? explode("|", $attrs['params']) : null; + $exclude_list = isset($attrs['except']) ? explode("|", $attrs['except']) : array(); + foreach ($referenced_method_info['arguments'] as $arg) + { + if ($arg['class'] == 'common-formatting') + continue; + if (($include_list === null) && (count($exclude_list) == 0)) + { + $arg['description'] = "Inherited from ".$referenced_method_info['name']." method."; + } + elseif ( + (($include_list === null) || in_array($arg['name'], $include_list)) + && (!in_array($arg['name'], $exclude_list)) + ) { + $arg['description'] = "Same as in the ".$referenced_method_info['name']." method."; + } else { + continue; + } + $arg['class'] = 'inherited'; + $result['arguments'][] = $arg; + } + } + if ($docs->{'common-format-params'}) + { + $result['arguments'][] = array( + 'name' => 'format', + 'is_required' => false, + 'is_deprecated' => false, + 'class' => 'common-formatting', + 'description' => "Standard common formatting argument." + ); + $result['arguments'][] = array( + 'name' => 'callback', + 'is_required' => false, + 'is_deprecated' => false, + 'class' => 'common-formatting', + 'description' => "Standard common formatting argument." + ); + } + foreach ($result['arguments'] as &$arg_ref) + if ($arg_ref['is_deprecated']) + $arg_ref['class'] .= " deprecated"; + if (!$docs->returns) + throw new Exception("Missing element in the $methodname.xml file. ". + "If your method does not return anything, you should document in nonetheless."); + $result['returns'] = self::get_inner_xml($docs->returns); + return Okapi::formatted_response($request, $result); + } } diff --git a/htdocs/okapi/services/apiref/method.xml b/htdocs/okapi/services/apiref/method.xml index 9ce1ea88..dda42de0 100644 --- a/htdocs/okapi/services/apiref/method.xml +++ b/htdocs/okapi/services/apiref/method.xml @@ -1,55 +1,55 @@ - Get information on a given OKAPI service method - 13 - -

    This method allows you to access API documentation (the same which you - are reading just now). Given a method name, it returns a complete method description.

    - - - Name of a method (begins with "services/"). - - - -

    A dictionary of the following structure:

    -
      -
    • name - name of the method,
    • -
    • short_name - name without a path,
    • -
    • issue_id - ID of the "general discussion" issue (in our project's - homepage Issue Tracker) associated with this method or null if this - method has associated issue,
    • -
    • description - HTML-formatted description of what the method does,
    • -
    • brief_description - brief (max 80 characters), single-line, - plain-text description of what the method does,
    • -
    • ref_url - URL of the documentation page with method description,
    • -
    • - auth_options - a dictionary which describes authentication - requirements for this method, it has a following structure: -
        -
      • min_auth_level - integer, in range from 0 to 3, - see Introduction page.
      • -
      • oauth_consumer - true, if requests are required to be signed - with OAuth Consumer Key (min_auth_level >= 2),
      • -
      • oauth_token - true, if requests are required to include an - OAuth Token (min_auth_level == 3).
      • -
      -
    • -
    • arguments - list of dictionaries, describes method - arguments. Each dictionary has a following structure: -
        -
      • name - name of an argument,
      • -
      • is_required - boolean, true if the argument is required,
      • -
      • is_deprecated - boolean, true if the argument is deprecated,
      • -
      • description - HTML-formatted description of an argument.
      • -
      • -

        class - space separated list of the following values: public, - inherited, common-formatting and deprecated - (other values might be introduced in future).

        -

        Currently these values do not mean anything specific. They are - used for different coloring/styling in the documentation pages.

        -
      • -
      -
    • -
    • returns - HTML-formatted description method's return value.
    • -
    -
    + Get information on a given OKAPI service method + 13 + +

    This method allows you to access API documentation (the same which you + are reading just now). Given a method name, it returns a complete method description.

    +
    + + Name of a method (begins with "services/"). + + + +

    A dictionary of the following structure:

    +
      +
    • name - name of the method,
    • +
    • short_name - name without a path,
    • +
    • issue_id - ID of the "general discussion" issue (in our project's + homepage Issue Tracker) associated with this method or null if this + method has associated issue,
    • +
    • description - HTML-formatted description of what the method does,
    • +
    • brief_description - brief (max 80 characters), single-line, + plain-text description of what the method does,
    • +
    • ref_url - URL of the documentation page with method description,
    • +
    • + auth_options - a dictionary which describes authentication + requirements for this method, it has a following structure: +
        +
      • min_auth_level - integer, in range from 0 to 3, + see Introduction page.
      • +
      • oauth_consumer - true, if requests are required to be signed + with OAuth Consumer Key (min_auth_level >= 2),
      • +
      • oauth_token - true, if requests are required to include an + OAuth Token (min_auth_level == 3).
      • +
      +
    • +
    • arguments - list of dictionaries, describes method + arguments. Each dictionary has a following structure: +
        +
      • name - name of an argument,
      • +
      • is_required - boolean, true if the argument is required,
      • +
      • is_deprecated - boolean, true if the argument is deprecated,
      • +
      • description - HTML-formatted description of an argument.
      • +
      • +

        class - space separated list of the following values: public, + inherited, common-formatting and deprecated + (other values might be introduced in future).

        +

        Currently these values do not mean anything specific. They are + used for different coloring/styling in the documentation pages.

        +
      • +
      +
    • +
    • returns - HTML-formatted description method's return value.
    • +
    +
    \ No newline at end of file diff --git a/htdocs/okapi/services/apiref/method_index.php b/htdocs/okapi/services/apiref/method_index.php index e549f7e1..c0880b38 100644 --- a/htdocs/okapi/services/apiref/method_index.php +++ b/htdocs/okapi/services/apiref/method_index.php @@ -15,33 +15,33 @@ use okapi\OkapiInternalRequest; class WebService { - public static function options() - { - return array( - 'min_auth_level' => 0 - ); - } + public static function options() + { + return array( + 'min_auth_level' => 0 + ); + } - public static function call(OkapiRequest $request) - { - $methodnames = OkapiServiceRunner::$all_names; - sort($methodnames); - $cache_key = "api_ref/method_index#".md5(implode("#", $methodnames)); - $results = Cache::get($cache_key); - if ($results == null) - { - $results = array(); - foreach ($methodnames as $methodname) - { - $info = OkapiServiceRunner::call('services/apiref/method', new OkapiInternalRequest( - new OkapiInternalConsumer(), null, array('name' => $methodname))); - $results[] = array( - 'name' => $info['name'], - 'brief_description' => $info['brief_description'], - ); - } - Cache::set($cache_key, $results, 3600); - } - return Okapi::formatted_response($request, $results); - } + public static function call(OkapiRequest $request) + { + $methodnames = OkapiServiceRunner::$all_names; + sort($methodnames); + $cache_key = "api_ref/method_index#".md5(implode("#", $methodnames)); + $results = Cache::get($cache_key); + if ($results == null) + { + $results = array(); + foreach ($methodnames as $methodname) + { + $info = OkapiServiceRunner::call('services/apiref/method', new OkapiInternalRequest( + new OkapiInternalConsumer(), null, array('name' => $methodname))); + $results[] = array( + 'name' => $info['name'], + 'brief_description' => $info['brief_description'], + ); + } + Cache::set($cache_key, $results, 3600); + } + return Okapi::formatted_response($request, $results); + } } diff --git a/htdocs/okapi/services/apiref/method_index.xml b/htdocs/okapi/services/apiref/method_index.xml index 6e7b7dbe..cb65695d 100644 --- a/htdocs/okapi/services/apiref/method_index.xml +++ b/htdocs/okapi/services/apiref/method_index.xml @@ -1,17 +1,17 @@ - Get a list of OKAPI methods with brief descriptions - 12 - -

    Get a list of OKAPI methods with brief descriptions.

    -
    - - - A list of dictionaries, each of which contains one API - method description in the following format: -
      -
    • name - name of a method,
    • -
    • brief_description - brief (max 80 characters), single-line, - plain-text description of what the method does.
    • -
    -
    + Get a list of OKAPI methods with brief descriptions + 12 + +

    Get a list of OKAPI methods with brief descriptions.

    +
    + + + A list of dictionaries, each of which contains one API + method description in the following format: +
      +
    • name - name of a method,
    • +
    • brief_description - brief (max 80 characters), single-line, + plain-text description of what the method does.
    • +
    +
    \ No newline at end of file diff --git a/htdocs/okapi/services/apisrv/installation.php b/htdocs/okapi/services/apisrv/installation.php index 40dba2c5..2b3b4e28 100644 --- a/htdocs/okapi/services/apisrv/installation.php +++ b/htdocs/okapi/services/apisrv/installation.php @@ -13,20 +13,20 @@ use okapi\OkapiInternalRequest; class WebService { - public static function options() - { - return array( - 'min_auth_level' => 0 - ); - } + public static function options() + { + return array( + 'min_auth_level' => 0 + ); + } - public static function call(OkapiRequest $request) - { - $result = array(); - $result['site_url'] = Settings::get('SITE_URL'); - $result['okapi_base_url'] = $result['site_url']."okapi/"; - $result['site_name'] = Okapi::get_normalized_site_name(); - $result['okapi_revision'] = Okapi::$revision; - return Okapi::formatted_response($request, $result); - } + public static function call(OkapiRequest $request) + { + $result = array(); + $result['site_url'] = Settings::get('SITE_URL'); + $result['okapi_base_url'] = $result['site_url']."okapi/"; + $result['site_name'] = Okapi::get_normalized_site_name(); + $result['okapi_revision'] = Okapi::$revision; + return Okapi::formatted_response($request, $result); + } } diff --git a/htdocs/okapi/services/apisrv/installation.xml b/htdocs/okapi/services/apisrv/installation.xml index 0e729797..4657fd26 100644 --- a/htdocs/okapi/services/apisrv/installation.xml +++ b/htdocs/okapi/services/apisrv/installation.xml @@ -1,31 +1,31 @@ - Get information on this OKAPI installation - 14 - - Retrieve some basic information about this OKAPI installation. - - - -

    A dictionary of the following structure:

    -
      -
    • - site_url - URL of the Opencaching site which is running - the OKAPI installation (usually this looks like - "http://www.opencaching.xx/", where xx is a top - level domain of a country). -
    • -
    • - okapi_base_url - URL of the OKAPI installation (usually this is - site_url with "okapi/" appended, but you should not assume - that); this value is to be used as a prefix when constructing service - method URLs, -
    • -
    • - site_name - international name of the Opencaching site, -
    • -
    • okapi_revision - integer, an SVN revision of the OKAPI project - installed on this Opencaching site, or null, when could not - determine revision number.
    • -
    -
    + Get information on this OKAPI installation + 14 + + Retrieve some basic information about this OKAPI installation. + + + +

    A dictionary of the following structure:

    +
      +
    • + site_url - URL of the Opencaching site which is running + the OKAPI installation (usually this looks like + "http://www.opencaching.xx/", where xx is a top + level domain of a country). +
    • +
    • + okapi_base_url - URL of the OKAPI installation (usually this is + site_url with "okapi/" appended, but you should not assume + that); this value is to be used as a prefix when constructing service + method URLs, +
    • +
    • + site_name - international name of the Opencaching site, +
    • +
    • okapi_revision - integer, an SVN revision of the OKAPI project + installed on this Opencaching site, or null, when could not + determine revision number.
    • +
    +
    \ No newline at end of file diff --git a/htdocs/okapi/services/apisrv/installations.php b/htdocs/okapi/services/apisrv/installations.php index b5bfcbad..876ff13d 100644 --- a/htdocs/okapi/services/apisrv/installations.php +++ b/htdocs/okapi/services/apisrv/installations.php @@ -15,104 +15,104 @@ use okapi\OkapiInternalRequest; class WebService { - public static function options() - { - return array( - 'min_auth_level' => 0 - ); - } + public static function options() + { + return array( + 'min_auth_level' => 0 + ); + } - public static function call(OkapiRequest $request) - { - # The list of installations is periodically refreshed by contacting OKAPI - # repository. This method usually displays the cached version of it. + public static function call(OkapiRequest $request) + { + # The list of installations is periodically refreshed by contacting OKAPI + # repository. This method usually displays the cached version of it. - $cachekey = 'apisrv/installations'; - $backupkey = 'apisrv/installations-backup'; - $results = Cache::get($cachekey); - if (!$results) - { - # Download the current list of OKAPI servers. + $cachekey = 'apisrv/installations'; + $backupkey = 'apisrv/installations-backup'; + $results = Cache::get($cachekey); + if (!$results) + { + # Download the current list of OKAPI servers. - try - { - $opts = array( - 'http' => array( - 'method' => "GET", - 'timeout' => 5.0 - ) - ); - $context = stream_context_create($opts); - $xml = file_get_contents("http://opencaching-api.googlecode.com/svn/trunk/etc/installations.xml", - false, $context); - } - catch (ErrorException $e) - { - # Google failed on us. Try to respond with a backup list. + try + { + $opts = array( + 'http' => array( + 'method' => "GET", + 'timeout' => 5.0 + ) + ); + $context = stream_context_create($opts); + $xml = file_get_contents("http://opencaching-api.googlecode.com/svn/trunk/etc/installations.xml", + false, $context); + } + catch (ErrorException $e) + { + # Google failed on us. Try to respond with a backup list. - $results = Cache::get($backupkey); - if ($results) - { - Cache::set($cachekey, $results, 12 * 3600); # so to retry no earlier than after 12 hours - return Okapi::formatted_response($request, $results); - } + $results = Cache::get($backupkey); + if ($results) + { + Cache::set($cachekey, $results, 12 * 3600); # so to retry no earlier than after 12 hours + return Okapi::formatted_response($request, $results); + } - # Backup has expired (or have never been cached). If we're on a development - # server then probably it's okay. In production this SHOULD NOT happen. + # Backup has expired (or have never been cached). If we're on a development + # server then probably it's okay. In production this SHOULD NOT happen. - $results = array( - array( - 'site_url' => Settings::get('SITE_URL'), - 'site_name' => "Unable to retrieve!", - 'okapi_base_url' => Settings::get('SITE_URL')."okapi/", - ) - ); - Cache::set($cachekey, $results, 12 * 3600); # so to retry no earlier than after 12 hours - return Okapi::formatted_response($request, $results); - } + $results = array( + array( + 'site_url' => Settings::get('SITE_URL'), + 'site_name' => "Unable to retrieve!", + 'okapi_base_url' => Settings::get('SITE_URL')."okapi/", + ) + ); + Cache::set($cachekey, $results, 12 * 3600); # so to retry no earlier than after 12 hours + return Okapi::formatted_response($request, $results); + } - $doc = simplexml_load_string($xml); - $results = array(); - $i_was_included = false; - foreach ($doc->installation as $inst) - { - $site_url = (string)$inst[0]['site_url']; - if ($inst[0]['okapi_base_url']) - $okapi_base_url = (string)$inst[0]['okapi_base_url']; - else - $okapi_base_url = $site_url."okapi/"; - if ($inst[0]['site_name']) - $site_name = (string)$inst[0]['site_name']; - else - $site_name = Okapi::get_normalized_site_name($site_url); - $results[] = array( - 'site_url' => $site_url, - 'site_name' => $site_name, - 'okapi_base_url' => $okapi_base_url, - ); - if ($site_url == Settings::get('SITE_URL')) - $i_was_included = true; - } + $doc = simplexml_load_string($xml); + $results = array(); + $i_was_included = false; + foreach ($doc->installation as $inst) + { + $site_url = (string)$inst[0]['site_url']; + if ($inst[0]['okapi_base_url']) + $okapi_base_url = (string)$inst[0]['okapi_base_url']; + else + $okapi_base_url = $site_url."okapi/"; + if ($inst[0]['site_name']) + $site_name = (string)$inst[0]['site_name']; + else + $site_name = Okapi::get_normalized_site_name($site_url); + $results[] = array( + 'site_url' => $site_url, + 'site_name' => $site_name, + 'okapi_base_url' => $okapi_base_url, + ); + if ($site_url == Settings::get('SITE_URL')) + $i_was_included = true; + } - # If running on a local development installation, then include the local - # installation URL. + # If running on a local development installation, then include the local + # installation URL. - if (!$i_was_included) - { - $results[] = array( - 'site_url' => Settings::get('SITE_URL'), - 'site_name' => "DEVELSITE", - 'okapi_base_url' => Settings::get('SITE_URL')."okapi/", - ); - # Contact OKAPI developers in order to get added to the official sites list! - } + if (!$i_was_included) + { + $results[] = array( + 'site_url' => Settings::get('SITE_URL'), + 'site_name' => "DEVELSITE", + 'okapi_base_url' => Settings::get('SITE_URL')."okapi/", + ); + # Contact OKAPI developers in order to get added to the official sites list! + } - # Cache it for one day. Also, save a backup (valid for 30 days). + # Cache it for one day. Also, save a backup (valid for 30 days). - Cache::set($cachekey, $results, 86400); - Cache::set($backupkey, $results, 86400*30); - } + Cache::set($cachekey, $results, 86400); + Cache::set($backupkey, $results, 86400*30); + } - return Okapi::formatted_response($request, $results); - } + return Okapi::formatted_response($request, $results); + } } diff --git a/htdocs/okapi/services/apisrv/installations.xml b/htdocs/okapi/services/apisrv/installations.xml index 4353c7e6..84b87a32 100644 --- a/htdocs/okapi/services/apisrv/installations.xml +++ b/htdocs/okapi/services/apisrv/installations.xml @@ -1,33 +1,33 @@ - Get the list of all public OKAPI installations - 39 - - Get the list of all public OKAPI installations. Keep in mind that - OKAPI installations might differ slightly. If you plan on using - multiple OKAPI installations in your application (which is a very - good thing!) you should test it against the one with the lowest - OKAPI revision number. - - - -

    A dictionary of the following structure:

    -
      -
    • - site_url - URL of the Opencaching site which is running - the OKAPI installation (usually this looks like - "http://www.opencaching.xx/", where xx is a top - level domain of a country). -
    • -
    • - site_name - universal name for this site (should be fine - for all languages), -
    • -
    • - okapi_base_url - URL of the OKAPI installation (usually this is - site_url with "okapi/" appended, but you should not assume - that); this value is to be used as a prefix when constructing service - method URLs. -
    • -
    -
    + Get the list of all public OKAPI installations + 39 + + Get the list of all public OKAPI installations. Keep in mind that + OKAPI installations might differ slightly. If you plan on using + multiple OKAPI installations in your application (which is a very + good thing!) you should test it against the one with the lowest + OKAPI revision number. + + + +

    A dictionary of the following structure:

    +
      +
    • + site_url - URL of the Opencaching site which is running + the OKAPI installation (usually this looks like + "http://www.opencaching.xx/", where xx is a top + level domain of a country). +
    • +
    • + site_name - universal name for this site (should be fine + for all languages), +
    • +
    • + okapi_base_url - URL of the OKAPI installation (usually this is + site_url with "okapi/" appended, but you should not assume + that); this value is to be used as a prefix when constructing service + method URLs. +
    • +
    +
    \ No newline at end of file diff --git a/htdocs/okapi/services/apisrv/stats.php b/htdocs/okapi/services/apisrv/stats.php index cb17421a..d8ff2675 100644 --- a/htdocs/okapi/services/apisrv/stats.php +++ b/htdocs/okapi/services/apisrv/stats.php @@ -15,48 +15,48 @@ use okapi\Settings; class WebService { - public static function options() - { - return array( - 'min_auth_level' => 0 - ); - } + public static function options() + { + return array( + 'min_auth_level' => 0 + ); + } - public static function call(OkapiRequest $request) - { - $cachekey = "apisrv/stats"; - $result = Cache::get($cachekey); - if (!$result) - { - $result = array( - 'cache_count' => 0 + Db::select_value(" - select count(*) from caches where status in (1,2,3) - "), - 'user_count' => 0 + Db::select_value(" - select count(*) from ( - select distinct user_id - from cache_logs - where - type in (1,2) - and ".((Settings::get('OC_BRANCH') == 'oc.pl') ? "deleted = 0" : "true")." - UNION DISTINCT - select distinct user_id - from caches - ) as t; - "), - 'apps_count' => 0 + Db::select_value("select count(*) from okapi_consumers;"), - 'apps_active' => 0 + Db::select_value(" - select count(distinct s.consumer_key) - from - okapi_stats_hourly s, - okapi_consumers c - where - s.consumer_key = c.`key` - and s.period_start > date_add(now(), interval -30 day) - "), - ); - Cache::set($cachekey, $result, 86400); # cache it for one day - } - return Okapi::formatted_response($request, $result); - } + public static function call(OkapiRequest $request) + { + $cachekey = "apisrv/stats"; + $result = Cache::get($cachekey); + if (!$result) + { + $result = array( + 'cache_count' => 0 + Db::select_value(" + select count(*) from caches where status in (1,2,3) + "), + 'user_count' => 0 + Db::select_value(" + select count(*) from ( + select distinct user_id + from cache_logs + where + type in (1,2) + and ".((Settings::get('OC_BRANCH') == 'oc.pl') ? "deleted = 0" : "true")." + UNION DISTINCT + select distinct user_id + from caches + ) as t; + "), + 'apps_count' => 0 + Db::select_value("select count(*) from okapi_consumers;"), + 'apps_active' => 0 + Db::select_value(" + select count(distinct s.consumer_key) + from + okapi_stats_hourly s, + okapi_consumers c + where + s.consumer_key = c.`key` + and s.period_start > date_add(now(), interval -30 day) + "), + ); + Cache::set($cachekey, $result, 86400); # cache it for one day + } + return Okapi::formatted_response($request, $result); + } } diff --git a/htdocs/okapi/services/apisrv/stats.xml b/htdocs/okapi/services/apisrv/stats.xml index 6f7b99fb..418cdcf1 100644 --- a/htdocs/okapi/services/apisrv/stats.xml +++ b/htdocs/okapi/services/apisrv/stats.xml @@ -1,20 +1,20 @@ - Get some basic stats about the site - 43 - - Retrieve some basic statistics about this OKAPI installation. - If you want some more stats, post a comment! - - - -

    A dictionary of the following structure:

    -
      -
    • cache_count - approximate total number of geocaches stored at this site,
    • -
    • user_count - approximate total number of active users of this site,
    • -
    • apps_count - approximate total number of all OKAPI applications (number of - registered API Keys).
    • -
    • apps_active - approximate number of active OKAPI applications (the ones which issued - at least one, non-anonymous OKAPI request during the last month).
    • -
    -
    + Get some basic stats about the site + 43 + + Retrieve some basic statistics about this OKAPI installation. + If you want some more stats, post a comment! + + + +

    A dictionary of the following structure:

    +
      +
    • cache_count - approximate total number of geocaches stored at this site,
    • +
    • user_count - approximate total number of active users of this site,
    • +
    • apps_count - approximate total number of all OKAPI applications (number of + registered API Keys).
    • +
    • apps_active - approximate number of active OKAPI applications (the ones which issued + at least one, non-anonymous OKAPI request during the last month).
    • +
    +
    \ No newline at end of file diff --git a/htdocs/okapi/services/attrs/attr_helper.inc.php b/htdocs/okapi/services/attrs/attr_helper.inc.php index e961b388..4f5e8bad 100644 --- a/htdocs/okapi/services/attrs/attr_helper.inc.php +++ b/htdocs/okapi/services/attrs/attr_helper.inc.php @@ -18,272 +18,272 @@ use SimpleXMLElement; class AttrHelper { - /** - * 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 - * temporarilly disable this behavior by settings this to false. - */ - private static $RELOAD_ON_DEBUG = true; + /** + * 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 + * temporarilly disable this behavior by settings this to false. + */ + 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 - * In order for the $RELOAD_ON_DEBUG to work properly when switching to/from - * DEBUG mode. - */ - private static function cache_key_suffix() - { - return (self::$RELOAD_ON_DEBUG) ? "#DBG" : ""; - } + /** + * 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 + * DEBUG mode. + */ + private static function cache_key_suffix() + { + return (self::$RELOAD_ON_DEBUG) ? "#DBG" : ""; + } - /** Return the timeout to be used for attribute caching. */ - private static function ttl() - { - return (Settings::get('DEBUG') && self::$RELOAD_ON_DEBUG) ? 2 : 86400; - } + /** Return the timeout to be used for attribute caching. */ + private static function ttl() + { + return (Settings::get('DEBUG') && self::$RELOAD_ON_DEBUG) ? 2 : 86400; + } - /** - * Forces an immediate refresh of the current attributes from the - * attribute-definitions.xml file. - */ - public static function refresh_now() - { - try - { - $path = $GLOBALS['rootpath']."okapi/services/attrs/attribute-definitions.xml"; - $xml = file_get_contents($path); - self::refresh_from_string($xml); - } - catch (Exception $e) - { - # Failed to read or parse the file (i.e. after a syntax error was - # commited). Let's check when the last successful parse occured. + /** + * Forces an immediate refresh of the current attributes from the + * attribute-definitions.xml file. + */ + public static function refresh_now() + { + try + { + $path = $GLOBALS['rootpath']."okapi/services/attrs/attribute-definitions.xml"; + $xml = file_get_contents($path); + self::refresh_from_string($xml); + } + catch (Exception $e) + { + # Failed to read or parse the file (i.e. after a syntax error was + # 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) - { - # 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. + if (self::$attr_dict === null) + { + # 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. - $cache_key = "attrhelper/dict#".Okapi::$revision.self::cache_key_suffix(); - $cachedvalue = array( - 'attr_dict' => array(), - ); - Cache::set($cache_key, $cachedvalue, self::ttl()); - } + $cache_key = "attrhelper/dict#".Okapi::$revision.self::cache_key_suffix(); + $cachedvalue = array( + 'attr_dict' => array(), + ); + Cache::set($cache_key, $cachedvalue, self::ttl()); + } - return; - } - } + return; + } + } - /** - * Refresh all attributes from the given XML. Usually, this file is - * downloaded from Google Code (using refresh_now). - */ - public static function refresh_from_string($xml) - { - /* The attribute-definitions.xml file defines relationships between - * attributes originating from various OC installations. Each - * installation uses internal IDs of its own. Which "attribute schema" - * is being used in THIS installation? */ + /** + * Refresh all attributes from the given XML. Usually, this file is + * downloaded from Google Code (using refresh_now). + */ + public static function refresh_from_string($xml) + { + /* The attribute-definitions.xml file defines relationships between + * attributes originating from various OC installations. Each + * installation uses internal IDs of its own. Which "attribute schema" + * is being used in THIS installation? */ - $my_schema = Settings::get('ORIGIN_URL'); + $my_schema = Settings::get('ORIGIN_URL'); - $doc = simplexml_load_string($xml); - $cachedvalue = array( - 'attr_dict' => array(), - ); + $doc = simplexml_load_string($xml); + $cachedvalue = array( + 'attr_dict' => array(), + ); - # Build cache attributes dictionary + # Build cache attributes dictionary - $all_internal_ids = array(); - foreach ($doc->attr as $attrnode) - { - $attr = array( - 'acode' => (string)$attrnode['acode'], - 'gc_equivs' => array(), - 'internal_id' => null, - 'names' => array(), - 'descriptions' => array(), - 'is_discontinued' => true - ); - foreach ($attrnode->groundspeak as $gsnode) - { - $attr['gc_equivs'][] = array( - 'id' => (int)$gsnode['id'], - 'inc' => in_array((string)$gsnode['inc'], array("true", "1")) ? 1 : 0, - 'name' => (string)$gsnode['name'] - ); - } - foreach ($attrnode->opencaching as $ocnode) - { - /* If it is used by at least one OC node, then it's NOT discontinued. */ - $attr['is_discontinued'] = false; + $all_internal_ids = array(); + foreach ($doc->attr as $attrnode) + { + $attr = array( + 'acode' => (string)$attrnode['acode'], + 'gc_equivs' => array(), + 'internal_id' => null, + 'names' => array(), + 'descriptions' => array(), + 'is_discontinued' => true + ); + foreach ($attrnode->groundspeak as $gsnode) + { + $attr['gc_equivs'][] = array( + 'id' => (int)$gsnode['id'], + 'inc' => in_array((string)$gsnode['inc'], array("true", "1")) ? 1 : 0, + 'name' => (string)$gsnode['name'] + ); + } + foreach ($attrnode->opencaching as $ocnode) + { + /* If it is used by at least one OC node, then it's NOT discontinued. */ + $attr['is_discontinued'] = false; - if ((string)$ocnode['schema'] == $my_schema) - { - /* It is used by THIS OC node. */ + if ((string)$ocnode['schema'] == $my_schema) + { + /* It is used by THIS OC node. */ - $internal_id = (int)$ocnode['id']; - if (isset($all_internal_ids[$internal_id])) - throw new Exception("The internal attribute ".$internal_id. - " has multiple assigments to OKAPI attributes."); - $all_internal_ids[$internal_id] = true; - if (!is_null($attr['internal_id'])) - throw new Exception("There are multiple internal IDs for the ". - $attr['acode']." attribute."); - $attr['internal_id'] = $internal_id; - } - } - foreach ($attrnode->lang as $langnode) - { - $lang = (string)$langnode['id']; - foreach ($langnode->name as $namenode) - { - if (isset($attr['names'][$lang])) - throw new Exception("Duplicate ".$lang." name of attribute ".$attr['acode']); - $attr['names'][$lang] = (string)$namenode; - } - foreach ($langnode->desc as $descnode) - { - if (isset($attr['descriptions'][$lang])) - throw new Exception("Duplicate ".$lang." description of attribute ".$attr['acode']); - $xml = $descnode->asxml(); /* contains "" and "" */ - $innerxml = preg_replace("/(^[^>]+>)|(<[^<]+$)/us", "", $xml); - $attr['descriptions'][$lang] = self::cleanup_string($innerxml); - } - } - $cachedvalue['attr_dict'][$attr['acode']] = $attr; - } + $internal_id = (int)$ocnode['id']; + if (isset($all_internal_ids[$internal_id])) + throw new Exception("The internal attribute ".$internal_id. + " has multiple assigments to OKAPI attributes."); + $all_internal_ids[$internal_id] = true; + if (!is_null($attr['internal_id'])) + throw new Exception("There are multiple internal IDs for the ". + $attr['acode']." attribute."); + $attr['internal_id'] = $internal_id; + } + } + foreach ($attrnode->lang as $langnode) + { + $lang = (string)$langnode['id']; + foreach ($langnode->name as $namenode) + { + if (isset($attr['names'][$lang])) + throw new Exception("Duplicate ".$lang." name of attribute ".$attr['acode']); + $attr['names'][$lang] = (string)$namenode; + } + foreach ($langnode->desc as $descnode) + { + if (isset($attr['descriptions'][$lang])) + throw new Exception("Duplicate ".$lang." description of attribute ".$attr['acode']); + $xml = $descnode->asxml(); /* contains "" and "" */ + $innerxml = preg_replace("/(^[^>]+>)|(<[^<]+$)/us", "", $xml); + $attr['descriptions'][$lang] = self::cleanup_string($innerxml); + } + } + $cachedvalue['attr_dict'][$attr['acode']] = $attr; + } - $cache_key = "attrhelper/dict#".Okapi::$revision.self::cache_key_suffix(); - Cache::set($cache_key, $cachedvalue, self::ttl()); - self::$attr_dict = $cachedvalue['attr_dict']; - } + $cache_key = "attrhelper/dict#".Okapi::$revision.self::cache_key_suffix(); + Cache::set($cache_key, $cachedvalue, self::ttl()); + self::$attr_dict = $cachedvalue['attr_dict']; + } - /** - * Object to be used for forward-compatibility (see the attributes method). - */ - public static function get_unknown_placeholder($acode) - { - return array( - 'acode' => $acode, - 'gc_equivs' => array(), - 'internal_id' => null, - 'names' => array( - 'en' => "Unknown attribute" - ), - 'descriptions' => array( - 'en' => ( - "This attribute ($acode) is unknown at ".Okapi::get_normalized_site_name(). - ". It might not exist, or it may be a new attribute, recognized ". - "only in newer OKAPI installations. Perhaps ".Okapi::get_normalized_site_name(). - " needs to have its OKAPI updated?" - ) - ), - 'is_discontinued' => true - ); - } + /** + * Object to be used for forward-compatibility (see the attributes method). + */ + public static function get_unknown_placeholder($acode) + { + return array( + 'acode' => $acode, + 'gc_equivs' => array(), + 'internal_id' => null, + 'names' => array( + 'en' => "Unknown attribute" + ), + 'descriptions' => array( + 'en' => ( + "This attribute ($acode) is unknown at ".Okapi::get_normalized_site_name(). + ". It might not exist, or it may be a new attribute, recognized ". + "only in newer OKAPI installations. Perhaps ".Okapi::get_normalized_site_name(). + " needs to have its OKAPI updated?" + ) + ), + 'is_discontinued' => true + ); + } - /** - * Initialize all the internal attributes (if not yet initialized). This - * 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. - */ - private static function init_from_cache($allow_refreshing=true) - { - if (self::$attr_dict !== null) - { - /* Already initialized. */ - return; - } - $cache_key = "attrhelper/dict#".Okapi::$revision.self::cache_key_suffix(); - $cachedvalue = Cache::get($cache_key); - if ($cachedvalue === null) - { - # I.e. after Okapi::$revision is changed, or cache got invalidated. + /** + * Initialize all the internal attributes (if not yet initialized). This + * 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. + */ + private static function init_from_cache($allow_refreshing=true) + { + if (self::$attr_dict !== null) + { + /* Already initialized. */ + return; + } + $cache_key = "attrhelper/dict#".Okapi::$revision.self::cache_key_suffix(); + $cachedvalue = Cache::get($cache_key); + if ($cachedvalue === null) + { + # I.e. after Okapi::$revision is changed, or cache got invalidated. - if ($allow_refreshing) - { - self::refresh_now(); - self::init_from_cache(false); - return; - } - else - { - $cachedvalue = array( - 'attr_dict' => array(), - ); - } - } - self::$attr_dict = $cachedvalue['attr_dict']; - } + if ($allow_refreshing) + { + self::refresh_now(); + self::init_from_cache(false); + return; + } + else + { + $cachedvalue = array( + 'attr_dict' => array(), + ); + } + } + self::$attr_dict = $cachedvalue['attr_dict']; + } - /** - * 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). - */ - public static function get_attrdict() - { - self::init_from_cache(); - return self::$attr_dict; - } + /** + * 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). + */ + public static function get_attrdict() + { + self::init_from_cache(); + return self::$attr_dict; + } - /** "\n\t\tBla blabla\n\t\tblabla.\n\t" => "Bla blabla blabla." */ - private static function cleanup_string($s) - { - return preg_replace('/(^\s+)|(\s+$)/us', "", preg_replace('/\s+/us', " ", $s)); - } + /** "\n\t\tBla blabla\n\t\tblabla.\n\t" => "Bla blabla blabla." */ + private static function cleanup_string($s) + { + return preg_replace('/(^\s+)|(\s+$)/us', "", preg_replace('/\s+/us', " ", $s)); + } - /** - * Get the mapping table between internal attribute id => OKAPI A-code. - * The result is cached! - */ - public static function get_internal_id_to_acode_mapping() - { - static $mapping = null; - if ($mapping !== null) - return $mapping; + /** + * Get the mapping table between internal attribute id => OKAPI A-code. + * The result is cached! + */ + public static function get_internal_id_to_acode_mapping() + { + static $mapping = null; + if ($mapping !== null) + return $mapping; - $cache_key = "attrhelper/id2acode/".Okapi::$revision.self::cache_key_suffix(); - $mapping = Cache::get($cache_key); - if (!$mapping) - { - self::init_from_cache(); - $mapping = array(); - foreach (self::$attr_dict as $acode => &$attr_ref) - $mapping[$attr_ref['internal_id']] = $acode; - Cache::set($cache_key, $mapping, self::ttl()); - } - return $mapping; - } + $cache_key = "attrhelper/id2acode/".Okapi::$revision.self::cache_key_suffix(); + $mapping = Cache::get($cache_key); + if (!$mapping) + { + self::init_from_cache(); + $mapping = array(); + foreach (self::$attr_dict as $acode => &$attr_ref) + $mapping[$attr_ref['internal_id']] = $acode; + Cache::set($cache_key, $mapping, self::ttl()); + } + return $mapping; + } - /** - * Get the mapping: A-codes => attribute name. The language for the name - * is selected based on the $langpref parameter. The result is cached! - */ - public static function get_acode_to_name_mapping($langpref) - { - static $mapping = null; - if ($mapping !== null) - return $mapping; + /** + * Get the mapping: A-codes => attribute name. The language for the name + * is selected based on the $langpref parameter. The result is cached! + */ + public static function get_acode_to_name_mapping($langpref) + { + static $mapping = null; + if ($mapping !== null) + return $mapping; - $cache_key = md5(serialize(array("attrhelper/acode2name", $langpref, - Okapi::$revision, self::cache_key_suffix()))); - $mapping = Cache::get($cache_key); - if (!$mapping) - { - self::init_from_cache(); - $mapping = array(); - foreach (self::$attr_dict as $acode => &$attr_ref) - { - $mapping[$acode] = Okapi::pick_best_language( - $attr_ref['names'], $langpref); - } - Cache::set($cache_key, $mapping, self::ttl()); - } - return $mapping; - } + $cache_key = md5(serialize(array("attrhelper/acode2name", $langpref, + Okapi::$revision, self::cache_key_suffix()))); + $mapping = Cache::get($cache_key); + if (!$mapping) + { + self::init_from_cache(); + $mapping = array(); + foreach (self::$attr_dict as $acode => &$attr_ref) + { + $mapping[$acode] = Okapi::pick_best_language( + $attr_ref['names'], $langpref); + } + Cache::set($cache_key, $mapping, self::ttl()); + } + return $mapping; + } } diff --git a/htdocs/okapi/services/attrs/attribute-definitions.xml b/htdocs/okapi/services/attrs/attribute-definitions.xml index 57e7f9cb..f7dd694d 100644 --- a/htdocs/okapi/services/attrs/attribute-definitions.xml +++ b/htdocs/okapi/services/attrs/attribute-definitions.xml @@ -1,10 +1,10 @@ - - - - - - - - Listed at Opencaching only - - This geocache is listed at Opencaching only. It is not listed - on any other geocaching site. - - - - Dostępna tylko na Opencaching - - Skrzynka "jednosystemowa", dostępna jedynie poprzez serwis Opencaching. - - - - nur bei Opencaching logbar - - Der Geocache ist nur bei Opencaching gelistet. Benutzer anderer - Geocache-Datenbanken haben so einen schnellen Überblick, welche Geocaches - es sich lohnt näher anzusehen. - - - - Alleen te loggen op Opencaching - - Deze cache is alleen beschikbaar en te loggen op opencaching. - - - - Solo loggeable en Opencaching - - Este geocachee esta publicado sólo en Opencaching. Este atributo permite - a los usuarios de otras plataformas encontrar rápidamente caches - interesantes de calidad OC geocaching. - - - - Loggabile solo su Opencaching - - Questa geocache è pubblicata solo su Opencaching. Questo attributo permette - agli utenti di altre piattaforme di geocaching di trovare velocemente - interessanti cache OC di qualità. - - - - - - - - - - Near a Survey Marker - - The cache is hidden in near proximity of a survey marker (also known - as geodetic marks). - - - - W pobliżu punktu geodezyjnego - - Skrzynka ukryta w pobliżu punktu geodezyjnego. - Więcej informacji. - - - - an einem Vermessungspunkt - - Der Cache ist in der Nähe eines festen Vermessungspunktes versteckt. - - - - Meetpunt - - Deze cache heeft te maken met een meetpunt of geodetisch punt. - - - - - - - - Wherigo Cache - - Cache description includes a file - the Whereigo cartridge. In order to - find the cache, you need to download the file and install it on - a proper compatible device. - - - - Wherigo Cache - - Opis skrzynki zawiera scenariusz Wherigo (w Polsce znany również pod - skrótem WIGO). Aby móc zdobyć skrzynkę należy pobrać scenariusz i - wgrać go do kompatybilnego z nim urządzenia. - Więcej informacji. - - - - Wherigo-Cache - - Zur Cachebeschreibung gehört eine Datei, die „Wherigo-Cartridge“. Sie muss - heruntergeladen und auf ein Gerät mit einer entsprechenden Abspielsoftware - installiert werden. Der Inhalt der Cartridge führt dich durch den Cache. - - - - - - - - - - Letterbox Cache - - There is a stamp in the cache for stamping your personal logbook, and the - cache’s logbook will be stamped with your personal stamp. Take care not - to mix up stamps and to leave the cache’s stamp in the cache! - - - - Skrzynka typu Letterbox - - W skrzynce znajduje się pieczątka, której nie można zabrać ze sobą. - Możesz jej użyć do ostemplowania swojego osobistego dziennika. - Logbook skrzynki powinien z kolei zostać ostemplowany Twoją własną - pieczątką. - Więcej informacji. - - - - Letterbox (benötigt Stempel) - - In dem Behälter vor Ort befindet sich ein Stempel, mit dem man sein - persönliches Logbuch abstempeln kann. Das Logbuch im Geocache wird - ebenfalls mit einem persönlichen Stempel signiert. Bitte achte unbedingt - darauf, dass du den Stempel aus dem Geocache nicht mitnimmst oder tauschst! - Weitere Informationen. - - - - Letterbox (een stempel nodig) - - Er bevind sich een stempel in de cache waarmee je een in een eigen logboek - kan stempelen. Met een eigen stempel kun je het logboek in de cache stempelen. - Let op om niet per ongeluk de stempels te verwisselen! - - - - Letterbox (necesita un estampador) - - Hay un sello en el cache para estamparlo en su libro de registro personal, - y el libro de registro del cache será sellada con su sello personal. - !Tenga cuidado de no mezclar los sellos! - - - - Letterbox (richiede un timbro) - - C'e un timbro nella cache per timbrare il tuo quaderno personale, - e il log della cache verra timbrato con il tuo timbro personale. - Fate attenzione a non confondere i timbri e a lasciare il timbro della - cache nella cache! - - - - - - - - - GeoHotel Cache - - Primary purpose of the "GeoHotel" caches is to exchange trackables - (TravelBugs, GeoKrets, etc.). - - - - GeoHotel Cache - - Atrybut ten oznacza skrzynki których głównym celem jest lokowanie - w tej skrzynce różnych wędrujących rzeczy np GeoKrety, GeoLutins, - GeoFish itp. - - - - Geohotel-Cache - - Der Cache dient vor allem zum Austausch von Reisenden (GeoKrets, - TravelBugs etc.). - - - - GeoHotel - - Deze cache is speciaal voor reizende items zoals: GeoKrets, - TravelBugs enz. - - - - - - - - - Magnetic Cache - - This geocache is attached with a magnet. - - - - Przyczepiona magnesem - - Skrzynka zawiera magnes i przymocowana jest za jego pomocą. - - - - magnetischer Cache - - Der Geocache ist mit einem Magneten angebracht. - - - - Bevestigd met magneet - - Deze cache is met een magneet bevestigt. - - - - - - - - - Description contains an audio file - - In order to find this cache, you must listen to an audio recording - (e.g. MP3 file), which is attached to the cache description. - - - - Opis zawiera plik audio - - Aby odnaleźć skrzynkę, należy odsłuchać plik dźwiękowy zawarty w - opisie skrzynki (np. w formacie MP3). Może on zawierać zakodowane - informacje, dźwięki otoczenia, opis dotarcia do skrzynki. - - - - Audio-Cache - - Um diesen Cache zu finden musst du eine Tonaufnahme anhören, - die der Cachbeschreibung beigefügt ist. - - - - Beschrijving bevat een audio bestand - - Om deze cache te vinden moeten instructies via een audio bestand - (bijv. MP3) beluisterd worden welke in de cachebeschrijving te vinden is. - - - - - - - - Offset cache - - A specific type of a MultiCache. The coordinates point to a starting - point. The description contains simple instructions to follow - once you are in the starting point (usually, an azimuth and a - distance). - - - - Offset cache - - Szczególny przypadek skrzynki typu multicache, składający się z - punktu startowego (określonego współrzędnymi) oraz jasnych - informacjami o sposobie dotarcia do finału (np. azymutu i - odległości). - - - - Peilungscache - - Die Koordinaten zeigen auf einen Startpunkt, von dem aus eine - Peilung zum Ziel führt, meist anhand einer Entfernungsangabe - und eines Winkels (Azimut). - - - - - - - - - - Garmin's wireless beacon - - Contains Garmin's wireless chirp beacon. - - - - Beacon - Garmin Chirp - - Skrzynka zawiera Beacon Garmin chirp. - - - - Funksignal – Garmin Chirp - - Zusammen mit dem Cache sind ein oder mehrere Garmin-Funksender - versteckt. Um den Cache zu finden, müssen die Funksignale empfangen - werden. Dazu wird ein passender Empfänger benötigt. - - - - Beacon - Garmin chirp - - Deze cache is gemaakt met 1 of meer Garmins chirp draadloze zenders. - - - - - - - - - Dead Drop USB cache - - The cache consists of an unmovable USB mass storage device, e.g. - fixed into a wall, curb etc. The device contains readme.txt file - with cache description and a logbook.txt file where you can log - your visit. - - - - Dead Drop USB skrzynka - - Skrzynka typu USB Dead Drop. Pen-drive przymocowany lub wmurowany - w ścianę, budynek, krawiężnik itp. Aby zalogować znalezienie, należy - wpisać się do pliku logbook.txt znajdującego się w pamięci urządzenia. - Wiecej informacji. - - - - Dead Drop USB-Cache - - Der Cache besteht aus einem fest angebrachten USB-Stick. Er enthält - eine readme.txt-Datei mit Erläuterungen zum Cache und eine Datei - logbook.txt zum Loggen. - - - - Dead Drop USB cache - - Deze cache bestaat uit een vast gemonteerde USB stick, zoals in een - muur of paal enz. Deze bevat een readme.txt bestand met de cache - beschrijving en een logboek.txt bestand om je bezoek te kunnen loggen. - Voor informatie. - - - - - - - - Has a moving target - - This geocache is moving around. For example, the owner might regularly - move the cache from one place to another, or the finders will do this - task and post new coordinates in their log entries. The owner must - update coordinates in the cache description after each move. - - - - bewegliches Ziel - - Der Geocache verändert seine Position und ist deshalb nicht immer am - gleichen Ort zu finden. Es gibt Varianten, bei denen der Geocache-Besitzer - den Geocache regelmäßig an anderen Orten versteckt, oder der Finder den - Geocache an einem neuen Ort versteckt. Danach muss der Besitzer jeweils - die Beschreibung aktualisieren. - - - - Objetivo en movimiento - - Este geocache está en movimiento. Por ejemplo, el propietario podía mover - la caché periódicamente de un lugar a otro, o un geobuscador podría hacer - esto y ofrecer nuevos detalles en su registro. El propietario debe - actualizar las coordenadas en la descripción del cache después de cada - movimiento. - - - - Oggetto in movimento - - Questa geocache e in muovimento. Per esempio, il proprietario potrebbe - spostare regolarmente la cache da un luogo ad un altro, o i cacher - potrebbero eseguire questa operazione e indicare nuove coordinate nel - loro log. Il proprietario deve aggiornare le coordinate nella descrizione - della cache dopo ogni mossa. - - - - - - - - a webcam is involved - - There is a webcam at the target location. You must record a webcam - picture of your visit and include it in your 'found' log entry. There - may be additional requirements like a geocaching banner on the photo. - The webcam’s address is included in the cache description. - - - - Webcam - - Am Ziel befindet sich eine Webcam, und für einen Fund muss man das Bild - der Webcam von sich selbst aufnehmen, um nachzuweisen, dass man vor Ort - war. Manche Webcam-Caches setzen auch weitere Bedingungen, z.B. einen - Geocaching-Banner auf dem Bild. Die Webadresse der Webcam ist in der - Beschreibung angegeben. - - - - Webcam Cache - - Hay una webcam en el lugar de destino. necesidad a alguien para registrar - con un pantallazo de la imagen de la webcam de su visita e incluirlo en - el registro. Puede haber requisitos adicionales como un signo de - geocaching en la imagen. La dirección de la webcam está incluido en la - descripción de la cache. - - - - Webcam Cache - - C'e una webcam nella posizione di destinazione. E' necessario registrare - una foto con la webcam della vostra visita e includerla nel log. Ci - possono essere requisiti addizionali come un segnale di geocaching sulla - foto. L'indirizzo della webcam e incluso nella descrizione della cache. - - - - - - - - Other cache type - - This is none of the standard, pre-defined types of cache. - Use this attribute for special, unusual caches. - - - - sonstiger Cachetyp - - Dieser Cache passt in keine der üblichen Kategorien von Caches (Cachearten). - - - - Otro tipo de cache - - Este es un cache que no pertenece a ninguna de las categorías predefinicas - - estándar. - - - - Altro tipo di cache - - Questa e una cache che non appartiene a nessuna delle categorie standard - perdefinite. Usa questo attributo per cache speciali e inusuali. - - - - - - - - Investigation required - - You must investigate additional information before you can seek this cache. - - - - Recherche - - Für diesen Cache muss vorab nach Informationen gesucht werden. - - - - Investigación - - Necesitas encontrar más información antes de poder buscar este cache. - - - - Ricerca - - E necessario trovare ulteriori informazioni prima di poter cercare - questa cache. - - - - - - - - - Puzzle / Mystery - - Puzzles or mysteries have to be solved before or while seeking this cache. - - - - Rätsel - - Bei diesem Cache sind als Vorarbeit oder während der Suche Rätsel zu lösen. - - - - Puzzle / Misterio - - Misterio o Puzzle para ser resuelto antes o durante la búsqueda de este cache. - - - - Puzzle / Mystery - - Puzzle o Mystery devono essere risolti prima o durante la ricerca di - questa cache. - - - - - - - - Arithmetical problem - - Before or while seeking this cache, arithmetical problems must be solved - which go beyond very basic calculations. - - - - Rechenaufgabe - - Es müssen vorab oder während der Suche Rechenaufgaben gelöst werden, die - über das kleine Geocacher-1x1 hinausgehen, zum Beispiel - Mittelpunktberechnungen oder Peilungen. - - - - Problema matemático - - Antes o durante la búsqueda de este cache, resolver problemas matemáticos - sencillos más difícil a los cálculos de la base. - - - - Problema matematico - - Prima o durante la ricerca di questa cache, devono essere risolti problemi - matematici piu difficili di semplici calcoli base. - - - - - - - - Ask owner for start conditions - - Before doing this cache, you must ask the owner for the starting conditions. - E.g. the cache may be linked to certain events at varying dates. - - - - Startbedingungen beim Owner erfragen - - Bei diesem Cache ist es nötig, sich vor dem Angehen des Caches beim - Eigentümer über die Bedingungen zum Angehen zu informieren. - - - - Ask owner for start conditions - - Pregunte a los propietarios por las condiciones iniciales - - - - Ask owner for start conditions - - Chiedere al proprietario per le condizioni di partenza - - - - - - - - - - Wheelchair accessible - - The cache is hidden in a way which makes it possible to be found - when moving on a wheelchair. - - - - Dostępna dla niepełnosprawnych - - Skrzynka ukryta w sposób umożliwiający jej znalezienie osobom - poruszającym się na wózku inwalidzkim. Dotyczy to zarówno - lokalizacji (np. dojazd alejką pod samą skrzynkę), jak i sposobu - ukrycia. - - - - rollstuhltauglich - - - Rolstoel toegankelijk - - Deze cache is zo gemaakt dat de cache ook met de rolstoel te vinden - en te loggen is. - - - - - - - - - Near the parking area - - The geocache is located close to a parking area, only a few steps away. - - - - nahe beim Auto - - Der Parkplatz befindet sich in unmittelbarer Nähe zum Geocache. - Es sind nicht mehr als einige Schritte notwendig, um den Geocache zu finden. - - - - Cerca de un Parking - - El geocache se encuentra cerca de un parking, a poca distancia. - - - - Vicino all'area di parcheggio - - La geocache e posta vicino ad un'area di parcheggio, solo poco distante. - - - - - - - - - Access only by walk - - The cache is accessible by walk only. - - - - Dostępna tylko pieszo - - Skrzynka dostępna tylko pieszo. - - - - nur per Fußweg - - Der Cache ist nur zu Fuß erreichbar. - - - - - - - - - - Long walk - - This cache requires a long walk - more than 5 km round trip. In the - mountains and other steep areas, the distance for a 'long walk' may be - shorter. Walking shoes and appropriate equipment are recommended. - - - - längere Wanderung - - Bei diesem Cache erwartet euch eine Wanderung von mehr als 5 Kilometer, - vom Ausgangspunkt bis zum Cache und wieder zurück. Im Gebirge und bei - entsprechenden Steigungen kann das Attribut auch bei kürzeren Wegstrecken - gesetzt sein. Gute Wanderschuhe und entsprechende Ausrüstung empfehlen sich. - - - - Larga caminata - - Esta cache requiere una larga caminata - más de 5 kilometros de ida y - vuelta. En las montanas escarpadas o en otras áreas. Recomendados calzado - para caminar y equipo adecuado. - - - - Lunga camminata - - Questa cache richiede una lunga camminata - piu di 5 km tra andata e - ritorno. In montagna o in altre aree ripide, la distanza per una cache - del genere puo essere minore. Sono raccomandati scarpe da escursione ed - equipaggiamento adeguato. - - - - Lange wandeling - - - - - - - - Swamp, marsh or wading - - This cache requires passing swampy or marshy ground our wading throuh - shallow water. Wear appropriate clothes. After rainfall, the terrain - may be very demanding or not passable at all. - - - - sumpfiges/matschiges Gelände / waten - - Bei diesem Cache geht es durch sumpfiges oder matschiges Gelände oder - es muss durch flaches Wasser gewatet werden. Entsprechende Kleidung - wird empfohlen. Nach Regenfällen kann das Gelände wesentlich schwerer - oder überhaupt nicht begehbar sein. - - - - Pantano / terreno fangoso - - Esta cache requiere la superación de pantanos. Usar ropa apropiada. - Después de la lluvia, el suelo puede ser difícil o no factible. - - - - Palude o marcita - - Questa cache richiede il superamento di terreno paludoso o - acquitrinoso. Indossare un abbigliamento adeguato. Dopo la pioggia, - il terreno puo essere molto impegnativo o non praticabile a tutti. - - - - - - - - Hilly area - - One or more ascents lie between you and the cache. - - - - hügeliges Gelände - - Auf dem Weg zum Geocache bzw. während der Cachesuche sind eine oder - mehrere Steigungen zu überwinden. - - - - Terreno montanoso - - Una o más pendientes para acceder al cache. - - - - Area collinare - - Una o piu salite si trovano tra voi e la cache. - - - - - - - - - Some climbing (no gear needed) - - This cache requires some climbing and you may have to use your hands, - but you won’t need climbing gear. Be very careful during rainy weather - or before thunderstorms! - - - - leichtes Klettern (ohne Ausrüstung) - - Während der Cachesuche ist leichtes Klettern notwendig, bei dem man sich - z.B. mit den Händen festhalten muss. Gute Trittsicherheit und - Schwindelfreiheit empfehlen sich. Es ist jedoch keine Spezialausrüstung - notwendig wie z.B. Sicherungsseil, Klettersteigset oder Steigeisen. - Besonders bei feuchter Witterung oder vor Gewittern sollte man mit der - entsprechenden Vorsicht handeln. - - - - fácil de subir (sin equipo) - - Esta cache requiere un poco de escalada y puede ser necesario usar las - manos, pero no es necesario material de montana. !Tenga mucho cuidado - durante la temporada de lluvias.! - - - - Arrampicata (attrezzatura non necessaria) - - Questa cache richiede un po' di arrampicata e potrebbe essere necessario - usare le mani, ma non c'e bisogno di attrezzatura da arrampicata. - Prestare molta attenzione durante la stagione delle piogge o prima dei - temporali! - - - - - - - - - Swimming required - - This cache requires crossing a river or a lake. The water can be steep. - - - - Schwimmen erforderlich - - Auf dem Weg zum Geocache muss ein Fluss oder See überquert werden. - Das Wasser ist tief genug um zu schwimmen. Je nach Örtlichkeit kann auch - ein Schlauchboot, Kajak oder ähnliches verwendet werden (näheres ist - in der Beschreibung zum Geocache zu finden). Die Entfernung ist aber ohne - besondere Ausdauer noch zu schwimmen. - - - - Requiere nadar - - Esta cache requiere cruzar un río o un lago. El agua es lo suficientemente - profundo para nadar. Puede utilizar un barco, pero la distancia es lo - suficientemente corto como para ser asequible para un nadador. - - - - Nuoto necessario - - Questa cache richiede l'attraversamento di un fiume o un lago. L'acqua e - abbastanza profonda per nuotare. E possibile utilizzare una barca, ma la - distanza e abbastanza breve per essere alla portata di un nuotatore medio. - - - - - - - - - Access or parking fee - - You must pay an access or parking fee to access this cache. - - - - Zugangs- bzw. Parkentgelt - - Um zum Cache zu gelangen, müsst ihr entweder einen Eintritt oder eine - Parkgebühr bezahlen. - - - - Acceso o parking pagando - - Deberas pagar un acceso o estacionamiento para acceder a esta cache. - - - - Tassa di ingresso o di parcheggio - - Devi pagare un accesso o parcheggio a pagamento per accedere a questa cache. - - - - - - - - - - Bikes allowed - - You can reach the cache by bike. - - - - Dostępna rowerem - - Można dojechać do skrzynki rowerem. - - - - Fahrräder erlaubt - - Der Cache ist mit dem Fahrrad erreichbar. - - - - Fietsen toegestaan - - Deze cache is ook met de fiets te doen. - - - - - - - - - Hidden in natural surroundings (forests, mountains, etc.) - - The cache is hidden in a remote and quick place - a forest, wild - meadow, a swamp, etc. - - - - Umiejscowiona na łonie natury (lasy, góry, itp.) - - Umiejscowiona na łonie natury, z dala od cywilizacji - las, dzika - łąka, mokradła. - - - - in der Natur - - Der Cache ist abseits der Zivilisation in natürlicher Umgebung - versteckt – in einem Wald oder einer Wiese, einem Sumpf etc. - - - - Cache met veel natuur - - Deze cache loopt door de natuur. Zoals bossen, heide, moerasgebieden enz. - - - - - - - - Historic site - - The cache is hidden near a historic site - a castle, battleplace, - cementary, old bunkers, etc. - - - - Miejsce historyczne - - W sąsiedztwie miejsca historycznego - zamku, pałacu, pola bitwy, - cmentarza, ale także fortów czy bunkrów. - - - - historischer Ort - - Der Cache ist an einem historisch bedeutsamen Ort versteckt – - einer Burg, einem Friedhof, einer ehemaligem Militäranlage etc. - - - - - - - - - Point of interest - - There is a point of interest at the cache, like a nice scenic view - or a larger castle ruin. This place is worth visiting it even - without a geocache nearby. - - - - interessanter Ort - - Der Geocache ist in unmittelbarer Nähe zu einer Sehenswürdigkeit - versteckt. Das kann ein z.B. schöner Aussichtspunkt oder eine größere - Burgruine sein. Ein Besuch würde sich auch ohne besonderen Anlass - (den Geocache) lohnen. - - - - Punto de interes - - Hay un monumento cerca del cache, como un paisaje hermoso o un castillo - en ruinas. Este lugar es digno de visitar, incluso sin un geocache cerca. - - - - Punto di interesse - - C'e un punto di interesse alla cache, come un bel panorama o di un - castello in rovina. Questo luogo merita una visita anche senza una - geocache vicina. - - - - Interesante plek - - - - - - - Hidden wihin enclosed rooms (caves, buildings etc.) - - This geocache is not hidden in the open air, but within a building, - a cave or similar. - - - - in geschlossenen Räumen (Höhle, Gebäude, etc.) - - Das Ziel des Geocaches liegt nicht im Freien, sondern zum Beispiel in - einem Gebäude oder einer Höhle. - - - - en espacios confinados (cuevas, edificios, etc) - - Este geocache no está al aire libre, esta oculto dentro de un edificio, - una cueva o similares. - - - - All'interno di stanze chiuse (caverne, edifici, ecc.) - - Questa geocache non e nascosta all'aria aperta ma all'interno di un - edificio, di una grotta o simili. - - - - - - - - Hidden under water - - This cache or one of the stages is placed underwater. - You will get wet when doing this cache. - - - - im Wasser versteckt - - Der Geocache oder eine der Stationen ist im Wasser versteckt. Um die - Aufgabe zu lösen muss man ggf. das Wasser betreten, schwimmen oder tauchen. - - - - En el agua - - Esta cache o una de sus etapas se encuentran bajo el agua. Usted debe - entrar en el agua, nadar o zambullirse. - - - - Nell'acqua - - Questa cache o uno dei suoi stadi sono posizionati sott'acqua. Devi - entrare in acqua, nuotare o fare una immersione. - - - - - - - - - - Parking area nearby - - A nearby parking area is situated as starting point for doing this cache. - - - - Parkplatz in der Nähe - - Es gibt in der Nähe einen Parklplatz, der sich als Startpunkt für die - Cachesuche eignet. - - - - Parking cercano - - Una zona de aparcamiento se encuentra cerca del punto de partida de - este cache. - - - - Parcheggio nei pressi - - Una area di parcheggio e situata nei pressi del punto di partenza di - questa cache. - - - - Parkeerplaats vlakbij - - Er is een parkeerplaats vlak bij om deze cache te beginnen. - - - - - - - - - Public transportation - - This cache is located outside of urban areas and has a public - transport station nearby. - - - - erreichbar mit ÖVM - - Dieser Cache lässt sich mit Hilfe von öffentlichen Verkehrsmitteln erreichen - und liegt außerhalb von Städten. - - - - Transporte Público - - Este cache se encuentra también fuera de las zonas urbanas y una - estación de transporte público. - - - - Trasporto pubblico - - Questa cache e situata al difuori di aree urbane e ha una stazione di - trasporto pubblico nelle vicinanze. - - - - - - - - - Drinking water nearby - - There is drinking water along the trail or near the cache. This may be - useful especially especially when doing event caches, longer hikes or - caches at probably dirty locations. - - - - Trinkwasser in der Nähe - - Während der Cachetour oder in der Nähe des Geocaches ist Trinkwasser - verfügbar. Besonders bei Event-Caches, längeren Multicaches und bei - Geocaches, wo man vermutlich schmutzig wird, kann dies hilfreich sein. - - - - Agua potable en las cercanias - - Hay agua potable a lo largo de la ruta o cerca de la cache. Este - atributo es especialmente útil en la planificación de Eventos, - o caches con viajes largos a lugares como las cuevas o minas - probablemente esté sucio. - - - - Acqua potabile nei pressi - - C'e acqua potabile lungo il percorso o nelle vicinanze della cache. - Questo attributo e utile soprattutto nella pianificazione di cache - evento, di lunghe escursioni o di cache in luoghi probabilmente - sporchi come le grotte o le miniere. - - - - - - - - - Public restrooms nearby - - There are public restrooms along the way or near the cache. - - - - öffentliche Toilette in der Nähe - - Während der Cachetour oder in der Nähe des Geocaches ist eine öffentliche - Toilette verfügbar. - - - - Aseos públicos cercanos - - Hay banos públicos a lo largo de la carretera o en las proximidades - del cache. - - - - Bagni pubblici nei pressi - - Ci sono WC pubblici lungo la strada o nelle vicinanze della cache. - - - - - - - - - Public phone nearby - - There is a public phone along the way or near the cache. - - - - Telefon in der Nähe - - Während der Cachetour oder in der Nähe des Geocaches gibt es ein - öffentliches Telefon. - - - - Teléfono Público en las cercanias - - Hay teléfonos públicos en la carretera o en las proximidades del cache. - - - - Telefono pubblico nei pressi - - Ci sono telefoni pubblici lungo la strada o nelle vicinanze della cache. - - - - - - - - First aid available - - There is a first aid station, call box, mountain rescue or similar - arrangement near the cache. - - - - Erste Hilfe verfügbar - - In der Nähe des Caches findet ihr eine Erste Hilfe-Station, Notrufsäule, - Bergwacht oder entsprechende Einrichtung. - - - - Disponible socorro rapido - - Hay un punto de socorro, un teléfono para pedir ayuda, un centro de - rescate de montana o similar cerca del cache. - - - - Disponibile pronto soccorso - - C'e un pronto soccorso, un telefono per chiamate di soccorso, un centro - di soccorso alpino o simili nelle vicinanze della cache. - - - - - - - - - Available 24/7 - - This cache can be found at any time of day or week. - - - - rund um die Uhr machbar - - Dieser Cache ist jederzeit machbar, sowohl am Tage als auch in der Nacht. - - - - Disponible las 24 horas - - Esta cache se puede encontrar tanto de día como de noche. - - - - Disponibile 24 ore - - Questa cache puo essere trovata sia di giorno che di notte. - - - - - - - - - Not 24/7 - - This cache can only be done at certain times of day or week - see the cache - description for more details. For example, the cache may be placed in an - area with restricted opening hours. - - - - Dostępna w określonych godzinach - - Dostępna w określonych dniach, godzinach, często wstęp płatny. - Często będzie to muzeum lub skansen. Szczegółowe informacje o - dostępności powinny znajdować się w opisie skrzynki. - - - - nur zu bestimmten Uhrzeiten - - Dieser Cache lässt sich nur zu bestimmten Tageszeiten absolvieren. - Nähere Angaben sind in der Beschreibung des Caches zu finden. - - - - Sólo disponible a ciertas horas - - Esta cache se puede hacer solamente en ciertos momentos del día - - véase la descripción de caché para obtener más detalles. - - - - Disponibile solo in certi orari - - Questa cache puo essere cercata solo a certe ore del giorno - - vedi la descrizione per ulteriori informazioni. - - - - - - - - - Not recommended at night - - Searching for this cache is not recommended by night. It might be - dangerous, or the cache may be hidden in an area where flashlights - may attract unwanted attention. - - - - nur tagsüber - - Dieser Cache lässt sich nur tagsüber angehen, zum Beispiel weil das Gelände - gefährlich ist oder die Suche mit Taschenlampen in einem Wohngebiet negativ - auffallen würde. - - - - solo por el día - - Deberas encontrar este cache sólo durante el día. Por ejemplo, el área pued - ser peligroso y contienen rocas o abismos. O bien, el uso de linternas puede - ser imposible porque sería sospechoso en una zona residencial. - - - - solo di giorno - - Si dovrebbe cercare questa cache solamente durante il giorno. Ad esempio, - l'area puo essere pericolosa e contenere scogliere o abissi. Oppure, - l'utilizzo di torce elettriche potrebbe essere impossibile perché - risulterebbe sospetto all'interno di una zona residenziale. - - - - - - - - - - Recommended at night - - It is recommended to search for this cache by night. I.e. there - might be some light-reflecting surfaces involved which are usually - invisible during daylight. - - - - Zalecane szukanie nocą - - Aby znaleźć skrzynkę zalecane jest poszukiwanie jej nocą, ze - względu na miejsce ukrycia lub użyte elementy odblaskowe, których - oświetlenie umożliwia odnalezienie skrzynki. - - - - am besten nachts findbar - - Es wird empfohlen, den Cache nachts zu suchen. Es können zum - Beispiel Reflektoren vorhanden sein, die tagsüber schwer oder gar - nicht erkennbar sind. - - - - - - - - - Only at night - - This geocache can be found at night only - it is a so-called night cache. - There may be reflectors which have to be flashlighted and will point - to the hiding place, or other special night-caching mechanisms. - - - - nur bei Nacht - - Der Geocache kann nur bei Nacht gelöst werden und wird deshalb Nachtcache - genannt. Zum Beispiel müssen Reflektoren mit einer Taschenlampe - angeleuchtet werden, die dann den Weg zum Versteck zeigen. - - - - Sólo por la noche - - Esta cache se puede encontrar solamente por la noche - tienes que - considerar cache notturna. Puede haber placas reflectantes que - brillaran y te llevaran al cache, o otros mecanismo especiales para - caches nocturnos. - - - - Solo di notte - - Questa cache puo essere trovata solo di notte - e una cosiddetta - cache notturna. Ci possono essere targhette riflettenti che devono - essere illuminate e ti conducono al nascondiglio, o altri speciali - meccanismi di caching notturno. - - - - - - - - - All seasons - - This cache can be found the whole year round, while difficulty may - depend on seasons. - - - - ganzjährig zugänglich - - Dieser Cache lässt sich während des gesamten Jahres finden, wobei je - nach Jahreszeit die Schwierigkeit bei der Suche schwanken kann. - - - - Todas las temporadas - - Esta cache se encuentrar durante todo el ano, mientras que la dificultad - puede depender de las estaciones. - - - - Tutte le stagioni - - Questa cache si trova tutto l'anno, mentre la difficolta puo dipendere - dalle stagioni. - - - - - - - - - Only available during specified seasons - - This cache can be done at certain seasons only - see the cache - description for more details. - - - - nur zu bestimmten Zeiten im Jahr - - Dieser Cache lässt sich nur zu bestimmten Zeite im Jahr absolvieren. - Näheres ist in der Cachebeschreibung angegeben. - - - - Sólo disponible durante las estaciones especificadas - - Esta cache se puede hacer en ciertas épocas del ano solamente - vea la - descripción de cache para obtener más detalles. - - - - Disponibile solo in certe stagioni - - Questa cache puo essere cercata solo in certe stagioni - vedi la - descrizione per ulteriori informazioni. - - - - - - - - Breeding season / protected nature - - Don’t seek this cache during animal breeding season! See the cache - description on which time of year must be avoided. Also, pay - attention to the local terms and signs regarding nature protection. - - - - Brutsaison / Naturschutz - - Dieser Cache sollte in der Brutsaison nicht absolviert werden. In der - Beschreibung sollte angegeben sein, welche Jahreszeit davon betroffen ist. - Achte bitte auch auf die örtliche Beschilderung zum Naturschutz. - - - - Temporada de reproducción / protección de la naturaleza - - !No intente esta cache durante la temporada de cría de los animales! - Vvéase la descripción del cache de la época del ano debe ser evitado. - Preste atención también a las condiciones o signos en cuanto al respeto - por la naturaleza. - - - - Stagione di riproduzione / natura protetta - - Non cercare questa cache durante il periodo riproduttivo degli animali! - Vedi descrizione della cache quale periodo dell'anno debba essere evitato. - Prestate anche attenzione alle condizioni o ai cartelli riguardo il - rispetto della natura. - - - - - - - - - Available during winter - - This cache can be found even after heavy snowing. All stages and the - geocache are hidden in a snow-safe way: they will not be covered by - fallen snow, or ice, etc. - - - - schneesicheres Versteck - - Dieser Cache lässt sich auch nach starkem Schneefall suchen. Die einzelnen - Stationen und der Geocache sind so versteckt, dass sie nicht von Schnee - verdeckt werden, bzw. von Schneehaufen, die durch Räumfahrzeuge entstehen. - - - - Nieve en el escondite - - Este cache también se puede encontrar después de fuertes nevadas. Todas - las fases y geocaches se esconde en lugares seguros para la caída de la - nieve, no será cubierto por acumulaciones de nieve. - - - - Luogo a prova di neve - - Questa cache puo essere trovata anche dopo forti nevicate. Tutte le fasi - e la geocache sono nascosti in luoghi sicuri per la neve: non saranno - coperti da neve caduta né da cumuli di neve creati ad esempio da veicoli - spalaneve. - - - - - - - - Not at high water level - - This cache can be done only at low or normal water level. It is - inaccessible during flood. - - - - nicht bei Hochwasser oder Flut - - Der Geocache kann nur bei bei niedrigem oder normalem Wasserstand - bzw. bei Ebbe gesucht werden. Bei Hochwasser oder Flut ist er - unzugänglich. - - - - - - - - - - Compass required - - A compass is required. - - - - Potrzebny kompas - - Kompas może okazać się niezbędny aby dotrzeć do wskazanego miejsca - skrzynki. - - - - Kompass - - Für diesen Cache braucht ihr einen funktionierenden Kompass für Peilungen - oder Orientierungen. - - - - Brújula - - Se necesita una brújula. - - - - Bussola - - E' necessaria una bussola. - - - - Kompas nodig - - - - - - - - Take something to write - - There is no pencil in the cache. Take something to write with. - - - - Weź coś do pisania - - Skrzynka nie zawiera ołówka, weź ze sobą coś do pisania. - - - - Stift wird benötigt - - Es ist kein Stift zum Loggen im Cache; bringe bitte einen eigenen mit. - - - - Neem iets mee om te schrijven - - - - - - - - You may need a shovel - - The cache may require more digging. A shovel might come in handy. - - - - Potrzebna łopatka - - Skrzynka jest zakopana w ziemi. - - - - Schaufel / vergrabener Cache - - Der Cache ist vergraben. Wahrscheinlich benötigst du eine Schaufel, - um ihn zu heben. - - - - - - - - - - - Flashlight required - - You will need a flashlight to find this cache. - - - - Potrzebna latarka - - Przy poszukiwaniach tej skrzynki potrzebna jest latarka. - - - - Taschenlampe - - Um diesen Cache anzugehen, benötigt ihr eine funktionstüchtige - Taschenlampe. Denkt auch an Ersatzbatterien. - - - - Linterna - - Es necesario una linterna para encontrar este cache. !No se olvide de las - baterías de repuesto! - - - - Lampada tascabile - - E' necessaria una torcia portatile per trovare questa cache. Non - dimenticate le batterie di riserva! - - - - - - - - - Climbing gear required - - For this cache, you will need climbing equipment and the knowledge - how to use it properly. If you are a beginner, don’t do it alone but - use the support of an experienced climber or mountaineer. - - - - Kletterzeug - - Um diesen Cache absolvieren zu können, benötigt ihr neben der normalen - Ausrüstung auch noch Kletterausrüstung, und entsprechendes Wissen um - deren Handhabung und ums Klettern. Laien sollten sich auf jeden Fall - von einem erfahrenen Kletterer oder Bergsteiger unterstützen lassen. - - - - Equipo de escalada - - Para este cache, tendrá que utilizar los equipos y saber cómo utilizarlo - correctamente. Si usted es un principiante, no lo haga solos, sino que - utiliza el apoyo de un experimentado escalador o alpinista. - - - - Attrezzatura per arrampicata - - Per questa cache, avrete bisogno di materiale da arrampicata e di saperlo - usare correttamente. Se sei un principiante, non farlo da solo, ma - utilizza il sostegno di uno scalatore esperto o un alpinista. - - - - - - - - Cave equipment required - - This geocache is hidden in a cave, and you should use appropriate - equipment to access it. Beware: Even small caves may confront you with - unforeseen problems and dangers, like thunder storms (water!) or a - sprained ankle. Have advice first from cave-experienced people! Also - take care of protected nature; e.g. bat places must not be disturbed. - - - - Höhlenzeug - - Der Geocache ist in einer Höhle versteckt und man sollte entsprechende - Ausrüstung mitbringen. Vorsicht: Bereits kleinste Höhlensysteme können - bei unvorhergesehenen Problemen, z.B. Gewittern (Wasser!) oder einem - verstauchten Knöchel, sehr gefährlich werden! Ihr solltet euch vorab - gründlich bei erfahreren Höhlengehern informieren. Beachtet auch den - Naturschutz – Fledermausquartiere dürfen nicht gestört werden! - - - - Equipación para cuevas - - Este geocache está escondido en una cueva, y se debe utilizar el equipo - adecuado para acceder a ella. Tenga en cuenta que incluso las pequenas - cuevas pueden prever los problemas imprevistos y peligros, como durante - las tormentas o con un esguince de tobillo. !Acceda con personas - experimentadas en cuevas! También debe protegerse la naturaleza sobre - todo en esos lugares donde los murciélagos no deben ser molestados. - - - - Attrezzatura per grotta - - Questa geocache e nascosta in una grotta, e si dovrebbe utilizzare - attrezzature adeguate per accedervi. Attenzione: anche piccole grotte - possono prevedere problemi imprevisti e pericoli, come in caso di - temporali (acqua!) o una caviglia slogata. Consigliatevi prima con - persone che abbiano esperienza di grotte! Abbiate anche cura della - natura protetta, ad esempio dei luoghi dove i pipistrelli non devono - essere disturbati. - - - - - - - - - Diving equipment required - - You will need diving equipment to find this geocache. The water depth - of the cache location is specified in the description. Please note that - secure diving requires special training. Without diving experience, - you may search this cache in company of a diving teacher. - - - - Taucherausrüstung - - Um den Geocache zu finden benötigt ihr eine Tauchausrüstung. In welcher - Tiefe der Geocache liegt, ist in der Beschreibung angegeben. Bitte beachtet, - dass Ihr für einen sicheren Tauchgang eine entsprechende Ausbildung - benötigt. Als Nicht-Taucher könnt ihr den Geocache evtl. zusammen mit - einem Tauchlehrer suchen. - - - - Diving equipment - - Necesitará un equipo de buceo para encontrar este geocache. La - profundidad del agua en la ubicación de la cache se especifica en la - descripción. Tenga en cuenta que el buceo requiere un entrenamiento - especial. Sin experiencia de buceo, puedes buscar por el caché, junto - con un buceador experimentado. - - - - Equipo de buceo - - Avrete bisogno di attrezzatura subacquea per trovare questa geocache. - La profondita d'acqua nella posizione della cache viene specificata nella - descrizione. Si prega di notare che l'immersione in tutta sicurezza - richiede una formazione specifica. Senza esperienza di immersioni, e - possibile cercare questa cache in compagnia di un insegnante di sub. - - - - - - - - - - - Special tools required - - You will need special equipment which is not specified by other attributes. - See the cache description on what tools are required. - - - - Wymagany dodatkowy sprzęt - - Niezbędny jest dodatkowy, niestandardowy sprzęt - może to być np. - kajak, sprzęt wspinaczkowy, ale również kalkulator, kalosze itp. - Ogólnie przedmioty, które nie należą do standardowego wyposażenia - poszukiwacza. - - - - spezielle Ausrüstung - - Für diesen Cache benötigst du weitere Ausrüstung, die nicht durch die - anderen Attribute angegeben ist und nicht zur Standardausrüstung eines - Geocachers gehört. Was genau du benötigst, ist in der Beschreibung - angegeben. - - - - Equipamiento especial - - Necesitarás un equipo especial no especificado por otros atributos. - - - - Equipaggiamento speciale - - Avrete bisogno di attrezzature speciali non specificate da altri attributi. - - - - - - - - - - - Requires a boat - - This cache can usually be found only when using a watercraft. - Swimming is difficult or impossible because of the distance or currents. - See the cache description for more details. - - - - Wymaga sprzętu pływającego - - Skrzynka z tym atrybutem najczęściej może być zdobyta jedynie przy - użyciu sprzętu pływającego (łodzi, pontonu, kajaka itp.) Dopłynięcie - wpław jest trudne lub niemożliwe, ze względu na dystans, silne - prądy itp. - - - - Wasserfahrzeug - - Der Geocache kann – normalerweise – nicht ohne ein Wasserfahrzeug gefunden - werden. Zum Geocache kann wegen der Entfernung oder Strömung nicht - geschwommen werden. Details dazu sind in der Beschreibung des Geocaches - angegeben. - - - - Barca - - Este cache por lo general sólo se puede encontrar con una moto de agua. - Nadando es imposible debido a la distancia o la corriente. Véase la - descripción del cache para obtener más detalles. - - - - Barca - - Questa cache di solito puo essere trovata solo con una moto d'acqua. Il - nuoto e impossibile a causa della distanza o delle correnti. Vedi la - descrizione della cache per maggiori dettagli. - - - - - - - - No GPS required - - This cache can be found without a GPS device. No additional coordinates - are used besides of the starting coordinates. - - - - ohne GPS findbar - - Dieser Cache lässt sich auch ohne GPS-Empfänger finden. Die Aufgaben - sind so gestellt, dass man außer den Startkoordinaten keine weiteren - Koordinaten verwenden muss. - - - - Sin GPS - - Esta cache se puede encuentra sin un dispositivo GPS. Detalles adicionales - no se utilizan, además de las coordenadas iniciales. - - - - Senza GPS - - Questa cache puo essere trovata senza un dispositivo GPS. Non sono - utilizzate coordinate addizionali oltre alle coordinate iniziali. - - - - - - - - - - - Dangerous area - - The cache is located within a dangerous area, and danges may not be - obvious, e.g. like high-traffic roads, steep ground or falling rocks. - Safety measures should be taken, especially when geocaching with - children, large groups of people or during bad weather conditions. - - - - Skrzynka niebezpieczna - - Skrzynka jest ukryta w niebezpiecznym terenie. Jej poszukiwania mogą - narazić na niebezpieczeństwo wypadku lub urazu. - - - - gefährliches Gebiet - - In dem Gebiet, wo der Geocache versteckt wurde, ist mit Gefahren zu - rechnen, die unter Umständen nicht auf den ersten Blick erkennbar sind. - Das können z.B. stark befahrene Straßen, steile Abhänge oder Steinschlag - sein. Deshalb sollte man bei Geocaching-Touren mit Kindern oder größeren - Gruppen entsprechende Vorsichtsmaßnahmen ergreifen und je nachdem auch - auf die Witterung achten (z.B. Regen bei steilen Abhängen). - Näheres zu den Gefahren ist in der Cachebeschreibung erläutert. - - - - Zona Peligrosa - - El cache está situado en una zona peligrosa, como tales como carreteras - con mucho tráfico, terreno empinado o caída de rocas. Usted debe tomar - medidas de seguridad o evitar ir a buscar el caché, sobre todo con ninos, - con grupos grandes o en condiciones meteorológicas adversas. - - - - Area pericolosa - - La cache e situata in un'area pericolosa come strade ad alto traffico, - terreno ripido o caduta sassi. Si dovrebbero adottare misure di sicurezza - o evitare di andare a cercare la cache, in particolare nel geocaching con - bambini, con gruppi numerosi o in condizioni climatiche sfavorevoli. - - - - - - - - Active railway nearby - - There are active railroads nearby. Please be careful, keep a safe - distance and cross the rails only at level crossings etc.! - - - - aktive Eisenbahnlinie in der Nähe - - In der Nähe dieses Caches gibt es genutzte Eisenbahnlinien. Bitte seid - entsprechend vorsichtig und achtet darauf, abseits von Bahnübergängen keine - Gleise zu betreten. - - - - Cerca del ferrocarril activo - - !Hay ferrocarriles activos en las proximidades. Por favor, tenga - cuidado, manteniendo una distancia segura y cruzar los rieles sólo - en los cruces de ferrocarril, etc.! - - - - Ferrovia attiva nei pressi - - Ci sono ferrovie attive nelle vicinanze. Per favore usate cautela, - tenendo una distanza di sicurezza e attraversando le rotaie solo ai - passaggi a livello ecc.! - - - - - - - - - Cliff / Rocks - - There are cliffs or dangerous rocks nearby. Beware of falling rocks - at the lower side, and be careful at the upper side of cliffs - - especially with children and while mountain biking. It can be very - dangerous to take a steep slope towards a cliff, because you may not - notice in time where the former ends and the latter starts. - - - - Klippen / Felsen - - In der Nähe des Caches gibt es Klippen oder Felsen. Unterhalb von - Felsen sollte man auf Steinschlag achten, von der Oberseite der Klippen - sollte man sich entsprechend vorsichtig nähern (insbesondere mit Kindern - oder Mountainbikes). Besonders gefährlich - und nicht immer erkennbar - - ist es, sich über einen Steilhang von oben an eine Klippe zu nähern. - - - - Acantilado / Rocas - - Hay acantilados o rocas peligrososas en las cercanas. Tenga cuidado - cuando esté bajo las piedras caídas, y tenga cuidado cuando esté sobre - el acantilado - especialmente con los ninos y el ciclismo. Puede ser - muy peligroso tomar un camino empinado para subir el acantilado porque - no se puede saber de antemano cuando el primero termina y comienza otra. - - - - Scogliera / Rocce - - Ci sono scogliere o rocce pericolose nelle vicinanze. Fate attenzione - alla caduta pietre quando siete sotto, e siate cauti quando siete sopra - la scogliera - specialmente con bambini e in bicicletta. Puo essere molto - pericoloso prendere un sentiero ripido per salire la scogliera, poiché - non potete sapere in anticipo quando la prima termina e inizia l'altra. - - - - - - - - - - Hunting grounds - - The geocache is placed within a hunting ground. At twilight and in the - dark, a flashlight or headlight should always be used for security - reasons. Be considerate when meeting hunters. - - - - Jagdgebiet - - Der Geocache liegt in einem Jagdgebiet. Bei Dämmerung oder Dunkelheit - sollte man aus Sicherheitsgründen immer eine Taschenlampe oder - Stirnlampe verwenden. Bei Begegnungen mit Jägern ist gegenseitige - Rücksichtnahme angebracht. - - - - Zona de Caza - - El geocache se coloca dentro de un coto de caza. Al caer la tarde y en - la oscuridad, una linterna o faro siempre debe utilizarse por razones - de seguridad. - - - - Caccia - - La geocache e situata nei pressi di una area di caccia. Al crepuscolo - e al buio, dovrebbe sempre essere usata una torcia portatile o frontale - per ragioni di sicurezza. Incontrando i cacciatori e opportuna una - reciproca gentilezza. - - - - Let op: jachtgebied - - - - - - - - - Look out for thorns - - There are thorns near the cache. Wear appropriate clothes. - - - - Dornen - - In er Nähe des Geocaches gibt es Dornen. Entsprechende Kleidung und - evtl. Handschuhe sind zu empfehlen. - - - - Espinas - - Hay espinas cerca de la caché. Use ropa apropiada. - - - - Spine - - Ci sono spine nei pressi della cache. Indossare indumenti appropriati. - - - - Let op: doornen - - Er kunnen doornen bij de cache zijn. Draag beschermende kleding. - - - - - - - - - - Look out for ticks - - There are seasonably many ticks in this area. It is recommended to wear - long trousers and to check yourself for ticks after geocaching. - There are regional risk maps for tick-borne encephalitis on the - internet. - - - - Zecken - - Je nach Saison gibt es in dem Gebiet besonders viele Zecken. Es wird - daher empfohlen, entsprechend lange Kleidung zu tragen und nach der - Cachetour nach Zecken Ausschau zu halten. FSME-Risikogebiete und - weitere Informationen zum Thema Zecken könnt ihr z.B. auf - www.meningitis.de nachsehen. - - - - Garrapatas - - Cada temporada hay un montón de garrapatas en este lubar. Y es - recomendable llevar pantalón largo y examinarse en busca de garrapatas - después de encontrar el cache. - - - - Zecche - - Stagionalmente ci sono molte zecche in questa area. E' raccomandabile - indossare pantaloni lunghi e ispezionarsi alla ricerca di zecche dopo - il geocaching. In internet ci sono mappe di rischio per encefalite - e borelliosi da morso di zecca. - - - - Let op: teken - - In dit gebied zouden teken voorkomen. Neem voorzorgen om geen teken - op te lopen door beschermende kleding te dragen. Vor meer informatie - kijk op www.meningitis.de - - - - - - - - - Abandoned mines - - This cache leads into a (former) mining area. There may be dangers by - collapsing adits, or you may need to enter adits. Be careful and use - appropriate equipment, especially in the dark. Old mines may be covered - by historic preservation. - - - - Folgen des Bergbaus - - Der Cache führt in eine (ehemalige) Bergbauregion. Möglicherweise - bestehen Gefahren durch verstürzte Stollenmundlöcher oder es müssen - Stollen betreten werden. Entsprechende Ausrüstung und Vorsicht, - besonders bei Dunkelheit, wird empfohlen. Historische Bergwerke stehen - möglicherweise unter Denkmalschutz. - - - - Mina abandonada - - Esta cache le llevará a un área de la mina (abandonado). Puede haber - peligro con el colapso de túneles o galerías que puede ser necesario para - cruzar. Tenga cuidado y use de equipo adecuado, especialmente en la - oscuridad. Las minas antiguas pueden ser objeto de preservación histórica. - - - - Miniere abbandonate - - Questa cache vi porta in una area di miniera (abbandonata). Ci possono - essere pericoli per crollo di gallerie, o potrebbe essere necessario - attraversare gallerie. Fare attenzione e utilizzate attrezzature adeguate, - soprattutto al buio. Le vecchie miniere possono essere oggetto di - conservazione storica. - - - - - - - - - Poisonous plants - - There are poisonous plants near the cache. Take care and prevent - children and dogs from touching or eating them. - - - - giftige Pflanzen - - In der Nähe des Caches gibt es giftige Pflanzen. Achtet also insbesondere - darauf, dass Kinder und Hunde diese nicht anfassen oder essen. - - - - Planta venenosa - - Hay plantas venenosas en las cercanías. Tenga cuidado y asegúrese de que - los ninos o los perros no las toquen ni tragarlas. - - - - Piante velenose - - Ci sono piante velenose nelle vicinanze. Fate attenzione e controllate - che bambini o cani non le tocchino o le ingoino. - - - - - - - - - Dangerous animals - - The area is inhabited by possibly dangerous animals, e.g. rabies areas, - venomous snakes, scorpions or bears. - - - - giftige/gefährliche Tiere - - In dem Gebiet sind Wildtiere angesiedelt, die für Menschen eine Gefahr - darstellen können, z.B. Tollwutgebiete, giftige Schlangen, Skorpione - oder Bären. - - - - Animales Peligrosos - - Esta zona es frecuentada por los animales potencialmente peligrosos, - por ejemplo. zorros rabiosos, serpientes venenosas, escorpiones, osos. - - - - Animali pericolosi - - Quest area e frequentata da animali potenzialmente pericolosi, ad es. - volpi rabide, serpenti velenosi, scorpioni, orsi. - - - - - - - - - Quick cache - - It shouldn't take more than 15 minutes to find this cache. Also, - there should be a parking nearby. - - - - Szybka skrzynka - - Jej znalezienie nie powinno zająć więcej niż 15 minut oraz jest - łatwy dojazd w pobliże skrzynki samochodem. - - - - schnell zu finden - - Der Cache ist üblicherweise in maximal 15 Minuten zu finden. - In der Nähe sollte auch ein Parkplatz sein. - - - - Een snelle oppikker - - - - - - - Overnight stay necessary - - This cache cannot be done within a single day or a single night. - You will have to visit the location for more than one time, - or you must stay overnight. Preparation time is not included in this - calculation, but only the time on site. - - - - Übernachtung erforderlich - - Der Geocache kann nicht mit einer einzigen Tages- oder Nachttour gelöst - werden. Er muss entweder mehrmals angefahren werden oder es muss vor Ort - übernachtet werden. Zeit für Recherchen vorab sind dabei nicht - berücksichtigt, sondern nur die Zeit vor Ort. - - - - Necesario pernoctar - - No puedrá encontrar este cache en un solo día o durante la noche. Usted - tendrá que visitar el lugar más de una vez, o necesitará pasar la noche. - El tiempo de preparación no está incluido en este cálculo, sólo el tiempo - en el sitio. - - - - Necessario pernottamento - - Non e possibile trovare questa cache in un solo giorno o una sola notte. - Dovrete visitare il percorso per piu di una volta, oppure e necessario il - pernottamento. Il tempo di preparazione non e incluso in questo calcolo, - ma solo il tempo sul sito. - - - - - - - - - - Take your children - - This search if simple and safe. It's okay to take small children - with you. - - - - Można zabrać dzieci - - Jej poszukiwanie jest przyjemne, bezpieczne i można bez obaw - wybrać się z małymi dziećmi. - - - - für kleine Kinder geeignet - - Der Cache ist einfach und – nach Einschätzung des Besitzers – - ohne besondere Gefahren findbar. - - - - Kindvriendelijke cache - - Dit is een makkelijke en veilige cache. De kleine kinderen - kunnen ook aan deze cache deelnemen. - - - - - - - - - Suited for children (10-12 yo) - - This geocache is suitable for children. All challenges can be solved by - child in the age of 10 to 12 years and the terrain has no risks - (like highways, abysms). There should be a large geocache container with - trading items inside and the challenges be interesting. - - - - kindgerecht (10-12 Jahre) - - Der Geocache ist kindgerecht aufgebaut: Alle Aufgaben sind von Kindern - im Alter von 10 bis 12 Jahren selbstständig lösbar und das Gelände ist - nicht gefährlich (keine Haupstraßen, Klippen o.ä.). Am Ende des - Geocaches sollte sich eine Box mit Tauschgegenständen befinden, und - die Aufgaben sollten interessant aufgebaut sein. - - - - Apto para ninos (10-12 anos) - - Este geocache se creó para los ninos. Todas las tareas se puede - completar por los ninos entre los anos 10 y 12 y el terrno no está - exenta de riesgo (tales como carreteras, acantilados). Hay un gra - contenedor con intercambio final y las tareas son interesantes. - - - - Suited for children (10-12 anni) - - Questa geocache e stata creata per i bambini. Tutte i compiti possono - essere portati a termine da bambini tra 10 e 12 anni e il terreno non - presenta rischi (come autostrade, abissi). C'e un grande contenitore - finale con oggetti di scambio e i compiti sono interessanti. - - - - - - - - Reverse Cache - - This geocache can be found at different places. The places to look for - are explained in the cache description. They must be located "away from - home" and exist for a considerable period of time, so that it can be - re-visited later. There is no container and no logbook; instead the - find must be documented by a photo and noting the coordinates. - - - - Safari-Cache - - Dieser Geocache kann an verschiedenen Orten gefunden werden. Die - gesuchten Orte sind in der Cachebeschreibung erläutert. Sie müssen - sich „außer Haus“ befinden und über längere Zeit Bestand haben, - sodass man sie nach einiger Zeit wieder besuchen kann. Es gibt keinen - Behälter und kein Logbuch, sondern der Fund ist mit einem Foto und - den Koordinaten des Ortes zu dokumentieren. - - - - - - - - - Available at specified hours (may require access fee) - - This cache can only be done at certain times of day or week - see the cache - description for more details. For example, the cache may be placed in an - area with restricted opening hours. Also, you may be required to pay a fee - in order to enter - see the cache description for details. - - - - Dostępna w określonych godzinach lub płatna - - Dostępna w określonych dniach, godzinach, często wstęp płatny. - Często będzie to muzeum lub skansen. Szczegółowe informacje o - dostępności powinny znajdować się w opisie skrzynki. - - - - nur zu bestimmten Uhrzeiten, Eintrittsgeld - - Der Cache kann nur zu bestimmten Uhrzeiten oder an bestimmten Wochentagen - gefunden werden; mehr dazu in der Cachebeschreibung. Häufig handelt es - sich um Caches in Museen etc., die Eintrittsgeld verlangen. - - - + + + + + + + Listed at Opencaching only + + This geocache is listed at Opencaching only. It is not listed + on any other geocaching site. + + + + Dostępna tylko na Opencaching + + Skrzynka "jednosystemowa", dostępna jedynie poprzez serwis Opencaching. + + + + nur bei Opencaching logbar + + Der Geocache ist nur bei Opencaching gelistet. Benutzer anderer + Geocache-Datenbanken haben so einen schnellen Überblick, welche Geocaches + es sich lohnt näher anzusehen. + + + + Alleen te loggen op Opencaching + + Deze cache is alleen beschikbaar en te loggen op opencaching. + + + + Solo loggeable en Opencaching + + Este geocachee esta publicado sólo en Opencaching. Este atributo permite + a los usuarios de otras plataformas encontrar rápidamente caches + interesantes de calidad OC geocaching. + + + + Loggabile solo su Opencaching + + Questa geocache è pubblicata solo su Opencaching. Questo attributo permette + agli utenti di altre piattaforme di geocaching di trovare velocemente + interessanti cache OC di qualità. + + + + + + + + + + Near a Survey Marker + + The cache is hidden in near proximity of a survey marker (also known + as geodetic marks). + + + + W pobliżu punktu geodezyjnego + + Skrzynka ukryta w pobliżu punktu geodezyjnego. + Więcej informacji. + + + + an einem Vermessungspunkt + + Der Cache ist in der Nähe eines festen Vermessungspunktes versteckt. + + + + Meetpunt + + Deze cache heeft te maken met een meetpunt of geodetisch punt. + + + + + + + + Wherigo Cache + + Cache description includes a file - the Whereigo cartridge. In order to + find the cache, you need to download the file and install it on + a proper compatible device. + + + + Wherigo Cache + + Opis skrzynki zawiera scenariusz Wherigo (w Polsce znany również pod + skrótem WIGO). Aby móc zdobyć skrzynkę należy pobrać scenariusz i + wgrać go do kompatybilnego z nim urządzenia. + Więcej informacji. + + + + Wherigo-Cache + + Zur Cachebeschreibung gehört eine Datei, die „Wherigo-Cartridge“. Sie muss + heruntergeladen und auf ein Gerät mit einer entsprechenden Abspielsoftware + installiert werden. Der Inhalt der Cartridge führt dich durch den Cache. + + + + + + + + + + Letterbox Cache + + There is a stamp in the cache for stamping your personal logbook, and the + cache’s logbook will be stamped with your personal stamp. Take care not + to mix up stamps and to leave the cache’s stamp in the cache! + + + + Skrzynka typu Letterbox + + W skrzynce znajduje się pieczątka, której nie można zabrać ze sobą. + Możesz jej użyć do ostemplowania swojego osobistego dziennika. + Logbook skrzynki powinien z kolei zostać ostemplowany Twoją własną + pieczątką. + Więcej informacji. + + + + Letterbox (benötigt Stempel) + + In dem Behälter vor Ort befindet sich ein Stempel, mit dem man sein + persönliches Logbuch abstempeln kann. Das Logbuch im Geocache wird + ebenfalls mit einem persönlichen Stempel signiert. Bitte achte unbedingt + darauf, dass du den Stempel aus dem Geocache nicht mitnimmst oder tauschst! + Weitere Informationen. + + + + Letterbox (een stempel nodig) + + Er bevind sich een stempel in de cache waarmee je een in een eigen logboek + kan stempelen. Met een eigen stempel kun je het logboek in de cache stempelen. + Let op om niet per ongeluk de stempels te verwisselen! + + + + Letterbox (necesita un estampador) + + Hay un sello en el cache para estamparlo en su libro de registro personal, + y el libro de registro del cache será sellada con su sello personal. + !Tenga cuidado de no mezclar los sellos! + + + + Letterbox (richiede un timbro) + + C'e un timbro nella cache per timbrare il tuo quaderno personale, + e il log della cache verra timbrato con il tuo timbro personale. + Fate attenzione a non confondere i timbri e a lasciare il timbro della + cache nella cache! + + + + + + + + + GeoHotel Cache + + Primary purpose of the "GeoHotel" caches is to exchange trackables + (TravelBugs, GeoKrets, etc.). + + + + GeoHotel Cache + + Atrybut ten oznacza skrzynki których głównym celem jest lokowanie + w tej skrzynce różnych wędrujących rzeczy np GeoKrety, GeoLutins, + GeoFish itp. + + + + Geohotel-Cache + + Der Cache dient vor allem zum Austausch von Reisenden (GeoKrets, + TravelBugs etc.). + + + + GeoHotel + + Deze cache is speciaal voor reizende items zoals: GeoKrets, + TravelBugs enz. + + + + + + + + + Magnetic Cache + + This geocache is attached with a magnet. + + + + Przyczepiona magnesem + + Skrzynka zawiera magnes i przymocowana jest za jego pomocą. + + + + magnetischer Cache + + Der Geocache ist mit einem Magneten angebracht. + + + + Bevestigd met magneet + + Deze cache is met een magneet bevestigt. + + + + + + + + + Description contains an audio file + + In order to find this cache, you must listen to an audio recording + (e.g. MP3 file), which is attached to the cache description. + + + + Opis zawiera plik audio + + Aby odnaleźć skrzynkę, należy odsłuchać plik dźwiękowy zawarty w + opisie skrzynki (np. w formacie MP3). Może on zawierać zakodowane + informacje, dźwięki otoczenia, opis dotarcia do skrzynki. + + + + Audio-Cache + + Um diesen Cache zu finden musst du eine Tonaufnahme anhören, + die der Cachbeschreibung beigefügt ist. + + + + Beschrijving bevat een audio bestand + + Om deze cache te vinden moeten instructies via een audio bestand + (bijv. MP3) beluisterd worden welke in de cachebeschrijving te vinden is. + + + + + + + + Offset cache + + A specific type of a MultiCache. The coordinates point to a starting + point. The description contains simple instructions to follow + once you are in the starting point (usually, an azimuth and a + distance). + + + + Offset cache + + Szczególny przypadek skrzynki typu multicache, składający się z + punktu startowego (określonego współrzędnymi) oraz jasnych + informacjami o sposobie dotarcia do finału (np. azymutu i + odległości). + + + + Peilungscache + + Die Koordinaten zeigen auf einen Startpunkt, von dem aus eine + Peilung zum Ziel führt, meist anhand einer Entfernungsangabe + und eines Winkels (Azimut). + + + + + + + + + + Garmin's wireless beacon + + Contains Garmin's wireless chirp beacon. + + + + Beacon - Garmin Chirp + + Skrzynka zawiera Beacon Garmin chirp. + + + + Funksignal – Garmin Chirp + + Zusammen mit dem Cache sind ein oder mehrere Garmin-Funksender + versteckt. Um den Cache zu finden, müssen die Funksignale empfangen + werden. Dazu wird ein passender Empfänger benötigt. + + + + Beacon - Garmin chirp + + Deze cache is gemaakt met 1 of meer Garmins chirp draadloze zenders. + + + + + + + + + Dead Drop USB cache + + The cache consists of an unmovable USB mass storage device, e.g. + fixed into a wall, curb etc. The device contains readme.txt file + with cache description and a logbook.txt file where you can log + your visit. + + + + Dead Drop USB skrzynka + + Skrzynka typu USB Dead Drop. Pen-drive przymocowany lub wmurowany + w ścianę, budynek, krawiężnik itp. Aby zalogować znalezienie, należy + wpisać się do pliku logbook.txt znajdującego się w pamięci urządzenia. + Wiecej informacji. + + + + Dead Drop USB-Cache + + Der Cache besteht aus einem fest angebrachten USB-Stick. Er enthält + eine readme.txt-Datei mit Erläuterungen zum Cache und eine Datei + logbook.txt zum Loggen. + + + + Dead Drop USB cache + + Deze cache bestaat uit een vast gemonteerde USB stick, zoals in een + muur of paal enz. Deze bevat een readme.txt bestand met de cache + beschrijving en een logboek.txt bestand om je bezoek te kunnen loggen. + Voor informatie. + + + + + + + + Has a moving target + + This geocache is moving around. For example, the owner might regularly + move the cache from one place to another, or the finders will do this + task and post new coordinates in their log entries. The owner must + update coordinates in the cache description after each move. + + + + bewegliches Ziel + + Der Geocache verändert seine Position und ist deshalb nicht immer am + gleichen Ort zu finden. Es gibt Varianten, bei denen der Geocache-Besitzer + den Geocache regelmäßig an anderen Orten versteckt, oder der Finder den + Geocache an einem neuen Ort versteckt. Danach muss der Besitzer jeweils + die Beschreibung aktualisieren. + + + + Objetivo en movimiento + + Este geocache está en movimiento. Por ejemplo, el propietario podía mover + la caché periódicamente de un lugar a otro, o un geobuscador podría hacer + esto y ofrecer nuevos detalles en su registro. El propietario debe + actualizar las coordenadas en la descripción del cache después de cada + movimiento. + + + + Oggetto in movimento + + Questa geocache e in muovimento. Per esempio, il proprietario potrebbe + spostare regolarmente la cache da un luogo ad un altro, o i cacher + potrebbero eseguire questa operazione e indicare nuove coordinate nel + loro log. Il proprietario deve aggiornare le coordinate nella descrizione + della cache dopo ogni mossa. + + + + + + + + a webcam is involved + + There is a webcam at the target location. You must record a webcam + picture of your visit and include it in your 'found' log entry. There + may be additional requirements like a geocaching banner on the photo. + The webcam’s address is included in the cache description. + + + + Webcam + + Am Ziel befindet sich eine Webcam, und für einen Fund muss man das Bild + der Webcam von sich selbst aufnehmen, um nachzuweisen, dass man vor Ort + war. Manche Webcam-Caches setzen auch weitere Bedingungen, z.B. einen + Geocaching-Banner auf dem Bild. Die Webadresse der Webcam ist in der + Beschreibung angegeben. + + + + Webcam Cache + + Hay una webcam en el lugar de destino. necesidad a alguien para registrar + con un pantallazo de la imagen de la webcam de su visita e incluirlo en + el registro. Puede haber requisitos adicionales como un signo de + geocaching en la imagen. La dirección de la webcam está incluido en la + descripción de la cache. + + + + Webcam Cache + + C'e una webcam nella posizione di destinazione. E' necessario registrare + una foto con la webcam della vostra visita e includerla nel log. Ci + possono essere requisiti addizionali come un segnale di geocaching sulla + foto. L'indirizzo della webcam e incluso nella descrizione della cache. + + + + + + + + Other cache type + + This is none of the standard, pre-defined types of cache. + Use this attribute for special, unusual caches. + + + + sonstiger Cachetyp + + Dieser Cache passt in keine der üblichen Kategorien von Caches (Cachearten). + + + + Otro tipo de cache + + Este es un cache que no pertenece a ninguna de las categorías predefinicas + - estándar. + + + + Altro tipo di cache + + Questa e una cache che non appartiene a nessuna delle categorie standard + perdefinite. Usa questo attributo per cache speciali e inusuali. + + + + + + + + Investigation required + + You must investigate additional information before you can seek this cache. + + + + Recherche + + Für diesen Cache muss vorab nach Informationen gesucht werden. + + + + Investigación + + Necesitas encontrar más información antes de poder buscar este cache. + + + + Ricerca + + E necessario trovare ulteriori informazioni prima di poter cercare + questa cache. + + + + + + + + + Puzzle / Mystery + + Puzzles or mysteries have to be solved before or while seeking this cache. + + + + Rätsel + + Bei diesem Cache sind als Vorarbeit oder während der Suche Rätsel zu lösen. + + + + Puzzle / Misterio + + Misterio o Puzzle para ser resuelto antes o durante la búsqueda de este cache. + + + + Puzzle / Mystery + + Puzzle o Mystery devono essere risolti prima o durante la ricerca di + questa cache. + + + + + + + + Arithmetical problem + + Before or while seeking this cache, arithmetical problems must be solved + which go beyond very basic calculations. + + + + Rechenaufgabe + + Es müssen vorab oder während der Suche Rechenaufgaben gelöst werden, die + über das kleine Geocacher-1x1 hinausgehen, zum Beispiel + Mittelpunktberechnungen oder Peilungen. + + + + Problema matemático + + Antes o durante la búsqueda de este cache, resolver problemas matemáticos + sencillos más difícil a los cálculos de la base. + + + + Problema matematico + + Prima o durante la ricerca di questa cache, devono essere risolti problemi + matematici piu difficili di semplici calcoli base. + + + + + + + + Ask owner for start conditions + + Before doing this cache, you must ask the owner for the starting conditions. + E.g. the cache may be linked to certain events at varying dates. + + + + Startbedingungen beim Owner erfragen + + Bei diesem Cache ist es nötig, sich vor dem Angehen des Caches beim + Eigentümer über die Bedingungen zum Angehen zu informieren. + + + + Ask owner for start conditions + + Pregunte a los propietarios por las condiciones iniciales + + + + Ask owner for start conditions + + Chiedere al proprietario per le condizioni di partenza + + + + + + + + + + Wheelchair accessible + + The cache is hidden in a way which makes it possible to be found + when moving on a wheelchair. + + + + Dostępna dla niepełnosprawnych + + Skrzynka ukryta w sposób umożliwiający jej znalezienie osobom + poruszającym się na wózku inwalidzkim. Dotyczy to zarówno + lokalizacji (np. dojazd alejką pod samą skrzynkę), jak i sposobu + ukrycia. + + + + rollstuhltauglich + + + Rolstoel toegankelijk + + Deze cache is zo gemaakt dat de cache ook met de rolstoel te vinden + en te loggen is. + + + + + + + + + Near the parking area + + The geocache is located close to a parking area, only a few steps away. + + + + nahe beim Auto + + Der Parkplatz befindet sich in unmittelbarer Nähe zum Geocache. + Es sind nicht mehr als einige Schritte notwendig, um den Geocache zu finden. + + + + Cerca de un Parking + + El geocache se encuentra cerca de un parking, a poca distancia. + + + + Vicino all'area di parcheggio + + La geocache e posta vicino ad un'area di parcheggio, solo poco distante. + + + + + + + + + Access only by walk + + The cache is accessible by walk only. + + + + Dostępna tylko pieszo + + Skrzynka dostępna tylko pieszo. + + + + nur per Fußweg + + Der Cache ist nur zu Fuß erreichbar. + + + + + + + + + + Long walk + + This cache requires a long walk - more than 5 km round trip. In the + mountains and other steep areas, the distance for a 'long walk' may be + shorter. Walking shoes and appropriate equipment are recommended. + + + + längere Wanderung + + Bei diesem Cache erwartet euch eine Wanderung von mehr als 5 Kilometer, + vom Ausgangspunkt bis zum Cache und wieder zurück. Im Gebirge und bei + entsprechenden Steigungen kann das Attribut auch bei kürzeren Wegstrecken + gesetzt sein. Gute Wanderschuhe und entsprechende Ausrüstung empfehlen sich. + + + + Larga caminata + + Esta cache requiere una larga caminata - más de 5 kilometros de ida y + vuelta. En las montanas escarpadas o en otras áreas. Recomendados calzado + para caminar y equipo adecuado. + + + + Lunga camminata + + Questa cache richiede una lunga camminata - piu di 5 km tra andata e + ritorno. In montagna o in altre aree ripide, la distanza per una cache + del genere puo essere minore. Sono raccomandati scarpe da escursione ed + equipaggiamento adeguato. + + + + Lange wandeling + + + + + + + + Swamp, marsh or wading + + This cache requires passing swampy or marshy ground our wading throuh + shallow water. Wear appropriate clothes. After rainfall, the terrain + may be very demanding or not passable at all. + + + + sumpfiges/matschiges Gelände / waten + + Bei diesem Cache geht es durch sumpfiges oder matschiges Gelände oder + es muss durch flaches Wasser gewatet werden. Entsprechende Kleidung + wird empfohlen. Nach Regenfällen kann das Gelände wesentlich schwerer + oder überhaupt nicht begehbar sein. + + + + Pantano / terreno fangoso + + Esta cache requiere la superación de pantanos. Usar ropa apropiada. + Después de la lluvia, el suelo puede ser difícil o no factible. + + + + Palude o marcita + + Questa cache richiede il superamento di terreno paludoso o + acquitrinoso. Indossare un abbigliamento adeguato. Dopo la pioggia, + il terreno puo essere molto impegnativo o non praticabile a tutti. + + + + + + + + Hilly area + + One or more ascents lie between you and the cache. + + + + hügeliges Gelände + + Auf dem Weg zum Geocache bzw. während der Cachesuche sind eine oder + mehrere Steigungen zu überwinden. + + + + Terreno montanoso + + Una o más pendientes para acceder al cache. + + + + Area collinare + + Una o piu salite si trovano tra voi e la cache. + + + + + + + + + Some climbing (no gear needed) + + This cache requires some climbing and you may have to use your hands, + but you won’t need climbing gear. Be very careful during rainy weather + or before thunderstorms! + + + + leichtes Klettern (ohne Ausrüstung) + + Während der Cachesuche ist leichtes Klettern notwendig, bei dem man sich + z.B. mit den Händen festhalten muss. Gute Trittsicherheit und + Schwindelfreiheit empfehlen sich. Es ist jedoch keine Spezialausrüstung + notwendig wie z.B. Sicherungsseil, Klettersteigset oder Steigeisen. + Besonders bei feuchter Witterung oder vor Gewittern sollte man mit der + entsprechenden Vorsicht handeln. + + + + fácil de subir (sin equipo) + + Esta cache requiere un poco de escalada y puede ser necesario usar las + manos, pero no es necesario material de montana. !Tenga mucho cuidado + durante la temporada de lluvias.! + + + + Arrampicata (attrezzatura non necessaria) + + Questa cache richiede un po' di arrampicata e potrebbe essere necessario + usare le mani, ma non c'e bisogno di attrezzatura da arrampicata. + Prestare molta attenzione durante la stagione delle piogge o prima dei + temporali! + + + + + + + + + Swimming required + + This cache requires crossing a river or a lake. The water can be steep. + + + + Schwimmen erforderlich + + Auf dem Weg zum Geocache muss ein Fluss oder See überquert werden. + Das Wasser ist tief genug um zu schwimmen. Je nach Örtlichkeit kann auch + ein Schlauchboot, Kajak oder ähnliches verwendet werden (näheres ist + in der Beschreibung zum Geocache zu finden). Die Entfernung ist aber ohne + besondere Ausdauer noch zu schwimmen. + + + + Requiere nadar + + Esta cache requiere cruzar un río o un lago. El agua es lo suficientemente + profundo para nadar. Puede utilizar un barco, pero la distancia es lo + suficientemente corto como para ser asequible para un nadador. + + + + Nuoto necessario + + Questa cache richiede l'attraversamento di un fiume o un lago. L'acqua e + abbastanza profonda per nuotare. E possibile utilizzare una barca, ma la + distanza e abbastanza breve per essere alla portata di un nuotatore medio. + + + + + + + + + Access or parking fee + + You must pay an access or parking fee to access this cache. + + + + Zugangs- bzw. Parkentgelt + + Um zum Cache zu gelangen, müsst ihr entweder einen Eintritt oder eine + Parkgebühr bezahlen. + + + + Acceso o parking pagando + + Deberas pagar un acceso o estacionamiento para acceder a esta cache. + + + + Tassa di ingresso o di parcheggio + + Devi pagare un accesso o parcheggio a pagamento per accedere a questa cache. + + + + + + + + + + Bikes allowed + + You can reach the cache by bike. + + + + Dostępna rowerem + + Można dojechać do skrzynki rowerem. + + + + Fahrräder erlaubt + + Der Cache ist mit dem Fahrrad erreichbar. + + + + Fietsen toegestaan + + Deze cache is ook met de fiets te doen. + + + + + + + + + Hidden in natural surroundings (forests, mountains, etc.) + + The cache is hidden in a remote and quick place - a forest, wild + meadow, a swamp, etc. + + + + Umiejscowiona na łonie natury (lasy, góry, itp.) + + Umiejscowiona na łonie natury, z dala od cywilizacji - las, dzika + łąka, mokradła. + + + + in der Natur + + Der Cache ist abseits der Zivilisation in natürlicher Umgebung + versteckt – in einem Wald oder einer Wiese, einem Sumpf etc. + + + + Cache met veel natuur + + Deze cache loopt door de natuur. Zoals bossen, heide, moerasgebieden enz. + + + + + + + + Historic site + + The cache is hidden near a historic site - a castle, battleplace, + cementary, old bunkers, etc. + + + + Miejsce historyczne + + W sąsiedztwie miejsca historycznego - zamku, pałacu, pola bitwy, + cmentarza, ale także fortów czy bunkrów. + + + + historischer Ort + + Der Cache ist an einem historisch bedeutsamen Ort versteckt – + einer Burg, einem Friedhof, einer ehemaligem Militäranlage etc. + + + + + + + + + Point of interest + + There is a point of interest at the cache, like a nice scenic view + or a larger castle ruin. This place is worth visiting it even + without a geocache nearby. + + + + interessanter Ort + + Der Geocache ist in unmittelbarer Nähe zu einer Sehenswürdigkeit + versteckt. Das kann ein z.B. schöner Aussichtspunkt oder eine größere + Burgruine sein. Ein Besuch würde sich auch ohne besonderen Anlass + (den Geocache) lohnen. + + + + Punto de interes + + Hay un monumento cerca del cache, como un paisaje hermoso o un castillo + en ruinas. Este lugar es digno de visitar, incluso sin un geocache cerca. + + + + Punto di interesse + + C'e un punto di interesse alla cache, come un bel panorama o di un + castello in rovina. Questo luogo merita una visita anche senza una + geocache vicina. + + + + Interesante plek + + + + + + + Hidden wihin enclosed rooms (caves, buildings etc.) + + This geocache is not hidden in the open air, but within a building, + a cave or similar. + + + + in geschlossenen Räumen (Höhle, Gebäude, etc.) + + Das Ziel des Geocaches liegt nicht im Freien, sondern zum Beispiel in + einem Gebäude oder einer Höhle. + + + + en espacios confinados (cuevas, edificios, etc) + + Este geocache no está al aire libre, esta oculto dentro de un edificio, + una cueva o similares. + + + + All'interno di stanze chiuse (caverne, edifici, ecc.) + + Questa geocache non e nascosta all'aria aperta ma all'interno di un + edificio, di una grotta o simili. + + + + + + + + Hidden under water + + This cache or one of the stages is placed underwater. + You will get wet when doing this cache. + + + + im Wasser versteckt + + Der Geocache oder eine der Stationen ist im Wasser versteckt. Um die + Aufgabe zu lösen muss man ggf. das Wasser betreten, schwimmen oder tauchen. + + + + En el agua + + Esta cache o una de sus etapas se encuentran bajo el agua. Usted debe + entrar en el agua, nadar o zambullirse. + + + + Nell'acqua + + Questa cache o uno dei suoi stadi sono posizionati sott'acqua. Devi + entrare in acqua, nuotare o fare una immersione. + + + + + + + + + + Parking area nearby + + A nearby parking area is situated as starting point for doing this cache. + + + + Parkplatz in der Nähe + + Es gibt in der Nähe einen Parklplatz, der sich als Startpunkt für die + Cachesuche eignet. + + + + Parking cercano + + Una zona de aparcamiento se encuentra cerca del punto de partida de + este cache. + + + + Parcheggio nei pressi + + Una area di parcheggio e situata nei pressi del punto di partenza di + questa cache. + + + + Parkeerplaats vlakbij + + Er is een parkeerplaats vlak bij om deze cache te beginnen. + + + + + + + + + Public transportation + + This cache is located outside of urban areas and has a public + transport station nearby. + + + + erreichbar mit ÖVM + + Dieser Cache lässt sich mit Hilfe von öffentlichen Verkehrsmitteln erreichen + und liegt außerhalb von Städten. + + + + Transporte Público + + Este cache se encuentra también fuera de las zonas urbanas y una + estación de transporte público. + + + + Trasporto pubblico + + Questa cache e situata al difuori di aree urbane e ha una stazione di + trasporto pubblico nelle vicinanze. + + + + + + + + + Drinking water nearby + + There is drinking water along the trail or near the cache. This may be + useful especially especially when doing event caches, longer hikes or + caches at probably dirty locations. + + + + Trinkwasser in der Nähe + + Während der Cachetour oder in der Nähe des Geocaches ist Trinkwasser + verfügbar. Besonders bei Event-Caches, längeren Multicaches und bei + Geocaches, wo man vermutlich schmutzig wird, kann dies hilfreich sein. + + + + Agua potable en las cercanias + + Hay agua potable a lo largo de la ruta o cerca de la cache. Este + atributo es especialmente útil en la planificación de Eventos, + o caches con viajes largos a lugares como las cuevas o minas + probablemente esté sucio. + + + + Acqua potabile nei pressi + + C'e acqua potabile lungo il percorso o nelle vicinanze della cache. + Questo attributo e utile soprattutto nella pianificazione di cache + evento, di lunghe escursioni o di cache in luoghi probabilmente + sporchi come le grotte o le miniere. + + + + + + + + + Public restrooms nearby + + There are public restrooms along the way or near the cache. + + + + öffentliche Toilette in der Nähe + + Während der Cachetour oder in der Nähe des Geocaches ist eine öffentliche + Toilette verfügbar. + + + + Aseos públicos cercanos + + Hay banos públicos a lo largo de la carretera o en las proximidades + del cache. + + + + Bagni pubblici nei pressi + + Ci sono WC pubblici lungo la strada o nelle vicinanze della cache. + + + + + + + + + Public phone nearby + + There is a public phone along the way or near the cache. + + + + Telefon in der Nähe + + Während der Cachetour oder in der Nähe des Geocaches gibt es ein + öffentliches Telefon. + + + + Teléfono Público en las cercanias + + Hay teléfonos públicos en la carretera o en las proximidades del cache. + + + + Telefono pubblico nei pressi + + Ci sono telefoni pubblici lungo la strada o nelle vicinanze della cache. + + + + + + + + First aid available + + There is a first aid station, call box, mountain rescue or similar + arrangement near the cache. + + + + Erste Hilfe verfügbar + + In der Nähe des Caches findet ihr eine Erste Hilfe-Station, Notrufsäule, + Bergwacht oder entsprechende Einrichtung. + + + + Disponible socorro rapido + + Hay un punto de socorro, un teléfono para pedir ayuda, un centro de + rescate de montana o similar cerca del cache. + + + + Disponibile pronto soccorso + + C'e un pronto soccorso, un telefono per chiamate di soccorso, un centro + di soccorso alpino o simili nelle vicinanze della cache. + + + + + + + + + + Available 24/7 + + This cache can be found at any time of day or week. + + + + 24/7 beschikbaar + + Deze cache kan 24/7 gedaan worden, zowel overdag als in de nacht. + + + + rund um die Uhr machbar + + Dieser Cache ist jederzeit machbar, sowohl am Tage als auch in der Nacht. + + + + Disponible las 24 horas + + Esta cache se puede encontrar tanto de día como de noche. + + + + Disponibile 24 ore + + Questa cache puo essere trovata sia di giorno che di notte. + + + + + + + + + + Not 24/7 + + This cache can only be done at certain times of day or week - see the cache + description for more details. For example, the cache may be placed in an + area with restricted opening hours. + + + + Dostępna w określonych godzinach + + Dostępna w określonych dniach, godzinach, często wstęp płatny. + Często będzie to muzeum lub skansen. Szczegółowe informacje o + dostępności powinny znajdować się w opisie skrzynki. + + + + Niet 24/7 beschikbaar + + Deze cache is niet alle tijden te doen. Zie de cachebeschrijving voor de details. + Dit kan bijvoorbeeld zijn in een gebied waar men alleen tussen zonsopgang of zonsondergang mag komen. + + + + nur zu bestimmten Uhrzeiten + + Dieser Cache lässt sich nur zu bestimmten Tageszeiten absolvieren. + Nähere Angaben sind in der Beschreibung des Caches zu finden. + + + + Sólo disponible a ciertas horas + + Esta cache se puede hacer solamente en ciertos momentos del día - + véase la descripción de caché para obtener más detalles. + + + + Disponibile solo in certi orari + + Questa cache puo essere cercata solo a certe ore del giorno - + vedi la descrizione per ulteriori informazioni. + + + + + + + + + Not recommended at night + + Searching for this cache is not recommended by night. It might be + dangerous, or the cache may be hidden in an area where flashlights + may attract unwanted attention. + + + + nur tagsüber + + Dieser Cache lässt sich nur tagsüber angehen, zum Beispiel weil das Gelände + gefährlich ist oder die Suche mit Taschenlampen in einem Wohngebiet negativ + auffallen würde. + + + + solo por el día + + Deberas encontrar este cache sólo durante el día. Por ejemplo, el área pued + ser peligroso y contienen rocas o abismos. O bien, el uso de linternas puede + ser imposible porque sería sospechoso en una zona residencial. + + + + solo di giorno + + Si dovrebbe cercare questa cache solamente durante il giorno. Ad esempio, + l'area puo essere pericolosa e contenere scogliere o abissi. Oppure, + l'utilizzo di torce elettriche potrebbe essere impossibile perché + risulterebbe sospetto all'interno di una zona residenziale. + + + + + + + + + + Recommended at night + + It is recommended to search for this cache by night. I.e. there + might be some light-reflecting surfaces involved which are usually + invisible during daylight. + + + + Zalecane szukanie nocą + + Aby znaleźć skrzynkę zalecane jest poszukiwanie jej nocą, ze + względu na miejsce ukrycia lub użyte elementy odblaskowe, których + oświetlenie umożliwia odnalezienie skrzynki. + + + + am besten nachts findbar + + Es wird empfohlen, den Cache nachts zu suchen. Es können zum + Beispiel Reflektoren vorhanden sein, die tagsüber schwer oder gar + nicht erkennbar sind. + + + + + + + + + Only at night + + This geocache can be found at night only - it is a so-called night cache. + There may be reflectors which have to be flashlighted and will point + to the hiding place, or other special night-caching mechanisms. + + + + nur bei Nacht + + Der Geocache kann nur bei Nacht gelöst werden und wird deshalb Nachtcache + genannt. Zum Beispiel müssen Reflektoren mit einer Taschenlampe + angeleuchtet werden, die dann den Weg zum Versteck zeigen. + + + + Sólo por la noche + + Esta cache se puede encontrar solamente por la noche - tienes que + considerar cache notturna. Puede haber placas reflectantes que + brillaran y te llevaran al cache, o otros mecanismo especiales para + caches nocturnos. + + + + Solo di notte + + Questa cache puo essere trovata solo di notte - e una cosiddetta + cache notturna. Ci possono essere targhette riflettenti che devono + essere illuminate e ti conducono al nascondiglio, o altri speciali + meccanismi di caching notturno. + + + + + + + + + All seasons + + This cache can be found the whole year round, while difficulty may + depend on seasons. + + + + ganzjährig zugänglich + + Dieser Cache lässt sich während des gesamten Jahres finden, wobei je + nach Jahreszeit die Schwierigkeit bei der Suche schwanken kann. + + + + Todas las temporadas + + Esta cache se encuentrar durante todo el ano, mientras que la dificultad + puede depender de las estaciones. + + + + Tutte le stagioni + + Questa cache si trova tutto l'anno, mentre la difficolta puo dipendere + dalle stagioni. + + + + + + + + + Only available during specified seasons + + This cache can be done at certain seasons only - see the cache + description for more details. + + + + nur zu bestimmten Zeiten im Jahr + + Dieser Cache lässt sich nur zu bestimmten Zeite im Jahr absolvieren. + Näheres ist in der Cachebeschreibung angegeben. + + + + Sólo disponible durante las estaciones especificadas + + Esta cache se puede hacer en ciertas épocas del ano solamente - vea la + descripción de cache para obtener más detalles. + + + + Disponibile solo in certe stagioni + + Questa cache puo essere cercata solo in certe stagioni - vedi la + descrizione per ulteriori informazioni. + + + + + + + + Breeding season / protected nature + + Don’t seek this cache during animal breeding season! See the cache + description on which time of year must be avoided. Also, pay + attention to the local terms and signs regarding nature protection. + + + + Brutsaison / Naturschutz + + Dieser Cache sollte in der Brutsaison nicht absolviert werden. In der + Beschreibung sollte angegeben sein, welche Jahreszeit davon betroffen ist. + Achte bitte auch auf die örtliche Beschilderung zum Naturschutz. + + + + Temporada de reproducción / protección de la naturaleza + + !No intente esta cache durante la temporada de cría de los animales! + Vvéase la descripción del cache de la época del ano debe ser evitado. + Preste atención también a las condiciones o signos en cuanto al respeto + por la naturaleza. + + + + Stagione di riproduzione / natura protetta + + Non cercare questa cache durante il periodo riproduttivo degli animali! + Vedi descrizione della cache quale periodo dell'anno debba essere evitato. + Prestate anche attenzione alle condizioni o ai cartelli riguardo il + rispetto della natura. + + + + + + + + + Available during winter + + This cache can be found even after heavy snowing. All stages and the + geocache are hidden in a snow-safe way: they will not be covered by + fallen snow, or ice, etc. + + + + schneesicheres Versteck + + Dieser Cache lässt sich auch nach starkem Schneefall suchen. Die einzelnen + Stationen und der Geocache sind so versteckt, dass sie nicht von Schnee + verdeckt werden, bzw. von Schneehaufen, die durch Räumfahrzeuge entstehen. + + + + Nieve en el escondite + + Este cache también se puede encontrar después de fuertes nevadas. Todas + las fases y geocaches se esconde en lugares seguros para la caída de la + nieve, no será cubierto por acumulaciones de nieve. + + + + Luogo a prova di neve + + Questa cache puo essere trovata anche dopo forti nevicate. Tutte le fasi + e la geocache sono nascosti in luoghi sicuri per la neve: non saranno + coperti da neve caduta né da cumuli di neve creati ad esempio da veicoli + spalaneve. + + + + + + + + Not at high water level + + This cache can be done only at low or normal water level. It is + inaccessible during flood. + + + + nicht bei Hochwasser oder Flut + + Der Geocache kann nur bei bei niedrigem oder normalem Wasserstand + bzw. bei Ebbe gesucht werden. Bei Hochwasser oder Flut ist er + unzugänglich. + + + + + + + + + + Compass required + + A compass is required. + + + + Potrzebny kompas + + Kompas może okazać się niezbędny aby dotrzeć do wskazanego miejsca + skrzynki. + + + + Kompass + + Für diesen Cache braucht ihr einen funktionierenden Kompass für Peilungen + oder Orientierungen. + + + + Brújula + + Se necesita una brújula. + + + + Bussola + + E' necessaria una bussola. + + + + Kompas nodig + + + + + + + + Take something to write + + There is no pencil in the cache. Take something to write with. + + + + Weź coś do pisania + + Skrzynka nie zawiera ołówka, weź ze sobą coś do pisania. + + + + Stift wird benötigt + + Es ist kein Stift zum Loggen im Cache; bringe bitte einen eigenen mit. + + + + Neem iets mee om te schrijven + + + + + + + + You may need a shovel + + The cache may require more digging. A shovel might come in handy. + + + + Potrzebna łopatka + + Skrzynka jest zakopana w ziemi. + + + + Schaufel / vergrabener Cache + + Der Cache ist vergraben. Wahrscheinlich benötigst du eine Schaufel, + um ihn zu heben. + + + + + + + + + + + Flashlight required + + You will need a flashlight to find this cache. + + + + Potrzebna latarka + + Przy poszukiwaniach tej skrzynki potrzebna jest latarka. + + + + Taschenlampe + + Um diesen Cache anzugehen, benötigt ihr eine funktionstüchtige + Taschenlampe. Denkt auch an Ersatzbatterien. + + + + Linterna + + Es necesario una linterna para encontrar este cache. !No se olvide de las + baterías de repuesto! + + + + Lampada tascabile + + E' necessaria una torcia portatile per trovare questa cache. Non + dimenticate le batterie di riserva! + + + + + + + + + Climbing gear required + + For this cache, you will need climbing equipment and the knowledge + how to use it properly. If you are a beginner, don’t do it alone but + use the support of an experienced climber or mountaineer. + + + + Kletterzeug + + Um diesen Cache absolvieren zu können, benötigt ihr neben der normalen + Ausrüstung auch noch Kletterausrüstung, und entsprechendes Wissen um + deren Handhabung und ums Klettern. Laien sollten sich auf jeden Fall + von einem erfahrenen Kletterer oder Bergsteiger unterstützen lassen. + + + + Equipo de escalada + + Para este cache, tendrá que utilizar los equipos y saber cómo utilizarlo + correctamente. Si usted es un principiante, no lo haga solos, sino que + utiliza el apoyo de un experimentado escalador o alpinista. + + + + Attrezzatura per arrampicata + + Per questa cache, avrete bisogno di materiale da arrampicata e di saperlo + usare correttamente. Se sei un principiante, non farlo da solo, ma + utilizza il sostegno di uno scalatore esperto o un alpinista. + + + + + + + + Cave equipment required + + This geocache is hidden in a cave, and you should use appropriate + equipment to access it. Beware: Even small caves may confront you with + unforeseen problems and dangers, like thunder storms (water!) or a + sprained ankle. Have advice first from cave-experienced people! Also + take care of protected nature; e.g. bat places must not be disturbed. + + + + Höhlenzeug + + Der Geocache ist in einer Höhle versteckt und man sollte entsprechende + Ausrüstung mitbringen. Vorsicht: Bereits kleinste Höhlensysteme können + bei unvorhergesehenen Problemen, z.B. Gewittern (Wasser!) oder einem + verstauchten Knöchel, sehr gefährlich werden! Ihr solltet euch vorab + gründlich bei erfahreren Höhlengehern informieren. Beachtet auch den + Naturschutz – Fledermausquartiere dürfen nicht gestört werden! + + + + Equipación para cuevas + + Este geocache está escondido en una cueva, y se debe utilizar el equipo + adecuado para acceder a ella. Tenga en cuenta que incluso las pequenas + cuevas pueden prever los problemas imprevistos y peligros, como durante + las tormentas o con un esguince de tobillo. !Acceda con personas + experimentadas en cuevas! También debe protegerse la naturaleza sobre + todo en esos lugares donde los murciélagos no deben ser molestados. + + + + Attrezzatura per grotta + + Questa geocache e nascosta in una grotta, e si dovrebbe utilizzare + attrezzature adeguate per accedervi. Attenzione: anche piccole grotte + possono prevedere problemi imprevisti e pericoli, come in caso di + temporali (acqua!) o una caviglia slogata. Consigliatevi prima con + persone che abbiano esperienza di grotte! Abbiate anche cura della + natura protetta, ad esempio dei luoghi dove i pipistrelli non devono + essere disturbati. + + + + + + + + + Diving equipment required + + You will need diving equipment to find this geocache. The water depth + of the cache location is specified in the description. Please note that + secure diving requires special training. Without diving experience, + you may search this cache in company of a diving teacher. + + + + Taucherausrüstung + + Um den Geocache zu finden benötigt ihr eine Tauchausrüstung. In welcher + Tiefe der Geocache liegt, ist in der Beschreibung angegeben. Bitte beachtet, + dass Ihr für einen sicheren Tauchgang eine entsprechende Ausbildung + benötigt. Als Nicht-Taucher könnt ihr den Geocache evtl. zusammen mit + einem Tauchlehrer suchen. + + + + Diving equipment + + Necesitará un equipo de buceo para encontrar este geocache. La + profundidad del agua en la ubicación de la cache se especifica en la + descripción. Tenga en cuenta que el buceo requiere un entrenamiento + especial. Sin experiencia de buceo, puedes buscar por el caché, junto + con un buceador experimentado. + + + + Equipo de buceo + + Avrete bisogno di attrezzatura subacquea per trovare questa geocache. + La profondita d'acqua nella posizione della cache viene specificata nella + descrizione. Si prega di notare che l'immersione in tutta sicurezza + richiede una formazione specifica. Senza esperienza di immersioni, e + possibile cercare questa cache in compagnia di un insegnante di sub. + + + + + + + + + + + Special tools required + + You will need special equipment which is not specified by other attributes. + See the cache description on what tools are required. + + + + Wymagany dodatkowy sprzęt + + Niezbędny jest dodatkowy, niestandardowy sprzęt - może to być np. + kajak, sprzęt wspinaczkowy, ale również kalkulator, kalosze itp. + Ogólnie przedmioty, które nie należą do standardowego wyposażenia + poszukiwacza. + + + + spezielle Ausrüstung + + Für diesen Cache benötigst du weitere Ausrüstung, die nicht durch die + anderen Attribute angegeben ist und nicht zur Standardausrüstung eines + Geocachers gehört. Was genau du benötigst, ist in der Beschreibung + angegeben. + + + + Equipamiento especial + + Necesitarás un equipo especial no especificado por otros atributos. + + + + Equipaggiamento speciale + + Avrete bisogno di attrezzature speciali non specificate da altri attributi. + + + + + + + + + + + Requires a boat + + This cache can usually be found only when using a watercraft. + Swimming is difficult or impossible because of the distance or currents. + See the cache description for more details. + + + + Wymaga sprzętu pływającego + + Skrzynka z tym atrybutem najczęściej może być zdobyta jedynie przy + użyciu sprzętu pływającego (łodzi, pontonu, kajaka itp.) Dopłynięcie + wpław jest trudne lub niemożliwe, ze względu na dystans, silne + prądy itp. + + + + Wasserfahrzeug + + Der Geocache kann – normalerweise – nicht ohne ein Wasserfahrzeug gefunden + werden. Zum Geocache kann wegen der Entfernung oder Strömung nicht + geschwommen werden. Details dazu sind in der Beschreibung des Geocaches + angegeben. + + + + Barca + + Este cache por lo general sólo se puede encontrar con una moto de agua. + Nadando es imposible debido a la distancia o la corriente. Véase la + descripción del cache para obtener más detalles. + + + + Barca + + Questa cache di solito puo essere trovata solo con una moto d'acqua. Il + nuoto e impossibile a causa della distanza o delle correnti. Vedi la + descrizione della cache per maggiori dettagli. + + + + + + + + No GPS required + + This cache can be found without a GPS device. No additional coordinates + are used besides of the starting coordinates. + + + + ohne GPS findbar + + Dieser Cache lässt sich auch ohne GPS-Empfänger finden. Die Aufgaben + sind so gestellt, dass man außer den Startkoordinaten keine weiteren + Koordinaten verwenden muss. + + + + Sin GPS + + Esta cache se puede encuentra sin un dispositivo GPS. Detalles adicionales + no se utilizan, además de las coordenadas iniciales. + + + + Senza GPS + + Questa cache puo essere trovata senza un dispositivo GPS. Non sono + utilizzate coordinate addizionali oltre alle coordinate iniziali. + + + + + + + + + + + Dangerous area + + The cache is located within a dangerous area, and danges may not be + obvious, e.g. like high-traffic roads, steep ground or falling rocks. + Safety measures should be taken, especially when geocaching with + children, large groups of people or during bad weather conditions. + + + + Skrzynka niebezpieczna + + Skrzynka jest ukryta w niebezpiecznym terenie. Jej poszukiwania mogą + narazić na niebezpieczeństwo wypadku lub urazu. + + + + gefährliches Gebiet + + In dem Gebiet, wo der Geocache versteckt wurde, ist mit Gefahren zu + rechnen, die unter Umständen nicht auf den ersten Blick erkennbar sind. + Das können z.B. stark befahrene Straßen, steile Abhänge oder Steinschlag + sein. Deshalb sollte man bei Geocaching-Touren mit Kindern oder größeren + Gruppen entsprechende Vorsichtsmaßnahmen ergreifen und je nachdem auch + auf die Witterung achten (z.B. Regen bei steilen Abhängen). + Näheres zu den Gefahren ist in der Cachebeschreibung erläutert. + + + + Zona Peligrosa + + El cache está situado en una zona peligrosa, como tales como carreteras + con mucho tráfico, terreno empinado o caída de rocas. Usted debe tomar + medidas de seguridad o evitar ir a buscar el caché, sobre todo con ninos, + con grupos grandes o en condiciones meteorológicas adversas. + + + + Area pericolosa + + La cache e situata in un'area pericolosa come strade ad alto traffico, + terreno ripido o caduta sassi. Si dovrebbero adottare misure di sicurezza + o evitare di andare a cercare la cache, in particolare nel geocaching con + bambini, con gruppi numerosi o in condizioni climatiche sfavorevoli. + + + + + + + + Active railway nearby + + There are active railroads nearby. Please be careful, keep a safe + distance and cross the rails only at level crossings etc.! + + + + aktive Eisenbahnlinie in der Nähe + + In der Nähe dieses Caches gibt es genutzte Eisenbahnlinien. Bitte seid + entsprechend vorsichtig und achtet darauf, abseits von Bahnübergängen keine + Gleise zu betreten. + + + + Cerca del ferrocarril activo + + !Hay ferrocarriles activos en las proximidades. Por favor, tenga + cuidado, manteniendo una distancia segura y cruzar los rieles sólo + en los cruces de ferrocarril, etc.! + + + + Ferrovia attiva nei pressi + + Ci sono ferrovie attive nelle vicinanze. Per favore usate cautela, + tenendo una distanza di sicurezza e attraversando le rotaie solo ai + passaggi a livello ecc.! + + + + + + + + + Cliff / Rocks + + There are cliffs or dangerous rocks nearby. Beware of falling rocks + at the lower side, and be careful at the upper side of cliffs - + especially with children and while mountain biking. It can be very + dangerous to take a steep slope towards a cliff, because you may not + notice in time where the former ends and the latter starts. + + + + Klippen / Felsen + + In der Nähe des Caches gibt es Klippen oder Felsen. Unterhalb von + Felsen sollte man auf Steinschlag achten, von der Oberseite der Klippen + sollte man sich entsprechend vorsichtig nähern (insbesondere mit Kindern + oder Mountainbikes). Besonders gefährlich - und nicht immer erkennbar - + ist es, sich über einen Steilhang von oben an eine Klippe zu nähern. + + + + Acantilado / Rocas + + Hay acantilados o rocas peligrososas en las cercanas. Tenga cuidado + cuando esté bajo las piedras caídas, y tenga cuidado cuando esté sobre + el acantilado - especialmente con los ninos y el ciclismo. Puede ser + muy peligroso tomar un camino empinado para subir el acantilado porque + no se puede saber de antemano cuando el primero termina y comienza otra. + + + + Scogliera / Rocce + + Ci sono scogliere o rocce pericolose nelle vicinanze. Fate attenzione + alla caduta pietre quando siete sotto, e siate cauti quando siete sopra + la scogliera - specialmente con bambini e in bicicletta. Puo essere molto + pericoloso prendere un sentiero ripido per salire la scogliera, poiché + non potete sapere in anticipo quando la prima termina e inizia l'altra. + + + + + + + + + + Hunting grounds + + The geocache is placed within a hunting ground. At twilight and in the + dark, a flashlight or headlight should always be used for security + reasons. Be considerate when meeting hunters. + + + + Jagdgebiet + + Der Geocache liegt in einem Jagdgebiet. Bei Dämmerung oder Dunkelheit + sollte man aus Sicherheitsgründen immer eine Taschenlampe oder + Stirnlampe verwenden. Bei Begegnungen mit Jägern ist gegenseitige + Rücksichtnahme angebracht. + + + + Zona de Caza + + El geocache se coloca dentro de un coto de caza. Al caer la tarde y en + la oscuridad, una linterna o faro siempre debe utilizarse por razones + de seguridad. + + + + Caccia + + La geocache e situata nei pressi di una area di caccia. Al crepuscolo + e al buio, dovrebbe sempre essere usata una torcia portatile o frontale + per ragioni di sicurezza. Incontrando i cacciatori e opportuna una + reciproca gentilezza. + + + + Let op: jachtgebied + + + + + + + + + Look out for thorns + + There are thorns near the cache. Wear appropriate clothes. + + + + Dornen + + In er Nähe des Geocaches gibt es Dornen. Entsprechende Kleidung und + evtl. Handschuhe sind zu empfehlen. + + + + Espinas + + Hay espinas cerca de la caché. Use ropa apropiada. + + + + Spine + + Ci sono spine nei pressi della cache. Indossare indumenti appropriati. + + + + Let op: doornen + + Er kunnen doornen bij de cache zijn. Draag beschermende kleding. + + + + + + + + + + Look out for ticks + + There are seasonably many ticks in this area. It is recommended to wear + long trousers and to check yourself for ticks after geocaching. + There are regional risk maps for tick-borne encephalitis on the + internet. + + + + Zecken + + Je nach Saison gibt es in dem Gebiet besonders viele Zecken. Es wird + daher empfohlen, entsprechend lange Kleidung zu tragen und nach der + Cachetour nach Zecken Ausschau zu halten. FSME-Risikogebiete und + weitere Informationen zum Thema Zecken könnt ihr z.B. auf + www.meningitis.de nachsehen. + + + + Garrapatas + + Cada temporada hay un montón de garrapatas en este lubar. Y es + recomendable llevar pantalón largo y examinarse en busca de garrapatas + después de encontrar el cache. + + + + Zecche + + Stagionalmente ci sono molte zecche in questa area. E' raccomandabile + indossare pantaloni lunghi e ispezionarsi alla ricerca di zecche dopo + il geocaching. In internet ci sono mappe di rischio per encefalite + e borelliosi da morso di zecca. + + + + Let op: teken + + In dit gebied zouden teken voorkomen. Neem voorzorgen om geen teken + op te lopen door beschermende kleding te dragen. Vor meer informatie + kijk op www.meningitis.de + + + + + + + + + Abandoned mines + + This cache leads into a (former) mining area. There may be dangers by + collapsing adits, or you may need to enter adits. Be careful and use + appropriate equipment, especially in the dark. Old mines may be covered + by historic preservation. + + + + Folgen des Bergbaus + + Der Cache führt in eine (ehemalige) Bergbauregion. Möglicherweise + bestehen Gefahren durch verstürzte Stollenmundlöcher oder es müssen + Stollen betreten werden. Entsprechende Ausrüstung und Vorsicht, + besonders bei Dunkelheit, wird empfohlen. Historische Bergwerke stehen + möglicherweise unter Denkmalschutz. + + + + Mina abandonada + + Esta cache le llevará a un área de la mina (abandonado). Puede haber + peligro con el colapso de túneles o galerías que puede ser necesario para + cruzar. Tenga cuidado y use de equipo adecuado, especialmente en la + oscuridad. Las minas antiguas pueden ser objeto de preservación histórica. + + + + Miniere abbandonate + + Questa cache vi porta in una area di miniera (abbandonata). Ci possono + essere pericoli per crollo di gallerie, o potrebbe essere necessario + attraversare gallerie. Fare attenzione e utilizzate attrezzature adeguate, + soprattutto al buio. Le vecchie miniere possono essere oggetto di + conservazione storica. + + + + + + + + + Poisonous plants + + There are poisonous plants near the cache. Take care and prevent + children and dogs from touching or eating them. + + + + giftige Pflanzen + + In der Nähe des Caches gibt es giftige Pflanzen. Achtet also insbesondere + darauf, dass Kinder und Hunde diese nicht anfassen oder essen. + + + + Planta venenosa + + Hay plantas venenosas en las cercanías. Tenga cuidado y asegúrese de que + los ninos o los perros no las toquen ni tragarlas. + + + + Piante velenose + + Ci sono piante velenose nelle vicinanze. Fate attenzione e controllate + che bambini o cani non le tocchino o le ingoino. + + + + + + + + + Dangerous animals + + The area is inhabited by possibly dangerous animals, e.g. rabies areas, + venomous snakes, scorpions or bears. + + + + giftige/gefährliche Tiere + + In dem Gebiet sind Wildtiere angesiedelt, die für Menschen eine Gefahr + darstellen können, z.B. Tollwutgebiete, giftige Schlangen, Skorpione + oder Bären. + + + + Animales Peligrosos + + Esta zona es frecuentada por los animales potencialmente peligrosos, + por ejemplo. zorros rabiosos, serpientes venenosas, escorpiones, osos. + + + + Animali pericolosi + + Quest area e frequentata da animali potenzialmente pericolosi, ad es. + volpi rabide, serpenti velenosi, scorpioni, orsi. + + + + + + + + + Quick cache + + It shouldn't take more than 15 minutes to find this cache. Also, + there should be a parking nearby. + + + + Szybka skrzynka + + Jej znalezienie nie powinno zająć więcej niż 15 minut oraz jest + łatwy dojazd w pobliże skrzynki samochodem. + + + + schnell zu finden + + Der Cache ist üblicherweise in maximal 15 Minuten zu finden. + In der Nähe sollte auch ein Parkplatz sein. + + + + Een snelle oppikker + + + + + + + Overnight stay necessary + + This cache cannot be done within a single day or a single night. + You will have to visit the location for more than one time, + or you must stay overnight. Preparation time is not included in this + calculation, but only the time on site. + + + + Übernachtung erforderlich + + Der Geocache kann nicht mit einer einzigen Tages- oder Nachttour gelöst + werden. Er muss entweder mehrmals angefahren werden oder es muss vor Ort + übernachtet werden. Zeit für Recherchen vorab sind dabei nicht + berücksichtigt, sondern nur die Zeit vor Ort. + + + + Necesario pernoctar + + No puedrá encontrar este cache en un solo día o durante la noche. Usted + tendrá que visitar el lugar más de una vez, o necesitará pasar la noche. + El tiempo de preparación no está incluido en este cálculo, sólo el tiempo + en el sitio. + + + + Necessario pernottamento + + Non e possibile trovare questa cache in un solo giorno o una sola notte. + Dovrete visitare il percorso per piu di una volta, oppure e necessario il + pernottamento. Il tempo di preparazione non e incluso in questo calcolo, + ma solo il tempo sul sito. + + + + + + + + + + Take your children + + This search if simple and safe. It's okay to take small children + with you. + + + + Można zabrać dzieci + + Jej poszukiwanie jest przyjemne, bezpieczne i można bez obaw + wybrać się z małymi dziećmi. + + + + für kleine Kinder geeignet + + Der Cache ist einfach und – nach Einschätzung des Besitzers – + ohne besondere Gefahren findbar. + + + + Kindvriendelijke cache + + Dit is een makkelijke en veilige cache. De kleine kinderen + kunnen ook aan deze cache deelnemen. + + + + + + + + + Suited for children (10-12 yo) + + This geocache is suitable for children. All challenges can be solved by + child in the age of 10 to 12 years and the terrain has no risks + (like highways, abysms). There should be a large geocache container with + trading items inside and the challenges be interesting. + + + + kindgerecht (10-12 Jahre) + + Der Geocache ist kindgerecht aufgebaut: Alle Aufgaben sind von Kindern + im Alter von 10 bis 12 Jahren selbstständig lösbar und das Gelände ist + nicht gefährlich (keine Haupstraßen, Klippen o.ä.). Am Ende des + Geocaches sollte sich eine Box mit Tauschgegenständen befinden, und + die Aufgaben sollten interessant aufgebaut sein. + + + + Apto para ninos (10-12 anos) + + Este geocache se creó para los ninos. Todas las tareas se puede + completar por los ninos entre los anos 10 y 12 y el terrno no está + exenta de riesgo (tales como carreteras, acantilados). Hay un gra + contenedor con intercambio final y las tareas son interesantes. + + + + Suited for children (10-12 anni) + + Questa geocache e stata creata per i bambini. Tutte i compiti possono + essere portati a termine da bambini tra 10 e 12 anni e il terreno non + presenta rischi (come autostrade, abissi). C'e un grande contenitore + finale con oggetti di scambio e i compiti sono interessanti. + + + + + + + + Reverse Cache + + This geocache can be found at different places. The places to look for + are explained in the cache description. They must be located "away from + home" and exist for a considerable period of time, so that it can be + re-visited later. There is no container and no logbook; instead the + find must be documented by a photo and noting the coordinates. + + + + Safari-Cache + + Dieser Geocache kann an verschiedenen Orten gefunden werden. Die + gesuchten Orte sind in der Cachebeschreibung erläutert. Sie müssen + sich „außer Haus“ befinden und über längere Zeit Bestand haben, + sodass man sie nach einiger Zeit wieder besuchen kann. Es gibt keinen + Behälter und kein Logbuch, sondern der Fund ist mit einem Foto und + den Koordinaten des Ortes zu dokumentieren. + + + + + + + + + Available at specified hours (may require access fee) + + This cache can only be done at certain times of day or week - see the cache + description for more details. For example, the cache may be placed in an + area with restricted opening hours. Also, you may be required to pay a fee + in order to enter - see the cache description for details. + + + + Dostępna w określonych godzinach lub płatna + + Dostępna w określonych dniach, godzinach, często wstęp płatny. + Często będzie to muzeum lub skansen. Szczegółowe informacje o + dostępności powinny znajdować się w opisie skrzynki. + + + + nur zu bestimmten Uhrzeiten, Eintrittsgeld + + Der Cache kann nur zu bestimmten Uhrzeiten oder an bestimmten Wochentagen + gefunden werden; mehr dazu in der Cachebeschreibung. Häufig handelt es + sich um Caches in Museen etc., die Eintrittsgeld verlangen. + + + diff --git a/htdocs/okapi/services/attrs/attribute.php b/htdocs/okapi/services/attrs/attribute.php index e12a5aa1..511697bf 100644 --- a/htdocs/okapi/services/attrs/attribute.php +++ b/htdocs/okapi/services/attrs/attribute.php @@ -17,45 +17,45 @@ use okapi\services\attrs\AttrHelper; class WebService { - public static function options() - { - return array( - 'min_auth_level' => 1 - ); - } + public static function options() + { + return array( + 'min_auth_level' => 1 + ); + } - public static function call(OkapiRequest $request) - { - # Read the parameters. + public static function call(OkapiRequest $request) + { + # Read the parameters. - $acode = $request->get_parameter('acode'); - if ($acode === null) throw new ParamMissing('acode'); + $acode = $request->get_parameter('acode'); + if ($acode === null) throw new ParamMissing('acode'); - $langpref = $request->get_parameter('langpref'); - if (!$langpref) $langpref = "en"; + $langpref = $request->get_parameter('langpref'); + if (!$langpref) $langpref = "en"; - $fields = $request->get_parameter('fields'); - if (!$fields) $fields = "name"; + $fields = $request->get_parameter('fields'); + if (!$fields) $fields = "name"; - $forward_compatible = $request->get_parameter('forward_compatible'); - if (!$forward_compatible) $forward_compatible = "true"; + $forward_compatible = $request->get_parameter('forward_compatible'); + if (!$forward_compatible) $forward_compatible = "true"; - # Pass them all to the attributes method. + # Pass them all to the attributes method. - $params = array( - 'acodes' => $acode, - 'langpref' => $langpref, - 'fields' => $fields, - 'forward_compatible' => $forward_compatible - ); - $results = OkapiServiceRunner::call('services/attrs/attributes', - new OkapiInternalRequest($request->consumer, $request->token, $params)); - $result = $results[$acode]; - if ($result === null) - { - /* Note, this can happen only when $forward_compatible is false. */ - throw new InvalidParam('acode', "Unknown A-code."); - } - return Okapi::formatted_response($request, $result); - } + $params = array( + 'acodes' => $acode, + 'langpref' => $langpref, + 'fields' => $fields, + 'forward_compatible' => $forward_compatible + ); + $results = OkapiServiceRunner::call('services/attrs/attributes', + new OkapiInternalRequest($request->consumer, $request->token, $params)); + $result = $results[$acode]; + if ($result === null) + { + /* Note, this can happen only when $forward_compatible is false. */ + throw new InvalidParam('acode', "Unknown A-code."); + } + return Okapi::formatted_response($request, $result); + } } diff --git a/htdocs/okapi/services/attrs/attribute.xml b/htdocs/okapi/services/attrs/attribute.xml index 63bb9e7f..29704806 100644 --- a/htdocs/okapi/services/attrs/attribute.xml +++ b/htdocs/okapi/services/attrs/attribute.xml @@ -1,134 +1,134 @@ - Retrieve data on a single attribute - 268 - -

    Retrieve data on a single OKAPI geocache-attribute.

    - -

    OKAPI attributes are identified by an unique ID called an A-code. - All OKAPI attributes are shared among all OKAPI servers. Once an attribute is - published (e.g. via the attribute_index method), it won't - 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 - by their original A-code.

    -
    - - The A-code of the attribute you're interested in. - - -

    Pipe-separated list of ISO 639-1 language codes. This indicates the - order of preference in which language will be chosen for fields like - name and description.

    -
    - -

    Pipe-separated list of field names which you are interested with. - See below for the list of available fields.

    -
    - - 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 - differently, then you may change this behavior by setting this parameter - to false. Then, OKAPI will return HTTP 400 error response, - instead of the placeholder (note that it behaves differently in the - attributes method). - - - -

    A dictionary of fields you have selected in the fields - parameter. Available fields:

    - -
      -
    • -

      acode - string, the A-code. Unique identifier of the - attribute.

      -
    • -
    • -

      name - plaintext string, name of the attribute (language is - selected based on your langpref parameter),

      - -

      If you think your language is missing, then feel free to add missing - translations directly to OKAPI repository. See - here.

      -
    • -
    • -

      names - a dictionary of all known names of the attribute, in - various languages (ISO 639-1 language code is used as dictionary - key).

      - -

      If you think your language is missing, then feel free to add missing - translations directly to OKAPI repository. See - here.

      -
    • -
    • - description - HTML string, description of the attribute (language is - selected based on your langpref parameter), - -

      If you think your language is missing, then feel free to add missing - translations directly to OKAPI repository. See - here.

      -
    • -
    • -

      descriptions - a dictionary of all known descriptions of the - attribute, in various languages (ISO 639-1 language code is used as - dictionary key).

      - -

      If you think your language is missing, then feel free to add missing - translations directly to OKAPI repository. See - here.

      -
    • -
    • -

      gc_equivs - a list of Geocaching.com (Groundspeak) - attributes, which have exactly the same (or a very similar) meaning. Each - attribute is described as a dictionary of the following structure:

      - -
        -
      • id - ID of the Geocaching.com attribute,
      • -
      • - inc - integer, either 1 or 0. See Geocaching.com's - XSD for details on its meaning, -
      • -
      • - name - the name of the attribute (as it is included - in Geocaching.com GPX files). -
      • -
      - -

      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.

      -
    • -
    • -

      is_locally_used - boolean, indicates if the attribute is currently - used by this Opencaching server. Or, to be more specific, true - means that the attribute can currently be included in the attr_acodes field - of the geocache method in this OKAPI installation.

      - -

      Note that this flag can change in time. Some attributes may get - introduced into other installations, whereas other attributes may - (temporarily or permanently) stop being used. In general, we are aiming - towards global unification of all attributes between all OC nodes, - but this process will take time (and probably it will never - be 100% complete).

      -
    • -
    • -

      local_icon_url - an URL pointing to an image associated with - this particular attribute in the local OC server, or null if the - current server does not have any image for this attribute.

      - -

      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. - In other words, if you want to use this attribute, then you must always be - prepared to receive null, or an image of unexpected dimensions.

      -
    • -
    • -

      is_discontinued - boolean, indicates if the attribute is discontinued. - 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, - it may still be in use at servers which have not been updated yet to the - current OKAPI version.

      - -

      Important: This flag can change in time. Discontinued attributes can - "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.

      -
    • -
    -
    + Retrieve data on a single attribute + 268 + +

    Retrieve data on a single OKAPI geocache-attribute.

    + +

    OKAPI attributes are identified by an unique ID called an A-code. + All OKAPI attributes are shared among all OKAPI servers. Once an attribute is + published (e.g. via the attribute_index method), it won't + 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 + by their original A-code.

    +
    + + The A-code of the attribute you're interested in. + + +

    Pipe-separated list of ISO 639-1 language codes. This indicates the + order of preference in which language will be chosen for fields like + name and description.

    +
    + +

    Pipe-separated list of field names which you are interested with. + See below for the list of available fields.

    +
    + + 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 + differently, then you may change this behavior by setting this parameter + to false. Then, OKAPI will return HTTP 400 error response, + instead of the placeholder (note that it behaves differently in the + attributes method). + + + +

    A dictionary of fields you have selected in the fields + parameter. Available fields:

    + +
      +
    • +

      acode - string, the A-code. Unique identifier of the + attribute.

      +
    • +
    • +

      name - plaintext string, name of the attribute (language is + selected based on your langpref parameter),

      + +

      If you think your language is missing, then feel free to add missing + translations directly to OKAPI repository. See + here.

      +
    • +
    • +

      names - a dictionary of all known names of the attribute, in + various languages (ISO 639-1 language code is used as dictionary + key).

      + +

      If you think your language is missing, then feel free to add missing + translations directly to OKAPI repository. See + here.

      +
    • +
    • + description - HTML string, description of the attribute (language is + selected based on your langpref parameter), + +

      If you think your language is missing, then feel free to add missing + translations directly to OKAPI repository. See + here.

      +
    • +
    • +

      descriptions - a dictionary of all known descriptions of the + attribute, in various languages (ISO 639-1 language code is used as + dictionary key).

      + +

      If you think your language is missing, then feel free to add missing + translations directly to OKAPI repository. See + here.

      +
    • +
    • +

      gc_equivs - a list of Geocaching.com (Groundspeak) + attributes, which have exactly the same (or a very similar) meaning. Each + attribute is described as a dictionary of the following structure:

      + +
        +
      • id - ID of the Geocaching.com attribute,
      • +
      • + inc - integer, either 1 or 0. See Geocaching.com's + XSD for details on its meaning, +
      • +
      • + name - the name of the attribute (as it is included + in Geocaching.com GPX files). +
      • +
      + +

      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.

      +
    • +
    • +

      is_locally_used - boolean, indicates if the attribute is currently + used by this Opencaching server. Or, to be more specific, true + means that the attribute can currently be included in the attr_acodes field + of the geocache method in this OKAPI installation.

      + +

      Note that this flag can change in time. Some attributes may get + introduced into other installations, whereas other attributes may + (temporarily or permanently) stop being used. In general, we are aiming + towards global unification of all attributes between all OC nodes, + but this process will take time (and probably it will never + be 100% complete).

      +
    • +
    • +

      local_icon_url - an URL pointing to an image associated with + this particular attribute in the local OC server, or null if the + current server does not have any image for this attribute.

      + +

      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. + In other words, if you want to use this attribute, then you must always be + prepared to receive null, or an image of unexpected dimensions.

      +
    • +
    • +

      is_discontinued - boolean, indicates if the attribute is discontinued. + 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, + it may still be in use at servers which have not been updated yet to the + current OKAPI version.

      + +

      Important: This flag can change in time. Discontinued attributes can + "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.

      +
    • +
    +
    \ No newline at end of file diff --git a/htdocs/okapi/services/attrs/attribute_index.php b/htdocs/okapi/services/attrs/attribute_index.php index ef6808b0..56a9756e 100644 --- a/htdocs/okapi/services/attrs/attribute_index.php +++ b/htdocs/okapi/services/attrs/attribute_index.php @@ -18,56 +18,56 @@ use okapi\services\attrs\AttrHelper; class WebService { - public static function options() - { - return array( - 'min_auth_level' => 1 - ); - } + public static function options() + { + return array( + 'min_auth_level' => 1 + ); + } - public static function call(OkapiRequest $request) - { - # Read the parameters. + public static function call(OkapiRequest $request) + { + # Read the parameters. - $langpref = $request->get_parameter('langpref'); - if (!$langpref) $langpref = "en"; + $langpref = $request->get_parameter('langpref'); + if (!$langpref) $langpref = "en"; - $fields = $request->get_parameter('fields'); - if (!$fields) $fields = "name"; + $fields = $request->get_parameter('fields'); + if (!$fields) $fields = "name"; - $only_locally_used = $request->get_parameter('only_locally_used'); - if (!$only_locally_used) $only_locally_used = "false"; - $only_locally_used = ($only_locally_used == "true"); + $only_locally_used = $request->get_parameter('only_locally_used'); + if (!$only_locally_used) $only_locally_used = "false"; + $only_locally_used = ($only_locally_used == "true"); - # Get the list of attributes and filter the A-codes based on the - # parameters. + # Get the list of attributes and filter the A-codes based on the + # parameters. - require_once 'attr_helper.inc.php'; - $attrdict = AttrHelper::get_attrdict(); - $acodes = array(); - foreach ($attrdict as $acode => &$attr_ref) - { - if ($only_locally_used && ($attr_ref['internal_id'] === null)) { - /* Skip. */ - continue; - } + require_once 'attr_helper.inc.php'; + $attrdict = AttrHelper::get_attrdict(); + $acodes = array(); + foreach ($attrdict as $acode => &$attr_ref) + { + if ($only_locally_used && ($attr_ref['internal_id'] === null)) { + /* Skip. */ + 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) { - $params = array( - 'acodes' => implode("|", $acodes), - 'langpref' => $langpref, - 'fields' => $fields, - ); - $results = OkapiServiceRunner::call('services/attrs/attributes', - new OkapiInternalRequest($request->consumer, $request->token, $params)); - } else { - $results = new ArrayObject(); - } - return Okapi::formatted_response($request, $results); - } + if (count($acodes) > 0) { + $params = array( + 'acodes' => implode("|", $acodes), + 'langpref' => $langpref, + 'fields' => $fields, + ); + $results = OkapiServiceRunner::call('services/attrs/attributes', + new OkapiInternalRequest($request->consumer, $request->token, $params)); + } else { + $results = new ArrayObject(); + } + return Okapi::formatted_response($request, $results); + } } diff --git a/htdocs/okapi/services/attrs/attribute_index.xml b/htdocs/okapi/services/attrs/attribute_index.xml index dd2dad95..ef3f46a7 100644 --- a/htdocs/okapi/services/attrs/attribute_index.xml +++ b/htdocs/okapi/services/attrs/attribute_index.xml @@ -1,35 +1,35 @@ - Get the list of all OKAPI attributes (A-codes) - 270 - -

    This method returns all currently defined OKAPI geocache-attributes. - It is useful when you want to cache the data on the client-side.

    - -

    Keep in mind that the number of attributes will grow. Hence, if your application - uses multiple OKAPI servers, then it's best to use one of the frequently - updated servers for pre-caching attribute data (currently, Opencaching.PL - 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!

    - -

    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).

    -
    - - Works the same as in the attribute method. - - - Works the same as in the attribute method. - - -

    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 - (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 - installation, then you may set this parameter to true.

    -
    - - - A dictionary. All A-codes will be mapped to dictionary keys, and - each value will be an object, as described in the attribute method. - + Get the list of all OKAPI attributes (A-codes) + 270 + +

    This method returns all currently defined OKAPI geocache-attributes. + It is useful when you want to cache the data on the client-side.

    + +

    Keep in mind that the number of attributes will grow. Hence, if your application + uses multiple OKAPI servers, then it's best to use one of the frequently + updated servers for pre-caching attribute data (currently, Opencaching.PL + 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!

    + +

    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).

    +
    + + Works the same as in the attribute method. + + + Works the same as in the attribute method. + + +

    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 + (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 + installation, then you may set this parameter to true.

    +
    + + + A dictionary. All A-codes will be mapped to dictionary keys, and + each value will be an object, as described in the attribute method. +
    \ No newline at end of file diff --git a/htdocs/okapi/services/attrs/attributes.php b/htdocs/okapi/services/attrs/attributes.php index aa0501e2..26ebcb19 100644 --- a/htdocs/okapi/services/attrs/attributes.php +++ b/htdocs/okapi/services/attrs/attributes.php @@ -18,120 +18,120 @@ use okapi\Db; class WebService { - public static function options() - { - return array( - 'min_auth_level' => 1 - ); - } + public static function options() + { + return array( + 'min_auth_level' => 1 + ); + } - private static $valid_field_names = array( - 'acode', 'name', 'names', 'description', 'descriptions', 'gc_equivs', - 'is_locally_used', 'is_deprecated', 'local_icon_url', 'is_discontinued' - ); + private static $valid_field_names = array( + 'acode', 'name', 'names', 'description', 'descriptions', 'gc_equivs', + 'is_locally_used', 'is_deprecated', 'local_icon_url', 'is_discontinued' + ); - public static function call(OkapiRequest $request) - { - # Read the parameters. + public static function call(OkapiRequest $request) + { + # Read the parameters. - $acodes = $request->get_parameter('acodes'); - if (!$acodes) throw new ParamMissing('acodes'); - $acodes = explode("|", $acodes); + $acodes = $request->get_parameter('acodes'); + if (!$acodes) throw new ParamMissing('acodes'); + $acodes = explode("|", $acodes); - $langpref = $request->get_parameter('langpref'); - if (!$langpref) $langpref = "en"; - $langpref = explode("|", $langpref); + $langpref = $request->get_parameter('langpref'); + if (!$langpref) $langpref = "en"; + $langpref = explode("|", $langpref); - $fields = $request->get_parameter('fields'); - if (!$fields) $fields = "name"; - $fields = explode("|", $fields); - foreach ($fields as $field) - { - if (!in_array($field, self::$valid_field_names)) - throw new InvalidParam('fields', "'$field' is not a valid field code."); - } + $fields = $request->get_parameter('fields'); + if (!$fields) $fields = "name"; + $fields = explode("|", $fields); + foreach ($fields as $field) + { + if (!in_array($field, self::$valid_field_names)) + throw new InvalidParam('fields', "'$field' is not a valid field code."); + } - $forward_compatible = $request->get_parameter('forward_compatible'); - if (!$forward_compatible) $forward_compatible = "true"; - if (!in_array($forward_compatible, array("true", "false"))) - throw new InvalidParam('forward_compatible'); - $forward_compatible = ($forward_compatible == "true"); + $forward_compatible = $request->get_parameter('forward_compatible'); + if (!$forward_compatible) $forward_compatible = "true"; + if (!in_array($forward_compatible, array("true", "false"))) + throw new InvalidParam('forward_compatible'); + $forward_compatible = ($forward_compatible == "true"); - # Load the attributes (all of them). + # Load the attributes (all of them). - require_once 'attr_helper.inc.php'; - $attrdict = AttrHelper::get_attrdict(); + require_once 'attr_helper.inc.php'; + $attrdict = AttrHelper::get_attrdict(); - # For each A-code, check if it exists, filter its fields and add it - # to the results. + # For each A-code, check if it exists, filter its fields and add it + # to the results. - $results = array(); - foreach ($acodes as $acode) - { - /* Please note, that the $attr variable from the $attrdict dictionary - * below is NOT fully compatible with the interface of the "attribute" - * method. Some of $attr's fields are private and should not be exposed, - * other fields don't exist and have to be added dynamically! */ + $results = array(); + foreach ($acodes as $acode) + { + /* Please note, that the $attr variable from the $attrdict dictionary + * below is NOT fully compatible with the interface of the "attribute" + * method. Some of $attr's fields are private and should not be exposed, + * other fields don't exist and have to be added dynamically! */ - if (isset($attrdict[$acode])) { - $attr = $attrdict[$acode]; - } elseif ($forward_compatible) { - $attr = AttrHelper::get_unknown_placeholder($acode); - } else { - $results[$acode] = null; - continue; - } + if (isset($attrdict[$acode])) { + $attr = $attrdict[$acode]; + } elseif ($forward_compatible) { + $attr = AttrHelper::get_unknown_placeholder($acode); + } else { + $results[$acode] = null; + continue; + } - # Fill langpref-specific fields. + # Fill langpref-specific fields. - $attr['name'] = Okapi::pick_best_language($attr['names'], $langpref); - $attr['description'] = Okapi::pick_best_language($attr['descriptions'], $langpref); + $attr['name'] = Okapi::pick_best_language($attr['names'], $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_deprecated'] = $attr['is_discontinued']; // deprecated and undocumetned field, see issue 70 + $attr['is_locally_used'] = ($attr['internal_id'] !== null); + $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 - # in the $attrdict because currently we have no way of knowing then they - # change.) + # 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 + # change.) - if (in_array('local_icon_url', $fields)) - { - $tmp = Db::select_all(" - select id, icon_large - from cache_attrib - "); - $map = array(); - foreach ($tmp as &$row_ref) { - $map[$row_ref['id']] = &$row_ref; - } - $prefix = Settings::get('SITE_URL'); - foreach ($results as &$attr_ref) { - $internal_id = $attr_ref['internal_id']; - if (isset($map[$internal_id])) { - $row = $map[$internal_id]; - $attr_ref['local_icon_url'] = $prefix.$row['icon_large']; - } else { - $attr_ref['local_icon_url'] = null; - } - } - } + if (in_array('local_icon_url', $fields)) + { + $tmp = Db::select_all(" + select id, icon_large + from cache_attrib + "); + $map = array(); + foreach ($tmp as &$row_ref) { + $map[$row_ref['id']] = &$row_ref; + } + $prefix = Settings::get('SITE_URL'); + foreach ($results as &$attr_ref) { + $internal_id = $attr_ref['internal_id']; + if (isset($map[$internal_id])) { + $row = $map[$internal_id]; + $attr_ref['local_icon_url'] = $prefix.$row['icon_large']; + } else { + $attr_ref['local_icon_url'] = null; + } + } + } - # Filter the fields. + # Filter the fields. - foreach ($results as &$attr_ref) { - $clean_row = array(); - foreach ($fields as $field) - $clean_row[$field] = $attr_ref[$field]; - $attr_ref = $clean_row; - } + foreach ($results as &$attr_ref) { + $clean_row = array(); + foreach ($fields as $field) + $clean_row[$field] = $attr_ref[$field]; + $attr_ref = $clean_row; + } - return Okapi::formatted_response($request, $results); - } + return Okapi::formatted_response($request, $results); + } } diff --git a/htdocs/okapi/services/attrs/attributes.xml b/htdocs/okapi/services/attrs/attributes.xml index e32e3c61..b1b5c8c4 100644 --- a/htdocs/okapi/services/attrs/attributes.xml +++ b/htdocs/okapi/services/attrs/attributes.xml @@ -1,30 +1,30 @@ - Retrieve data on multiple attributes at once - 269 - -

    This method works like the attribute method, but - with multiple A-codes instead of only one. Read the docs for - the attribute method first!

    -
    - - Pipe-separated list of A-codes you're interested in. - - - Works the same as in the attribute method. - - - Works the same as in the attribute method. - - - This has a similar meaning as in the attribute method, but works - differently. If set to false, OKAPI will return nulls - for unknown keys (A-codes). You will still receive an HTTP 200 response - though! - - - -

    A dictionary. A-codes you provide will be mapped to dictionary keys, - and each value will be an object, as described in the attribute - method.

    -
    + Retrieve data on multiple attributes at once + 269 + +

    This method works like the attribute method, but + with multiple A-codes instead of only one. Read the docs for + the attribute method first!

    +
    + + Pipe-separated list of A-codes you're interested in. + + + Works the same as in the attribute method. + + + Works the same as in the attribute method. + + + This has a similar meaning as in the attribute method, but works + differently. If set to false, OKAPI will return nulls + for unknown keys (A-codes). You will still receive an HTTP 200 response + though! + + + +

    A dictionary. A-codes you provide will be mapped to dictionary keys, + and each value will be an object, as described in the attribute + method.

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

    Produce a ZIP file with content compatible with Geocaching-enabled Garmin - GPS devices. The general idea is, you should extract the contents of the ZIP - file directly into the root directory of your Garmin's internal memory storage - (currently it doesn't work if you extract the data to the external MicroSD card). The device - will then be filled with all the data we are currently able to fill it with.

    -

    Currently, the ZIP file will contain GPX file and all JPEG images associated - with the chosen geocaches. Options for the GPX file are fixed, use the - services/caches/formatters/gpx method if - you want a custom-tailored GPX file.

    -

    Important note: The contents of the returned ZIP archive may change in the - future. You should not parse the contents, nor assume that they have any specific - structure.

    -

    Important note: We might be increasing required authentication level for - this method to level 2 or 3. You should be prepared for that. Use the highest authentication - level you've got. Also, if you want the GPX file to include personal data (like user's notes), - you have to use Level 3 Authentication anyway.

    -

    Note: All non-JPEG images will be skipped. Currently OKAPI does not convert - other types of images to JPEG.

    -
    - -

    Pipe-separated list of cache codes which you are interested in. - No more than 50 codes are allowed. (This limit is smaller than usual, because - we're afraid about the sizes of ZIP files produced.) - This CAN be an empty string (it will still result in a valid ZIP file).

    -
    - -

    Pipe-separated list of ISO 639-1 language codes. This indicates the - order of preference in which language will be chosen for GPX entities.

    -
    - -

    One of the following values:

    -
      -
    • none - no images will be included in the result,
    • -
    • all - all images will be included in the result,
    • -
    • spoilers - only spoiler images will be included in the result,
    • -
    • nonspoilers - only non-spoiler images will be included in the result.
    • -
    -
    - -

    ZIP file. You should extract it's contents directly into the root - directory of your Garmin's internal memory storage.

    -
    + Retrieve ZIP file for Garmin devices + 99 + +

    Produce a ZIP file with content compatible with Geocaching-enabled Garmin + GPS devices. The general idea is, you should extract the contents of the ZIP + file directly into the root directory of your Garmin's internal memory storage + (currently it doesn't work if you extract the data to the external MicroSD card). The device + will then be filled with all the data we are currently able to fill it with.

    +

    Currently, the ZIP file will contain GPX file and all JPEG images associated + with the chosen geocaches. Options for the GPX file are fixed, use the + services/caches/formatters/gpx method if + you want a custom-tailored GPX file.

    +

    Important note: The contents of the returned ZIP archive may change in the + future. You should not parse the contents, nor assume that they have any specific + structure.

    +

    Important note: We might be increasing required authentication level for + this method to level 2 or 3. You should be prepared for that. Use the highest authentication + level you've got. Also, if you want the GPX file to include personal data (like user's notes), + you have to use Level 3 Authentication anyway.

    +

    Note: All non-JPEG images will be skipped. Currently OKAPI does not convert + other types of images to JPEG.

    +
    + +

    Pipe-separated list of cache codes which you are interested in. + No more than 50 codes are allowed. (This limit is smaller than usual, because + we're afraid about the sizes of ZIP files produced.) + This CAN be an empty string (it will still result in a valid ZIP file).

    +
    + +

    Pipe-separated list of ISO 639-1 language codes. This indicates the + order of preference in which language will be chosen for GPX entities.

    +
    + +

    One of the following values:

    +
      +
    • none - no images will be included in the result,
    • +
    • all - all images will be included in the result,
    • +
    • spoilers - only spoiler images will be included in the result,
    • +
    • nonspoilers - only non-spoiler images will be included in the result.
    • +
    +
    + + Same as in the + services/caches/formatters/gpx method. + + + Same as in the + services/caches/formatters/gpx method. + + +

    ZIP file. You should extract it's contents directly into the root + directory of your Garmin's internal memory storage.

    +
    \ No newline at end of file diff --git a/htdocs/okapi/services/caches/formatters/gpx.php b/htdocs/okapi/services/caches/formatters/gpx.php index 8ae40cc9..389ff358 100644 --- a/htdocs/okapi/services/caches/formatters/gpx.php +++ b/htdocs/okapi/services/caches/formatters/gpx.php @@ -19,297 +19,383 @@ use okapi\services\attrs\AttrHelper; class WebService { - public static function options() - { - return array( - 'min_auth_level' => 1 - ); - } + public static function options() + { + return array( + 'min_auth_level' => 1 + ); + } - /** Maps OKAPI cache type codes to Geocaching.com GPX cache types. */ - public static $cache_GPX_types = array( - 'Traditional' => 'Traditional Cache', - 'Multi' => 'Multi-Cache', - 'Quiz' => 'Unknown Cache', - 'Event' => 'Event Cache', - 'Virtual' => 'Virtual Cache', - 'Webcam' => 'Webcam Cache', - 'Moving' => 'Unknown Cache', - 'Math/Physics' => 'Unknown Cache', - 'Drive-In' => 'Traditional Cache', - 'Own' => 'Unknown Cache', - 'Other' => 'Unknown Cache' - ); + /** Maps OKAPI cache type codes to Geocaching.com GPX cache types. */ + public static $cache_GPX_types = array( + 'Traditional' => 'Traditional Cache', + 'Multi' => 'Multi-Cache', + 'Quiz' => 'Unknown Cache', + 'Event' => 'Event Cache', + 'Virtual' => 'Virtual Cache', + 'Webcam' => 'Webcam Cache', + 'Moving' => 'Unknown Cache', + 'Math/Physics' => 'Unknown Cache', + 'Drive-In' => 'Traditional Cache', + 'Podcast' => 'Unknown Cache', + 'Own' => 'Unknown Cache', + 'Other' => 'Unknown Cache' + ); - /** Maps OKAPI's 'size2' values to geocaching.com size codes. */ - public static $cache_GPX_sizes = array( - 'none' => 'Virtual', - 'nano' => 'Micro', - 'micro' => 'Micro', - 'small' => 'Small', - 'regular' => 'Regular', - 'large' => 'Large', - 'xlarge' => 'Large', - 'other' => 'Other', - ); + /** Maps OKAPI's 'size2' values to geocaching.com size codes. */ + public static $cache_GPX_sizes = array( + 'none' => 'Virtual', + 'nano' => 'Micro', + 'micro' => 'Micro', + 'small' => 'Small', + 'regular' => 'Regular', + 'large' => 'Large', + 'xlarge' => 'Large', + 'other' => 'Other', + ); - public static function call(OkapiRequest $request) - { - $vars = array(); + public static function call(OkapiRequest $request) + { + $vars = array(); - # Validating arguments. We will also assign some of them to the - # $vars variable which we will use later in the GPS template. + # Validating arguments. We will also assign some of them to the + # $vars variable which we will use later in the GPS template. - $cache_codes = $request->get_parameter('cache_codes'); - if ($cache_codes === null) throw new ParamMissing('cache_codes'); + $cache_codes = $request->get_parameter('cache_codes'); + if ($cache_codes === null) throw new ParamMissing('cache_codes'); - # Issue 106 requires us to allow empty list of cache codes to be passed into this method. - # All of the queries below have to be ready for $cache_codes to be empty! + # Issue 106 requires us to allow empty list of cache codes to be passed into this method. + # All of the queries below have to be ready for $cache_codes to be empty! - $langpref = $request->get_parameter('langpref'); - if (!$langpref) $langpref = "en"; - foreach (array('ns_ground', 'ns_gsak', 'ns_ox', 'latest_logs', 'alt_wpts', 'mark_found') as $param) - { - $val = $request->get_parameter($param); - if (!$val) $val = "false"; - elseif (!in_array($val, array("true", "false"))) - throw new InvalidParam($param); - $vars[$param] = ($val == "true"); - } - if ($vars['latest_logs'] && (!$vars['ns_ground'])) - throw new BadRequest("In order for 'latest_logs' to work you have to also include 'ns_ground' extensions."); + $langpref = $request->get_parameter('langpref'); + if (!$langpref) $langpref = "en"; + foreach (array('ns_ground', 'ns_gsak', 'ns_ox', 'latest_logs', 'alt_wpts', 'mark_found') as $param) + { + $val = $request->get_parameter($param); + if (!$val) $val = "false"; + elseif (!in_array($val, array("true", "false"))) + throw new InvalidParam($param); + $vars[$param] = ($val == "true"); + } + if ($vars['latest_logs'] && (!$vars['ns_ground'])) + throw new BadRequest("In order for 'latest_logs' to work you have to also include 'ns_ground' extensions."); - $tmp = $request->get_parameter('my_notes'); - $vars['my_notes'] = array(); - if ($tmp && $tmp != 'none') { - $tmp = explode('|', $tmp); - foreach ($tmp as $elem) { - if ($elem == 'none') { - /* pass */ - } elseif (in_array($elem, array('desc:text', 'gc:personal_note'))) { - if (in_array('none', $tmp)) { - throw new InvalidParam( - 'my_notes', "You cannot mix 'none' and '$elem'" - ); - } - if ($request->token == null) { - throw new BadRequest( - "Level 3 Authentication is required to access my_notes data." - ); - } - $vars['my_notes'][] = $elem; - } else { - throw new InvalidParam('my_notes', "Invalid list entry: '$elem'"); - } - } - } + $tmp = $request->get_parameter('my_notes'); + $vars['my_notes'] = array(); + if ($tmp && $tmp != 'none') { + $tmp = explode('|', $tmp); + foreach ($tmp as $elem) { + if ($elem == 'none') { + /* pass */ + } elseif (in_array($elem, array('desc:text', 'gc:personal_note'))) { + if (in_array('none', $tmp)) { + throw new InvalidParam( + 'my_notes', "You cannot mix 'none' and '$elem'" + ); + } + if ($request->token == null) { + throw new BadRequest( + "Level 3 Authentication is required to access my_notes data." + ); + } + $vars['my_notes'][] = $elem; + } else { + throw new InvalidParam('my_notes', "Invalid list entry: '$elem'"); + } + } + } - $images = $request->get_parameter('images'); - if (!$images) $images = 'descrefs:nonspoilers'; - if (!in_array($images, array('none', 'descrefs:thumblinks', 'descrefs:nonspoilers', 'descrefs:all', 'ox:all'))) - throw new InvalidParam('images', "'$images'"); - $vars['images'] = $images; + $images = $request->get_parameter('images'); + if (!$images) $images = 'descrefs:nonspoilers'; + if (!in_array($images, array('none', 'descrefs:thumblinks', 'descrefs:nonspoilers', 'descrefs:all', 'ox:all'))) + throw new InvalidParam('images', "'$images'"); + $vars['images'] = $images; - $tmp = $request->get_parameter('attrs'); - if (!$tmp) $tmp = 'desc:text'; - $tmp = explode("|", $tmp); - $vars['attrs'] = array(); - foreach ($tmp as $elem) - { - if ($elem == 'none') { - /* pass */ - } 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') - $vars['attrs'][] = 'gc:attrs'; - else - $vars['attrs'][] = $elem; - } else { - throw new InvalidParam('attrs', "Invalid list entry: '$elem'"); - } - } + $tmp = $request->get_parameter('attrs'); + if (!$tmp) $tmp = 'desc:text'; + $tmp = explode("|", $tmp); + $vars['attrs'] = array(); + foreach ($tmp as $elem) + { + if ($elem == 'none') { + /* pass */ + } 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') + $vars['attrs'][] = 'gc:attrs'; + else + $vars['attrs'][] = $elem; + } else { + throw new InvalidParam('attrs', "Invalid list entry: '$elem'"); + } + } - $protection_areas = $request->get_parameter('protection_areas'); - if (!$protection_areas || $protection_areas == 'desc:auto') - { - if (Settings::get('OC_BRANCH') == 'oc.de') $protection_areas = 'desc:text'; - else $protection_areas = 'none'; - } - if (!in_array($protection_areas, array('none', 'desc:text'))) - throw new InvalidParam('protection_areas',"'$protection_areas'"); - $vars['protection_areas'] = $protection_areas; + $protection_areas = $request->get_parameter('protection_areas'); + if (!$protection_areas || $protection_areas == 'desc:auto') + { + if (Settings::get('OC_BRANCH') == 'oc.de') $protection_areas = 'desc:text'; + else $protection_areas = 'none'; + } + if (!in_array($protection_areas, array('none', 'desc:text'))) + throw new InvalidParam('protection_areas',"'$protection_areas'"); + $vars['protection_areas'] = $protection_areas; - $tmp = $request->get_parameter('trackables'); - if (!$tmp) $tmp = 'none'; - if (!in_array($tmp, array('none', 'desc:list', 'desc:count'))) - throw new InvalidParam('trackables', "'$tmp'"); - $vars['trackables'] = $tmp; + $tmp = $request->get_parameter('trackables'); + if (!$tmp) $tmp = 'none'; + if (!in_array($tmp, array('none', 'desc:list', 'desc:count'))) + throw new InvalidParam('trackables', "'$tmp'"); + $vars['trackables'] = $tmp; - $tmp = $request->get_parameter('recommendations'); - if (!$tmp) $tmp = 'none'; - if (!in_array($tmp, array('none', 'desc:count'))) - throw new InvalidParam('recommendations', "'$tmp'"); - $vars['recommendations'] = $tmp; + $tmp = $request->get_parameter('recommendations'); + if (!$tmp) $tmp = 'none'; + if (!in_array($tmp, array('none', 'desc:count'))) + throw new InvalidParam('recommendations', "'$tmp'"); + $vars['recommendations'] = $tmp; - $lpc = $request->get_parameter('lpc'); - if ($lpc === null) $lpc = 10; # will be checked in services/caches/geocaches call + $lpc = $request->get_parameter('lpc'); + if ($lpc === null) $lpc = 10; # will be checked in services/caches/geocaches call - $user_uuid = $request->get_parameter('user_uuid'); + $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'. - '|difficulty|terrain|description|hint2|rating|owner|url|internal_id'. - '|protection_areas'; - if ($vars['images'] != 'none') - $fields .= "|images"; - if (count($vars['attrs']) > 0) - $fields .= "|attrnames|attr_acodes"; - if ($vars['trackables'] == 'desc:list') - $fields .= "|trackables"; - elseif ($vars['trackables'] == 'desc:count') - $fields .= "|trackables_count"; - if ($vars['alt_wpts'] == 'true') - $fields .= "|alt_wpts"; - if ($vars['recommendations'] != 'none') - $fields .= "|recommendations|founds"; - if (count($vars['my_notes']) > 0) - $fields .= "|my_notes"; - if ($vars['latest_logs']) - $fields .= "|latest_logs"; - if ($vars['mark_found']) - $fields .= "|is_found"; + $location_source = $request->get_parameter('location_source'); + if (!$location_source) + { + $location_source = 'default-coords'; + } + # Make sure location_source has prefix alt_wpt: + if ($location_source != 'default-coords' && strncmp($location_source, 'alt_wpt:', 8) != 0) + { + throw new InvalidParam('location_source', '\''.$location_source.'\''); + } - $vars['caches'] = OkapiServiceRunner::call( - 'services/caches/geocaches', new OkapiInternalRequest( - $request->consumer, $request->token, array( - 'cache_codes' => $cache_codes, - 'langpref' => $langpref, - 'fields' => $fields, - 'lpc' => $lpc, - 'user_uuid' => $user_uuid, - 'log_fields' => 'uuid|date|user|type|comment|internal_id|was_recommended' - ) - ) - ); + # Make sure we have sufficient authorization + if ($location_source == 'alt_wpt:user-coords' && $request->token == null) + { + throw new BadRequest("Level 3 Authentication is required to access 'alt_wpt:user-coords'."); + } - # Get all the other data need. + # Which fields of the services/caches/geocaches method do we need? - $vars['installation'] = OkapiServiceRunner::call( - 'services/apisrv/installation', new OkapiInternalRequest( - new OkapiInternalConsumer(), null, array() - ) - ); - $vars['cache_GPX_types'] = self::$cache_GPX_types; - $vars['cache_GPX_sizes'] = self::$cache_GPX_sizes; + $fields = 'code|name|location|date_created|url|type|status|size|size2|oxsize'. + '|difficulty|terrain|description|hint2|rating|owner|url|internal_id'. + '|protection_areas|short_description'; + if ($vars['images'] != 'none') + $fields .= "|images"; + if (count($vars['attrs']) > 0) + $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) - { - /* The user asked for some kind of attribute output. We'll fetch all - * the data we MAY need. This is often far too much, but thanks to - * caching, it will work fast. */ + $vars['caches'] = OkapiServiceRunner::call( + 'services/caches/geocaches', new OkapiInternalRequest( + $request->consumer, $request->token, array( + 'cache_codes' => $cache_codes, + 'langpref' => $langpref, + 'fields' => $fields, + 'lpc' => $lpc, + 'user_uuid' => $user_uuid, + 'log_fields' => 'uuid|date|user|type|comment|internal_id|was_recommended' + ) + ) + ); - $vars['attr_index'] = OkapiServiceRunner::call( - 'services/attrs/attribute_index', new OkapiInternalRequest( - $request->consumer, $request->token, array( - 'only_locally_used' => 'true', - 'langpref' => $langpref, - 'fields' => 'name|gc_equivs' - ) - ) - ); + # Get rid of invalid cache references. - # 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']); - $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. + # Get all the other data need. - $ocde_attrnames = Db::select_group_by('id'," - select id, name - from cache_attrib - "); - $attr_dict = AttrHelper::get_attrdict(); - } + $vars['installation'] = OkapiServiceRunner::call( + 'services/apisrv/installation', new OkapiInternalRequest( + new OkapiInternalConsumer(), null, array() + ) + ); + $vars['cache_GPX_types'] = self::$cache_GPX_types; + $vars['cache_GPX_sizes'] = self::$cache_GPX_sizes; - foreach ($vars['caches'] as &$cache) - { - $cache['gc_attrs'] = array(); - foreach ($cache['attr_acodes'] as $acode) - { - $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. + if (count($vars['attrs']) > 0) + { + /* The user asked for some kind of attribute output. We'll fetch all + * the data we MAY need. This is often far too much, but thanks to + * caching, it will work fast. */ - $cache['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. + $vars['attr_index'] = OkapiServiceRunner::call( + 'services/attrs/attribute_index', new OkapiInternalRequest( + $request->consumer, $request->token, array( + 'only_locally_used' => 'true', + 'langpref' => $langpref, + 'fields' => 'name|gc_equivs' + ) + ) + ); - $internal_id = $attr_dict[$acode]['internal_id']; - $cache['gc_attrs'][100 + $internal_id] = array( - 'inc' => 1, - 'name' => $ocde_attrnames[$internal_id][0]['name'], - ); - } - } - } - } - } + # prepare GS attribute data - /* 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. */ + $vars['gc_attrs'] = in_array('gc:attrs', $vars['attrs']); + $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. - $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); + $ocde_attrnames = Db::select_group_by('id'," + select id, name + from cache_attrib + "); + $attr_dict = AttrHelper::get_attrdict(); + } - $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; - } + foreach ($vars['caches'] as &$cache_ref) + { + $cache_ref['gc_attrs'] = array(); + foreach ($cache_ref['attr_acodes'] as $acode) + { + $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_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'] = _( + "Geocache coordinates have been changed. They have been replaced with ". + "your own custom coordinates which you have provided for this geocache." + ); + } else { + # Default warning + $cache_ref['warning_prefix'] = _( + "Geocache coordinates have been changed. 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; + } } diff --git a/htdocs/okapi/services/caches/formatters/gpx.xml b/htdocs/okapi/services/caches/formatters/gpx.xml index b503710d..bbc49b1a 100644 --- a/htdocs/okapi/services/caches/formatters/gpx.xml +++ b/htdocs/okapi/services/caches/formatters/gpx.xml @@ -1,198 +1,257 @@ - Retrieve geocaches in GPX format - 40 - -

    Produces a standards-compliant Geocaching GPX file.

    -

    Unlike the services/caches/geocaches method responses, GPX files cannot - contain names and descriptions in separate multiple languages. This method - will attempt to choose the best language based on your preference - (see langpref argument).

    -

    Remember that a full-blown GPX file is much larger than a basic one. - This method can produce many various types of GPX files. The simplest list of - waypoints takes ~50 times less space than the same list with cache descriptions - and log entries included.

    -

    Note, that GPX files may contain unvalidated - HTML.

    -
    - -

    Pipe-separated list of cache codes which you are interested in. - No more than 500 codes are allowed. This CAN be an empty string (it will - result in an empty, but valid, GPX file). All invalid cache codes will - be skipped without any notice!

    -
    - -

    Pipe-separated list of ISO 639-1 language codes. This indicates the - order of preference in which language will be chosen for GPX entities.

    -
    - - Boolean. If true then response will include - Groundspeak's - GPX extension. This namespace declares an extra <cache> element - used by geocaching.com and many others. - 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. - - -

    Boolean. If true then response will include - GSAK GPX extension. - This namespace declares an extra <wptExtension> element, - which allows including "waypoint inheritance" information (parent-child relations). - This in turn allows us to include "alternate waypoints" in the response.

    -

    We know of at least one tool that - makes use of this.

    -
    - - Boolean. If true then response will include Garmin's - OpenCaching.com - GPX extension. This namespace declares an extra <opencaching> element - used by Garmin's Opencaching.com. - The element includes information on cache difficulty, ratings, tags and images. - It is used within Garmin's Geocaching-enabled handheld GPS devices. - - -

    Which images to include (and how to include them). One of the following values:

    -
      -
    • none - no images will be included,
    • -
    • descrefs:thumblinks - all images will be included as "thumbnail" - <img> references at the end of each cache description, with a - replacement image used for spoilers; the thumbnails are linked to the - large images,
    • -
    • descrefs:nonspoilers - all non-spoiler images will be included - as <img> references at the end of each cache description,
    • -
    • descrefs:all - all images will be included (including spoilers) - as <img> references at the end of each cache description,
    • -
    • ox:all - all images will be included (including spoilers) - as Garmin's <ox:image> references.
    • -
    -

    Note: When using "descrefs:" mode, remember to set ns_ground to true. - The default value is "descrefs:nonspoilers" for compatibilty reasons, but for - most applications "descrefs:thumblinks" will be the better choice.

    -

    Note: When using "ox:" mode, remember to set ns_ox to true. You must also - include JPEG files along the GPX for this to work properly - see - services/caches/formatters/garmin for more information.

    -

    In the future, more generic ways of including images in GPX files may emerge.

    -
    - -

    This argument controls whether cache attributes are included and how they are included. - Pipe-separated list consisting of any set of the following values:

    - -
      -
    • desc:text - attributes will be listed (in plain-text) within - the cache description (ns_ground has to be true),
    • -
    • ox:tags - attributes will be listed as Garmin's ox:tag - elements (ns_ox has to be true),
    • -
    • -

      gc:attrs - attributes will be listed as Groundspeak's - groundspeak:attribute elements (ns_ground has to be true), - compatible with Geocaching.com (see the list - here).

      -

      Note that most Opencaching attributes don't have Geocaching.com counterparts. - Such attributes cannot be included as gc:attrs and will be ignored.

      -
    • -
    • -

      gc_ocde:attrs - attributes will be listed as Groundspeak's - groundspeak:attribute elements (ns_ground has to be true). - Opencaching attributes which have no Geocaching.com counterparts will be - included according to an Opencaching.DE convention, using "makeshift IDs" - which may change in the future.

      -

      Note that this option is supported only by some OC sites. Other sites will - handle gc_ocde:attrs in the same way as gc:attrs.

      -
    • -
    - -

    If you don't want any attributes to be included, you must set the attrs - parameter to none. Using an empty string won't work this way - it will - trigger the default (desc:text) to be selected, for backward-compatibility.

    -
    - -

    This argument controls whether protection area information is included and how - it is included.

    -
      -
    • desc:text - if the cache (probably) is located within one or more - protection areas, e.g. a nature reserve, a list of the protection areas will - be included in the cache description.
    • -
    • desc:auto - protection area information may be included in the - cache description, depending on the installation.
    • -
    • none - no protection area information will be included in the - GPX data.
    • -
    -

    Note that information on protection areas may be incomplete or outdated - or completely missing on some installations.

    -
    - -

    This argument controls whether information on trackables is included and how it is included. - One of the following values:

    -
      -
    • none - no trackables-data will be included,
    • -
    • desc:list - trackables will be listed (in plain-text) within the cache description,
    • -
    • desc:count - total number of trackables will be included (in plain-text) within the cache description.
    • -
    -

    Note: When using "desc:" mode, remember to set ns_ground to true.

    -
    - -

    This argument controls whether information on recommendations is included and how it is included. - One of the following values:

    -
      -
    • none - no recommendations-info will be included,
    • -
    • desc:count - number of recommendations (and founds) will be included - (in plain-text) within the cache description.
    • -
    -

    Note: When using "desc:" mode, remember to set ns_ground to true.

    -
    - -

    Allows you to include personal user's notes on each cache. This parameter - takes either none (default value), or pipe separeted list of the - following values:

    - -
      -
    • desc:text - include personal notes as part of the cache - description.
    • - -
    • -

      gc:personal_note - include personal notes as - <groundspeak:personal_note> element (under - <groundspeak:cache> element).

      - -

      Warning: This element is not a part of - http://www.groundspeak.com/cache/1/0/1 groundspeak schema, but it - is a http://www.groundspeak.com/cache/1/0/2 schema element. Using - this option will generate formally invalid XML file.

      -
    • -
    -

    Note: You need to use Level 3 Authentication in order to set it to anything else than "none".

    -
    - -

    Boolean. If true then response will include a couple of - latest log entries for this cache (see also the lpc argument).

    -

    You must set ns_ground argument to true if you want to use this.

    -
    - - 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 ns_ground - and latest_logs arguments must be set to true in order for the logs to be included. - - -

    Boolean. If true then we will include additional (alternate) waypoints in the response. - These are all places associated with the cache.

    -

    Combine this with ns_gsak if you want the result to include additional references between - waypoints and their geocaches. Please note that not many applications are currently able to - properly understand GSAK metadata.

    -
    - -

    Boolean. If true then all caches which are already found, will be marked appropriately - (i.e. with an "found cache" symbol).

    -

    This field requires you to use the user_uuid parameter (or Level 3 Authentication).

    -
    - -

    User'd ID. Required to mark found caches using Level 1 Authentication.

    -

    If you use Level 3 Authentication, you shouldn't use this parameter. Or, to be exact:

    -
      -
    • user_uuid will be extracted from the Access Token you use. You don't have to provide it.
    • -
    • If you provide user_uuid, then it HAS TO match your Access Token. If it doesn't, OKAPI - will respond with HTTP 400 error.
    • -
    -
    - -

    GPX file. All invalid cache codes will be skipped without any notice!

    -
    + Retrieve geocaches in GPX format + 40 + +

    Produces a standards-compliant Geocaching GPX file.

    +

    Unlike the services/caches/geocaches method responses, GPX files cannot + contain names and descriptions in separate multiple languages. This method + will attempt to choose the best language based on your preference + (see langpref argument).

    +

    Remember that a full-blown GPX file is much larger than a basic one. + This method can produce many various types of GPX files. The simplest list of + waypoints takes ~50 times less space than the same list with cache descriptions + and log entries included.

    +

    Note, that GPX files may contain unvalidated + HTML.

    +
    + +

    Pipe-separated list of cache codes which you are interested in. + No more than 500 codes are allowed. This CAN be an empty string (it will + result in an empty, but valid, GPX file). All invalid cache codes will + be skipped without any notice!

    +
    + +

    Pipe-separated list of ISO 639-1 language codes. This indicates the + order of preference in which language will be chosen for GPX entities.

    +
    + + Boolean. If true then response will include + Groundspeak's + GPX extension. This namespace declares an extra <cache> element + used by geocaching.com and many others. + 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. + + +

    Boolean. If true then response will include + GSAK GPX extension. + This namespace declares an extra <wptExtension> element, + which allows including "waypoint inheritance" information (parent-child relations). + This in turn allows us to include "alternate waypoints" in the response.

    +

    We know of at least one tool that + makes use of this.

    +
    + + Boolean. If true then response will include Garmin's + OpenCaching.com + GPX extension. This namespace declares an extra <opencaching> element + used by Garmin's Opencaching.com. + The element includes information on cache difficulty, ratings, tags and images. + It is used within Garmin's Geocaching-enabled handheld GPS devices. + + +

    Which images to include (and how to include them). One of the following values:

    +
      +
    • none - no images will be included,
    • +
    • descrefs:thumblinks - all images will be included as "thumbnail" + <img> references at the end of each cache description, with a + replacement image used for spoilers; the thumbnails are linked to the + large images,
    • +
    • descrefs:nonspoilers - all non-spoiler images will be included + as <img> references at the end of each cache description,
    • +
    • descrefs:all - all images will be included (including spoilers) + as <img> references at the end of each cache description,
    • +
    • ox:all - all images will be included (including spoilers) + as Garmin's <ox:image> references.
    • +
    +

    Note: When using "descrefs:" mode, remember to set ns_ground to true. + The default value is "descrefs:nonspoilers" for compatibilty reasons, but for + most applications "descrefs:thumblinks" will be the better choice.

    +

    Note: When using "ox:" mode, remember to set ns_ox to true. You must also + include JPEG files along the GPX for this to work properly - see + services/caches/formatters/garmin for more information.

    +

    In the future, more generic ways of including images in GPX files may emerge.

    +
    + +

    This argument controls whether cache attributes are included and how they are included. + Pipe-separated list consisting of any set of the following values:

    + +
      +
    • desc:text - attributes will be listed (in plain-text) within + the cache description (ns_ground has to be true),
    • +
    • ox:tags - attributes will be listed as Garmin's ox:tag + elements (ns_ox has to be true),
    • +
    • +

      gc:attrs - attributes will be listed as Groundspeak's + groundspeak:attribute elements (ns_ground has to be true), + compatible with Geocaching.com (see the list + here).

      +

      Note that most Opencaching attributes don't have Geocaching.com counterparts. + Such attributes cannot be included as gc:attrs and will be ignored.

      +
    • +
    • +

      gc_ocde:attrs - attributes will be listed as Groundspeak's + groundspeak:attribute elements (ns_ground has to be true). + Opencaching attributes which have no Geocaching.com counterparts will be + included according to an Opencaching.DE convention, using "makeshift IDs" + which may change in the future.

      +

      Note that this option is supported only by some OC sites. Other sites will + handle gc_ocde:attrs in the same way as gc:attrs.

      +
    • +
    + +

    If you don't want any attributes to be included, you must set the attrs + parameter to none. Using an empty string won't work this way - it will + trigger the default (desc:text) to be selected, for backward-compatibility.

    +
    + +

    This argument controls whether protection area information is included and how + it is included.

    +
      +
    • desc:text - if the cache (probably) is located within one or more + protection areas, e.g. a nature reserve, a list of the protection areas will + be included in the cache description.
    • +
    • desc:auto - protection area information may be included in the + cache description, depending on the installation.
    • +
    • none - no protection area information will be included in the + GPX data.
    • +
    +

    Note that information on protection areas may be incomplete or outdated + or completely missing on some installations.

    +
    + +

    This argument controls whether information on trackables is included and how it is included. + One of the following values:

    +
      +
    • none - no trackables-data will be included,
    • +
    • desc:list - trackables will be listed (in plain-text) within the cache description,
    • +
    • desc:count - total number of trackables will be included (in plain-text) within the cache description.
    • +
    +

    Note: When using "desc:" mode, remember to set ns_ground to true.

    +
    + +

    This argument controls whether information on recommendations is included and how it is included. + One of the following values:

    +
      +
    • none - no recommendations-info will be included,
    • +
    • desc:count - number of recommendations (and founds) will be included + (in plain-text) within the cache description.
    • +
    +

    Note: When using "desc:" mode, remember to set ns_ground to true.

    +
    + +

    Allows you to include personal user's notes on each cache. This parameter + takes either none (default value), or pipe separeted list of the + following values:

    + +
      +
    • desc:text - include personal notes as part of the cache + description.
    • + +
    • +

      gc:personal_note - include personal notes as + <groundspeak:personal_note> element (under + <groundspeak:cache> element).

      + +

      Warning: This element is not a part of + http://www.groundspeak.com/cache/1/0/1 groundspeak schema, but it + is a http://www.groundspeak.com/cache/1/0/2 schema element. Using + this option will generate formally invalid XML file.

      +
    • +
    +

    Note: You need to use Level 3 Authentication in order to set it to anything else than "none".

    +
    + +

    Boolean. If true then response will include a couple of + latest log entries for this cache (see also the lpc argument).

    +

    You must set ns_ground argument to true if you want to use this.

    +
    + + 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 ns_ground + and latest_logs arguments must be set to true in order for the logs to be included. + + +

    Boolean. If true then we will include additional (alternate) waypoints in the response. + These are all places associated with the cache.

    +

    Combine this with ns_gsak if you want the result to include additional references between + waypoints and their geocaches. Please note that not many applications are currently able to + properly understand GSAK metadata.

    +
    + +

    Boolean. If true then all caches which are already found, will be marked appropriately + (i.e. with an "found cache" symbol).

    +

    This field requires you to use the user_uuid parameter (or Level 3 Authentication).

    +
    + +

    User'd ID. Required to mark found caches using Level 1 Authentication.

    +

    If you use Level 3 Authentication, you shouldn't use this parameter. Or, to be exact:

    +
      +
    • user_uuid will be extracted from the Access Token you use. You don't have to provide it.
    • +
    • If you provide user_uuid, then it HAS TO match your Access Token. If it doesn't, OKAPI + will respond with HTTP 400 error.
    • +
    +
    + +

    If you supply a value, then it must be prefixed with alt_wpt:, + and should match one of alternate waypoint types documented in the + alt_wpts + field returned by services/caches/geocache + method. If the type doesn't match, it will be ignored.

    + +

    By default, the lat and lon attributes of the <wpt> 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 alt_wpts field mentioned above.

    + +

    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 location_source to alt_wpt:parking then you'll be + pre-fetching parking locations (if provided) without the need of loading all + other alternate waypoints in your requests.

    + +

    Other use case: setting this to alt_wpt:user-coords allows + personalized results in some OC installations.

    + +
      +
    • If the type provided doesn't match any alternate waypoints, the default + coordinates of the geocache will be used.
    • + +
    • +

      If the type matches exactly one alternate waypoint, then:

      + +
        +
      • The lat and lon attributes of the "primary" + <wpt> element will be replaced with the location of + the matched alternate waypoint,
      • + +
      • The name of the geocache will be prefixed with the value suppiled in + location_change_prefix + parameter,
      • + +
      • The desription of the waypoint will be included in the + header of the cache description,
      • + +
      • The matched alternate waypoint will be removed from the list + of alternate waypoins (in order to avoid duplicate locations),
      • + +
      • Extra alternate waypoint will be created, with the original + location of the geocache.
      • +
      +
    • + +
    • If the type provided matches multiple alternate waypoints, then + the first one will be chosen.
    • +
    + +
    + +

    Prefix to be added to the geocache name, in case its location has been changed due to + the location_source parameter + matching any of the alternate waypoint.

    +
    + +

    GPX file. All invalid cache codes will be skipped without any notice!

    +
    \ No newline at end of file diff --git a/htdocs/okapi/services/caches/formatters/gpxfile.tpl.php b/htdocs/okapi/services/caches/formatters/gpxfile.tpl.php index a2ed3976..ae62663d 100644 --- a/htdocs/okapi/services/caches/formatters/gpxfile.tpl.php +++ b/htdocs/okapi/services/caches/formatters/gpxfile.tpl.php @@ -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://www.gsak.net/xmlv1/5 http://www.gsak.net/xmlv1/5/gsak.xsd "> - Geocache Search Results - Geocache Search Results, downloaded via OKAPI - - - - - - - - - - - - :: Cache (//) - - - - Geocache| - element? */ ?> - " available="" id="" xmlns:groundspeak="http://www.groundspeak.com/cache/1/0/1"> - - - - - - - - $gc_attr) { - print ""; - print Okapi::xmlescape($gc_attr['name']); - print ""; - } - ?> - - - - - - <p> - <a href=""></a> - <a href=''></a><br/> - - - (). - - - . - - </p> - - <p><b>:</b><br></p> - + Geocache Search Results + Geocache Search Results, downloaded via OKAPI - + + + + + + + + + + :: Cache (//) + + + + Geocache| + element? */ ?> + " available="" id="" xmlns:groundspeak="http://www.groundspeak.com/cache/1/0/1"> + + + + + + + + $gc_attr) { + print ""; + print Okapi::xmlescape($gc_attr['name']); + print ""; + } + ?> + + + + + + + + + + <p style='font-size: 120%'></p> + + <p> + <a href=""></a> + <a href=''></a><br/> + + + (). + + + . + + </p> + + <p><b>:</b><br></p> + - 0) { /* Does user want us to include attributes? */ ?> - <p>:</p> - <ul><li></li></ul> - - 0) { /* Does user want us to include trackables list? */ ?> - <p>:</p> - <ul> - - <li><a href=''></a> ()</li> - - </ul> - - - 0) { /* Does user want us to include references in cache descriptions? */ - if ($vars['images'] == "descrefs:thumblinks") { ?> - <h2> ()</h2> - <div> - - <div style='float:left; padding:6px'><a href=''><img src=''></a><br> - </div> - - </div> - - 0) { ?> - <h2> ()</h2> - - <p><img src=''><br> - </p> - - - 0 && $vars['images'] == 'descrefs:all') { ?> - <h2> ()</h2> - - <p><img src=''><br> - </p> - - - - - 0) { /* Include image descriptions (for ox:image numbers)? */ ?> - <p>:</p> - <ul> - $img) { ?> - <li>. </li> - - </ul> - - - <p></p> - <ul> - - <li></li> - - </ul;> - - - - Issue 294 */ ?> - - - - - - - - - - - - - - - - - - element? */ ?> - - - - - - - - 0) { /* Does user want us to include ox:tags? */ ?> - ", $c['attrnames']) ?> - - 0) { /* Does user want us to include ox:image references? */ ?> - - $img) { ?> - - .jpg - 0 - false - - - - - - - - - - - - - - - - - - - - Waypoint| - - - - - - - - - + 0) { /* Does user want us to include attributes? */ ?> + <p>:</p> + <ul><li></li></ul> + + 0) { /* Does user want us to include trackables list? */ ?> + <p>:</p> + <ul> + + <li><a href=''></a> ()</li> + + </ul> + + + 0) { /* Does user want us to include references in cache descriptions? */ + if ($vars['images'] == "descrefs:thumblinks") { ?> + <h2> ()</h2> + <div> + + <div style='float:left; padding:6px'><a href=''><img src=''></a><br> + </div> + + </div> + + 0) { ?> + <h2> ()</h2> + + <p><img src=''><br> + </p> + + + 0 && $vars['images'] == 'descrefs:all') { ?> + <h2> ()</h2> + + <p><img src=''><br> + </p> + + + + + 0) { /* Include image descriptions (for ox:image numbers)? */ ?> + <p>:</p> + <ul> + $img) { ?> + <li>. </li> + + </ul> + + + <p></p> + <ul> + + <li></li> + + </ul;> + + + + Issue 294 */ ?> + + + + + + + + + + + + + + + + + + element? */ ?> + + + + + + + + 0) { /* Does user want us to include ox:tags? */ ?> + ", $c['attrnames']) ?> + + 0) { /* Does user want us to include ox:image references? */ ?> + + $img) { ?> + + .jpg + 0 + false + + + + + + + + + + + + + + + + + + + + Waypoint| + + + + + + + + + diff --git a/htdocs/okapi/services/caches/geocache.php b/htdocs/okapi/services/caches/geocache.php index 6113c619..08296179 100644 --- a/htdocs/okapi/services/caches/geocache.php +++ b/htdocs/okapi/services/caches/geocache.php @@ -12,51 +12,51 @@ use okapi\services\caches\search\SearchAssistant; class WebService { - public static function options() - { - return array( - 'min_auth_level' => 1 - ); - } + public static function options() + { + return array( + 'min_auth_level' => 1 + ); + } - public static function call(OkapiRequest $request) - { - $cache_code = $request->get_parameter('cache_code'); - if (!$cache_code) throw new ParamMissing('cache_code'); - if (strpos($cache_code, "|") !== false) throw new InvalidParam('cache_code'); - $langpref = $request->get_parameter('langpref'); - if (!$langpref) $langpref = "en"; - $fields = $request->get_parameter('fields'); - if (!$fields) $fields = "code|name|location|type|status"; - $log_fields = $request->get_parameter('log_fields'); - if (!$log_fields) $log_fields = "uuid|date|user|type|comment"; - $lpc = $request->get_parameter('lpc'); - if (!$lpc) $lpc = 10; - $attribution_append = $request->get_parameter('attribution_append'); - if (!$attribution_append) $attribution_append = 'full'; - $params = array( - 'cache_codes' => $cache_code, - 'langpref' => $langpref, - 'fields' => $fields, - 'attribution_append' => $attribution_append, - 'lpc' => $lpc, - 'log_fields' => $log_fields - ); - $my_location = $request->get_parameter('my_location'); - if ($my_location) - $params['my_location'] = $my_location; - $user_uuid = $request->get_parameter('user_uuid'); - if ($user_uuid) - $params['user_uuid'] = $user_uuid; + public static function call(OkapiRequest $request) + { + $cache_code = $request->get_parameter('cache_code'); + if (!$cache_code) throw new ParamMissing('cache_code'); + if (strpos($cache_code, "|") !== false) throw new InvalidParam('cache_code'); + $langpref = $request->get_parameter('langpref'); + if (!$langpref) $langpref = "en"; + $fields = $request->get_parameter('fields'); + if (!$fields) $fields = "code|name|location|type|status"; + $log_fields = $request->get_parameter('log_fields'); + if (!$log_fields) $log_fields = "uuid|date|user|type|comment"; + $lpc = $request->get_parameter('lpc'); + if (!$lpc) $lpc = 10; + $attribution_append = $request->get_parameter('attribution_append'); + if (!$attribution_append) $attribution_append = 'full'; + $params = array( + 'cache_codes' => $cache_code, + 'langpref' => $langpref, + 'fields' => $fields, + 'attribution_append' => $attribution_append, + 'lpc' => $lpc, + 'log_fields' => $log_fields + ); + $my_location = $request->get_parameter('my_location'); + if ($my_location) + $params['my_location'] = $my_location; + $user_uuid = $request->get_parameter('user_uuid'); + if ($user_uuid) + $params['user_uuid'] = $user_uuid; - # There's no need to validate the fields/lpc parameters as the 'geocaches' - # method does this (it will raise a proper exception on invalid values). + # There's no need to validate the fields/lpc parameters as the 'geocaches' + # method does this (it will raise a proper exception on invalid values). - $results = OkapiServiceRunner::call('services/caches/geocaches', new OkapiInternalRequest( - $request->consumer, $request->token, $params)); - $result = $results[$cache_code]; - if ($result === null) - throw new InvalidParam('cache_code', "This cache does not exist."); - return Okapi::formatted_response($request, $result); - } + $results = OkapiServiceRunner::call('services/caches/geocaches', new OkapiInternalRequest( + $request->consumer, $request->token, $params)); + $result = $results[$cache_code]; + if ($result === null) + throw new InvalidParam('cache_code', "This cache does not exist."); + return Okapi::formatted_response($request, $result); + } } diff --git a/htdocs/okapi/services/caches/geocache.xml b/htdocs/okapi/services/caches/geocache.xml index 8114dd95..dbed8746 100644 --- a/htdocs/okapi/services/caches/geocache.xml +++ b/htdocs/okapi/services/caches/geocache.xml @@ -1,413 +1,440 @@ - Retrieve information on a single geocache - 19 - -

    Retrieve information on a single geocache.

    -
    - Unique code of the geocache - -

    Pipe-separated list of ISO 639-1 language codes. This indicates the - order of preference in which language will be chosen for fields like - name and description.

    -

    Please note, that you may also access caches' descriptions in all - available languages. If you want to do this, you should use fields - names, descriptions (etc.) instead of name and - description (etc.).

    -
    - -

    Pipe-separated list of field names which you are interested with. - Selected fields will be included in the response. See below for the - list of available fields.

    -
    - -

    By default, OKAPI appends the value of attribution_note field to all the - cache descriptions. If you would like to display the attribution note separately, - you may use this parameter. Use one of the following values:

    -
      -
    • - full - OKAPI will append the attribution note to the descriptions of the - geocache. The language of the note will match the language of the description - (i.e. each value in descriptions may contain attribution notes in - a different language). -
    • -
    • -

      none - OKAPI will not append the attribution note to geocache - descriptions. You will use the attribution_note instead (which depends - on the langpref parameter).

      - -

      Important note: You are still required to display the - attribution_note field in some other way.

      -
    • -
    • - static - 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 - (why?), - 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. -
    • -
    -
    - - Log-entries per cache - the number of logs returned in the latest_logs field. - This should be an integer or a special "all" value. Please note, that you must include - the latest_logs field in fields in order to see the log entries. - - - Pipe-separated list of log fields to include in the latest_logs field. - For valid field names, see logs/entry method. - - -

    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 distance and/or bearing - fields.

    -

    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").

    -
    - -

    User'd ID. Required to access fields like is_found using Level 1 Authentication.

    -

    Please note that if you want to access private fields (like my_notes), then - this parameter won't help you. You have to use Level 3 Authentication instead.

    -

    If you use Level 3 Authentication, you shouldn't use this parameter. Or, to be exact:

    -
      -
    • user_uuid will be extracted from the Access Token you use. You don't have to provide it.
    • -
    • If you provide user_uuid, then it HAS TO match your Access Token. If it doesn't, OKAPI - will respond with HTTP 400 error.
    • -
    -
    - - -

    A dictionary of fields you have selected. Currently available fields:

    - -
      -
    • code - unique Opencaching code of the geocache,
    • -
    • name - name of the geocache,
    • -
    • names - 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),
    • -
    • location - location of the cache in the "lat|lon" format - (lat and lon are in full degrees with a dot as a decimal point),
    • -
    • -

      type - cache type. This might be pretty much everything, - but there are some predefined types that you might want to treat - in a special way (separate icons etc.). Primary types include:

      -
        -
      • Traditional,
      • -
      • Multi,
      • -
      • Quiz,
      • -
      • Virtual,
      • -
      • Event,
      • -
      • (any other value is valid too)
      • -
      -

      Event is a peculiar type of geocache which is NOT a geocache - at all, but it is 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.

      -
    • -
    • -

      status - cache status. Valid cache status codes currently include:

      -
        -
      • Available - Cache is available and ready for search,
      • -
      • Temporarily unavailable - Cache is probably unavailable (i.e. in need of maintenance)
      • -
      • Archived - Cache is permanently unavailable (moved to the archives).
      • -
      -
    • -
    • url - URL of the cache's web page,
    • -
    • -

      owner - dictionary of:

      -
        -
      • uuid - geocache owner's user ID,
      • -
      • username - name of the user,
      • -
      • profile_url - URL of the user profile page,
      • -
      -
    • -
    • -

      gc_code - Geocaching.com code (GC code) of the geocache - or null if the cache is not listed on GC or the GC code is - unknown.

      - -

      Please note that this information is supplied by cache owners - and it is often missing, obsolete or otherwise incorrect.

      -
    • -
    • -

      distance - float, the distance to a cache, in meters. - This requires my_location parameter to be provided.

      -

      Please note, that sometimes it is faster to compute this yourself, on client-side, instead of querying OKAPI.

      -
    • -
    • -

      bearing - float, the absolute bearing to the cache, measured in degrees from north, - or null if it cannot be calculated (i.e. when you are exactly at the target's location). - This requires my_location parameter to be provided.

      -

      Please note, that sometimes it is faster to compute this yourself, on client-side, instead of querying OKAPI.

      -
    • -
    • -

      bearing2 - 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 my_location parameter to be provided.

      -

      Please note, that sometimes it is faster to compute this yourself, on client-side, instead of querying OKAPI.

      -
    • -
    • -

      bearing3 - 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 my_location parameter to be provided.

      -

      Please note, that sometimes it is faster to compute this yourself, on client-side, instead of querying OKAPI.

      -
    • -
    • -

      is_found - boolean, true if the user already found this cache. - See also found_status parameter of the services/caches/search/all - method.

      -

      This field requires you to use the user_uuid parameter - (or Level 3 Authentication). Please note, that this will also return true - for attended Event Caches.

      -
    • -
    • -

      is_not_found - boolean, true if the user has submitted "Didn't find it" log entry for this cache.

      -

      This field requires you to use the user_uuid parameter (or Level 3 Authentication).

      -
    • -
    • -

      is_watched - 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).

      -

      This is private data. You will need Level 3 Authentication to access this field.

      -
    • -
    • -

      is_ignored - boolean, true if the user is ignoring this cache. (See also exclude_ignored - parameter of all search methods.)

      -

      This is private data. You will need Level 3 Authentication to access this field.

      -
    • -
    • -

      founds - number of times the geocache was successfully found - (or attended, in case of Event Caches),

      -
    • -
    • -

      notfounds - number of times the geocache was not found - (in case of Event Caches this will always be zero),

      -
    • -
    • -

      willattends - in case of Event Caches, this is the number of - "Will attend" log entries. In case of any other cache type, it will - be zero (not null, for backward - compatibility),

      -
    • -
    • - size - deprecated - (why?), - use size2 instead. Float (between 1 and 5), size rating of the container, or - null if geocache does not have a container, -
    • -
    • -

      size2 - string indicating the size of the container, so called - "size2 code". One of the following values: - 'none', 'nano', 'micro', 'small', 'regular', 'large', 'xlarge', 'other'.

      -
    • -
    • -

      oxsize - 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).

      -

      Note: The mapping is undocumented and may change without notice.

      -

      Note: Some of OC's size values cannot be properly mapped to oxsize, - i.e. the 'other' size.

      -
    • -
    • difficulty - float (between 1 and 5), difficulty rating of the cache,
    • -
    • terrain - float (between 1 and 5), terrain rating of the cache,
    • -
    • - trip_time - float, approximate total amount of time needed to - find the cache, in hours; this will usually include the time to reach the - cache location and go back (from/to a parking spot, etc.); - null if unknown, -
    • -
    • trip_distance - float, approximate total distance needed to - find the cache, in kilometers; this will usually include the time to reach the - cache location and go back (from/to a parking spot, etc.); - null if unknown, -
    • -
    • -

      rating - float (between 1 and 5), an overall rating of the cache, - or null when the geocache does not have a sufficient amount of votes - to have a rating.

      -

      Please note: some OC installations do not use the rating/voting system. The rating will - always be null on such installations.

      -
    • -
    • -

      rating_votes - number of votes submitted for the rating of this cache.

      -

      Please note: some OC installations do not use the rating/voting system. The rating_votes will - always be 0 on such installations.

      -
    • -
    • recommendations - number of recommendations for this cache,
    • -
    • req_passwd - boolean; states if this cache requires a password - in order to submit a "Found it" log entry,
    • -
    • description - HTML string, - description of the cache,
    • -
    • descriptions - a dictionary (language code => - HTML string) of cache descriptions,
    • -
    • hint - HTML-encoded - string, cache hints/spoilers; deprecated - (why?), - use hint2 instead,
    • -
    • hints - a dictionary (language code => - HTML-encoded string) of cache hints/spoilers; - deprecated, use hints2 instead,
    • -
    • hint2 - plain-text string, cache hints/spoilers; in general, hints - should not be displayed to the user unless user specifically asks for them,
    • -
    • hints2 - a dictionary (language code => - plain-text string) of cache hints/spoilers,
    • -
    • -

      images - list of dictionaries, each dictionary represents one - image saved along the cache description; each dictionary has the - following structure:

      -
        -
      • uuid - UUID of the image,
      • -
      • url - URL of the image,
      • -
      • thumb_url - URL of a small (thumb) version of the image,
      • -
      • caption - plain-text string, caption of the image,
      • -
      • unique_caption - 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),
      • -
      • is_spoiler - boolean, if true then the image is - a spoiler image and should not be displayed to the user unless - the user explicitly asks for it,
      • -
      -
    • -
    • -

      preview_image - On some installations, owners may select one of the images - (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 null value here.

      + Retrieve information on a single geocache + 19 + +

      Retrieve information on a single geocache.

      +
      + Unique code of the geocache + +

      Pipe-separated list of ISO 639-1 language codes. This indicates the + order of preference in which language will be chosen for fields like + name and description.

      +

      Please note, that you may also access caches' descriptions in all + available languages. If you want to do this, you should use fields + names, descriptions (etc.) instead of name and + description (etc.).

      +
      + +

      Pipe-separated list of field names which you are interested with. + Selected fields will be included in the response. See below for the + list of available fields.

      +
      + +

      By default, OKAPI appends the value of attribution_note field to all the + cache descriptions. If you would like to display the attribution note separately, + you may use this parameter. Use one of the following values:

      +
        +
      • + full - OKAPI will append the attribution note to the descriptions of the + geocache. The language of the note will match the language of the description + (i.e. each value in descriptions may contain attribution notes in + a different language). +
      • +
      • +

        none - OKAPI will not append the attribution note to geocache + descriptions. You will use the attribution_note instead (which depends + on the langpref parameter).

        -

        The value of preview_image is either null or a dictionary describing - an image. The structure of this dictionary is the same as of a single entry on - the images list described above.

        -
      • -
      • -

        attr_acodes - unordered list of OKAPI geocache-attribute IDs (A-codes) with - which the cache was tagged. Use the attrs module to retrieve information on - these attributes.

        -
      • -
      • - attrnames - if you don't want to dig into attr_acodes just now, then - you can use these as a simple alternative. attrnames is an unordered list of - names of the attributes with which the cache was tagged; the language will be - selected based on the langpref parameter. -
      • -
      • -

        attribution_note - 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 attribution_append parameter).

        -
      • -
      • -

        latest_logs - a couple of latest log entries in the cache. - The format is the same as the one returned by the services/logs/logs method.

        -

        Notice: The number of logs returned can be set with the lpc option.

        -
      • -
      • -

        my_notes - user's notes on the cache, in plain-text, or null - if user hadn't defined any notes.

        -

        This field requires you to use the Level 3 Authentication.

        -
      • -
      • trackables_count - a total number of trackables hidden within the cache.
      • -
      • -

        trackables - list of dictionaries, each dictionary represents one - trackable hidden within the cache container; each dictionary has the - following structure:

        -
          -
        • code - code of the trackable,
        • -
        • name - plain text name of the trackable,
        • -
        • url - trackable's own webpage address or null, if OKAPI - cannot automatically determine this address.
        • -
        -
      • -
      • -

        alt_wpts - list of alternate/additional waypoints associated - with this geocache. Each item is a dictionary of the following structure:

        -
          -
        • name - plain-text, short, unique "codename" for the waypoint,
        • -
        • location - location of the waypoint in the "lat|lon" format - (lat and lon are in full degrees with a dot as a decimal point),
        • -
        • type - string, unique identifier for the type of waypoint; one - of the following: parking, path, stage, - physical-stage, virtual-stage, final, poi, other; - more types may be added; unknown types should be treated as other, -
        • -
        • type_name - 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, -
        • -
        • - sym - 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"), -
        • -
        • description - plain-text longer description of the waypoint.
        • -
        -
      • -
      • -

        country - name of the country the cache is placed in; - may be empty ("") if the country is unknown.

        -

        Note: 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 langpref - parameter in the future).

        -
      • -
      • -

        state - name of the state the cache is placed in; - may be empty ("") if the state is unknown.

        -

        Note: 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 langpref - parameter in the future).

        -
      • -
      • -

        protection_areas - list of dictionaries, each representing a - protection area in which the cache probably is located; each dictionary - has the following structure:

        -
          -
        • type - the type of the protection area, e.g. - "National Nature Reserve",
        • -
        • name - the name of the protection area, e.g. - "East Dartmoor Woods and Heaths".
        • -
        -

        The types and names currently are in a local language of the OC site but - may be translated depending on the langpref parameter in the future.

        -

        Note that this information is not authoritative. It may be outdated - or incomplete, and it is completely missing on some OC installations.

        -
      • -
      • -

        last_found - date and time (ISO 8601) when the - geocache was last found or null when it hasn't been yet found.

        -

        Note, that some Opencaching servers don't store the exact times along - with the log entries.

        -
      • -
      • last_modified - date and time (ISO 8601) when the - geocache was last modified (changed status, attributes, etc.),
      • -
      • date_created - date and time (ISO 8601) when the - geocache was listed at the Opencaching site,
      • -
      • date_hidden - date and time (ISO 8601) when -
          -
        • the geocache was first hidden (for physical caches), or
        • -
        • the geocache was first published (for virtual caches), or
        • -
        • the event takes place (for event caches),
        • -
        -
      • - -
      • internal_id - internal ID of the cache (do not use - this, unless you know what you're doing; use the "code" as an identifier).
      • -
      +

      Important note: You are still required to display the + attribution_note field in some other way.

      +
    • +
    • + static - 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 + (why?), + 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. +
    • +
    + + + Log-entries per cache - the number of logs returned in the latest_logs field. + This should be an integer or a special "all" value. Please note, that you must include + the latest_logs field in fields in order to see the log entries. + + + Pipe-separated list of log fields to include in the latest_logs field. + For valid field names, see logs/entry method. + + +

    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 distance and/or bearing + fields.

    +

    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").

    +
    + +

    User'd ID. Required to access fields like is_found using Level 1 Authentication.

    +

    Please note that if you want to access private fields (like my_notes), then + this parameter won't help you. You have to use Level 3 Authentication instead.

    +

    If you use Level 3 Authentication, you shouldn't use this parameter. Or, to be exact:

    +
      +
    • user_uuid will be extracted from the Access Token you use. You don't have to provide it.
    • +
    • If you provide user_uuid, then it HAS TO match your Access Token. If it doesn't, OKAPI + will respond with HTTP 400 error.
    • +
    +
    + + +

    A dictionary of fields you have selected. Currently available fields:

    -

    For example, for geocache?cache_code=OP3D96&fields=type - query, the result might look something link this:

    -
    {"type": "Traditional"}
    -

    If given cache code does not exist, the method will - respond with an HTTP 400 error.

    -
    +
      +
    • code - unique Opencaching code of the geocache,
    • +
    • name - name of the geocache,
    • +
    • names - 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),
    • +
    • + location - location of the cache in the "lat|lon" format + (lat and lon are in full degrees with a dot as a decimal point), +
    • +
    • +

      type - cache type. This might be pretty much everything, + but there are some predefined types that you might want to treat + in a special way (separate icons etc.). Primary types include:

      +
        +
      • Traditional,
      • +
      • Multi,
      • +
      • Quiz,
      • +
      • Virtual,
      • +
      • Event,
      • +
      • (any other value is valid too)
      • +
      +

      Event is a peculiar type of geocache which is NOT a geocache + at all, but it is 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.

      +
    • +
    • +

      status - cache status. Valid cache status codes currently include:

      +
        +
      • Available - Cache is available and ready for search,
      • +
      • Temporarily unavailable - Cache is probably unavailable (i.e. in need of maintenance)
      • +
      • Archived - Cache is permanently unavailable (moved to the archives).
      • +
      +
    • +
    • url - URL of the cache's web page,
    • +
    • +

      owner - dictionary of:

      +
        +
      • uuid - geocache owner's user ID,
      • +
      • username - name of the user,
      • +
      • profile_url - URL of the user profile page,
      • +
      +
    • +
    • +

      gc_code - Geocaching.com code (GC code) of the geocache + or null if the cache is not listed on GC or the GC code is + unknown.

      + +

      Please note that this information is supplied by cache owners + and it is often missing, obsolete or otherwise incorrect.

      +
    • +
    • +

      distance - float, the distance to a cache, in meters. + This requires my_location parameter to be provided.

      +

      Please note, that sometimes it is faster to compute this yourself, on client-side, instead of querying OKAPI.

      +
    • +
    • +

      bearing - float, the absolute bearing to the cache, measured in degrees from north, + or null if it cannot be calculated (i.e. when you are exactly at the target's location). + This requires my_location parameter to be provided.

      +

      Please note, that sometimes it is faster to compute this yourself, on client-side, instead of querying OKAPI.

      +
    • +
    • +

      bearing2 - 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 my_location parameter to be provided.

      +

      Please note, that sometimes it is faster to compute this yourself, on client-side, instead of querying OKAPI.

      +
    • +
    • +

      bearing3 - 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 my_location parameter to be provided.

      +

      Please note, that sometimes it is faster to compute this yourself, on client-side, instead of querying OKAPI.

      +
    • +
    • +

      is_found - boolean, true if the user already found this cache. + See also found_status parameter of the services/caches/search/all + method.

      +

      This field requires you to use the user_uuid parameter + (or Level 3 Authentication). Please note, that this will also return true + for attended Event Caches.

      +
    • +
    • +

      is_not_found - boolean, true if the user has submitted "Didn't find it" log entry for this cache.

      +

      This field requires you to use the user_uuid parameter (or Level 3 Authentication).

      +
    • +
    • +

      is_watched - 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).

      +

      This is private data. You will need Level 3 Authentication to access this field.

      +
    • +
    • +

      is_ignored - boolean, true if the user is ignoring this cache. (See also exclude_ignored + parameter of all search methods.)

      +

      This is private data. You will need Level 3 Authentication to access this field.

      +
    • +
    • +

      founds - number of times the geocache was successfully found + (or attended, in case of Event Caches),

      +
    • +
    • +

      notfounds - number of times the geocache was not found + (in case of Event Caches this will always be zero),

      +
    • +
    • +

      willattends - in case of Event Caches, this is the number of + "Will attend" log entries. In case of any other cache type, it will + be zero (not null, for backward + compatibility),

      +
    • +
    • + size - deprecated + (why?), + use size2 instead. Float (between 1 and 5), size rating of the container, or + null if geocache does not have a container, +
    • +
    • +

      size2 - string indicating the size of the container, so called + "size2 code". One of the following values: + 'none', 'nano', 'micro', 'small', 'regular', 'large', 'xlarge', 'other'.

      +
    • +
    • +

      oxsize - 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).

      +

      Note: The mapping is undocumented and may change without notice.

      +

      Note: Some of OC's size values cannot be properly mapped to oxsize, + i.e. the 'other' size.

      +
    • +
    • difficulty - float (between 1 and 5), difficulty rating of the cache,
    • +
    • terrain - float (between 1 and 5), terrain rating of the cache,
    • +
    • + trip_time - float, approximate total amount of time needed to + find the cache, in hours; this will usually include the time to reach the + cache location and go back (from/to a parking spot, etc.); + null if unknown, +
    • +
    • trip_distance - float, approximate total distance needed to + find the cache, in kilometers; this will usually include the time to reach the + cache location and go back (from/to a parking spot, etc.); + null if unknown, +
    • +
    • +

      rating - float (between 1 and 5), an overall rating of the cache, + or null when the geocache does not have a sufficient amount of votes + to have a rating.

      +

      Please note: some OC installations do not use the rating/voting system. The rating will + always be null on such installations.

      +
    • +
    • +

      rating_votes - number of votes submitted for the rating of this cache.

      +

      Please note: some OC installations do not use the rating/voting system. The rating_votes will + always be 0 on such installations.

      +
    • +
    • recommendations - number of recommendations for this cache,
    • +
    • req_passwd - boolean; states if this cache requires a password + in order to submit a "Found it" log entry,
    • +
    • short_description - a plaintext string with a single line (very short) + description of the cache (kind of a "tagline text"),
    • +
    • short_descriptions - a dictionary (language code => + plaintext string) of short cache descriptions,
    • +
    • description - HTML string, + description of the cache,
    • +
    • descriptions - a dictionary (language code => + HTML string) of cache descriptions,
    • +
    • hint - HTML-encoded + string, cache hints/spoilers; deprecated + (why?), + use hint2 instead,
    • +
    • hints - a dictionary (language code => + HTML-encoded string) of cache hints/spoilers; + deprecated, use hints2 instead,
    • +
    • hint2 - plain-text string, cache hints/spoilers; in general, hints + should not be displayed to the user unless user specifically asks for them,
    • +
    • hints2 - a dictionary (language code => + plain-text string) of cache hints/spoilers,
    • +
    • +

      images - list of dictionaries, each dictionary represents one + image saved along the cache description; each dictionary has the + following structure:

      +
        +
      • uuid - UUID of the image,
      • +
      • url - URL of the image,
      • +
      • thumb_url - URL of a small (thumb) version of the image,
      • +
      • caption - plain-text string, caption of the image,
      • +
      • unique_caption - 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),
      • +
      • is_spoiler - boolean, if true then the image is + a spoiler image and should not be displayed to the user unless + the user explicitly asks for it,
      • +
      +
    • +
    • +

      preview_image - On some installations, owners may select one of the images + (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 null value here.

      + +

      The value of preview_image is either null or a dictionary describing + an image. The structure of this dictionary is the same as of a single entry on + the images list described above.

      +
    • +
    • +

      attr_acodes - unordered list of OKAPI geocache-attribute IDs (A-codes) with + which the cache was tagged. Use the attrs module to retrieve information on + these attributes.

      +
    • +
    • + attrnames - if you don't want to dig into attr_acodes just now, then + you can use these as a simple alternative. attrnames is an unordered list of + names of the attributes with which the cache was tagged; the language will be + selected based on the langpref parameter. +
    • +
    • +

      attribution_note - 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 attribution_append parameter).

      +
    • +
    • +

      latest_logs - a couple of latest log entries in the cache. + The format is the same as the one returned by the services/logs/logs method.

      +

      Notice: The number of logs returned can be set with the lpc option.

      +
    • +
    • +

      my_notes - user's notes on the cache, in plain-text, or null + if user hadn't defined any notes.

      +

      This field requires you to use the Level 3 Authentication.

      +
    • +
    • trackables_count - a total number of trackables hidden within the cache.
    • +
    • +

      trackables - list of dictionaries, each dictionary represents one + trackable hidden within the cache container; each dictionary has the + following structure:

      +
        +
      • code - code of the trackable,
      • +
      • name - plain text name of the trackable,
      • +
      • url - trackable's own webpage address or null, if OKAPI + cannot automatically determine this address.
      • +
      +
    • +
    • +

      alt_wpts - list of alternate/additional waypoints associated + with this geocache. Each item is a dictionary of the following structure:

      +
        +
      • name - plain-text, short, unique "codename" for the waypoint,
      • +
      • location - location of the waypoint in the "lat|lon" format + (lat and lon are in full degrees with a dot as a decimal point),
      • +
      • +

        type - string, unique identifier for the type of waypoint; + one of the following:

        + +
          +
        • parking, path, stage, physical-stage, + virtual-stage, final, poi, other - used by + OC itself, for detailed descriptions of these you'll have to refer to + external Opencaching documenation,
        • + +
        • user-coords - extra coordinates supplied by the user who + had found the cache (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,
        • + +
        • more types may be added at any moment; unknown types should be + treated as other.
        • +
        +
      • +
      • + type_name - 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, +
      • +
      • +

        sym - 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").

        + +

        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.

        +
      • +
      • description - plain-text longer description of the waypoint.
      • +
      +
    • +
    • +

      country - name of the country the cache is placed in; + may be empty ("") if the country is unknown.

      +

      Note: 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 langpref + parameter in the future).

      +
    • +
    • +

      state - name of the state the cache is placed in; + may be empty ("") if the state is unknown.

      +

      Note: 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 langpref + parameter in the future).

      +
    • +
    • +

      protection_areas - list of dictionaries, each representing a + protection area in which the cache probably is located; each dictionary + has the following structure:

      +
        +
      • type - the type of the protection area, e.g. + "National Nature Reserve",
      • +
      • name - the name of the protection area, e.g. + "East Dartmoor Woods and Heaths".
      • +
      +

      The types and names currently are in a local language of the OC site but + may be translated depending on the langpref parameter in the future.

      +

      Note that this information is not authoritative. It may be outdated + or incomplete, and it is completely missing on some OC installations.

      +
    • +
    • +

      last_found - date and time (ISO 8601) when the + geocache was last found or null when it hasn't been yet found.

      +

      Note, that some Opencaching servers don't store the exact times along + with the log entries.

      +
    • +
    • last_modified - date and time (ISO 8601) when the + geocache was last modified (changed status, attributes, etc.),
    • +
    • date_created - date and time (ISO 8601) when the + geocache was listed at the Opencaching site,
    • +
    • date_hidden - date and time (ISO 8601) when +
        +
      • the geocache was first hidden (for physical caches), or
      • +
      • the geocache was first published (for virtual caches), or
      • +
      • the event takes place (for event caches),
      • +
      +
    • + +
    • internal_id - internal ID of the cache (do not use + this, unless you know what you're doing; use the "code" as an identifier).
    • +
    + +

    For example, for geocache?cache_code=OP3D96&fields=type + query, the result might look something link this:

    +
    {"type": "Traditional"}
    +

    If given cache code does not exist, the method will + respond with an HTTP 400 error.

    +
    \ No newline at end of file diff --git a/htdocs/okapi/services/caches/geocaches.php b/htdocs/okapi/services/caches/geocaches.php index ec0a3edc..848af8d5 100644 --- a/htdocs/okapi/services/caches/geocaches.php +++ b/htdocs/okapi/services/caches/geocaches.php @@ -19,1282 +19,1367 @@ use okapi\services\attrs\AttrHelper; class WebService { - public static function options() - { - return array( - 'min_auth_level' => 1 - ); - } - - private static $valid_field_names = array('code', 'name', 'names', 'location', 'type', - 'status', 'url', 'owner', 'distance', 'bearing', 'bearing2', 'bearing3', 'is_found', - 'is_not_found', 'founds', 'notfounds', 'size', 'size2', 'oxsize', 'difficulty', 'terrain', - 'rating', 'rating_votes', 'recommendations', 'req_passwd', 'description', - 'descriptions', 'hint', 'hints', 'images', 'attr_acodes', 'attrnames', 'latest_logs', - 'my_notes', 'trackables_count', 'trackables', 'alt_wpts', 'last_found', - 'last_modified', 'date_created', 'date_hidden', 'internal_id', 'is_watched', - 'is_ignored', 'willattends', 'country', 'state', 'preview_image', - 'trip_time', 'trip_distance', 'attribution_note','gc_code', 'hint2', 'hints2', - 'protection_areas'); - - public static function call(OkapiRequest $request) - { - $cache_codes = $request->get_parameter('cache_codes'); - if ($cache_codes === null) throw new ParamMissing('cache_codes'); - if ($cache_codes === "") - { - # Issue 106 requires us to allow empty list of cache codes to be passed into this method. - # All of the queries below have to be ready for $cache_codes to be empty! - $cache_codes = array(); - } - else - $cache_codes = explode("|", $cache_codes); - - if ((count($cache_codes) > 500) && (!$request->skip_limits)) - throw new InvalidParam('cache_codes', "Maximum allowed number of referenced ". - "caches is 500. You provided ".count($cache_codes)." cache codes."); - if (count($cache_codes) != count(array_unique($cache_codes))) - throw new InvalidParam('cache_codes', "Duplicate codes detected (make sure each cache is referenced only once)."); - - $langpref = $request->get_parameter('langpref'); - if (!$langpref) $langpref = "en"; - $langpref = explode("|", $langpref); - - $fields = $request->get_parameter('fields'); - if (!$fields) $fields = "code|name|location|type|status"; - $fields = explode("|", $fields); - foreach ($fields as $field) - if (!in_array($field, self::$valid_field_names)) - throw new InvalidParam('fields', "'$field' is not a valid field code."); - - # Some fields need to be temporarily included whenever the "description" - # or "attribution_note" field are included. That's a little ugly, but - # helps performance and conforms to the DRY rule. - - $fields_to_remove_later = array(); - if ( - in_array('description', $fields) || in_array('descriptions', $fields) - || in_array('hint', $fields) || in_array('hints', $fields) - || in_array('hint2', $fields) || in_array('hints2', $fields) - || in_array('attribution_note', $fields) - ) - { - if (!in_array('owner', $fields)) - { - $fields[] = "owner"; - $fields_to_remove_later[] = "owner"; - } - if (!in_array('internal_id', $fields)) - { - $fields[] = "internal_id"; - $fields_to_remove_later[] = "internal_id"; - } - } - - $attribution_append = $request->get_parameter('attribution_append'); - if (!$attribution_append) $attribution_append = 'full'; - if (!in_array($attribution_append, array('none', 'static', 'full'))) - throw new InvalidParam('attribution_append'); - - $log_fields = $request->get_parameter('log_fields'); - if (!$log_fields) $log_fields = "uuid|date|user|type|comment"; // validation is done on call - - $user_uuid = $request->get_parameter('user_uuid'); - if ($user_uuid != null) - { - $user_id = Db::select_value("select user_id from user where uuid='".mysql_real_escape_string($user_uuid)."'"); - if ($user_id == null) - throw new InvalidParam('user_uuid', "User not found."); - if (($request->token != null) && ($request->token->user_id != $user_id)) - throw new InvalidParam('user_uuid', "User does not match the Access Token used."); - } - elseif (($user_uuid == null) && ($request->token != null)) - $user_id = $request->token->user_id; - else - $user_id = null; - - $lpc = $request->get_parameter('lpc'); - if ($lpc === null) $lpc = 10; - if ($lpc == 'all') - $lpc = null; - else - { - if (!is_numeric($lpc)) - throw new InvalidParam('lpc', "Invalid number: '$lpc'"); - $lpc = intval($lpc); - if ($lpc < 0) - throw new InvalidParam('lpc', "Must be a positive value."); - } - - if (in_array('distance', $fields) || in_array('bearing', $fields) || in_array('bearing2', $fields) - || in_array('bearing3', $fields)) - { - $tmp = $request->get_parameter('my_location'); - if (!$tmp) - throw new BadRequest("When using 'distance' or 'bearing' fields, you have to supply 'my_location' parameter."); - $parts = explode('|', $tmp); - if (count($parts) != 2) - throw new InvalidParam('my_location', "Expecting 2 pipe-separated parts, got ".count($parts)."."); - foreach ($parts as &$part_ref) - { - if (!preg_match("/^-?[0-9]+(\.?[0-9]*)$/", $part_ref)) - throw new InvalidParam('my_location', "'$part_ref' is not a valid float number."); - $part_ref = floatval($part_ref); - } - list($center_lat, $center_lon) = $parts; - if ($center_lat > 90 || $center_lat < -90) - throw new InvalidParam('current_position', "Latitudes have to be within -90..90 range."); - if ($center_lon > 180 || $center_lon < -180) - throw new InvalidParam('current_position', "Longitudes have to be within -180..180 range."); - } - - if (Settings::get('OC_BRANCH') == 'oc.de') - { - # DE branch: - # - Caches do not have ratings. - # - Total numbers of founds and notfounds are kept in the "stat_caches" table. - # - search_time and way_length are both round trip values and cannot be null; - # 0 = not specified - # - will-attend-count is stored in separate field - - $rs = Db::query(" - select - c.cache_id, c.name, c.longitude, c.latitude, c.listing_last_modified as last_modified, - c.date_created, c.type, c.status, c.date_hidden, c.size, c.difficulty, - c.terrain, c.wp_oc, c.wp_gc, c.logpw, c.user_id, - if(c.search_time=0, null, c.search_time) as trip_time, - if(c.way_length=0, null, c.way_length) as trip_distance, - - ifnull(sc.toprating, 0) as topratings, - ifnull(sc.found, 0) as founds, - ifnull(sc.notfound, 0) as notfounds, - ifnull(sc.will_attend, 0) as willattends, - sc.last_found, - 0 as votes, 0 as score - -- SEE ALSO OC.PL BRANCH BELOW - from - caches c - left join stat_caches as sc on c.cache_id = sc.cache_id - where - wp_oc in ('".implode("','", array_map('mysql_real_escape_string', $cache_codes))."') - and status in (1,2,3) - "); - } - elseif (Settings::get('OC_BRANCH') == 'oc.pl') - { - # PL branch: - # - Caches have ratings. - # - Total numbers of found and notfounds are kept in the "caches" table. - # - search_time is round trip and way_length one way or both ways (this is different on OCDE!); - # both can be null; 0 or null = not specified - # - will-attend-count is stored in caches.notfounds - - $rs = Db::query(" - select - c.cache_id, c.name, c.longitude, c.latitude, c.last_modified, - c.date_created, c.type, c.status, c.date_hidden, c.size, c.difficulty, - c.terrain, c.wp_oc, c.wp_gc, c.logpw, c.user_id, - if(c.search_time=0, null, c.search_time) as trip_time, - if(c.way_length=0, null, c.way_length) as trip_distance, - - c.topratings, - c.founds, - c.notfounds, - c.notfounds as willattends, - c.last_found, - c.votes, c.score - -- SEE ALSO OC.DE BRANCH ABOVE - from - caches c - where - wp_oc in ('".implode("','", array_map('mysql_real_escape_string', $cache_codes))."') - and c.status in (1,2,3) - "); - } - - $results = new ArrayObject(); - $cacheid2wptcode = array(); - $owner_ids = array(); - while ($row = mysql_fetch_assoc($rs)) - { - $entry = array(); - $cacheid2wptcode[$row['cache_id']] = $row['wp_oc']; - foreach ($fields as $field) - { - switch ($field) - { - case 'code': $entry['code'] = $row['wp_oc']; break; - case 'gc_code': - // OC software allows entering anything here, and that's what users do. - // We do a formal verification so that only a valid GC code is returned: - if (preg_match('/^\s*[Gg][Cc][A-Za-z0-9]+\s*$/', $row['wp_gc'])) - $entry['gc_code'] = strtoupper(trim($row['wp_gc'])); - else - $entry['gc_code'] = null; - break; - case 'name': $entry['name'] = $row['name']; break; - case 'names': $entry['names'] = array(Settings::get('SITELANG') => $row['name']); break; // for the future - case 'location': $entry['location'] = round($row['latitude'], 6)."|".round($row['longitude'], 6); break; - case 'type': $entry['type'] = Okapi::cache_type_id2name($row['type']); break; - case 'status': $entry['status'] = Okapi::cache_status_id2name($row['status']); break; - case 'url': $entry['url'] = Settings::get('SITE_URL')."viewcache.php?wp=".$row['wp_oc']; break; - case 'owner': - $owner_ids[$row['wp_oc']] = $row['user_id']; - /* continued later */ - break; - case 'distance': - $entry['distance'] = (int)Okapi::get_distance($center_lat, $center_lon, $row['latitude'], $row['longitude']); - break; - case 'bearing': - $tmp = Okapi::get_bearing($center_lat, $center_lon, $row['latitude'], $row['longitude']); - $entry['bearing'] = ($tmp !== null) ? ((int)(10*$tmp)) / 10.0 : null; - break; - case 'bearing2': - $tmp = Okapi::get_bearing($center_lat, $center_lon, $row['latitude'], $row['longitude']); - $entry['bearing2'] = Okapi::bearing_as_two_letters($tmp); - break; - case 'bearing3': - $tmp = Okapi::get_bearing($center_lat, $center_lon, $row['latitude'], $row['longitude']); - $entry['bearing3'] = Okapi::bearing_as_three_letters($tmp); - break; - case 'is_found': /* handled separately */ break; - case 'is_not_found': /* handled separately */ break; - case 'is_watched': /* handled separately */ break; - case 'is_ignored': /* handled separately */ break; - case 'founds': $entry['founds'] = $row['founds'] + 0; break; - case 'notfounds': - if ($row['type'] != 6) { # non-event - $entry['notfounds'] = $row['notfounds'] + 0; - } else { # event - $entry['notfounds'] = 0; - } - break; - case 'willattends': - if ($row['type'] == 6) { # event - $entry['willattends'] = $row['willattends'] + 0; - } else { # non-event - $entry['willattends'] = 0; - } - break; - case 'size': - # Deprecated. Leave it for backward-compatibility. See issue 155. - switch (Okapi::cache_sizeid_to_size2($row['size'])) - { - case 'none': $entry['size'] = null; break; - case 'nano': $entry['size'] = 1.0; break; # same as micro - case 'micro': $entry['size'] = 1.0; break; - case 'small': $entry['size'] = 2.0; break; - case 'regular': $entry['size'] = 3.0; break; - case 'large': $entry['size'] = 4.0; break; - case 'xlarge': $entry['size'] = 5.0; break; - case 'other': $entry['size'] = null; break; # same as none - default: throw new Exception(); - } - break; - case 'size2': $entry['size2'] = Okapi::cache_sizeid_to_size2($row['size']); break; - case 'oxsize': $entry['oxsize'] = Okapi::cache_size2_to_oxsize(Okapi::cache_sizeid_to_size2($row['size'])); break; - case 'difficulty': $entry['difficulty'] = round($row['difficulty'] / 2.0, 1); break; - case 'terrain': $entry['terrain'] = round($row['terrain'] / 2.0, 1); break; - case 'trip_time': - # search time is entered in hours:minutes and converted to decimal hours, - # which can produce lots of unneeded decimal places; 2 of them are sufficient here - $entry['trip_time'] = $row['trip_time'] === null ? null : round($row['trip_time'],2); break; - break; - case 'trip_distance': - # way length is entered in km as decimal fraction, but number conversions can - # create fake digits which should be stripped; meter precision is sufficient here - $entry['trip_distance'] = $row['trip_distance'] === null ? null : round($row['trip_distance'],3); break; - break; - case 'rating': - if ($row['votes'] < 3) $entry['rating'] = null; - elseif ($row['score'] >= 2.2) $entry['rating'] = 5.0; - elseif ($row['score'] >= 1.4) $entry['rating'] = 4.0; - elseif ($row['score'] >= 0.1) $entry['rating'] = 3.0; - elseif ($row['score'] >= -1.0) $entry['rating'] = 2.0; - else $entry['rating'] = 1.0; - break; - case 'rating_votes': $entry['rating_votes'] = $row['votes'] + 0; break; - case 'recommendations': $entry['recommendations'] = $row['topratings'] + 0; break; - case 'req_passwd': $entry['req_passwd'] = $row['logpw'] ? true : false; break; - case 'description': /* handled separately */ break; - case 'descriptions': /* handled separately */ break; - case 'hint': /* handled separately */ break; - case 'hints': /* handled separately */ break; - case 'hint2': /* handled separately */ break; - case 'hints2': /* handled separately */ break; - case 'images': /* handled separately */ break; - case 'preview_image': /* handled separately */ break; - case 'attr_acodes': /* handled separately */ break; - case 'attrnames': /* handled separately */ break; - case 'latest_logs': /* handled separately */ break; - case 'my_notes': /* handles separately */ break; - case 'trackables_count': /* handled separately */ break; - case 'trackables': /* handled separately */ break; - case 'alt_wpts': /* handled separately */ break; - case 'country': /* handled separately */ break; - case 'state': /* handled separately */ break; - case 'last_found': $entry['last_found'] = ($row['last_found'] > '1980') ? date('c', strtotime($row['last_found'])) : null; break; - case 'last_modified': $entry['last_modified'] = date('c', strtotime($row['last_modified'])); break; - case 'date_created': $entry['date_created'] = date('c', strtotime($row['date_created'])); break; - case 'date_hidden': $entry['date_hidden'] = date('c', strtotime($row['date_hidden'])); break; - case 'internal_id': $entry['internal_id'] = $row['cache_id']; break; - case 'attribution_note': /* handled separately */ break; - case 'protection_areas': /* handled separately */ break; - default: throw new Exception("Missing field case: ".$field); - } - } - $results[$row['wp_oc']] = $entry; - } - mysql_free_result($rs); - - # owner - - if (in_array('owner', $fields) && (count($results) > 0)) - { - $rs = Db::query(" - select user_id, uuid, username - from user - where user_id in ('".implode("','", array_map('mysql_real_escape_string', array_values($owner_ids)))."') - "); - $tmp = array(); - while ($row = mysql_fetch_assoc($rs)) - $tmp[$row['user_id']] = $row; - foreach ($results as $cache_code => &$result_ref) - { - $row = $tmp[$owner_ids[$cache_code]]; - $result_ref['owner'] = array( - 'uuid' => $row['uuid'], - 'username' => $row['username'], - 'profile_url' => Settings::get('SITE_URL')."viewprofile.php?userid=".$row['user_id'] - ); - } - } - - # is_found - - if (in_array('is_found', $fields)) - { - if ($user_id == null) - throw new BadRequest("Either 'user_uuid' parameter OR Level 3 Authentication is required to access 'is_found' field."); - $tmp = Db::select_column(" - select c.wp_oc - from - caches c, - cache_logs cl - where - c.cache_id = cl.cache_id - and cl.type in ( - '".mysql_real_escape_string(Okapi::logtypename2id("Found it"))."', - '".mysql_real_escape_string(Okapi::logtypename2id("Attended"))."' - ) - and cl.user_id = '".mysql_real_escape_string($user_id)."' - ".((Settings::get('OC_BRANCH') == 'oc.pl') ? "and cl.deleted = 0" : "")." - "); - $tmp2 = array(); - foreach ($tmp as $cache_code) - $tmp2[$cache_code] = true; - foreach ($results as $cache_code => &$result_ref) - $result_ref['is_found'] = isset($tmp2[$cache_code]); - } - - # is_not_found - - if (in_array('is_not_found', $fields)) - { - if ($user_id == null) - throw new BadRequest("Either 'user_uuid' parameter OR Level 3 Authentication is required to access 'is_not_found' field."); - $tmp = Db::select_column(" - select c.wp_oc - from - caches c, - cache_logs cl - where - c.cache_id = cl.cache_id - and cl.type = '".mysql_real_escape_string(Okapi::logtypename2id("Didn't find it"))."' - and cl.user_id = '".mysql_real_escape_string($user_id)."' - ".((Settings::get('OC_BRANCH') == 'oc.pl') ? "and cl.deleted = 0" : "")." - "); - $tmp2 = array(); - foreach ($tmp as $cache_code) - $tmp2[$cache_code] = true; - foreach ($results as $cache_code => &$result_ref) - $result_ref['is_not_found'] = isset($tmp2[$cache_code]); - } - - # is_watched - - if (in_array('is_watched', $fields)) - { - if ($request->token == null) - throw new BadRequest("Level 3 Authentication is required to access 'is_watched' field."); - $tmp = Db::select_column(" - select c.wp_oc - from - caches c, - cache_watches cw - where - c.cache_id = cw.cache_id - and cw.user_id = '".mysql_real_escape_string($request->token->user_id)."' - "); - $tmp2 = array(); - foreach ($tmp as $cache_code) - $tmp2[$cache_code] = true; - foreach ($results as $cache_code => &$result_ref) - $result_ref['is_watched'] = isset($tmp2[$cache_code]); - } - - # is_ignored - - if (in_array('is_ignored', $fields)) - { - if ($request->token == null) - throw new BadRequest("Level 3 Authentication is required to access 'is_ignored' field."); - $tmp = Db::select_column(" - select c.wp_oc - from - caches c, - cache_ignore ci - where - c.cache_id = ci.cache_id - and ci.user_id = '".mysql_real_escape_string($request->token->user_id)."' - "); - $tmp2 = array(); - foreach ($tmp as $cache_code) - $tmp2[$cache_code] = true; - foreach ($results as $cache_code => &$result_ref) - $result_ref['is_ignored'] = isset($tmp2[$cache_code]); - } - - # Descriptions and hints. - - if (in_array('description', $fields) || in_array('descriptions', $fields) - || in_array('hint', $fields) || in_array('hints', $fields) - || in_array('hint2', $fields) || in_array('hints2', $fields)) - { - # At first, we will fill all those 4 fields, even if user requested just one - # of them. We will chop off the remaining three at the end. - - foreach ($results as &$result_ref) - { - $result_ref['descriptions'] = array(); - $result_ref['hints'] = array(); - $result_ref['hints2'] = array(); - } - - # Get cache descriptions and hints. - - $rs = Db::query(" - select cache_id, language, `desc`, hint - from cache_desc - where cache_id in ('".implode("','", array_map('mysql_real_escape_string', array_keys($cacheid2wptcode)))."') - "); - while ($row = mysql_fetch_assoc($rs)) - { - $cache_code = $cacheid2wptcode[$row['cache_id']]; - // strtolower - ISO 639-1 codes are lowercase - if ($row['desc']) - { - /* Note, that the "owner" and "internal_id" fields are automatically included, - * whenever the cache description is included. */ - - $tmp = Okapi::fix_oc_html($row['desc']); - - if ($attribution_append != 'none') - { - $tmp .= "\n

    ". - self::get_cache_attribution_note( - $row['cache_id'], strtolower($row['language']), $langpref, - $results[$cache_code]['owner'], $attribution_append - ). - "

    "; - } - $results[$cache_code]['descriptions'][strtolower($row['language'])] = $tmp; - } - if ($row['hint']) - { - $results[$cache_code]['hints'][strtolower($row['language'])] = $row['hint']; - $results[$cache_code]['hints2'][strtolower($row['language'])] - = htmlspecialchars_decode(mb_ereg_replace("
    ", "" , $row['hint']), ENT_COMPAT); - } - } - foreach ($results as &$result_ref) - { - $result_ref['description'] = Okapi::pick_best_language($result_ref['descriptions'], $langpref); - $result_ref['hint'] = Okapi::pick_best_language($result_ref['hints'], $langpref); - $result_ref['hint2'] = Okapi::pick_best_language($result_ref['hints2'], $langpref); - } - - # Remove unwanted fields. - - foreach (array('description', 'descriptions', 'hint', 'hints', 'hint2', 'hints2') as $field) - if (!in_array($field, $fields)) - foreach ($results as &$result_ref) - unset($result_ref[$field]); - } - - # Images. - - if (in_array('images', $fields) || in_array('preview_image', $fields)) - { - if (in_array('images', $fields)) - foreach ($results as &$result_ref) - $result_ref['images'] = array(); - if (in_array('preview_image', $fields)) - foreach ($results as &$result_ref) - $result_ref['preview_image'] = null; - - if (Db::field_exists('pictures', 'mappreview')) - $preview_field = "mappreview"; - else - $preview_field = "0"; - $rs = Db::query(" - select object_id, uuid, url, title, spoiler, ".$preview_field." as preview - from pictures - where - object_id in ('".implode("','", array_map('mysql_real_escape_string', array_keys($cacheid2wptcode)))."') - and display = 1 - and object_type = 2 - and unknown_format = 0 - order by object_id, date_created - "); - $prev_cache_code = null; - while ($row = mysql_fetch_assoc($rs)) - { - $cache_code = $cacheid2wptcode[$row['object_id']]; - if ($cache_code != $prev_cache_code) - { - # Group images together. Images must have unique captions within one cache. - self::reset_unique_captions(); - $prev_cache_code = $cache_code; - } - $image = array( - 'uuid' => $row['uuid'], - 'url' => $row['url'], - 'thumb_url' => Settings::get('SITE_URL') . 'thumbs.php?uuid=' . $row['uuid'], - 'caption' => $row['title'], - 'unique_caption' => self::get_unique_caption($row['title']), - 'is_spoiler' => ($row['spoiler'] ? true : false), - ); - if (in_array('images', $fields)) - $results[$cache_code]['images'][] = $image; - if ($row['preview'] != 0 && in_array('preview_image', $fields)) - $results[$cache_code]['preview_image'] = $image; - } - } - - # A-codes and attrnames - - if (in_array('attr_acodes', $fields) || in_array('attrnames', $fields)) - { - # Either case, we'll need acodes. If the user didn't want them, - # remember to remove them later. - - if (!in_array('attr_acodes', $fields)) - { - $fields_to_remove_later[] = 'attr_acodes'; - } - foreach ($results as &$result_ref) - $result_ref['attr_acodes'] = array(); - - # Load internal_attr_id => acode mapping. - - require_once($GLOBALS['rootpath'].'okapi/services/attrs/attr_helper.inc.php'); - $internal2acode = AttrHelper::get_internal_id_to_acode_mapping(); - - $rs = Db::query(" - select cache_id, attrib_id - from caches_attributes - where cache_id in ('".implode("','", array_map('mysql_real_escape_string', array_keys($cacheid2wptcode)))."') - "); - while ($row = mysql_fetch_assoc($rs)) - { - $cache_code = $cacheid2wptcode[$row['cache_id']]; - $attr_internal_id = $row['attrib_id']; - if (!isset($internal2acode[$attr_internal_id])) - { - # Unknown attribute. Ignore. - continue; - } - $results[$cache_code]['attr_acodes'][] = $internal2acode[$attr_internal_id]; - } - - # Now, each cache object has a list of its acodes. We can get - # the attrnames now. - - if (in_array('attrnames', $fields)) - { - $acode2bestname = AttrHelper::get_acode_to_name_mapping($langpref); - foreach ($results as &$result_ref) - { - $result_ref['attrnames'] = array(); - foreach ($result_ref['attr_acodes'] as $acode) - $result_ref['attrnames'][] = $acode2bestname[$acode]; - } - } - } - - # Latest log entries. - - if (in_array('latest_logs', $fields)) - { - foreach ($results as &$result_ref) - $result_ref['latest_logs'] = array(); - - # Get all log IDs with dates. Sort in groups. Filter out latest ones. This is the fastest - # technique I could think of... - - $rs = Db::query(" - select cache_id, uuid, date - from cache_logs - where - cache_id in ('".implode("','", array_map('mysql_real_escape_string', array_keys($cacheid2wptcode)))."') - and ".((Settings::get('OC_BRANCH') == 'oc.pl') ? "deleted = 0" : "true")." - order by cache_id, date desc, date_created desc - "); - $loguuids = array(); - $log2cache_map = array(); - if ($lpc !== null) - { - # User wants some of the latest logs. - $tmp = array(); - while ($row = mysql_fetch_assoc($rs)) - $tmp[$row['cache_id']][] = $row; - foreach ($tmp as $cache_key => &$rowslist_ref) - { - usort($rowslist_ref, function($rowa, $rowb) { - # (reverse order by date) - return ($rowa['date'] < $rowb['date']) ? 1 : (($rowa['date'] == $rowb['date']) ? 0 : -1); - }); - for ($i = 0; $i < min(count($rowslist_ref), $lpc); $i++) - { - $loguuids[] = $rowslist_ref[$i]['uuid']; - $log2cache_map[$rowslist_ref[$i]['uuid']] = $cacheid2wptcode[$rowslist_ref[$i]['cache_id']]; - } - } - } - else - { - # User wants ALL logs. - while ($row = mysql_fetch_assoc($rs)) - { - $loguuids[] = $row['uuid']; - $log2cache_map[$row['uuid']] = $cacheid2wptcode[$row['cache_id']]; - } - } - - # We need to retrieve logs/entry for each of the $logids. We do this in groups - # (there is a limit for log uuids passed to logs/entries method). - - try - { - foreach (Okapi::make_groups($loguuids, 500) as $subset) - { - $entries = OkapiServiceRunner::call( - "services/logs/entries", - new OkapiInternalRequest( - $request->consumer, $request->token, array( - 'log_uuids' => implode("|", $subset), - 'fields' => $log_fields - ) - ) - ); - foreach ($subset as $log_uuid) - { - if ($entries[$log_uuid]) - $results[$log2cache_map[$log_uuid]]['latest_logs'][] = $entries[$log_uuid]; - } - } - } - catch (Exception $e) - { - if (($e instanceof InvalidParam) && ($e->paramName == 'fields')) - { - throw new InvalidParam('log_fields', $e->whats_wrong_about_it); - } - else - { - /* Something is wrong with OUR code. */ - throw new Exception($e); - } - } - } - - # My notes - - if (in_array('my_notes', $fields)) - { - if ($request->token == null) - throw new BadRequest("Level 3 Authentication is required to access 'my_notes' field."); - foreach ($results as &$result_ref) - $result_ref['my_notes'] = null; - if (Settings::get('OC_BRANCH') == 'oc.pl') - { - # OCPL uses cache_notes table to store notes. - - $rs = Db::query(" - select cache_id, max(date) as date, group_concat(`desc`) as `desc` - from cache_notes - where - cache_id in ('".implode("','", array_map('mysql_real_escape_string', array_keys($cacheid2wptcode)))."') - and user_id = '".mysql_real_escape_string($request->token->user_id)."' - group by cache_id - "); - } - else - { - # OCDE uses coordinates table (with type == 2) to store notes (this is somewhat weird). - - $rs = Db::query(" - select cache_id, null as date, group_concat(description) as `desc` - from coordinates - where - type = 2 -- personal note - and cache_id in ('".implode("','", array_map('mysql_real_escape_string', array_keys($cacheid2wptcode)))."') - and user_id = '".mysql_real_escape_string($request->token->user_id)."' - group by cache_id - "); - } - while ($row = mysql_fetch_assoc($rs)) - { - # This one is plain-text. We may add my_notes_html for those who want it in HTML. - $results[$cacheid2wptcode[$row['cache_id']]]['my_notes'] = strip_tags($row['desc']); - } - } - - if (in_array('trackables', $fields)) - { - # Currently we support Geokrety only. But this interface should remain - # compatible. In future, other trackables might be returned the same way. - - $rs = Db::query(" - select - gkiw.wp as cache_code, - gki.id as gk_id, - gki.name - from - gk_item_waypoint gkiw, - gk_item gki - where - gkiw.id = gki.id - and gkiw.wp in ('".implode("','", array_map('mysql_real_escape_string', $cache_codes))."') - "); - $trs = array(); - while ($row = mysql_fetch_assoc($rs)) - $trs[$row['cache_code']][] = $row; - foreach ($results as $cache_code => &$result_ref) - { - $result_ref['trackables'] = array(); - if (!isset($trs[$cache_code])) - continue; - foreach ($trs[$cache_code] as $t) - { - $result_ref['trackables'][] = array( - 'code' => 'GK'.str_pad(strtoupper(dechex($t['gk_id'])), 4, "0", STR_PAD_LEFT), - 'name' => $t['name'], - 'url' => 'http://geokrety.org/konkret.php?id='.$t['gk_id'] - ); - } - } - unset($trs); - } - if (in_array('trackables_count', $fields)) - { - if (in_array('trackables', $fields)) - { - # We already got all trackables data, no need to query database again. - foreach ($results as $cache_code => &$result_ref) - $result_ref['trackables_count'] = count($result_ref['trackables']); - } - else - { - $rs = Db::query(" - select wp as cache_code, count(*) as count - from gk_item_waypoint - where wp in ('".implode("','", array_map('mysql_real_escape_string', $cache_codes))."') - group by wp - "); - $tr_counts = array(); - while ($row = mysql_fetch_assoc($rs)) - $tr_counts[$row['cache_code']] = $row['count']; - foreach ($results as $cache_code => &$result_ref) - { - if (isset($tr_counts[$cache_code])) - $result_ref['trackables_count'] = $tr_counts[$cache_code] + 0; - else - $result_ref['trackables_count'] = 0; - } - unset($tr_counts); - } - } - - # Alternate/Additional waypoints. - - if (in_array('alt_wpts', $fields)) - { - $internal_wpt_type_id2names = array(); - if (Settings::get('OC_BRANCH') == 'oc.de') - { - $rs = Db::query(" - select - ct.id, - LOWER(stt.lang) as language, - stt.`text` - from - coordinates_type ct - left join sys_trans_text stt on stt.trans_id = ct.trans_id - "); - while ($row = mysql_fetch_assoc($rs)) - $internal_wpt_type_id2names[$row['id']][$row['language']] = $row['text']; - mysql_free_result($rs); - } - else - { - $rs = Db::query(" - select id, pl, en - from waypoint_type - where id > 0 - "); - while ($row = mysql_fetch_assoc($rs)) - { - $internal_wpt_type_id2names[$row['id']]['pl'] = $row['pl']; - $internal_wpt_type_id2names[$row['id']]['en'] = $row['en']; - } - } - - foreach ($results as &$result_ref) - $result_ref['alt_wpts'] = array(); - $cache_codes_escaped_and_imploded = "'".implode("','", array_map('mysql_real_escape_string', array_keys($cacheid2wptcode)))."'"; - - if (Settings::get('OC_BRANCH') == 'oc.pl') - { - # OCPL uses 'waypoints' table to store additional waypoints and defines - # waypoint types in 'waypoint_type' table. - # OCPL also have a special 'status' field to denote a hidden waypoint - # (i.e. final location of a multicache). Such hidden waypoints are not - # exposed by OKAPI. - - $cacheid2waypoints = Db::select_group_by("cache_id", " - select - cache_id, stage, latitude, longitude, `desc`, - type as internal_type_id, - case type - when 3 then 'Flag, Red' - when 4 then 'Circle with X' - when 5 then 'Parking Area' - else 'Flag, Green' - end as sym, - case type - when 1 then 'physical-stage' - when 2 then 'virtual-stage' - when 3 then 'final' - when 4 then 'poi' - when 5 then 'parking' - else 'other' - end as okapi_type - from waypoints - where - cache_id in (".$cache_codes_escaped_and_imploded.") - and status = 1 - order by cache_id, stage, `desc` - "); - } - else - { - # OCDE uses 'coordinates' table (with type=1) to store additional waypoints - # and defines waypoint types in 'coordinates_type' table. - # All additional waypoints are public. - - $cacheid2waypoints = Db::select_group_by("cache_id", " - select - cache_id, - false as stage, - latitude, longitude, - description as `desc`, - subtype as internal_type_id, - case subtype - when 1 then 'Parking Area' - when 3 then 'Flag, Blue' - when 4 then 'Circle with X' - when 5 then 'Diamond, Green' - else 'Flag, Green' - end as sym, - case subtype - when 1 then 'parking' - when 2 then 'stage' - when 3 then 'path' - when 4 then 'final' - when 5 then 'poi' - else 'other' - end as okapi_type - from coordinates - where - type = 1 - and cache_id in (".$cache_codes_escaped_and_imploded.") - order by cache_id, id - "); - } - - foreach ($cacheid2waypoints as $cache_id => $waypoints) - { - $cache_code = $cacheid2wptcode[$cache_id]; - $wpt_format = $cache_code."-%0".strlen(count($waypoints))."d"; - $index = 0; - foreach ($waypoints as $row) - { - if (!isset($internal_wpt_type_id2names[$row['internal_type_id']])) - { - # Sanity check. Waypoints of undefined type won't be accessible via OKAPI. - # See issue 219. - continue; - } - $index++; - $results[$cache_code]['alt_wpts'][] = array( - 'name' => sprintf($wpt_format, $index), - 'location' => round($row['latitude'], 6)."|".round($row['longitude'], 6), - 'type' => $row['okapi_type'], - 'type_name' => Okapi::pick_best_language($internal_wpt_type_id2names[$row['internal_type_id']], $langpref), - 'sym' => $row['sym'], - 'description' => ($row['stage'] ? _("Stage")." ".$row['stage'].": " : "").$row['desc'], - ); - } - } - } - - # Country and/or state. - - if (in_array('country', $fields) || in_array('state', $fields)) - { - $countries = array(); - $states = array(); - - if (Settings::get('OC_BRANCH') == 'oc.de') - { - # OCDE: - # - cache_location entries are created by a cronjob *after* listing the - # caches and may not yet exist. - # - The state is in adm2 field. - # - caches.country overrides cache_location.code1/adm1. If both differ, - # cache_location.adm2 to adm4 is invalid and the state unknown. - # - OCDE databases may contain caches with invalid country code. - # Such errors must be handled gracefully. - # - adm1 should always be ignored. Instead, code1 should be translated - # into a country name, depending on langpref. - - # build country code translation table - $rs = Db::query(" - select distinct - c.country, - lower(stt.lang) as language, - stt.`text` - from - caches c - inner join countries on countries.short=c.country - inner join sys_trans_text stt on stt.trans_id = countries.trans_id - where - c.wp_oc in ('".implode("','", array_map('mysql_real_escape_string', $cache_codes))."') - "); - $country_codes2names = array(); - while ($row = mysql_fetch_assoc($rs)) - $country_codes2names[$row['country']][$row['language']] = $row['text']; - mysql_free_result($rs); - - # get geocache countries and states - $rs = Db::query(" - select - c.wp_oc as cache_code, - c.country as country_code, - ifnull(if(c.country<>cl.code1,'',cl.adm2),'') as state - from - caches c - left join cache_location cl on c.cache_id = cl.cache_id - where - c.wp_oc in ('".implode("','", array_map('mysql_real_escape_string', $cache_codes))."') - "); - while ($row = mysql_fetch_assoc($rs)) - { - if (!isset($country_codes2names[$row['country_code']])) - $countries[$row['cache_code']] = ''; - else - $countries[$row['cache_code']] = Okapi::pick_best_language($country_codes2names[$row['country_code']], $langpref); - $states[$row['cache_code']] = $row['state']; - } - mysql_free_result($rs); - } - else - { - # OCPL: - # - cache_location data is entered by the user. - # - The state is in adm3 field. - - # get geocache countries and states - $rs = Db::query(" - select - c.wp_oc as cache_code, - cl.adm1 as country, - cl.adm3 as state - from - caches c, - cache_location cl - where - c.wp_oc in ('".implode("','", array_map('mysql_real_escape_string', $cache_codes))."') - and c.cache_id = cl.cache_id - "); - while ($row = mysql_fetch_assoc($rs)) - { - $countries[$row['cache_code']] = $row['country']; - $states[$row['cache_code']] = $row['state']; - } - mysql_free_result($rs); - } - - if (in_array('country', $fields)) - { - foreach ($results as $cache_code => &$row_ref) - $row_ref['country'] = isset($countries[$cache_code]) ? $countries[$cache_code] : null; - } - if (in_array('state', $fields)) - { - foreach ($results as $cache_code => &$row_ref) - $row_ref['state'] = isset($states[$cache_code]) ? $states[$cache_code] : null; - } - unset($countries); - unset($states); - } - - # Attribution note - - if (in_array('attribution_note', $fields)) - { - /* Note, that the "owner" and "internal_id" fields are automatically included, - * whenever the attribution_note is included. */ - - foreach ($results as $cache_code => &$result_ref) - $result_ref['attribution_note'] = - self::get_cache_attribution_note( - $result_ref['internal_id'], $langpref[0], $langpref, - $results[$cache_code]['owner'], 'full' - ); - } - - # Protection areas - - if (in_array('protection_areas', $fields)) - { - $cache_ids_escaped_and_imploded = "'".implode("','", array_map('mysql_real_escape_string', array_keys($cacheid2wptcode)))."'"; - - if (Settings::get('OC_BRANCH') == 'oc.de') - { - $rs = Db::query(" - select - c.wp_oc as cache_code, - npa_types.name as type, - npa_areas.name as name - from - caches c - inner join cache_npa_areas on cache_npa_areas.cache_id=c.cache_id - inner join npa_areas on cache_npa_areas.npa_id = npa_areas.id - inner join npa_types on npa_areas.type_id = npa_types.id - where - c.cache_id in (".$cache_ids_escaped_and_imploded.") - group by npa_areas.type_id, npa_areas.name - order by npa_types.ordinal - "); - } - else if (Settings::get('ORIGIN_URL') == 'http://opencaching.pl/' || - Settings::get('ORIGIN_URL') == 'http://www.opencaching.nl/') - { - # Current OCPL table definitions use collation 'latin1' for parkipl - # and 'utf8' for np_areas. Union needs identical collations. - # To be sure, we convert both to utf8. - $rs = Db::query(" - select - c.wp_oc as cache_code, - '"._('National Park / Landscape')."' as type, - CONVERT(parkipl.name USING utf8) as name - from - caches c - inner join cache_npa_areas on cache_npa_areas.cache_id=c.cache_id - inner join parkipl on cache_npa_areas.parki_id=parkipl.id - where - c.cache_id in (".$cache_ids_escaped_and_imploded.") - and cache_npa_areas.parki_id != 0 - union - select - c.wp_oc as cache_code, - 'Natura 2000' as type, - CONVERT(npa_areas.sitename USING utf8) as name - from - caches c - inner join cache_npa_areas on cache_npa_areas.cache_id=c.cache_id - inner join npa_areas on cache_npa_areas.npa_id=npa_areas.id - where - c.cache_id in (".$cache_ids_escaped_and_imploded.") - and cache_npa_areas.npa_id != 0 - "); - } - else - { - # OC.US and .UK do not have a 'parkipl' table. - # OC.US has a 'us_parks' table instead. - # Natura 2000 is Europe-only. - $rs = null; - } - - foreach ($results as &$result_ref) - $result_ref['protection_areas'] = array(); - if ($rs) - { - while ($row = mysql_fetch_assoc($rs)) - { - $results[$row['cache_code']]['protection_areas'][] = array( - 'type' => $row['type'], - 'name' => $row['name'], - ); - } - mysql_free_result($rs); - } - } - - # Check which cache codes were not found and mark them with null. - foreach ($cache_codes as $cache_code) - if (!isset($results[$cache_code])) - $results[$cache_code] = null; - - if (count($fields_to_remove_later) > 0) - { - # Some of the fields in $results were added only temporarily - # (the Consumer did not ask for them). We will remove these fields now. - - foreach ($results as &$result_ref) - foreach ($fields_to_remove_later as $field) - unset($result_ref[$field]); - } - - # Order the results in the same order as the input codes were given. - # This might come in handy for languages which support ordered dictionaries - # (especially with conjunction with the search_and_retrieve method). - # See issue#97. PHP dictionaries (assoc arrays) are ordered structures, - # so we just have to rewrite it (sequentially). - - $ordered_results = new ArrayObject(); - foreach ($cache_codes as $cache_code) - $ordered_results[$cache_code] = $results[$cache_code]; - - return Okapi::formatted_response($request, $ordered_results); - } - - /** - * Create unique caption, safe to be used as a file name for images - * uploaded into Garmin's GPS devices. Use reset_unique_captions to reset - * unique counter. - */ - private static function get_unique_caption($caption) - { - # Garmins keep hanging on long file names. We don't have any specification from - # Garmin and cannot determine WHY. That's why we won't use captions until we - # know more. - - $caption = self::$caption_no.""; - self::$caption_no++; - return $caption; - - /* This code is harmful for Garmins! - $caption = preg_replace('#[^\\pL\d ]+#u', '-', $caption); - $caption = trim($caption, '-'); - if (function_exists('iconv')) - { - $new = iconv("utf-8", "ASCII//TRANSLIT", $caption); - if (!$new) - $new = iconv("UTF-8", "ASCII//IGNORE", $caption); - } else { - $new = $caption; - } - $new = str_replace(array('/', '\\', '?', '%', '*', ':', '|', '"', '<', '>', '.'), '', $new); - $new = trim($new); - if ($new == "") - $new = "(no caption)"; - if (strlen($new) > 240) - $new = substr($new, 0, 240); - $new = self::$caption_no." - ".$new; - self::$caption_no++; - return $new;*/ - } - private static $caption_no = 1; - private static function reset_unique_captions() - { - self::$caption_no = 1; - } - - /** - * Return attribution note for the given geocache. - * - * The $lang parameter identifies the language of the cache description - * to which the attribution note will be appended to (one cache may - * have descriptions in multiple languages!). - * - * The $langpref parameter is *an array* of language preferences - * extracted from the langpref parameter passed to the method by the - * OKAPI Consumer. - * - * Both values ($lang and $langpref) will be taken into account when - * generating the attribution note, but $lang will have a higher - * priority than $langpref (we don't want to mix the languages in the - * descriptions if we don't have to). - * - * $owner is in object describing the user, it has the same format as - * defined in "geocache" method specs (see the "owner" field). - * - * The $type is either "full" or "static". Full attributions may contain - * dates and are not suitable for the replicate module. Static attributions - * don't change that frequently. - */ - public static function get_cache_attribution_note( - $cache_id, $lang, array $langpref, $owner, $type - ) { - $site_url = Settings::get('SITE_URL'); - $site_name = Okapi::get_normalized_site_name(); - $cache_url = $site_url."viewcache.php?cacheid=$cache_id"; - - Okapi::gettext_domain_init(array_merge(array($lang), $langpref)); - if (Settings::get('OC_BRANCH') == 'oc.pl') - { - # This does not vary on $type (yet). - - $note = sprintf( - _("This geocache description comes from the %s site."), - $cache_url, $site_url, $site_name - ); - } - else - { - # OC.de wants the tld in lowercase here - $site_name = ucfirst(strtolower($site_name)); - if ($type == 'full') - { - $note = sprintf( - _( - "© %s, %s, ". - "CC-BY-NC-ND, ". - "as of %s; all log entries © their authors" - ), - $owner['profile_url'], $owner['username'], $cache_url, $site_name, strftime('%x') - ); - } - elseif ($type == 'static') - { - $note = sprintf( - _( - "© %s, %s, ". - "CC-BY-NC-ND; ". - "all log entries © their authors" - ), - $owner['profile_url'], $owner['username'], $cache_url, $site_name - ); - } - } - - Okapi::gettext_domain_restore(); - - return $note; - } + public static function options() + { + return array( + 'min_auth_level' => 1 + ); + } + + private static $valid_field_names = array('code', 'name', 'names', 'location', 'type', + 'status', 'url', 'owner', 'distance', 'bearing', 'bearing2', 'bearing3', 'is_found', + 'is_not_found', 'founds', 'notfounds', 'size', 'size2', 'oxsize', 'difficulty', 'terrain', + 'rating', 'rating_votes', 'recommendations', 'req_passwd', 'description', + 'descriptions', 'hint', 'hints', 'images', 'attr_acodes', 'attrnames', 'latest_logs', + 'my_notes', 'trackables_count', 'trackables', 'alt_wpts', 'last_found', + 'last_modified', 'date_created', 'date_hidden', 'internal_id', 'is_watched', + 'is_ignored', 'willattends', 'country', 'state', 'preview_image', + 'trip_time', 'trip_distance', 'attribution_note','gc_code', 'hint2', 'hints2', + 'protection_areas', 'short_description', 'short_descriptions'); + + public static function call(OkapiRequest $request) + { + $cache_codes = $request->get_parameter('cache_codes'); + if ($cache_codes === null) throw new ParamMissing('cache_codes'); + if ($cache_codes === "") + { + # Issue 106 requires us to allow empty list of cache codes to be passed into this method. + # All of the queries below have to be ready for $cache_codes to be empty! + $cache_codes = array(); + } + else + $cache_codes = explode("|", $cache_codes); + + if ((count($cache_codes) > 500) && (!$request->skip_limits)) + throw new InvalidParam('cache_codes', "Maximum allowed number of referenced ". + "caches is 500. You provided ".count($cache_codes)." cache codes."); + if (count($cache_codes) != count(array_unique($cache_codes))) + throw new InvalidParam('cache_codes', "Duplicate codes detected (make sure each cache is referenced only once)."); + + $langpref = $request->get_parameter('langpref'); + if (!$langpref) $langpref = "en"; + $langpref = explode("|", $langpref); + + $fields = $request->get_parameter('fields'); + if (!$fields) $fields = "code|name|location|type|status"; + $fields = explode("|", $fields); + foreach ($fields as $field) + if (!in_array($field, self::$valid_field_names)) + throw new InvalidParam('fields', "'$field' is not a valid field code."); + + # Some fields need to be temporarily included whenever the "description" + # or "attribution_note" field are included. That's a little ugly, but + # helps performance and conforms to the DRY rule. + + $fields_to_remove_later = array(); + if ( + in_array('description', $fields) || in_array('descriptions', $fields) + || in_array('short_description', $fields) || in_array('short_descriptions', $fields) + || in_array('hint', $fields) || in_array('hints', $fields) + || in_array('hint2', $fields) || in_array('hints2', $fields) + || in_array('attribution_note', $fields) + ) + { + if (!in_array('owner', $fields)) + { + $fields[] = "owner"; + $fields_to_remove_later[] = "owner"; + } + if (!in_array('internal_id', $fields)) + { + $fields[] = "internal_id"; + $fields_to_remove_later[] = "internal_id"; + } + } + + $attribution_append = $request->get_parameter('attribution_append'); + if (!$attribution_append) $attribution_append = 'full'; + if (!in_array($attribution_append, array('none', 'static', 'full'))) + throw new InvalidParam('attribution_append'); + + $log_fields = $request->get_parameter('log_fields'); + if (!$log_fields) $log_fields = "uuid|date|user|type|comment"; // validation is done on call + + $user_uuid = $request->get_parameter('user_uuid'); + if ($user_uuid != null) + { + $user_id = Db::select_value("select user_id from user where uuid='".mysql_real_escape_string($user_uuid)."'"); + if ($user_id == null) + throw new InvalidParam('user_uuid', "User not found."); + if (($request->token != null) && ($request->token->user_id != $user_id)) + throw new InvalidParam('user_uuid', "User does not match the Access Token used."); + } + elseif (($user_uuid == null) && ($request->token != null)) + $user_id = $request->token->user_id; + else + $user_id = null; + + $lpc = $request->get_parameter('lpc'); + if ($lpc === null) $lpc = 10; + if ($lpc == 'all') + $lpc = null; + else + { + if (!is_numeric($lpc)) + throw new InvalidParam('lpc', "Invalid number: '$lpc'"); + $lpc = intval($lpc); + if ($lpc < 0) + throw new InvalidParam('lpc', "Must be a positive value."); + } + + if (in_array('distance', $fields) || in_array('bearing', $fields) || in_array('bearing2', $fields) + || in_array('bearing3', $fields)) + { + $tmp = $request->get_parameter('my_location'); + if (!$tmp) + throw new BadRequest("When using 'distance' or 'bearing' fields, you have to supply 'my_location' parameter."); + $parts = explode('|', $tmp); + if (count($parts) != 2) + throw new InvalidParam('my_location', "Expecting 2 pipe-separated parts, got ".count($parts)."."); + foreach ($parts as &$part_ref) + { + if (!preg_match("/^-?[0-9]+(\.?[0-9]*)$/", $part_ref)) + throw new InvalidParam('my_location', "'$part_ref' is not a valid float number."); + $part_ref = floatval($part_ref); + } + list($center_lat, $center_lon) = $parts; + if ($center_lat > 90 || $center_lat < -90) + throw new InvalidParam('current_position', "Latitudes have to be within -90..90 range."); + if ($center_lon > 180 || $center_lon < -180) + throw new InvalidParam('current_position', "Longitudes have to be within -180..180 range."); + } + + if (Settings::get('OC_BRANCH') == 'oc.de') + { + # DE branch: + # - Caches do not have ratings. + # - Total numbers of founds and notfounds are kept in the "stat_caches" table. + # - search_time and way_length are both round trip values and cannot be null; + # 0 = not specified + # - will-attend-count is stored in separate field + + $rs = Db::query(" + select + c.cache_id, c.name, c.longitude, c.latitude, c.listing_last_modified as last_modified, + c.date_created, c.type, c.status, c.date_hidden, c.size, c.difficulty, + c.terrain, c.wp_oc, c.wp_gc, c.logpw, c.user_id, + if(c.search_time=0, null, c.search_time) as trip_time, + if(c.way_length=0, null, c.way_length) as trip_distance, + + ifnull(sc.toprating, 0) as topratings, + ifnull(sc.found, 0) as founds, + ifnull(sc.notfound, 0) as notfounds, + ifnull(sc.will_attend, 0) as willattends, + sc.last_found, + 0 as votes, 0 as score + -- SEE ALSO OC.PL BRANCH BELOW + from + caches c + left join stat_caches as sc on c.cache_id = sc.cache_id + where + wp_oc in ('".implode("','", array_map('mysql_real_escape_string', $cache_codes))."') + and status in (1,2,3) + "); + } + elseif (Settings::get('OC_BRANCH') == 'oc.pl') + { + # PL branch: + # - Caches have ratings. + # - Total numbers of found and notfounds are kept in the "caches" table. + # - search_time is round trip and way_length one way or both ways (this is different on OCDE!); + # both can be null; 0 or null = not specified + # - will-attend-count is stored in caches.notfounds + + $rs = Db::query(" + select + c.cache_id, c.name, c.longitude, c.latitude, c.last_modified, + c.date_created, c.type, c.status, c.date_hidden, c.size, c.difficulty, + c.terrain, c.wp_oc, c.wp_gc, c.logpw, c.user_id, + if(c.search_time=0, null, c.search_time) as trip_time, + if(c.way_length=0, null, c.way_length) as trip_distance, + + c.topratings, + c.founds, + c.notfounds, + c.notfounds as willattends, + c.last_found, + c.votes, c.score + -- SEE ALSO OC.DE BRANCH ABOVE + from + caches c + where + wp_oc in ('".implode("','", array_map('mysql_real_escape_string', $cache_codes))."') + and c.status in (1,2,3) + "); + } + + $results = new ArrayObject(); + $cacheid2wptcode = array(); + $owner_ids = array(); + while ($row = mysql_fetch_assoc($rs)) + { + $entry = array(); + $cacheid2wptcode[$row['cache_id']] = $row['wp_oc']; + foreach ($fields as $field) + { + switch ($field) + { + case 'code': $entry['code'] = $row['wp_oc']; break; + case 'gc_code': + // OC software allows entering anything here, and that's what users do. + // We do a formal verification so that only a valid GC code is returned: + if (preg_match('/^\s*[Gg][Cc][A-Za-z0-9]+\s*$/', $row['wp_gc'])) + $entry['gc_code'] = strtoupper(trim($row['wp_gc'])); + else + $entry['gc_code'] = null; + break; + case 'name': $entry['name'] = $row['name']; break; + case 'names': $entry['names'] = array(Settings::get('SITELANG') => $row['name']); break; // for the future + case 'location': $entry['location'] = round($row['latitude'], 6)."|".round($row['longitude'], 6); break; + case 'type': $entry['type'] = Okapi::cache_type_id2name($row['type']); break; + case 'status': $entry['status'] = Okapi::cache_status_id2name($row['status']); break; + case 'url': $entry['url'] = Settings::get('SITE_URL')."viewcache.php?wp=".$row['wp_oc']; break; + case 'owner': + $owner_ids[$row['wp_oc']] = $row['user_id']; + /* continued later */ + break; + case 'distance': + $entry['distance'] = (int)Okapi::get_distance($center_lat, $center_lon, $row['latitude'], $row['longitude']); + break; + case 'bearing': + $tmp = Okapi::get_bearing($center_lat, $center_lon, $row['latitude'], $row['longitude']); + $entry['bearing'] = ($tmp !== null) ? ((int)(10*$tmp)) / 10.0 : null; + break; + case 'bearing2': + $tmp = Okapi::get_bearing($center_lat, $center_lon, $row['latitude'], $row['longitude']); + $entry['bearing2'] = Okapi::bearing_as_two_letters($tmp); + break; + case 'bearing3': + $tmp = Okapi::get_bearing($center_lat, $center_lon, $row['latitude'], $row['longitude']); + $entry['bearing3'] = Okapi::bearing_as_three_letters($tmp); + break; + case 'is_found': /* handled separately */ break; + case 'is_not_found': /* handled separately */ break; + case 'is_watched': /* handled separately */ break; + case 'is_ignored': /* handled separately */ break; + case 'founds': $entry['founds'] = $row['founds'] + 0; break; + case 'notfounds': + if ($row['type'] != 6) { # non-event + $entry['notfounds'] = $row['notfounds'] + 0; + } else { # event + $entry['notfounds'] = 0; + } + break; + case 'willattends': + if ($row['type'] == 6) { # event + $entry['willattends'] = $row['willattends'] + 0; + } else { # non-event + $entry['willattends'] = 0; + } + break; + case 'size': + # Deprecated. Leave it for backward-compatibility. See issue 155. + switch (Okapi::cache_sizeid_to_size2($row['size'])) + { + case 'none': $entry['size'] = null; break; + case 'nano': $entry['size'] = 1.0; break; # same as micro + case 'micro': $entry['size'] = 1.0; break; + case 'small': $entry['size'] = 2.0; break; + case 'regular': $entry['size'] = 3.0; break; + case 'large': $entry['size'] = 4.0; break; + case 'xlarge': $entry['size'] = 5.0; break; + case 'other': $entry['size'] = null; break; # same as none + default: throw new Exception(); + } + break; + case 'size2': $entry['size2'] = Okapi::cache_sizeid_to_size2($row['size']); break; + case 'oxsize': $entry['oxsize'] = Okapi::cache_size2_to_oxsize(Okapi::cache_sizeid_to_size2($row['size'])); break; + case 'difficulty': $entry['difficulty'] = round($row['difficulty'] / 2.0, 1); break; + case 'terrain': $entry['terrain'] = round($row['terrain'] / 2.0, 1); break; + case 'trip_time': + # search time is entered in hours:minutes and converted to decimal hours, + # which can produce lots of unneeded decimal places; 2 of them are sufficient here + $entry['trip_time'] = $row['trip_time'] === null ? null : round($row['trip_time'],2); break; + break; + case 'trip_distance': + # way length is entered in km as decimal fraction, but number conversions can + # create fake digits which should be stripped; meter precision is sufficient here + $entry['trip_distance'] = $row['trip_distance'] === null ? null : round($row['trip_distance'],3); break; + break; + case 'rating': + if ($row['votes'] < 3) $entry['rating'] = null; + elseif ($row['score'] >= 2.2) $entry['rating'] = 5.0; + elseif ($row['score'] >= 1.4) $entry['rating'] = 4.0; + elseif ($row['score'] >= 0.1) $entry['rating'] = 3.0; + elseif ($row['score'] >= -1.0) $entry['rating'] = 2.0; + else $entry['rating'] = 1.0; + break; + case 'rating_votes': $entry['rating_votes'] = $row['votes'] + 0; break; + case 'recommendations': $entry['recommendations'] = $row['topratings'] + 0; break; + case 'req_passwd': $entry['req_passwd'] = $row['logpw'] ? true : false; break; + case 'short_description': /* handled separately */ break; + case 'short_descriptions': /* handled separately */ break; + case 'description': /* handled separately */ break; + case 'descriptions': /* handled separately */ break; + case 'hint': /* handled separately */ break; + case 'hints': /* handled separately */ break; + case 'hint2': /* handled separately */ break; + case 'hints2': /* handled separately */ break; + case 'images': /* handled separately */ break; + case 'preview_image': /* handled separately */ break; + case 'attr_acodes': /* handled separately */ break; + case 'attrnames': /* handled separately */ break; + case 'latest_logs': /* handled separately */ break; + case 'my_notes': /* handles separately */ break; + case 'trackables_count': /* handled separately */ break; + case 'trackables': /* handled separately */ break; + case 'alt_wpts': /* handled separately */ break; + case 'country': /* handled separately */ break; + case 'state': /* handled separately */ break; + case 'last_found': $entry['last_found'] = ($row['last_found'] > '1980') ? date('c', strtotime($row['last_found'])) : null; break; + case 'last_modified': $entry['last_modified'] = date('c', strtotime($row['last_modified'])); break; + case 'date_created': $entry['date_created'] = date('c', strtotime($row['date_created'])); break; + case 'date_hidden': $entry['date_hidden'] = date('c', strtotime($row['date_hidden'])); break; + case 'internal_id': $entry['internal_id'] = $row['cache_id']; break; + case 'attribution_note': /* handled separately */ break; + case 'protection_areas': /* handled separately */ break; + default: throw new Exception("Missing field case: ".$field); + } + } + $results[$row['wp_oc']] = $entry; + } + mysql_free_result($rs); + + # owner + + if (in_array('owner', $fields) && (count($results) > 0)) + { + $rs = Db::query(" + select user_id, uuid, username + from user + where user_id in ('".implode("','", array_map('mysql_real_escape_string', array_values($owner_ids)))."') + "); + $tmp = array(); + while ($row = mysql_fetch_assoc($rs)) + $tmp[$row['user_id']] = $row; + foreach ($results as $cache_code => &$result_ref) + { + $row = $tmp[$owner_ids[$cache_code]]; + $result_ref['owner'] = array( + 'uuid' => $row['uuid'], + 'username' => $row['username'], + 'profile_url' => Settings::get('SITE_URL')."viewprofile.php?userid=".$row['user_id'] + ); + } + } + + # is_found + + if (in_array('is_found', $fields)) + { + if ($user_id == null) + throw new BadRequest("Either 'user_uuid' parameter OR Level 3 Authentication is required to access 'is_found' field."); + $tmp = Db::select_column(" + select c.wp_oc + from + caches c, + cache_logs cl + where + c.cache_id = cl.cache_id + and cl.type in ( + '".mysql_real_escape_string(Okapi::logtypename2id("Found it"))."', + '".mysql_real_escape_string(Okapi::logtypename2id("Attended"))."' + ) + and cl.user_id = '".mysql_real_escape_string($user_id)."' + ".((Settings::get('OC_BRANCH') == 'oc.pl') ? "and cl.deleted = 0" : "")." + "); + $tmp2 = array(); + foreach ($tmp as $cache_code) + $tmp2[$cache_code] = true; + foreach ($results as $cache_code => &$result_ref) + $result_ref['is_found'] = isset($tmp2[$cache_code]); + } + + # is_not_found + + if (in_array('is_not_found', $fields)) + { + if ($user_id == null) + throw new BadRequest("Either 'user_uuid' parameter OR Level 3 Authentication is required to access 'is_not_found' field."); + $tmp = Db::select_column(" + select c.wp_oc + from + caches c, + cache_logs cl + where + c.cache_id = cl.cache_id + and cl.type = '".mysql_real_escape_string(Okapi::logtypename2id("Didn't find it"))."' + and cl.user_id = '".mysql_real_escape_string($user_id)."' + ".((Settings::get('OC_BRANCH') == 'oc.pl') ? "and cl.deleted = 0" : "")." + "); + $tmp2 = array(); + foreach ($tmp as $cache_code) + $tmp2[$cache_code] = true; + foreach ($results as $cache_code => &$result_ref) + $result_ref['is_not_found'] = isset($tmp2[$cache_code]); + } + + # is_watched + + if (in_array('is_watched', $fields)) + { + if ($request->token == null) + throw new BadRequest("Level 3 Authentication is required to access 'is_watched' field."); + $tmp = Db::select_column(" + select c.wp_oc + from + caches c, + cache_watches cw + where + c.cache_id = cw.cache_id + and cw.user_id = '".mysql_real_escape_string($request->token->user_id)."' + "); + $tmp2 = array(); + foreach ($tmp as $cache_code) + $tmp2[$cache_code] = true; + foreach ($results as $cache_code => &$result_ref) + $result_ref['is_watched'] = isset($tmp2[$cache_code]); + } + + # is_ignored + + if (in_array('is_ignored', $fields)) + { + if ($request->token == null) + throw new BadRequest("Level 3 Authentication is required to access 'is_ignored' field."); + $tmp = Db::select_column(" + select c.wp_oc + from + caches c, + cache_ignore ci + where + c.cache_id = ci.cache_id + and ci.user_id = '".mysql_real_escape_string($request->token->user_id)."' + "); + $tmp2 = array(); + foreach ($tmp as $cache_code) + $tmp2[$cache_code] = true; + foreach ($results as $cache_code => &$result_ref) + $result_ref['is_ignored'] = isset($tmp2[$cache_code]); + } + + # Descriptions and hints. + + if (in_array('description', $fields) || in_array('descriptions', $fields) + || in_array('short_description', $fields) || in_array('short_descriptions', $fields) + || in_array('hint', $fields) || in_array('hints', $fields) + || in_array('hint2', $fields) || in_array('hints2', $fields)) + { + # At first, we will fill all those fields, even if user requested just one + # of them. We will chop off the unwanted ones at the end. + + foreach ($results as &$result_ref) + { + $result_ref['short_descriptions'] = new ArrayObject(); + $result_ref['descriptions'] = new ArrayObject(); + $result_ref['hints'] = new ArrayObject(); + $result_ref['hints2'] = new ArrayObject(); + } + + # Get cache descriptions and hints. + + $rs = Db::query(" + select cache_id, language, `desc`, short_desc, hint + from cache_desc + where cache_id in ('".implode("','", array_map('mysql_real_escape_string', array_keys($cacheid2wptcode)))."') + "); + while ($row = mysql_fetch_assoc($rs)) + { + $cache_code = $cacheid2wptcode[$row['cache_id']]; + // strtolower - ISO 639-1 codes are lowercase + if ($row['desc']) + { + /* Note, that the "owner" and "internal_id" fields are automatically included, + * whenever the cache description is included. */ + + $tmp = Okapi::fix_oc_html($row['desc']); + + if ($attribution_append != 'none') + { + $tmp .= "\n

    ". + self::get_cache_attribution_note( + $row['cache_id'], strtolower($row['language']), $langpref, + $results[$cache_code]['owner'], $attribution_append + ). + "

    "; + } + $results[$cache_code]['descriptions'][strtolower($row['language'])] = $tmp; + } + if ($row['short_desc']) + { + $results[$cache_code]['short_descriptions'][strtolower($row['language'])] = $row['short_desc']; + } + if ($row['hint']) + { + $results[$cache_code]['hints'][strtolower($row['language'])] = $row['hint']; + $results[$cache_code]['hints2'][strtolower($row['language'])] + = htmlspecialchars_decode(mb_ereg_replace("
    ", "" , $row['hint']), ENT_COMPAT); + } + } + foreach ($results as &$result_ref) + { + $result_ref['short_description'] = Okapi::pick_best_language($result_ref['short_descriptions'], $langpref); + $result_ref['description'] = Okapi::pick_best_language($result_ref['descriptions'], $langpref); + $result_ref['hint'] = Okapi::pick_best_language($result_ref['hints'], $langpref); + $result_ref['hint2'] = Okapi::pick_best_language($result_ref['hints2'], $langpref); + } + + # Remove unwanted fields. + + foreach (array( + 'short_description', 'short_descriptions', 'description', 'descriptions', + 'hint', 'hints', 'hint2', 'hints2' + ) as $field) + if (!in_array($field, $fields)) + foreach ($results as &$result_ref) + unset($result_ref[$field]); + } + + # Images. + + if (in_array('images', $fields) || in_array('preview_image', $fields)) + { + if (in_array('images', $fields)) + foreach ($results as &$result_ref) + $result_ref['images'] = array(); + if (in_array('preview_image', $fields)) + foreach ($results as &$result_ref) + $result_ref['preview_image'] = null; + + if (Db::field_exists('pictures', 'mappreview')) + $preview_field = "mappreview"; + else + $preview_field = "0"; + $rs = Db::query(" + select object_id, uuid, url, title, spoiler, ".$preview_field." as preview + from pictures + where + object_id in ('".implode("','", array_map('mysql_real_escape_string', array_keys($cacheid2wptcode)))."') + and display = 1 + and object_type = 2 + and unknown_format = 0 + order by object_id, date_created + "); + $prev_cache_code = null; + while ($row = mysql_fetch_assoc($rs)) + { + $cache_code = $cacheid2wptcode[$row['object_id']]; + if ($cache_code != $prev_cache_code) + { + # Group images together. Images must have unique captions within one cache. + self::reset_unique_captions(); + $prev_cache_code = $cache_code; + } + $image = array( + 'uuid' => $row['uuid'], + 'url' => $row['url'], + 'thumb_url' => Settings::get('SITE_URL') . 'thumbs.php?uuid=' . $row['uuid'], + 'caption' => $row['title'], + 'unique_caption' => self::get_unique_caption($row['title']), + 'is_spoiler' => ($row['spoiler'] ? true : false), + ); + if (in_array('images', $fields)) + $results[$cache_code]['images'][] = $image; + if ($row['preview'] != 0 && in_array('preview_image', $fields)) + $results[$cache_code]['preview_image'] = $image; + } + } + + # A-codes and attrnames + + if (in_array('attr_acodes', $fields) || in_array('attrnames', $fields)) + { + # Either case, we'll need acodes. If the user didn't want them, + # remember to remove them later. + + if (!in_array('attr_acodes', $fields)) + { + $fields_to_remove_later[] = 'attr_acodes'; + } + foreach ($results as &$result_ref) + $result_ref['attr_acodes'] = array(); + + # Load internal_attr_id => acode mapping. + + require_once($GLOBALS['rootpath'].'okapi/services/attrs/attr_helper.inc.php'); + $internal2acode = AttrHelper::get_internal_id_to_acode_mapping(); + + $rs = Db::query(" + select cache_id, attrib_id + from caches_attributes + where cache_id in ('".implode("','", array_map('mysql_real_escape_string', array_keys($cacheid2wptcode)))."') + "); + while ($row = mysql_fetch_assoc($rs)) + { + $cache_code = $cacheid2wptcode[$row['cache_id']]; + $attr_internal_id = $row['attrib_id']; + if (!isset($internal2acode[$attr_internal_id])) + { + # Unknown attribute. Ignore. + continue; + } + $results[$cache_code]['attr_acodes'][] = $internal2acode[$attr_internal_id]; + } + + # Now, each cache object has a list of its acodes. We can get + # the attrnames now. + + if (in_array('attrnames', $fields)) + { + $acode2bestname = AttrHelper::get_acode_to_name_mapping($langpref); + foreach ($results as &$result_ref) + { + $result_ref['attrnames'] = array(); + foreach ($result_ref['attr_acodes'] as $acode) + $result_ref['attrnames'][] = $acode2bestname[$acode]; + } + } + } + + # Latest log entries. + + if (in_array('latest_logs', $fields)) + { + foreach ($results as &$result_ref) + $result_ref['latest_logs'] = array(); + + # Get all log IDs with dates. Sort in groups. Filter out latest ones. This is the fastest + # technique I could think of... + + $rs = Db::query(" + select cache_id, uuid, date + from cache_logs + where + cache_id in ('".implode("','", array_map('mysql_real_escape_string', array_keys($cacheid2wptcode)))."') + and ".((Settings::get('OC_BRANCH') == 'oc.pl') ? "deleted = 0" : "true")." + order by cache_id, date desc, date_created desc + "); + $loguuids = array(); + $log2cache_map = array(); + if ($lpc !== null) + { + # User wants some of the latest logs. + $tmp = array(); + while ($row = mysql_fetch_assoc($rs)) + $tmp[$row['cache_id']][] = $row; + foreach ($tmp as $cache_key => &$rowslist_ref) + { + usort($rowslist_ref, function($rowa, $rowb) { + # (reverse order by date) + return ($rowa['date'] < $rowb['date']) ? 1 : (($rowa['date'] == $rowb['date']) ? 0 : -1); + }); + for ($i = 0; $i < min(count($rowslist_ref), $lpc); $i++) + { + $loguuids[] = $rowslist_ref[$i]['uuid']; + $log2cache_map[$rowslist_ref[$i]['uuid']] = $cacheid2wptcode[$rowslist_ref[$i]['cache_id']]; + } + } + } + else + { + # User wants ALL logs. + while ($row = mysql_fetch_assoc($rs)) + { + $loguuids[] = $row['uuid']; + $log2cache_map[$row['uuid']] = $cacheid2wptcode[$row['cache_id']]; + } + } + + # We need to retrieve logs/entry for each of the $logids. We do this in groups + # (there is a limit for log uuids passed to logs/entries method). + + try + { + foreach (Okapi::make_groups($loguuids, 500) as $subset) + { + $entries = OkapiServiceRunner::call( + "services/logs/entries", + new OkapiInternalRequest( + $request->consumer, $request->token, array( + 'log_uuids' => implode("|", $subset), + 'fields' => $log_fields + ) + ) + ); + foreach ($subset as $log_uuid) + { + if ($entries[$log_uuid]) + $results[$log2cache_map[$log_uuid]]['latest_logs'][] = $entries[$log_uuid]; + } + } + } + catch (Exception $e) + { + if (($e instanceof InvalidParam) && ($e->paramName == 'fields')) + { + throw new InvalidParam('log_fields', $e->whats_wrong_about_it); + } + else + { + /* Something is wrong with OUR code. */ + throw new Exception($e); + } + } + } + + # My notes + + if (in_array('my_notes', $fields)) + { + if ($request->token == null) + throw new BadRequest("Level 3 Authentication is required to access 'my_notes' field."); + foreach ($results as &$result_ref) + $result_ref['my_notes'] = null; + if (Settings::get('OC_BRANCH') == 'oc.pl') + { + # OCPL uses cache_notes table to store notes. + + $rs = Db::query(" + select cache_id, max(date) as date, group_concat(`desc`) as `desc` + from cache_notes + where + cache_id in ('".implode("','", array_map('mysql_real_escape_string', array_keys($cacheid2wptcode)))."') + and user_id = '".mysql_real_escape_string($request->token->user_id)."' + group by cache_id + "); + } + else + { + # OCDE uses coordinates table (with type == 2) to store notes (this is somewhat weird). + + $rs = Db::query(" + select cache_id, null as date, group_concat(description) as `desc` + from coordinates + where + type = 2 -- personal note + and cache_id in ('".implode("','", array_map('mysql_real_escape_string', array_keys($cacheid2wptcode)))."') + and user_id = '".mysql_real_escape_string($request->token->user_id)."' + group by cache_id + "); + } + while ($row = mysql_fetch_assoc($rs)) + { + # This one is plain-text. We may add my_notes_html for those who want it in HTML. + $results[$cacheid2wptcode[$row['cache_id']]]['my_notes'] = strip_tags($row['desc']); + } + } + + if (in_array('trackables', $fields)) + { + # Currently we support Geokrety only. But this interface should remain + # compatible. In future, other trackables might be returned the same way. + + $rs = Db::query(" + select + gkiw.wp as cache_code, + gki.id as gk_id, + gki.name + from + gk_item_waypoint gkiw, + gk_item gki + where + gkiw.id = gki.id + and gkiw.wp in ('".implode("','", array_map('mysql_real_escape_string', $cache_codes))."') + "); + $trs = array(); + while ($row = mysql_fetch_assoc($rs)) + $trs[$row['cache_code']][] = $row; + foreach ($results as $cache_code => &$result_ref) + { + $result_ref['trackables'] = array(); + if (!isset($trs[$cache_code])) + continue; + foreach ($trs[$cache_code] as $t) + { + $result_ref['trackables'][] = array( + 'code' => 'GK'.str_pad(strtoupper(dechex($t['gk_id'])), 4, "0", STR_PAD_LEFT), + 'name' => $t['name'], + 'url' => 'http://geokrety.org/konkret.php?id='.$t['gk_id'] + ); + } + } + unset($trs); + } + if (in_array('trackables_count', $fields)) + { + if (in_array('trackables', $fields)) + { + # We already got all trackables data, no need to query database again. + foreach ($results as $cache_code => &$result_ref) + $result_ref['trackables_count'] = count($result_ref['trackables']); + } + else + { + $rs = Db::query(" + select wp as cache_code, count(*) as count + from gk_item_waypoint + where wp in ('".implode("','", array_map('mysql_real_escape_string', $cache_codes))."') + group by wp + "); + $tr_counts = new ArrayObject(); + while ($row = mysql_fetch_assoc($rs)) + $tr_counts[$row['cache_code']] = $row['count']; + foreach ($results as $cache_code => &$result_ref) + { + if (isset($tr_counts[$cache_code])) + $result_ref['trackables_count'] = $tr_counts[$cache_code] + 0; + else + $result_ref['trackables_count'] = 0; + } + unset($tr_counts); + } + } + + # Alternate/Additional waypoints. + + if (in_array('alt_wpts', $fields)) + { + $internal_wpt_type_id2names = array(); + if (Settings::get('OC_BRANCH') == 'oc.de') + { + $rs = Db::query(" + select + ct.id, + LOWER(stt.lang) as language, + stt.`text` + from + coordinates_type ct + left join sys_trans_text stt on stt.trans_id = ct.trans_id + "); + while ($row = mysql_fetch_assoc($rs)) + $internal_wpt_type_id2names[$row['id']][$row['language']] = $row['text']; + mysql_free_result($rs); + } + else + { + $rs = Db::query(" + select id, pl, en + from waypoint_type + where id > 0 + "); + while ($row = mysql_fetch_assoc($rs)) + { + $internal_wpt_type_id2names[$row['id']]['pl'] = $row['pl']; + $internal_wpt_type_id2names[$row['id']]['en'] = $row['en']; + } + } + + foreach ($results as &$result_ref) + $result_ref['alt_wpts'] = array(); + $cache_codes_escaped_and_imploded = "'".implode("','", array_map('mysql_real_escape_string', array_keys($cacheid2wptcode)))."'"; + + if (Settings::get('OC_BRANCH') == 'oc.pl') + { + # OCPL uses 'waypoints' table to store additional waypoints and defines + # waypoint types in 'waypoint_type' table. + # OCPL also have a special 'status' field to denote a hidden waypoint + # (i.e. final location of a multicache). Such hidden waypoints are not + # exposed by OKAPI. + + $cacheid2waypoints = Db::select_group_by("cache_id", " + select + cache_id, stage, latitude, longitude, `desc`, + type as internal_type_id, + case type + when 3 then 'Flag, Red' + when 4 then 'Circle with X' + when 5 then 'Parking Area' + else 'Flag, Green' + end as sym, + case type + when 1 then 'physical-stage' + when 2 then 'virtual-stage' + when 3 then 'final' + when 4 then 'poi' + when 5 then 'parking' + else 'other' + end as okapi_type + from waypoints + where + cache_id in (".$cache_codes_escaped_and_imploded.") + and status = 1 + order by cache_id, stage, `desc` + "); + } + else + { + # OCDE uses 'coordinates' table (with type=1) to store additional waypoints + # and defines waypoint types in 'coordinates_type' table. + # All additional waypoints are public. + + $cacheid2waypoints = Db::select_group_by("cache_id", " + select + cache_id, + false as stage, + latitude, longitude, + description as `desc`, + subtype as internal_type_id, + case subtype + when 1 then 'Parking Area' + when 3 then 'Flag, Blue' + when 4 then 'Circle with X' + when 5 then 'Diamond, Green' + else 'Flag, Green' + end as sym, + case subtype + when 1 then 'parking' + when 2 then 'stage' + when 3 then 'path' + when 4 then 'final' + when 5 then 'poi' + else 'other' + end as okapi_type + from coordinates + where + type = 1 + and cache_id in (".$cache_codes_escaped_and_imploded.") + order by cache_id, id + "); + } + + foreach ($cacheid2waypoints as $cache_id => $waypoints) + { + $cache_code = $cacheid2wptcode[$cache_id]; + $wpt_format = $cache_code."-%0".strlen(count($waypoints))."d"; + $index = 0; + foreach ($waypoints as $row) + { + if (!isset($internal_wpt_type_id2names[$row['internal_type_id']])) + { + # Sanity check. Waypoints of undefined type won't be accessible via OKAPI. + # See issue 219. + continue; + } + $index++; + $results[$cache_code]['alt_wpts'][] = array( + 'name' => sprintf($wpt_format, $index), + 'location' => round($row['latitude'], 6)."|".round($row['longitude'], 6), + 'type' => $row['okapi_type'], + 'type_name' => Okapi::pick_best_language($internal_wpt_type_id2names[$row['internal_type_id']], $langpref), + 'sym' => $row['sym'], + 'description' => ($row['stage'] ? _("Stage")." ".$row['stage'].": " : "").$row['desc'], + ); + } + } + + # Issue #298 - User coordinates implemented in oc.pl + # Issue #305 - User coordinates implemented in oc.de + if ($request->token != null) + { + # Query DB for user provided coordinates + if (Settings::get('OC_BRANCH') == 'oc.pl') + { + $cacheid2user_coords = Db::select_group_by('cache_id', " + select + cache_id, longitude, latitude + from cache_mod_cords + where + cache_id in ($cache_codes_escaped_and_imploded) + and user_id = '".mysql_real_escape_string($request->token->user_id)."' + "); + } else { + # oc.de + $cacheid2user_coords = Db::select_group_by('cache_id', " + select + cache_id, longitude, latitude + from coordinates + where + cache_id in ($cache_codes_escaped_and_imploded) + and user_id = '".mysql_real_escape_string($request->token->user_id)."' + and type = 2 + and longitude != 0 + and latitude != 0 + "); + } + foreach ($cacheid2user_coords as $cache_id => $waypoints) + { + $cache_code = $cacheid2wptcode[$cache_id]; + foreach ($waypoints as $row) + { + # there should be only one user waypoint per cache... + $results[$cache_code]['alt_wpts'][] = array( + 'name' => $cache_code.'-USER-COORDS', + 'location' => round($row['latitude'], 6)."|".round($row['longitude'], 6), + 'type' => 'user-coords', + 'type_name' => _("User location"), + 'sym' => 'Block, Green', + 'description' => sprintf( + _("Your own custom coordinates for the %s geocache"), + $cache_code + ), + ); + } + } + } + } + + # Country and/or state. + + if (in_array('country', $fields) || in_array('state', $fields)) + { + $countries = array(); + $states = array(); + + if (Settings::get('OC_BRANCH') == 'oc.de') + { + # OCDE: + # - cache_location entries are created by a cronjob *after* listing the + # caches and may not yet exist. + # - The state is in adm2 field. + # - caches.country overrides cache_location.code1/adm1. If both differ, + # cache_location.adm2 to adm4 is invalid and the state unknown. + # - OCDE databases may contain caches with invalid country code. + # Such errors must be handled gracefully. + # - adm1 should always be ignored. Instead, code1 should be translated + # into a country name, depending on langpref. + + # build country code translation table + $rs = Db::query(" + select distinct + c.country, + lower(stt.lang) as language, + stt.`text` + from + caches c + inner join countries on countries.short=c.country + inner join sys_trans_text stt on stt.trans_id = countries.trans_id + where + c.wp_oc in ('".implode("','", array_map('mysql_real_escape_string', $cache_codes))."') + "); + $country_codes2names = array(); + while ($row = mysql_fetch_assoc($rs)) + $country_codes2names[$row['country']][$row['language']] = $row['text']; + mysql_free_result($rs); + + # get geocache countries and states + $rs = Db::query(" + select + c.wp_oc as cache_code, + c.country as country_code, + ifnull(if(c.country<>cl.code1,'',cl.adm2),'') as state + from + caches c + left join cache_location cl on c.cache_id = cl.cache_id + where + c.wp_oc in ('".implode("','", array_map('mysql_real_escape_string', $cache_codes))."') + "); + while ($row = mysql_fetch_assoc($rs)) + { + if (!isset($country_codes2names[$row['country_code']])) + $countries[$row['cache_code']] = ''; + else + $countries[$row['cache_code']] = Okapi::pick_best_language($country_codes2names[$row['country_code']], $langpref); + $states[$row['cache_code']] = $row['state']; + } + mysql_free_result($rs); + } + else + { + # OCPL: + # - cache_location data is entered by the user. + # - The state is in adm3 field. + + # get geocache countries and states + $rs = Db::query(" + select + c.wp_oc as cache_code, + cl.adm1 as country, + cl.adm3 as state + from + caches c, + cache_location cl + where + c.wp_oc in ('".implode("','", array_map('mysql_real_escape_string', $cache_codes))."') + and c.cache_id = cl.cache_id + "); + while ($row = mysql_fetch_assoc($rs)) + { + $countries[$row['cache_code']] = $row['country']; + $states[$row['cache_code']] = $row['state']; + } + mysql_free_result($rs); + } + + if (in_array('country', $fields)) + { + foreach ($results as $cache_code => &$row_ref) + $row_ref['country'] = isset($countries[$cache_code]) ? $countries[$cache_code] : null; + } + if (in_array('state', $fields)) + { + foreach ($results as $cache_code => &$row_ref) + $row_ref['state'] = isset($states[$cache_code]) ? $states[$cache_code] : null; + } + unset($countries); + unset($states); + } + + # Attribution note + + if (in_array('attribution_note', $fields)) + { + /* Note, that the "owner" and "internal_id" fields are automatically included, + * whenever the attribution_note is included. */ + + foreach ($results as $cache_code => &$result_ref) + $result_ref['attribution_note'] = + self::get_cache_attribution_note( + $result_ref['internal_id'], $langpref[0], $langpref, + $results[$cache_code]['owner'], 'full' + ); + } + + # Protection areas + + if (in_array('protection_areas', $fields)) + { + $cache_ids_escaped_and_imploded = "'".implode("','", array_map('mysql_real_escape_string', array_keys($cacheid2wptcode)))."'"; + + if (Settings::get('OC_BRANCH') == 'oc.de') + { + $rs = Db::query(" + select + c.wp_oc as cache_code, + npa_types.name as type, + npa_areas.name as name + from + caches c + inner join cache_npa_areas on cache_npa_areas.cache_id=c.cache_id + inner join npa_areas on cache_npa_areas.npa_id = npa_areas.id + inner join npa_types on npa_areas.type_id = npa_types.id + where + c.cache_id in (".$cache_ids_escaped_and_imploded.") + group by npa_areas.type_id, npa_areas.name + order by npa_types.ordinal + "); + } + else if (Settings::get('ORIGIN_URL') == 'http://opencaching.pl/' || + Settings::get('ORIGIN_URL') == 'http://www.opencaching.nl/') + { + # Current OCPL table definitions use collation 'latin1' for parkipl + # and 'utf8' for np_areas. Union needs identical collations. + # To be sure, we convert both to utf8. + $rs = Db::query(" + select + c.wp_oc as cache_code, + '"._('National Park / Landscape')."' as type, + CONVERT(parkipl.name USING utf8) as name + from + caches c + inner join cache_npa_areas on cache_npa_areas.cache_id=c.cache_id + inner join parkipl on cache_npa_areas.parki_id=parkipl.id + where + c.cache_id in (".$cache_ids_escaped_and_imploded.") + and cache_npa_areas.parki_id != 0 + union + select + c.wp_oc as cache_code, + 'Natura 2000' as type, + CONVERT(npa_areas.sitename USING utf8) as name + from + caches c + inner join cache_npa_areas on cache_npa_areas.cache_id=c.cache_id + inner join npa_areas on cache_npa_areas.npa_id=npa_areas.id + where + c.cache_id in (".$cache_ids_escaped_and_imploded.") + and cache_npa_areas.npa_id != 0 + "); + } + else + { + # OC.US and .UK do not have a 'parkipl' table. + # OC.US has a 'us_parks' table instead. + # Natura 2000 is Europe-only. + $rs = null; + } + + foreach ($results as &$result_ref) + $result_ref['protection_areas'] = array(); + if ($rs) + { + while ($row = mysql_fetch_assoc($rs)) + { + $results[$row['cache_code']]['protection_areas'][] = array( + 'type' => $row['type'], + 'name' => $row['name'], + ); + } + mysql_free_result($rs); + } + } + + # Check which cache codes were not found and mark them with null. + foreach ($cache_codes as $cache_code) + if (!isset($results[$cache_code])) + $results[$cache_code] = null; + + if (count($fields_to_remove_later) > 0) + { + # Some of the fields in $results were added only temporarily + # (the Consumer did not ask for them). We will remove these fields now. + + foreach ($results as &$result_ref) + foreach ($fields_to_remove_later as $field) + unset($result_ref[$field]); + } + + # Order the results in the same order as the input codes were given. + # This might come in handy for languages which support ordered dictionaries + # (especially with conjunction with the search_and_retrieve method). + # See issue#97. PHP dictionaries (assoc arrays) are ordered structures, + # so we just have to rewrite it (sequentially). + + $ordered_results = new ArrayObject(); + foreach ($cache_codes as $cache_code) + $ordered_results[$cache_code] = $results[$cache_code]; + + /* Handle OCPL's "access logs" feature. */ + + if ( + (Settings::get('OC_BRANCH') == 'oc.pl') + && Settings::get('OCPL_ENABLE_GEOCACHE_ACCESS_LOGS') + ) { + $cache_ids = array_keys($cacheid2wptcode); + + /* Log this event only if some specific fields were accessed. */ + + if ( + in_array('location', $fields) + && (count(array_intersect(array( + 'hint', 'hints', 'hint2', 'hints2', + 'description', 'descriptions' + ), $fields)) > 0) + ) { + require_once($GLOBALS['rootpath'].'okapi/lib/ocpl_access_logs.php'); + \okapi\OCPLAccessLogs::log_geocache_access($request, $cache_ids); + } + } + + return Okapi::formatted_response($request, $ordered_results); + } + + /** + * Create unique caption, safe to be used as a file name for images + * uploaded into Garmin's GPS devices. Use reset_unique_captions to reset + * unique counter. + */ + private static function get_unique_caption($caption) + { + # Garmins keep hanging on long file names. We don't have any specification from + # Garmin and cannot determine WHY. That's why we won't use captions until we + # know more. + + $caption = self::$caption_no.""; + self::$caption_no++; + return $caption; + + /* This code is harmful for Garmins! + $caption = preg_replace('#[^\\pL\d ]+#u', '-', $caption); + $caption = trim($caption, '-'); + if (function_exists('iconv')) + { + $new = iconv("utf-8", "ASCII//TRANSLIT", $caption); + if (!$new) + $new = iconv("UTF-8", "ASCII//IGNORE", $caption); + } else { + $new = $caption; + } + $new = str_replace(array('/', '\\', '?', '%', '*', ':', '|', '"', '<', '>', '.'), '', $new); + $new = trim($new); + if ($new == "") + $new = "(no caption)"; + if (strlen($new) > 240) + $new = substr($new, 0, 240); + $new = self::$caption_no." - ".$new; + self::$caption_no++; + return $new;*/ + } + private static $caption_no = 1; + private static function reset_unique_captions() + { + self::$caption_no = 1; + } + + /** + * Return attribution note for the given geocache. + * + * The $lang parameter identifies the language of the cache description + * to which the attribution note will be appended to (one cache may + * have descriptions in multiple languages!). + * + * The $langpref parameter is *an array* of language preferences + * extracted from the langpref parameter passed to the method by the + * OKAPI Consumer. + * + * Both values ($lang and $langpref) will be taken into account when + * generating the attribution note, but $lang will have a higher + * priority than $langpref (we don't want to mix the languages in the + * descriptions if we don't have to). + * + * $owner is in object describing the user, it has the same format as + * defined in "geocache" method specs (see the "owner" field). + * + * The $type is either "full" or "static". Full attributions may contain + * dates and are not suitable for the replicate module. Static attributions + * don't change that frequently. + */ + public static function get_cache_attribution_note( + $cache_id, $lang, array $langpref, $owner, $type + ) { + $site_url = Settings::get('SITE_URL'); + $site_name = Okapi::get_normalized_site_name(); + $cache_url = $site_url."viewcache.php?cacheid=$cache_id"; + + Okapi::gettext_domain_init(array_merge(array($lang), $langpref)); + if (Settings::get('OC_BRANCH') == 'oc.pl') + { + # This does not vary on $type (yet). + + $note = sprintf( + _("This geocache description comes from the %s site."), + $cache_url, $site_url, $site_name + ); + } + else + { + # OC.de wants the tld in lowercase here + $site_name = ucfirst(strtolower($site_name)); + if ($type == 'full') + { + $note = sprintf( + _( + "© %s, %s, ". + "CC-BY-NC-ND, ". + "as of %s; all log entries © their authors" + ), + $owner['profile_url'], $owner['username'], $cache_url, $site_name, strftime('%x') + ); + } + elseif ($type == 'static') + { + $note = sprintf( + _( + "© %s, %s, ". + "CC-BY-NC-ND; ". + "all log entries © their authors" + ), + $owner['profile_url'], $owner['username'], $cache_url, $site_name + ); + } + } + + Okapi::gettext_domain_restore(); + + return $note; + } } diff --git a/htdocs/okapi/services/caches/geocaches.xml b/htdocs/okapi/services/caches/geocaches.xml index 6f830a74..a83632bb 100644 --- a/htdocs/okapi/services/caches/geocaches.xml +++ b/htdocs/okapi/services/caches/geocaches.xml @@ -1,51 +1,54 @@ - Retrieve information on multiple geocaches - 20 - -

    This method works like the services/caches/geocache method, but works - with multiple geocaches (instead of only one).

    -
    - -

    Pipe-separated list of cache cache codes. These represent the - geocaches you are interested in. No more than 500 codes are allowed. - Unlike in the "geocache" method, this CAN be an empty string (it will - result in an empty, but valid, response).

    -
    - -

    Pipe-separated list of ISO 639-1 language codes. This indicates the - order of preference in which language will be chosen for fields like - name and description.

    -

    Please note, that you may also access caches' descriptions in all - available languages. If you want to do this, you should use fields - names, descriptions (etc.) instead of name and - description (etc.).

    -
    - -

    Same as in the services/caches/geocache method. Pipe-separated list - of field names which you are interested with. - See services/caches/geocache method for a list available values.

    -
    - -

    Same as in the services/caches/geocache method.

    -
    - - Same as in the services/caches/geocache method. - - - Same as in the services/caches/geocache method. - - - Same as in the services/caches/geocache method. - - - -

    A dictionary. Cache codes you provide will be mapped to dictionary keys, - and each value will be a dictionary of fields you have selected.

    -

    For example, for geocaches?cache_codes=OP3D96|OC124&fields=type - query, the result might look something link this:

    -
    {"OP3D96": {"type": "Traditional"}, "OC124": null}
    -

    Value of null 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.)

    -
    + Retrieve information on multiple geocaches + 20 + +

    This method works like the services/caches/geocache method, but works + with multiple geocaches (instead of only one).

    +
    + +

    Pipe-separated list of cache cache codes. These represent the + geocaches you are interested in. No more than 500 codes are allowed. + Unlike in the "geocache" method, this CAN be an empty string (it will + result in an empty, but valid, response).

    +
    + +

    Pipe-separated list of ISO 639-1 language codes. This indicates the + order of preference in which language will be chosen for fields like + name and description.

    +

    Please note, that you may also access caches' descriptions in all + available languages. If you want to do this, you should use fields + names, descriptions (etc.) instead of name and + description (etc.).

    +
    + +

    Same as in the services/caches/geocache method. Pipe-separated list + of field names which you are interested with. + See services/caches/geocache method for a list available values.

    +
    + +

    Same as in the services/caches/geocache method.

    +
    + + Same as in the services/caches/geocache method. + + + Same as in the services/caches/geocache method. + + + Same as in the services/caches/geocache method. + + + Same as in the services/caches/geocache method. + + + +

    A dictionary. Cache codes you provide will be mapped to dictionary keys, + and each value will be a dictionary of fields you have selected.

    +

    For example, for geocaches?cache_codes=OP3D96|OC124&fields=type + query, the result might look something link this:

    +
    {"OP3D96": {"type": "Traditional"}, "OC124": null}
    +

    Value of null 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.)

    +
    \ No newline at end of file diff --git a/htdocs/okapi/services/caches/map/replicate_listener.inc.php b/htdocs/okapi/services/caches/map/replicate_listener.inc.php index 6e4fb9f7..e58ae8c7 100644 --- a/htdocs/okapi/services/caches/map/replicate_listener.inc.php +++ b/htdocs/okapi/services/caches/map/replicate_listener.inc.php @@ -23,230 +23,246 @@ require_once 'tiletree.inc.php'; class ReplicateListener { - public static function receive($changelog) - { - # This will be called every time new items arrive from replicate module's - # changelog. The format of $changelog is described in the replicate module - # (NOT the entire response, just the "changelog" key). + public static function receive($changelog) + { + # This will be called every time new items arrive from replicate module's + # changelog. The format of $changelog is described in the replicate module + # (NOT the entire response, just the "changelog" key). - foreach ($changelog as $c) - { - if ($c['object_type'] == 'geocache') - { - if ($c['change_type'] == 'replace') - self::handle_geocache_replace($c); - else - self::handle_geocache_delete($c); - } - } - } + foreach ($changelog as $c) + { + if ($c['object_type'] == 'geocache') + { + if ($c['change_type'] == 'replace') + self::handle_geocache_replace($c); + else + self::handle_geocache_delete($c); + } + } + } - public static function reset($mail_admins = true) - { - # This will be called when there are "too many" entries in the changelog - # and the replicate module thinks it better to just reset the entire TileTree. - # For the first hours after such reset maps may work a little slower. + public static function reset($mail_admins = true) + { + # This will be called when there are "too many" entries in the changelog + # and the replicate module thinks it better to just reset the entire TileTree. + # For the first hours after such reset maps may work a little slower. - Db::execute("delete from okapi_tile_status"); - Db::execute("delete from okapi_tile_caches"); - } + Db::execute("delete from okapi_tile_status"); + Db::execute("delete from okapi_tile_caches"); + } - private static function handle_geocache_replace($c) - { - # Check if any relevant geocache attributes have changed. - # We will pick up "our" copy of the cache from zero-zoom level. + private static function handle_geocache_replace($c) + { + # Check if any relevant geocache attributes have changed. + # We will pick up "our" copy of the cache from zero-zoom level. - try { - $cache = OkapiServiceRunner::call("services/caches/geocache", new OkapiInternalRequest(new OkapiInternalConsumer(), null, array( - 'cache_code' => $c['object_key']['code'], - 'fields' => 'internal_id|code|name|location|type|status|rating|recommendations|founds|trackables_count' - ))); - } catch (InvalidParam $e) { - # Unprobable, but possible. Ignore changelog entry. - return; - } + try { + $cache = OkapiServiceRunner::call("services/caches/geocache", new OkapiInternalRequest(new OkapiInternalConsumer(), null, array( + 'cache_code' => $c['object_key']['code'], + 'fields' => 'internal_id|code|name|location|type|status|rating|recommendations|founds|trackables_count' + ))); + } catch (InvalidParam $e) { + # Unprobable, but possible. Ignore changelog entry. + return; + } - $theirs = TileTree::generate_short_row($cache); - $ours = mysql_fetch_row(Db::query(" - select cache_id, z21x, z21y, status, type, rating, flags - from okapi_tile_caches - where - z=0 - and cache_id = '".mysql_real_escape_string($cache['internal_id'])."' - ")); - if (!$ours) - { - # Aaah, a new geocache! How nice... ;) + # Fetch our copy of the cache. - self::add_geocache_to_cached_tiles($theirs); - } - elseif (($ours[1] != $theirs[1]) || ($ours[2] != $theirs[2])) # z21x & z21y fields - { - # Location changed. + $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'])."' + ")); - 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. - } - } + # Caches near the poles caused our computations to break here. We will + # ignore such caches! - 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. + list($lat, $lon) = explode("|", $cache['location']); + if ((floatval($lat) >= 89.99) || (floatval($lat) <= -89.99)) { + if ($ours) { + self::remove_geocache_from_cached_tiles($ours[0]); + } + return; + } - Db::execute(" - delete from okapi_tile_caches - where cache_id = '".mysql_real_escape_string($cache_id)."' - "); + # Compute the new row for okapi_tile_caches. Compare with the old one. - # Note, that after this operation, okapi_tile_status may be out-of-date. - # There might exist some rows with status==2, but they should be in status==1. - # Currently, we can ignore this, because status==1 is just a shortcut to - # avoid making unnecessary queries. - } + $theirs = TileTree::generate_short_row($cache); + if (!$ours) + { + # Aaah, a new geocache! How nice... ;) - private static function add_geocache_to_cached_tiles(&$row) - { - # This one is the most complicated. We need to identify all tiles - # where the cache should be present. This include 22 obvious "exact match" - # tiles (one per each zoom level), *and* all "just outside the border" - # tiles (one geocache can be present in up to 4 tiles per zoom level). - # This gives us max. 88 tiles to add the geocache to. + self::add_geocache_to_cached_tiles($theirs); + } + elseif (($ours[1] != $theirs[1]) || ($ours[2] != $theirs[2])) # z21x & z21y fields + { + # Location changed. - $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]; - $z21y = $row[2]; - $ex = $z21x >> 8; # initially, z21x / - $ey = $z21y >> 8; # initially, z21y / - 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. + Db::execute(" + delete from okapi_tile_caches + where cache_id = '".mysql_real_escape_string($cache_id)."' + "); - $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); + # Note, that after this operation, okapi_tile_status may be out-of-date. + # There might exist some rows with status==2, but they should be in status==1. + # Currently, we can ignore this, because status==1 is just a shortcut to + # avoid making unnecessary queries. + } - foreach ($tiles_in_this_region as $coords) - { - list($x, $y) = $coords; + private static function add_geocache_to_cached_tiles(&$row) + { + # This one is the most complicated. We need to identify all tiles + # where the cache should be present. This include 22 obvious "exact match" + # tiles (one per each zoom level), *and* all "just outside the border" + # tiles (one geocache can be present in up to 4 tiles per zoom level). + # This gives us max. 88 tiles to add the geocache to. - $scale = 8 + 21 - $zoom; - $margin = 1 << ($scale - 3); # 32px of current $zoom level, measured in z21 pixels. + $tiles_to_update = array(); - $left_z21x = ($x << $scale) - $margin; - $right_z21x = (($x + 1) << $scale) + $margin; - $top_z21y = ($y << $scale) - $margin; - $bottom_z21y = (($y + 1) << $scale) + $margin; + # We will begin at zoom 21 and then go down to zoom 0. - if ($z21x < $left_z21x) - continue; - if ($z21x > $right_z21x) - continue; - if ($z21y < $top_z21y) - continue; - if ($z21y > $bottom_z21y) - continue; + $z21x = $row[1]; + $z21y = $row[2]; + $ex = $z21x >> 8; # initially, z21x / + $ey = $z21y >> 8; # initially, z21y / + 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. - # 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. - # Most of these tiles aren't cached at all. We need to update - # only the cached ones. + $scale = 8 + 21 - $zoom; + $margin = 1 << ($scale - 3); # 32px of current $zoom level, measured in z21 pixels. - $alternatives_escaped = array(); - foreach ($tiles_to_update as $coords) - { - list($z, $x, $y) = $coords; - $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) - "); + $left_z21x = ($x << $scale) - $margin; + $right_z21x = (($x + 1) << $scale) + $margin; + $top_z21y = ($y << $scale) - $margin; + $bottom_z21y = (($y + 1) << $scale) + $margin; - # We might have just filled some empty tiles (status 1) with data. - # We need to update their status to 2. + if ($z21x < $left_z21x) + continue; + if ($z21x > $right_z21x) + continue; + if ($z21y < $top_z21y) + continue; + if ($z21y > $bottom_z21y) + continue; - Db::execute(" - update okapi_tile_status - set status=2 - where - (".implode(" or ", $alternatives_escaped).") - and status=1 - "); - } + # We found a match. Store it for later. - # 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) - { - # 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! + # We have a list of all possible tiles that need updating. + # Most of these tiles aren't cached at all. We need to update + # only the cached ones. - 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])."' - "); - } + $alternatives_escaped = array(); + foreach ($tiles_to_update as $coords) + { + list($z, $x, $y) = $coords; + $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) + "); - private static function handle_geocache_delete($c) - { - # Simply delete the cache at all zoom levels. + # We might have just filled some empty tiles (status 1) with data. + # We need to update their status to 2. - $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); - } + Db::execute(" + update okapi_tile_status + set status=2 + where + (".implode(" or ", $alternatives_escaped).") + 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); + } } \ No newline at end of file diff --git a/htdocs/okapi/services/caches/map/tile.php b/htdocs/okapi/services/caches/map/tile.php index 17fba7cd..ef418f14 100644 --- a/htdocs/okapi/services/caches/map/tile.php +++ b/htdocs/okapi/services/caches/map/tile.php @@ -29,200 +29,200 @@ require_once($GLOBALS['rootpath']."okapi/services/caches/search/searching.inc.ph class WebService { - /** - * Should be always true. You may temporarily set it to false, when you're - * testing/debugging the tile renderer. - */ - private static $USE_ETAGS_CACHE = true; + /** + * Should be always true. You may temporarily set it to false, when you're + * testing/debugging the tile renderer. + */ + private static $USE_ETAGS_CACHE = true; - /** - * Should be always true. You may temporarily set it to false, when you're - * testing/debugging the tile renderer. - */ - private static $USE_IMAGE_CACHE = true; + /** + * Should be always true. You may temporarily set it to false, when you're + * testing/debugging the tile renderer. + */ + private static $USE_IMAGE_CACHE = true; - /** - * Should be always true. You may temporarily set it to false, when you're - * testing/debugging. Grep the code to check when this flag is used. - */ - private static $USE_OTHER_CACHE = true; + /** + * Should be always true. You may temporarily set it to false, when you're + * testing/debugging. Grep the code to check when this flag is used. + */ + private static $USE_OTHER_CACHE = true; - public static function options() - { - return array( - 'min_auth_level' => 3 - ); - } + public static function options() + { + return array( + 'min_auth_level' => 3 + ); + } - private static function require_uint($request, $name, $min_value = 0) - { - $val = $request->get_parameter($name); - if ($val === null) - throw new ParamMissing($name); - $ret = intval($val); - if ($ret < 0 || ("$ret" !== $val)) - throw new InvalidParam($name, "Expecting non-negative integer."); - return $ret; - } + private static function require_uint($request, $name, $min_value = 0) + { + $val = $request->get_parameter($name); + if ($val === null) + throw new ParamMissing($name); + $ret = intval($val); + if ($ret < 0 || ("$ret" !== $val)) + throw new InvalidParam($name, "Expecting non-negative integer."); + return $ret; + } - public static function call(OkapiRequest $request) - { - $checkpointA_started = microtime(true); + public static function call(OkapiRequest $request) + { + $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'))) - throw new BadRequest("Your Consumer Key has not been allowed to access this method."); + if (!in_array($request->consumer->key, array('internal', 'facade'))) + throw new BadRequest("Your Consumer Key has not been allowed to access this method."); - # zoom, x, y - required tile-specific parameters. + # zoom, x, y - required tile-specific parameters. - $zoom = self::require_uint($request, 'z'); - if ($zoom > 21) - throw new InvalidParam('z', "Maximum value for this parameter is 21."); - $x = self::require_uint($request, 'x'); - $y = self::require_uint($request, 'y'); - if ($x >= 1<<$zoom) - throw new InvalidParam('x', "Should be in 0..".((1<<$zoom) - 1)."."); - if ($y >= 1<<$zoom) - throw new InvalidParam('y', "Should be in 0..".((1<<$zoom) - 1)."."); + $zoom = self::require_uint($request, 'z'); + if ($zoom > 21) + throw new InvalidParam('z', "Maximum value for this parameter is 21."); + $x = self::require_uint($request, 'x'); + $y = self::require_uint($request, 'y'); + if ($x >= 1<<$zoom) + throw new InvalidParam('x', "Should be in 0..".((1<<$zoom) - 1)."."); + if ($y >= 1<<$zoom) + throw new InvalidParam('y', "Should be in 0..".((1<<$zoom) - 1)."."); - # Now, we will create a search set (or use one previously created). - # Instead of creating a new OkapiInternalRequest object, we will pass - # the current request directly. We can do that, because we inherit all - # of the "save" method's parameters. + # Now, we will create a search set (or use one previously created). + # Instead of creating a new OkapiInternalRequest object, we will pass + # the current request directly. We can do that, because we inherit all + # of the "save" method's parameters. - $search_set = OkapiServiceRunner::call('services/caches/search/save', $request); - $set_id = $search_set['set_id']; + $search_set = OkapiServiceRunner::call('services/caches/search/save', $request); + $set_id = $search_set['set_id']; - # Get caches which are present in the result set AND within the tile - # (+ those around the borders). + # Get caches which are present in the result set AND within the tile + # (+ those around the borders). - $rs = TileTree::query_fast($zoom, $x, $y, $set_id); - $rows = array(); - if ($rs !== null) - { - while ($row = mysql_fetch_row($rs)) - $rows[] = $row; - unset($row); - } - OkapiServiceRunner::save_stats_extra("caches/map/tile/checkpointA", null, - microtime(true) - $checkpointA_started); - $checkpointB_started = microtime(true); + $rs = TileTree::query_fast($zoom, $x, $y, $set_id); + $rows = array(); + if ($rs !== null) + { + while ($row = mysql_fetch_row($rs)) + $rows[] = $row; + unset($row); + } + OkapiServiceRunner::save_stats_extra("caches/map/tile/checkpointA", null, + microtime(true) - $checkpointA_started); + $checkpointB_started = microtime(true); - # Add dynamic, user-related flags. + # Add dynamic, user-related flags. - if (count($rows) > 0) - { - # Load user-related cache ids. + if (count($rows) > 0) + { + # Load user-related cache ids. - $cache_key = "tileuser/".$request->token->user_id; - $user = self::$USE_OTHER_CACHE ? Cache::get($cache_key) : null; - if ($user === null) - { - $user = array(); + $cache_key = "tileuser/".$request->token->user_id; + $user = self::$USE_OTHER_CACHE ? Cache::get($cache_key) : null; + if ($user === null) + { + $user = array(); - # Ignored caches. + # Ignored caches. - $rs = Db::query(" - select cache_id - from cache_ignore - where user_id = '".mysql_real_escape_string($request->token->user_id)."' - "); - $user['ignored'] = array(); - while (list($cache_id) = mysql_fetch_row($rs)) - $user['ignored'][$cache_id] = true; + $rs = Db::query(" + select cache_id + from cache_ignore + where user_id = '".mysql_real_escape_string($request->token->user_id)."' + "); + $user['ignored'] = array(); + while (list($cache_id) = mysql_fetch_row($rs)) + $user['ignored'][$cache_id] = true; - # Found caches. + # Found caches. - $rs = Db::query(" - select distinct cache_id - from cache_logs - where - user_id = '".mysql_real_escape_string($request->token->user_id)."' - and type = 1 - and ".((Settings::get('OC_BRANCH') == 'oc.pl') ? "deleted = 0" : "true")." - "); - $user['found'] = array(); - while (list($cache_id) = mysql_fetch_row($rs)) - $user['found'][$cache_id] = true; + $rs = Db::query(" + select distinct cache_id + from cache_logs + where + user_id = '".mysql_real_escape_string($request->token->user_id)."' + and type = 1 + and ".((Settings::get('OC_BRANCH') == 'oc.pl') ? "deleted = 0" : "true")." + "); + $user['found'] = array(); + while (list($cache_id) = mysql_fetch_row($rs)) + $user['found'][$cache_id] = true; - # Own caches. + # Own caches. - $rs = Db::query(" - select distinct cache_id - from caches - where user_id = '".mysql_real_escape_string($request->token->user_id)."' - "); - $user['own'] = array(); - while (list($cache_id) = mysql_fetch_row($rs)) - $user['own'][$cache_id] = true; + $rs = Db::query(" + select distinct cache_id + from caches + where user_id = '".mysql_real_escape_string($request->token->user_id)."' + "); + $user['own'] = array(); + while (list($cache_id) = mysql_fetch_row($rs)) + $user['own'][$cache_id] = true; - Cache::set($cache_key, $user, 30); - } + Cache::set($cache_key, $user, 30); + } - # Add extra flags to geocaches. + # Add extra flags to geocaches. - foreach ($rows as &$row_ref) - { - # Add the "found" flag (to indicate that this cache needs - # to be drawn as found) and the "own" flag (to indicate that - # the current user is the owner). + foreach ($rows as &$row_ref) + { + # Add the "found" flag (to indicate that this cache needs + # to be drawn as found) and the "own" flag (to indicate that + # the current user is the owner). - if (isset($user['found'][$row_ref[0]])) - $row_ref[6] |= TileTree::$FLAG_FOUND; # $row[6] is "flags" - if (isset($user['own'][$row_ref[0]])) - $row_ref[6] |= TileTree::$FLAG_OWN; # $row[6] is "flags" - } - } + if (isset($user['found'][$row_ref[0]])) + $row_ref[6] |= TileTree::$FLAG_FOUND; # $row[6] is "flags" + if (isset($user['own'][$row_ref[0]])) + $row_ref[6] |= TileTree::$FLAG_OWN; # $row[6] is "flags" + } + } - # Compute the image hash/fingerprint. This will be used both for ETags - # and internal cache ($cache_key). + # Compute the image hash/fingerprint. This will be used both for ETags + # and internal cache ($cache_key). - $tile = new DefaultTileRenderer($zoom, $rows); - $image_fingerprint = $tile->get_unique_hash(); + $tile = new DefaultTileRenderer($zoom, $rows); + $image_fingerprint = $tile->get_unique_hash(); - # Start creating response. + # Start creating response. - $response = new OkapiHttpResponse(); - $response->content_type = $tile->get_content_type(); - $response->cache_control = "Cache-Control: private, max-age=600"; - $response->etag = 'W/"'.$image_fingerprint.'"'; + $response = new OkapiHttpResponse(); + $response->content_type = $tile->get_content_type(); + $response->cache_control = "Cache-Control: private, max-age=600"; + $response->etag = 'W/"'.$image_fingerprint.'"'; - # Check if the request didn't include the same ETag. + # Check if the request didn't include the same ETag. - OkapiServiceRunner::save_stats_extra("caches/map/tile/checkpointB", null, - microtime(true) - $checkpointB_started); - $checkpointC_started = microtime(true); - if (self::$USE_ETAGS_CACHE && ($request->etag == $response->etag)) - { - # Hit. Report the content was unmodified. + OkapiServiceRunner::save_stats_extra("caches/map/tile/checkpointB", null, + microtime(true) - $checkpointB_started); + $checkpointC_started = microtime(true); + if (self::$USE_ETAGS_CACHE && ($request->etag == $response->etag)) + { + # Hit. Report the content was unmodified. - $response->etag = null; - $response->status = "304 Not Modified"; - return $response; - } + $response->etag = null; + $response->status = "304 Not Modified"; + 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; - $response->body = self::$USE_IMAGE_CACHE ? Cache::get($cache_key) : null; - OkapiServiceRunner::save_stats_extra("caches/map/tile/checkpointC", null, - microtime(true) - $checkpointC_started); - $checkpointD_started = microtime(true); - if ($response->body !== null) - { - # Hit. We will use the cached version of the image. + $cache_key = "tile/".$image_fingerprint; + $response->body = self::$USE_IMAGE_CACHE ? Cache::get($cache_key) : null; + OkapiServiceRunner::save_stats_extra("caches/map/tile/checkpointC", null, + microtime(true) - $checkpointC_started); + $checkpointD_started = microtime(true); + if ($response->body !== null) + { + # Hit. We will use the cached version of the image. - return $response; - } + return $response; + } - # Miss. Render the image. Cache the result. + # Miss. Render the image. Cache the result. - $response->body = $tile->render(); - Cache::set_scored($cache_key, $response->body); - OkapiServiceRunner::save_stats_extra("caches/map/tile/checkpointD", null, - microtime(true) - $checkpointD_started); + $response->body = $tile->render(); + Cache::set_scored($cache_key, $response->body); + OkapiServiceRunner::save_stats_extra("caches/map/tile/checkpointD", null, + microtime(true) - $checkpointD_started); - return $response; - } + return $response; + } } diff --git a/htdocs/okapi/services/caches/map/tile.xml b/htdocs/okapi/services/caches/map/tile.xml index c8e71112..d1aecde3 100644 --- a/htdocs/okapi/services/caches/map/tile.xml +++ b/htdocs/okapi/services/caches/map/tile.xml @@ -1,18 +1,18 @@ - Get cache map tile - 150 - -

    CLOSED BETA version. Due to long-term performance tests, currently - this method is not publicly accessible. Let us know if you're interested - in using it.

    -

    Use this method to retrieve a tile-map of all caches included in your search - result.

    -
    - Zoom level (0..21). - Tile number on the X axis. - Tile number on the Y axis. - - - The PNG image with the requested map tile. - + Get cache map tile + 150 + +

    CLOSED BETA version. Due to long-term performance tests, currently + this method is not publicly accessible. Let us know if you're interested + in using it.

    +

    Use this method to retrieve a tile-map of all caches included in your search + result.

    +
    + Zoom level (0..21). + Tile number on the X axis. + Tile number on the Y axis. + + + The PNG image with the requested map tile. +
    \ No newline at end of file diff --git a/htdocs/okapi/services/caches/map/tilerenderer.inc.php b/htdocs/okapi/services/caches/map/tilerenderer.inc.php index 26ba188c..670ab643 100644 --- a/htdocs/okapi/services/caches/map/tilerenderer.inc.php +++ b/htdocs/okapi/services/caches/map/tilerenderer.inc.php @@ -11,618 +11,618 @@ use okapi\FileCache; // WRTODO interface TileRenderer { - /** - * Return the unique hash of the tile being rendered. This method will be - * called only once, prior the render method. You may (but don't have to) - * throw an Exception on every subsequent call. - */ - public function get_unique_hash(); + /** + * Return the unique hash of the tile being rendered. This method will be + * called only once, prior the render method. You may (but don't have to) + * throw an Exception on every subsequent call. + */ + public function get_unique_hash(); - /** Get the content type of the data returned by the render method. */ - public function get_content_type(); + /** Get the content type of the data returned by the render method. */ + public function get_content_type(); - /** - * Render the image. This function will be called only once, after calling - * get_unique_hash method. - */ - public function render(); + /** + * Render the image. This function will be called only once, after calling + * get_unique_hash method. + */ + public function render(); } class DefaultTileRenderer implements TileRenderer { - /** - * Changing this will affect all generated hashes. You should increment it - * whenever you alter anything in the drawing algorithm. - */ - private static $VERSION = 59; - - /** - * Should be always true. You may temporarily set it to false, when you're - * testing/debugging a new set of static icons. - */ - private static $USE_STATIC_IMAGE_CACHE = true; - - /** - * Should be always true. You may temporarily set it to false, when you're - * testing/debugging a new captions renderer. - */ - private static $USE_CAPTIONS_CACHE = true; - - private $zoom; - private $rows_ref; - private $im; - - /** - * Takes the zoom level and the list of geocache descriptions. Note, that - * $rows_ref can be altered after calling render. If you don't want it to, - * you should pass a deep copy. - */ - public function __construct($zoom, &$rows_ref) - { - $this->zoom = $zoom; - $this->rows_ref = &$rows_ref; - } - - public function get_unique_hash() - { - return md5(json_encode(array( - "DefaultTileRenderer", - self::$VERSION, - $this->zoom, - $this->rows_ref - ))); - } - - public function get_content_type() - { - return "image/png"; - } - - public function render() - { - # Preprocess the rows. - - if ($this->zoom >= 5) - $this->decide_which_get_captions(); - - # Make a background. - - $this->im = imagecreatetruecolor(256, 256); - imagealphablending($this->im, false); - if ($this->zoom >= 13) $opacity = 15; - elseif ($this->zoom <= 12) $opacity = max(0, $this->zoom * 2 - 14); - $transparent = imagecolorallocatealpha($this->im, 0, 0, 0, 127 - $opacity); - imagefilledrectangle($this->im, 0, 0, 256, 256, $transparent); - imagealphablending($this->im, true); - - # Draw the caches. - - foreach ($this->rows_ref as &$row_ref) - $this->draw_cache($row_ref); - - # Return the result. - - ob_start(); - imagesavealpha($this->im, true); - imagepng($this->im); - imagedestroy($this->im); - return ob_get_clean(); - } - - private static function get_image($name, $opacity=1, $brightness=0, - $contrast=0, $r=0, $g=0, $b=0) - { - static $locmem_cache = array(); - - # Check locmem cache. - - $key = "$name/$opacity/$brightness/$contrast/$r/$g/$b"; - if (!isset($locmem_cache[$key])) - { - # Miss. Check default cache. WRTODO: upgrade to normal Cache? - - try - { - $cache_key = "tilesrc/".Okapi::$revision."/".self::$VERSION."/".$key; - $gd2_path = self::$USE_STATIC_IMAGE_CACHE - ? FileCache::get_file_path($cache_key) : null; - if ($gd2_path === null) - throw new Exception("Not in cache"); - # File cache hit. GD2 files are much faster to read than PNGs. - # This can throw an Exception (see bug#160). - $locmem_cache[$key] = imagecreatefromgd2($gd2_path); - } - catch (Exception $e) - { - # Miss again (or error decoding). Read the image from PNG. - - $locmem_cache[$key] = imagecreatefrompng($GLOBALS['rootpath']."okapi/static/tilemap/$name.png"); - - # Apply all wanted effects. - - if ($opacity != 1) - self::change_opacity($locmem_cache[$key], $opacity); - if ($contrast != 0) - imagefilter($locmem_cache[$key], IMG_FILTER_CONTRAST, $contrast); - if ($brightness != 0) - imagefilter($locmem_cache[$key], IMG_FILTER_BRIGHTNESS, $brightness); - if (($r != 0) || ($g != 0) || ($b != 0)) - { - imagefilter($locmem_cache[$key], IMG_FILTER_GRAYSCALE); - imagefilter($locmem_cache[$key], IMG_FILTER_COLORIZE, $r, $g, $b); - } - - # Cache the result. - - ob_start(); - imagegd2($locmem_cache[$key]); - $gd2 = ob_get_clean(); - FileCache::set($cache_key, $gd2); - } - } - return $locmem_cache[$key]; - } - - /** - * Extremely slow! Remember to cache the result! - */ - private static function change_opacity($im, $ratio) - { - imagealphablending($im, false); - - $w = imagesx($im); - $h = imagesy($im); - - for($x = 0; $x < $w; $x++) - { - for($y = 0; $y < $h; $y++) - { - $color = imagecolorat($im, $x, $y); - $new_color = ((max(0, floor(127 - ((127 - (($color >> 24) & 0x7f)) * $ratio))) & 0x7f) << 24) | ($color & 0x80ffffff); - imagesetpixel($im, $x, $y, $new_color); - } - } - - imagealphablending($im, true); - } - - private function draw_cache(&$cache_struct) - { - $capt = ($cache_struct[6] & TileTree::$FLAG_DRAW_CAPTION); - if (($this->zoom <= 8) && (!$capt)) - $this->draw_cache_tiny($cache_struct); - elseif (($this->zoom <= 13) && (!$capt)) - $this->draw_cache_medium($cache_struct); - else - $this->draw_cache_large($cache_struct); - - # Put caption (this flag is set only when there is plenty of space around). - - if ($cache_struct[6] & TileTree::$FLAG_DRAW_CAPTION) - { - $caption = $this->get_caption($cache_struct[0]); - imagecopy($this->im, $caption, $cache_struct[1] - 32, $cache_struct[2] + 6, 0, 0, 64, 26); - } - - } - - - private function draw_cache_large(&$cache_struct) - { - list($cache_id, $px, $py, $status, $type, $rating, $flags, $count) = $cache_struct; - - $found = $flags & TileTree::$FLAG_FOUND; - $own = $flags & TileTree::$FLAG_OWN; - $new = $flags & TileTree::$FLAG_NEW; - - # Prepare vars. - - if ($own) { - $key = 'large_outer_own'; - $a = 1; $br = 0; $c = 0; - $r = 0; $g = 0; $b = 0; - } elseif ($found) { - $key = 'large_outer_found'; - $a = ($flags & TileTree::$FLAG_DRAW_CAPTION) ? .7 : .35; - $br = 40; $c = 20; - //$a = 0.5; $br = 0; $c = 0; - $r = 0; $g = 0; $b = 0; - } elseif ($new) { - $key = 'large_outer_new'; - $a = 1; $br = 0; $c = 0; - $r = 0; $g = 0; $b = 0; - } else { - $key = 'large_outer'; - $a = 1; $br = 0; $c = 0; - $r = 0; $g = 0; $b = 0; - } - - # Put the outer marker (indicates the found/new/own status). - - $outer_marker = self::get_image($key, $a); - - $width = 40; - $height = 32; - $center_x = 12; - $center_y = 26; - $markercenter_x = 12; - $markercenter_y = 12; - - if ($count > 1) - imagecopy($this->im, $outer_marker, $px - $center_x + 3, $py - $center_y - 2, 0, 0, $width, $height); - imagecopy($this->im, $outer_marker, $px - $center_x, $py - $center_y, 0, 0, $width, $height); - - # Put the inner marker (indicates the type). - - $inner_marker = self::get_image("large_inner_".self::get_type_suffix( - $type, true), $a, $br, $c, $r, $g, $b); - imagecopy($this->im, $inner_marker, $px - 7, $py - 22, 0, 0, 16, 16); - - # If the cache is unavailable, mark it with X. - - if (($status != 1) && ($count == 1)) - { - $icon = self::get_image(($status == 2) ? "status_unavailable" - : "status_archived", $a); - imagecopy($this->im, $icon, $px - 1, $py - $center_y - 4, 0, 0, 16, 16); - } - - # Put the rating smile. :) - - if ($status == 1) - { - if ($rating >= 4.2) - { - if ($flags & TileTree::$FLAG_STAR) { - $icon = self::get_image("rating_grin", $a, $br, $c, $r, $g, $b); - imagecopy($this->im, $icon, $px - 7 - 6, $py - $center_y - 8, 0, 0, 16, 16); - $icon = self::get_image("rating_star", $a, $br, $c, $r, $g, $b); - imagecopy($this->im, $icon, $px - 7 + 6, $py - $center_y - 8, 0, 0, 16, 16); - } else { - $icon = self::get_image("rating_grin", $a, $br, $c, $r, $g, $b); - imagecopy($this->im, $icon, $px - 7, $py - $center_y - 8, 0, 0, 16, 16); - } - } - # This was commented out because users complained about too many smiles ;) - // elseif ($rating >= 3.6) { - // $icon = self::get_image("rating_smile", $a, $br, $c, $r, $g, $b); - // imagecopy($this->im, $icon, $px - 7, $py - $center_y - 8, 0, 0, 16, 16); - // } - } - - # Mark found caches with V. - - if ($found) - { - $icon = self::get_image("found", 0.7*$a, $br, $c, $r, $g, $b); - imagecopy($this->im, $icon, $px - 2, $py - $center_y - 3, 0, 0, 16, 16); - } - - } - - /** - * Split lines so that they fit inside the specified width. - */ - private static function wordwrap($font, $size, $maxWidth, $text) - { - $words = explode(" ", $text); - $lines = array(); - $line = ""; - $nextBonus = ""; - for ($i=0; ($i0); $i++) { - $word = isset($words[$i])?$words[$i]:""; - if (mb_strlen($nextBonus) > 0) - $word = $nextBonus." ".$word; - $nextBonus = ""; - while (true) { - $bbox = imagettfbbox($size, 0, $font, $line.$word); - $width = $bbox[2]-$bbox[0]; - if ($width <= $maxWidth) { - $line .= $word." "; - continue 2; - } - if (mb_strlen($line) > 0) { - $lines[] = trim($line); - $line = ""; - continue; - } - $nextBonus = $word[mb_strlen($word)-1].$nextBonus; - $word = mb_substr($word, 0, mb_strlen($word)-1); - continue; - } - } - if (mb_strlen($line) > 0) - $lines[] = trim($line); - return implode("\n", $lines); - } - - /** - * Return 64x26 bitmap with the caption (name) for the given geocache. - */ - private function get_caption($cache_id) - { - # Check cache. - - $cache_key = "tilecaption/".self::$VERSION."/".$cache_id; - $gd2 = self::$USE_CAPTIONS_CACHE ? Cache::get($cache_key) : null; - if ($gd2 === null) - { - # We'll work with 16x bigger image to get smoother interpolation. - - $im = imagecreatetruecolor(64*4, 26*4); - imagealphablending($im, false); - $transparent = imagecolorallocatealpha($im, 255, 255, 255, 127); - imagefilledrectangle($im, 0, 0, 64*4, 26*4, $transparent); - imagealphablending($im, true); - - # Get the name of the cache. - - $name = Db::select_value(" - select name - from caches - where cache_id = '".mysql_real_escape_string($cache_id)."' - "); - - # Split the name into a couple of lines. - - //$font = $GLOBALS['rootpath'].'util.sec/bt.ttf'; - $font = $GLOBALS['rootpath'].'okapi/static/tilemap/tahoma.ttf'; - $size = 25; - $lines = explode("\n", self::wordwrap($font, $size, 64*4 - 6*2, $name)); - - # For each line, compute its (x, y) so that the text is centered. - - $y = 0; - $positions = array(); - foreach ($lines as $line) - { - $bbox = imagettfbbox($size, 0, $font, $line); - $width = $bbox[2]-$bbox[0]; - $x = 128 - ($width >> 1); - $positions[] = array($x, $y); - $y += 36; - } - $drawer = function($x, $y, $color) use (&$lines, &$positions, &$im, &$size, &$font) - { - $len = count($lines); - for ($i=0; $i<$len; $i++) - { - $line = $lines[$i]; - list($offset_x, $offset_y) = $positions[$i]; - imagettftext($im, $size, 0, $offset_x + $x, $offset_y + $y, $color, $font, $line); - } - }; - - # Draw an outline. - - $outline_color = imagecolorallocatealpha($im, 255, 255, 255, 80); - for ($x=0; $x<=12; $x+=3) - for ($y=$size-3; $y<=$size+9; $y+=3) - $drawer($x, $y, $outline_color); - - # Add a slight shadow effect (on top of the outline). - - $drawer(9, $size + 3, imagecolorallocatealpha($im, 0, 0, 0, 110)); - - # Draw the caption. - - $drawer(6, $size + 3, imagecolorallocatealpha($im, 150, 0, 0, 40)); - - # Resample. - - imagealphablending($im, false); - $small = imagecreatetruecolor(64, 26); - imagealphablending($small, false); - imagecopyresampled($small, $im, 0, 0, 0, 0, 64, 26, 64*4, 26*4); - - # Cache it! - - ob_start(); - imagegd2($small); - $gd2 = ob_get_clean(); - Cache::set_scored($cache_key, $gd2); - } - - return imagecreatefromstring($gd2); - } - - private function draw_cache_medium(&$cache_struct) - { - list($cache_id, $px, $py, $status, $type, $rating, $flags, $count) = $cache_struct; - - $found = $flags & TileTree::$FLAG_FOUND; - $own = $flags & TileTree::$FLAG_OWN; - $new = $flags & TileTree::$FLAG_NEW; - if ($found && (!($flags & TileTree::$FLAG_DRAW_CAPTION))) - $a = .35; - else - $a = 1; - - # Put the marker (indicates the type). - - $marker = self::get_image("medium_".self::get_type_suffix($type, false), $a); - $width = 14; - $height = 14; - $center_x = 7; - $center_y = 8; - $markercenter_x = 7; - $markercenter_y = 8; - - if ($count > 1) - { - imagecopy($this->im, $marker, $px - $center_x + 3, $py - $center_y - 2, 0, 0, $width, $height); - imagecopy($this->im, $marker, $px - $center_x, $py - $center_y, 0, 0, $width, $height); - } - elseif ($status == 1) # don't put the marker for unavailable caches (X only) - { - imagecopy($this->im, $marker, $px - $center_x, $py - $center_y, 0, 0, $width, $height); - } - - # If the cache is unavailable, mark it with X. - - if (($status != 1) && ($count == 1)) - { - $icon = self::get_image(($status == 2) ? "status_unavailable" - : "status_archived"); - imagecopy($this->im, $icon, $px - ($center_x - $markercenter_x) - 6, - $py - ($center_y - $markercenter_y) - 8, 0, 0, 16, 16); - } - - # Put small versions of rating icons. - - if ($status == 1) - { - if ($rating >= 4.2) - { - if ($flags & TileTree::$FLAG_STAR) { - $icon = self::get_image("rating_grin_small", max(0.6, $a)); - imagecopy($this->im, $icon, $px - 5, $py - $center_y - 1, 0, 0, 6, 6); - $icon = self::get_image("rating_star_small", max(0.6, $a)); - imagecopy($this->im, $icon, $px - 2, $py - $center_y - 3, 0, 0, 10, 10); - } else { - $icon = self::get_image("rating_grin_small", max(0.6, $a)); - imagecopy($this->im, $icon, $px - 3, $py - $center_y - 1, 0, 0, 6, 6); - } - } - } - - if ($own) - { - # Mark own caches with additional overlay. - - $overlay = self::get_image("medium_overlay_own"); - imagecopy($this->im, $overlay, $px - $center_x, $py - $center_y, 0, 0, $width, $height); - } - elseif ($found) - { - # Mark found caches with V. - - $icon = self::get_image("found", 0.7*$a); - imagecopy($this->im, $icon, $px - ($center_x - $markercenter_x) - 7, - $py - ($center_y - $markercenter_y) - 9, 0, 0, 16, 16); - } - elseif ($new) - { - # Mark new caches with additional overlay. - - $overlay = self::get_image("medium_overlay_new"); - imagecopy($this->im, $overlay, $px - $center_x, $py - $center_y, 0, 0, $width, $height); - } - } - - private static function get_type_suffix($type, $extended_set) - { - switch ($type) { - case 2: return 'traditional'; - case 3: return 'multi'; - case 6: return 'event'; - case 7: return 'quiz'; - case 4: return 'virtual'; - case 1: return 'unknown'; - } - if ($extended_set) - { - switch ($type) { - case 10: return 'own'; - case 8: return 'moving'; - case 5: return 'webcam'; - } - } - return 'other'; - } - - private function draw_cache_tiny(&$cache_struct) - { - list($cache_id, $px, $py, $status, $type, $rating, $flags, $count) = $cache_struct; - - $found = $flags & TileTree::$FLAG_FOUND; - $own = $flags & TileTree::$FLAG_OWN; - $new = $flags & TileTree::$FLAG_NEW; - - $marker = self::get_image("tiny_".self::get_type_suffix($type, false)); - $width = 10; - $height = 10; - $center_x = 5; - $center_y = 6; - $markercenter_x = 5; - $markercenter_y = 6; - - # Put the marker. If cache covers more caches, then put two markers instead of one. - - if ($count > 1) - { - imagecopy($this->im, $marker, $px - $center_x + 3, $py - $center_y - 2, 0, 0, $width, $height); - imagecopy($this->im, $marker, $px - $center_x, $py - $center_y, 0, 0, $width, $height); - } - elseif ($status == 1) - { - imagecopy($this->im, $marker, $px - $center_x, $py - $center_y, 0, 0, $width, $height); - } - - # If the cache is unavailable, mark it with X. - - if (($status != 1) && ($count == 1)) - { - $icon = self::get_image(($status == 2) ? "status_unavailable" - : "status_archived"); - imagecopy($this->im, $icon, $px - ($center_x - $markercenter_x) - 6, - $py - ($center_y - $markercenter_y) - 8, 0, 0, 16, 16); - } - } - - /** - * Examine all rows and decide which of them will get captions. - * Mark selected rows with TileTree::$FLAG_DRAW_CAPTION. - * - * Note: Calling this will alter the rows! - */ - private function decide_which_get_captions() - { - # We will split the tile (along with its margins) into 12x12 squares. - # A single geocache placed in square (x, y) gets the caption only - # when there are no other geocaches in any of the adjacent squares. - # This is efficient and yields acceptable results. - - $matrix = array(); - for ($i=0; $i<12; $i++) - $matrix[] = array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); - - foreach ($this->rows_ref as &$row_ref) - { - $mx = ($row_ref[1] + 64) >> 5; - $my = ($row_ref[2] + 64) >> 5; - if (($mx >= 12) || ($my >= 12)) continue; - if (($matrix[$mx][$my] === 0) && ($row_ref[7] == 1)) # 7 is count - $matrix[$mx][$my] = $row_ref[0]; # 0 is cache_id - else - $matrix[$mx][$my] = -1; - } - $selected_cache_ids = array(); - for ($mx=1; $mx<11; $mx++) - { - for ($my=1; $my<11; $my++) - { - if ($matrix[$mx][$my] > 0) # cache_id - { - # Check all adjacent squares. - - if ( ($matrix[$mx-1][$my-1] === 0) - && ($matrix[$mx-1][$my ] === 0) - && ($matrix[$mx-1][$my+1] === 0) - && ($matrix[$mx ][$my-1] === 0) - && ($matrix[$mx ][$my+1] === 0) - && ($matrix[$mx+1][$my-1] === 0) - && ($matrix[$mx+1][$my ] === 0) - && ($matrix[$mx+1][$my+1] === 0) - ) - $selected_cache_ids[] = $matrix[$mx][$my]; - } - } - } - - foreach ($this->rows_ref as &$row_ref) - if (in_array($row_ref[0], $selected_cache_ids)) - $row_ref[6] |= TileTree::$FLAG_DRAW_CAPTION; - } + /** + * Changing this will affect all generated hashes. You should increment it + * whenever you alter anything in the drawing algorithm. + */ + private static $VERSION = 59; + + /** + * Should be always true. You may temporarily set it to false, when you're + * testing/debugging a new set of static icons. + */ + private static $USE_STATIC_IMAGE_CACHE = true; + + /** + * Should be always true. You may temporarily set it to false, when you're + * testing/debugging a new captions renderer. + */ + private static $USE_CAPTIONS_CACHE = true; + + private $zoom; + private $rows_ref; + private $im; + + /** + * Takes the zoom level and the list of geocache descriptions. Note, that + * $rows_ref can be altered after calling render. If you don't want it to, + * you should pass a deep copy. + */ + public function __construct($zoom, &$rows_ref) + { + $this->zoom = $zoom; + $this->rows_ref = &$rows_ref; + } + + public function get_unique_hash() + { + return md5(json_encode(array( + "DefaultTileRenderer", + self::$VERSION, + $this->zoom, + $this->rows_ref + ))); + } + + public function get_content_type() + { + return "image/png"; + } + + public function render() + { + # Preprocess the rows. + + if ($this->zoom >= 5) + $this->decide_which_get_captions(); + + # Make a background. + + $this->im = imagecreatetruecolor(256, 256); + imagealphablending($this->im, false); + if ($this->zoom >= 13) $opacity = 15; + elseif ($this->zoom <= 12) $opacity = max(0, $this->zoom * 2 - 14); + $transparent = imagecolorallocatealpha($this->im, 0, 0, 0, 127 - $opacity); + imagefilledrectangle($this->im, 0, 0, 256, 256, $transparent); + imagealphablending($this->im, true); + + # Draw the caches. + + foreach ($this->rows_ref as &$row_ref) + $this->draw_cache($row_ref); + + # Return the result. + + ob_start(); + imagesavealpha($this->im, true); + imagepng($this->im); + imagedestroy($this->im); + return ob_get_clean(); + } + + private static function get_image($name, $opacity=1, $brightness=0, + $contrast=0, $r=0, $g=0, $b=0) + { + static $locmem_cache = array(); + + # Check locmem cache. + + $key = "$name/$opacity/$brightness/$contrast/$r/$g/$b"; + if (!isset($locmem_cache[$key])) + { + # Miss. Check default cache. WRTODO: upgrade to normal Cache? + + try + { + $cache_key = "tilesrc/".Okapi::$revision."/".self::$VERSION."/".$key; + $gd2_path = self::$USE_STATIC_IMAGE_CACHE + ? FileCache::get_file_path($cache_key) : null; + if ($gd2_path === null) + throw new Exception("Not in cache"); + # File cache hit. GD2 files are much faster to read than PNGs. + # This can throw an Exception (see bug#160). + $locmem_cache[$key] = imagecreatefromgd2($gd2_path); + } + catch (Exception $e) + { + # Miss again (or error decoding). Read the image from PNG. + + $locmem_cache[$key] = imagecreatefrompng($GLOBALS['rootpath']."okapi/static/tilemap/$name.png"); + + # Apply all wanted effects. + + if ($opacity != 1) + self::change_opacity($locmem_cache[$key], $opacity); + if ($contrast != 0) + imagefilter($locmem_cache[$key], IMG_FILTER_CONTRAST, $contrast); + if ($brightness != 0) + imagefilter($locmem_cache[$key], IMG_FILTER_BRIGHTNESS, $brightness); + if (($r != 0) || ($g != 0) || ($b != 0)) + { + imagefilter($locmem_cache[$key], IMG_FILTER_GRAYSCALE); + imagefilter($locmem_cache[$key], IMG_FILTER_COLORIZE, $r, $g, $b); + } + + # Cache the result. + + ob_start(); + imagegd2($locmem_cache[$key]); + $gd2 = ob_get_clean(); + FileCache::set($cache_key, $gd2); + } + } + return $locmem_cache[$key]; + } + + /** + * Extremely slow! Remember to cache the result! + */ + private static function change_opacity($im, $ratio) + { + imagealphablending($im, false); + + $w = imagesx($im); + $h = imagesy($im); + + for($x = 0; $x < $w; $x++) + { + for($y = 0; $y < $h; $y++) + { + $color = imagecolorat($im, $x, $y); + $new_color = ((max(0, floor(127 - ((127 - (($color >> 24) & 0x7f)) * $ratio))) & 0x7f) << 24) | ($color & 0x80ffffff); + imagesetpixel($im, $x, $y, $new_color); + } + } + + imagealphablending($im, true); + } + + private function draw_cache(&$cache_struct) + { + $capt = ($cache_struct[6] & TileTree::$FLAG_DRAW_CAPTION); + if (($this->zoom <= 8) && (!$capt)) + $this->draw_cache_tiny($cache_struct); + elseif (($this->zoom <= 13) && (!$capt)) + $this->draw_cache_medium($cache_struct); + else + $this->draw_cache_large($cache_struct); + + # Put caption (this flag is set only when there is plenty of space around). + + if ($cache_struct[6] & TileTree::$FLAG_DRAW_CAPTION) + { + $caption = $this->get_caption($cache_struct[0]); + imagecopy($this->im, $caption, $cache_struct[1] - 32, $cache_struct[2] + 6, 0, 0, 64, 26); + } + + } + + + private function draw_cache_large(&$cache_struct) + { + list($cache_id, $px, $py, $status, $type, $rating, $flags, $count) = $cache_struct; + + $found = $flags & TileTree::$FLAG_FOUND; + $own = $flags & TileTree::$FLAG_OWN; + $new = $flags & TileTree::$FLAG_NEW; + + # Prepare vars. + + if ($own) { + $key = 'large_outer_own'; + $a = 1; $br = 0; $c = 0; + $r = 0; $g = 0; $b = 0; + } elseif ($found) { + $key = 'large_outer_found'; + $a = ($flags & TileTree::$FLAG_DRAW_CAPTION) ? .7 : .35; + $br = 40; $c = 20; + //$a = 0.5; $br = 0; $c = 0; + $r = 0; $g = 0; $b = 0; + } elseif ($new) { + $key = 'large_outer_new'; + $a = 1; $br = 0; $c = 0; + $r = 0; $g = 0; $b = 0; + } else { + $key = 'large_outer'; + $a = 1; $br = 0; $c = 0; + $r = 0; $g = 0; $b = 0; + } + + # Put the outer marker (indicates the found/new/own status). + + $outer_marker = self::get_image($key, $a); + + $width = 40; + $height = 32; + $center_x = 12; + $center_y = 26; + $markercenter_x = 12; + $markercenter_y = 12; + + if ($count > 1) + imagecopy($this->im, $outer_marker, $px - $center_x + 3, $py - $center_y - 2, 0, 0, $width, $height); + imagecopy($this->im, $outer_marker, $px - $center_x, $py - $center_y, 0, 0, $width, $height); + + # Put the inner marker (indicates the type). + + $inner_marker = self::get_image("large_inner_".self::get_type_suffix( + $type, true), $a, $br, $c, $r, $g, $b); + imagecopy($this->im, $inner_marker, $px - 7, $py - 22, 0, 0, 16, 16); + + # If the cache is unavailable, mark it with X. + + if (($status != 1) && ($count == 1)) + { + $icon = self::get_image(($status == 2) ? "status_unavailable" + : "status_archived", $a); + imagecopy($this->im, $icon, $px - 1, $py - $center_y - 4, 0, 0, 16, 16); + } + + # Put the rating smile. :) + + if ($status == 1) + { + if ($rating >= 4.2) + { + if ($flags & TileTree::$FLAG_STAR) { + $icon = self::get_image("rating_grin", $a, $br, $c, $r, $g, $b); + imagecopy($this->im, $icon, $px - 7 - 6, $py - $center_y - 8, 0, 0, 16, 16); + $icon = self::get_image("rating_star", $a, $br, $c, $r, $g, $b); + imagecopy($this->im, $icon, $px - 7 + 6, $py - $center_y - 8, 0, 0, 16, 16); + } else { + $icon = self::get_image("rating_grin", $a, $br, $c, $r, $g, $b); + imagecopy($this->im, $icon, $px - 7, $py - $center_y - 8, 0, 0, 16, 16); + } + } + # This was commented out because users complained about too many smiles ;) + // elseif ($rating >= 3.6) { + // $icon = self::get_image("rating_smile", $a, $br, $c, $r, $g, $b); + // imagecopy($this->im, $icon, $px - 7, $py - $center_y - 8, 0, 0, 16, 16); + // } + } + + # Mark found caches with V. + + if ($found) + { + $icon = self::get_image("found", 0.7*$a, $br, $c, $r, $g, $b); + imagecopy($this->im, $icon, $px - 2, $py - $center_y - 3, 0, 0, 16, 16); + } + + } + + /** + * Split lines so that they fit inside the specified width. + */ + private static function wordwrap($font, $size, $maxWidth, $text) + { + $words = explode(" ", $text); + $lines = array(); + $line = ""; + $nextBonus = ""; + for ($i=0; ($i0); $i++) { + $word = isset($words[$i])?$words[$i]:""; + if (mb_strlen($nextBonus) > 0) + $word = $nextBonus." ".$word; + $nextBonus = ""; + while (true) { + $bbox = imagettfbbox($size, 0, $font, $line.$word); + $width = $bbox[2]-$bbox[0]; + if ($width <= $maxWidth) { + $line .= $word." "; + continue 2; + } + if (mb_strlen($line) > 0) { + $lines[] = trim($line); + $line = ""; + continue; + } + $nextBonus = $word[mb_strlen($word)-1].$nextBonus; + $word = mb_substr($word, 0, mb_strlen($word)-1); + continue; + } + } + if (mb_strlen($line) > 0) + $lines[] = trim($line); + return implode("\n", $lines); + } + + /** + * Return 64x26 bitmap with the caption (name) for the given geocache. + */ + private function get_caption($cache_id) + { + # Check cache. + + $cache_key = "tilecaption/".self::$VERSION."/".$cache_id; + $gd2 = self::$USE_CAPTIONS_CACHE ? Cache::get($cache_key) : null; + if ($gd2 === null) + { + # We'll work with 16x bigger image to get smoother interpolation. + + $im = imagecreatetruecolor(64*4, 26*4); + imagealphablending($im, false); + $transparent = imagecolorallocatealpha($im, 255, 255, 255, 127); + imagefilledrectangle($im, 0, 0, 64*4, 26*4, $transparent); + imagealphablending($im, true); + + # Get the name of the cache. + + $name = Db::select_value(" + select name + from caches + where cache_id = '".mysql_real_escape_string($cache_id)."' + "); + + # Split the name into a couple of lines. + + //$font = $GLOBALS['rootpath'].'util.sec/bt.ttf'; + $font = $GLOBALS['rootpath'].'okapi/static/tilemap/tahoma.ttf'; + $size = 25; + $lines = explode("\n", self::wordwrap($font, $size, 64*4 - 6*2, $name)); + + # For each line, compute its (x, y) so that the text is centered. + + $y = 0; + $positions = array(); + foreach ($lines as $line) + { + $bbox = imagettfbbox($size, 0, $font, $line); + $width = $bbox[2]-$bbox[0]; + $x = 128 - ($width >> 1); + $positions[] = array($x, $y); + $y += 36; + } + $drawer = function($x, $y, $color) use (&$lines, &$positions, &$im, &$size, &$font) + { + $len = count($lines); + for ($i=0; $i<$len; $i++) + { + $line = $lines[$i]; + list($offset_x, $offset_y) = $positions[$i]; + imagettftext($im, $size, 0, $offset_x + $x, $offset_y + $y, $color, $font, $line); + } + }; + + # Draw an outline. + + $outline_color = imagecolorallocatealpha($im, 255, 255, 255, 80); + for ($x=0; $x<=12; $x+=3) + for ($y=$size-3; $y<=$size+9; $y+=3) + $drawer($x, $y, $outline_color); + + # Add a slight shadow effect (on top of the outline). + + $drawer(9, $size + 3, imagecolorallocatealpha($im, 0, 0, 0, 110)); + + # Draw the caption. + + $drawer(6, $size + 3, imagecolorallocatealpha($im, 150, 0, 0, 40)); + + # Resample. + + imagealphablending($im, false); + $small = imagecreatetruecolor(64, 26); + imagealphablending($small, false); + imagecopyresampled($small, $im, 0, 0, 0, 0, 64, 26, 64*4, 26*4); + + # Cache it! + + ob_start(); + imagegd2($small); + $gd2 = ob_get_clean(); + Cache::set_scored($cache_key, $gd2); + } + + return imagecreatefromstring($gd2); + } + + private function draw_cache_medium(&$cache_struct) + { + list($cache_id, $px, $py, $status, $type, $rating, $flags, $count) = $cache_struct; + + $found = $flags & TileTree::$FLAG_FOUND; + $own = $flags & TileTree::$FLAG_OWN; + $new = $flags & TileTree::$FLAG_NEW; + if ($found && (!($flags & TileTree::$FLAG_DRAW_CAPTION))) + $a = .35; + else + $a = 1; + + # Put the marker (indicates the type). + + $marker = self::get_image("medium_".self::get_type_suffix($type, false), $a); + $width = 14; + $height = 14; + $center_x = 7; + $center_y = 8; + $markercenter_x = 7; + $markercenter_y = 8; + + if ($count > 1) + { + imagecopy($this->im, $marker, $px - $center_x + 3, $py - $center_y - 2, 0, 0, $width, $height); + imagecopy($this->im, $marker, $px - $center_x, $py - $center_y, 0, 0, $width, $height); + } + elseif ($status == 1) # don't put the marker for unavailable caches (X only) + { + imagecopy($this->im, $marker, $px - $center_x, $py - $center_y, 0, 0, $width, $height); + } + + # If the cache is unavailable, mark it with X. + + if (($status != 1) && ($count == 1)) + { + $icon = self::get_image(($status == 2) ? "status_unavailable" + : "status_archived"); + imagecopy($this->im, $icon, $px - ($center_x - $markercenter_x) - 6, + $py - ($center_y - $markercenter_y) - 8, 0, 0, 16, 16); + } + + # Put small versions of rating icons. + + if ($status == 1) + { + if ($rating >= 4.2) + { + if ($flags & TileTree::$FLAG_STAR) { + $icon = self::get_image("rating_grin_small", max(0.6, $a)); + imagecopy($this->im, $icon, $px - 5, $py - $center_y - 1, 0, 0, 6, 6); + $icon = self::get_image("rating_star_small", max(0.6, $a)); + imagecopy($this->im, $icon, $px - 2, $py - $center_y - 3, 0, 0, 10, 10); + } else { + $icon = self::get_image("rating_grin_small", max(0.6, $a)); + imagecopy($this->im, $icon, $px - 3, $py - $center_y - 1, 0, 0, 6, 6); + } + } + } + + if ($own) + { + # Mark own caches with additional overlay. + + $overlay = self::get_image("medium_overlay_own"); + imagecopy($this->im, $overlay, $px - $center_x, $py - $center_y, 0, 0, $width, $height); + } + elseif ($found) + { + # Mark found caches with V. + + $icon = self::get_image("found", 0.7*$a); + imagecopy($this->im, $icon, $px - ($center_x - $markercenter_x) - 7, + $py - ($center_y - $markercenter_y) - 9, 0, 0, 16, 16); + } + elseif ($new) + { + # Mark new caches with additional overlay. + + $overlay = self::get_image("medium_overlay_new"); + imagecopy($this->im, $overlay, $px - $center_x, $py - $center_y, 0, 0, $width, $height); + } + } + + private static function get_type_suffix($type, $extended_set) + { + switch ($type) { + case 2: return 'traditional'; + case 3: return 'multi'; + case 6: return 'event'; + case 7: return 'quiz'; + case 4: return 'virtual'; + case 1: return 'unknown'; + } + if ($extended_set) + { + switch ($type) { + case 10: return 'own'; + case 8: return 'moving'; + case 5: return 'webcam'; + } + } + return 'other'; + } + + private function draw_cache_tiny(&$cache_struct) + { + list($cache_id, $px, $py, $status, $type, $rating, $flags, $count) = $cache_struct; + + $found = $flags & TileTree::$FLAG_FOUND; + $own = $flags & TileTree::$FLAG_OWN; + $new = $flags & TileTree::$FLAG_NEW; + + $marker = self::get_image("tiny_".self::get_type_suffix($type, false)); + $width = 10; + $height = 10; + $center_x = 5; + $center_y = 6; + $markercenter_x = 5; + $markercenter_y = 6; + + # Put the marker. If cache covers more caches, then put two markers instead of one. + + if ($count > 1) + { + imagecopy($this->im, $marker, $px - $center_x + 3, $py - $center_y - 2, 0, 0, $width, $height); + imagecopy($this->im, $marker, $px - $center_x, $py - $center_y, 0, 0, $width, $height); + } + elseif ($status == 1) + { + imagecopy($this->im, $marker, $px - $center_x, $py - $center_y, 0, 0, $width, $height); + } + + # If the cache is unavailable, mark it with X. + + if (($status != 1) && ($count == 1)) + { + $icon = self::get_image(($status == 2) ? "status_unavailable" + : "status_archived"); + imagecopy($this->im, $icon, $px - ($center_x - $markercenter_x) - 6, + $py - ($center_y - $markercenter_y) - 8, 0, 0, 16, 16); + } + } + + /** + * Examine all rows and decide which of them will get captions. + * Mark selected rows with TileTree::$FLAG_DRAW_CAPTION. + * + * Note: Calling this will alter the rows! + */ + private function decide_which_get_captions() + { + # We will split the tile (along with its margins) into 12x12 squares. + # A single geocache placed in square (x, y) gets the caption only + # when there are no other geocaches in any of the adjacent squares. + # This is efficient and yields acceptable results. + + $matrix = array(); + for ($i=0; $i<12; $i++) + $matrix[] = array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + + foreach ($this->rows_ref as &$row_ref) + { + $mx = ($row_ref[1] + 64) >> 5; + $my = ($row_ref[2] + 64) >> 5; + if (($mx >= 12) || ($my >= 12)) continue; + if (($matrix[$mx][$my] === 0) && ($row_ref[7] == 1)) # 7 is count + $matrix[$mx][$my] = $row_ref[0]; # 0 is cache_id + else + $matrix[$mx][$my] = -1; + } + $selected_cache_ids = array(); + for ($mx=1; $mx<11; $mx++) + { + for ($my=1; $my<11; $my++) + { + if ($matrix[$mx][$my] > 0) # cache_id + { + # Check all adjacent squares. + + if ( ($matrix[$mx-1][$my-1] === 0) + && ($matrix[$mx-1][$my ] === 0) + && ($matrix[$mx-1][$my+1] === 0) + && ($matrix[$mx ][$my-1] === 0) + && ($matrix[$mx ][$my+1] === 0) + && ($matrix[$mx+1][$my-1] === 0) + && ($matrix[$mx+1][$my ] === 0) + && ($matrix[$mx+1][$my+1] === 0) + ) + $selected_cache_ids[] = $matrix[$mx][$my]; + } + } + } + + foreach ($this->rows_ref as &$row_ref) + if (in_array($row_ref[0], $selected_cache_ids)) + $row_ref[6] |= TileTree::$FLAG_DRAW_CAPTION; + } } \ No newline at end of file diff --git a/htdocs/okapi/services/caches/map/tiletree.inc.php b/htdocs/okapi/services/caches/map/tiletree.inc.php index 4cac67d6..ba7fc6d2 100644 --- a/htdocs/okapi/services/caches/map/tiletree.inc.php +++ b/htdocs/okapi/services/caches/map/tiletree.inc.php @@ -21,274 +21,283 @@ use okapi\OkapiLock; class TileTree { - # Static flags (stored in the database). - public static $FLAG_STAR = 0x01; - public static $FLAG_HAS_TRACKABLES = 0x02; - public static $FLAG_NOT_YET_FOUND = 0x04; + # Static flags (stored in the database). + public static $FLAG_STAR = 0x01; + public static $FLAG_HAS_TRACKABLES = 0x02; + public static $FLAG_NOT_YET_FOUND = 0x04; - # Dynamic flags (added at runtime). - public static $FLAG_FOUND = 0x0100; - public static $FLAG_OWN = 0x0200; - public static $FLAG_NEW = 0x0400; - public static $FLAG_DRAW_CAPTION = 0x0800; + # Dynamic flags (added at runtime). + public static $FLAG_FOUND = 0x0100; + public static $FLAG_OWN = 0x0200; + public static $FLAG_NEW = 0x0400; + public static $FLAG_DRAW_CAPTION = 0x0800; - /** - * Return null if not computed, 1 if computed and empty, 2 if computed and not empty. - */ - public static function get_tile_status($zoom, $x, $y) - { - return Db::select_value(" - select status - from okapi_tile_status - where - z = '".mysql_real_escape_string($zoom)."' - and x = '".mysql_real_escape_string($x)."' - and y = '".mysql_real_escape_string($y)."' - "); - } + /** + * Return null if not computed, 1 if computed and empty, 2 if computed and not empty. + */ + public static function get_tile_status($zoom, $x, $y) + { + return Db::select_value(" + select status + from okapi_tile_status + where + z = '".mysql_real_escape_string($zoom)."' + and x = '".mysql_real_escape_string($x)."' + and y = '".mysql_real_escape_string($y)."' + "); + } - /** - * Return MySQL's result set iterator over all caches which are present - * in the given result set AND in the given tile. - * - * Each row is an array of the following format: - * list(cache_id, $pixel_x, $pixel_y, status, type, rating, flags, count). - * - * Note that $pixels can also be negative or >=256 (up to a margin of 32px). - * Count is the number of other caches "eclipsed" by this geocache (such - * eclipsed geocaches are not included in the result). - */ - public static function query_fast($zoom, $x, $y, $set_id) - { - # First, we check if the cache-set for this tile was already computed - # (and if it was, was it empty). + /** + * Return MySQL's result set iterator over all caches which are present + * in the given result set AND in the given tile. + * + * Each row is an array of the following format: + * list(cache_id, $pixel_x, $pixel_y, status, type, rating, flags, count). + * + * Note that $pixels can also be negative or >=256 (up to a margin of 32px). + * Count is the number of other caches "eclipsed" by this geocache (such + * eclipsed geocaches are not included in the result). + */ + public static function query_fast($zoom, $x, $y, $set_id) + { + # First, we check if the cache-set for this tile was already computed + # (and if it was, was it empty). - $status = self::get_tile_status($zoom, $x, $y); - if ($status === null) # Not yet computed. - { - # Note, that computing the tile does not involve taking any - # search parameters. + $status = self::get_tile_status($zoom, $x, $y); + if ($status === null) # Not yet computed. + { + # Note, that computing the tile does not involve taking any + # search parameters. - $status = self::compute_tile($zoom, $x, $y); - } + $status = self::compute_tile($zoom, $x, $y); + } - if ($status === 1) # Computed and empty. - { - # This tile was already computed and it is empty. - return null; - } + if ($status === 1) # Computed and empty. + { + # This tile was already computed and it is empty. + return null; + } - # If we got here, then the tile is computed and not empty (status 2). + # If we got here, then the tile is computed and not empty (status 2). - $tile_upper_x = $x << 8; - $tile_leftmost_y = $y << 8; + $tile_upper_x = $x << 8; + $tile_leftmost_y = $y << 8; - $zoom_escaped = "'".mysql_real_escape_string($zoom)."'"; - $tile_upper_x_escaped = "'".mysql_real_escape_string($tile_upper_x)."'"; - $tile_leftmost_y_escaped = "'".mysql_real_escape_string($tile_leftmost_y)."'"; - return Db::query(" - select - otc.cache_id, - 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, - otc.status, otc.type, otc.rating, otc.flags, count(*) - from - okapi_tile_caches otc, - okapi_search_results osr - where - z = $zoom_escaped - and x = '".mysql_real_escape_string($x)."' - and y = '".mysql_real_escape_string($y)."' - and otc.cache_id = osr.cache_id - and osr.set_id = '".mysql_real_escape_string($set_id)."' - group by - z21x >> (3 + (21 - $zoom_escaped)), - z21y >> (3 + (21 - $zoom_escaped)) - order by - z21y >> (3 + (21 - $zoom_escaped)), - z21x >> (3 + (21 - $zoom_escaped)) - "); - } + $zoom_escaped = "'".mysql_real_escape_string($zoom)."'"; + $tile_upper_x_escaped = "'".mysql_real_escape_string($tile_upper_x)."'"; + $tile_leftmost_y_escaped = "'".mysql_real_escape_string($tile_leftmost_y)."'"; + return Db::query(" + select + otc.cache_id, + 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, + otc.status, otc.type, otc.rating, otc.flags, count(*) + from + okapi_tile_caches otc, + okapi_search_results osr + where + z = $zoom_escaped + and x = '".mysql_real_escape_string($x)."' + and y = '".mysql_real_escape_string($y)."' + and otc.cache_id = osr.cache_id + and osr.set_id = '".mysql_real_escape_string($set_id)."' + group by + z21x >> (3 + (21 - $zoom_escaped)), + z21y >> (3 + (21 - $zoom_escaped)) + order by + z21y >> (3 + (21 - $zoom_escaped)), + z21x >> (3 + (21 - $zoom_escaped)) + "); + } - /** - * Precache the ($zoom, $x, $y) slot in the okapi_tile_caches table. - */ - public static function compute_tile($zoom, $x, $y) - { - $time_started = microtime(true); + /** + * Precache the ($zoom, $x, $y) slot in the okapi_tile_caches table. + */ + public static function compute_tile($zoom, $x, $y) + { + $time_started = microtime(true); - # Note, that multiple threads may try to compute tiles simulatanously. - # For low-level tiles, this can be expensive. WRTODO: Think of some - # appropriate locks. + # Note, that multiple threads may try to compute tiles simulatanously. + # For low-level tiles, this can be expensive. WRTODO: Think of some + # appropriate locks. - $status = self::get_tile_status($zoom, $x, $y); - if ($status !== null) - return $status; + $status = self::get_tile_status($zoom, $x, $y); + if ($status !== null) + return $status; - if ($zoom === 0) - { - # When computing zoom zero, we don't have a parent to speed up - # the computation. We need to use the caches table. Note, that - # zoom level 0 contains *entire world*, so we don't have to use - # any WHERE condition in the following query. + if ($zoom === 0) + { + # When computing zoom zero, we don't have a parent to speed up + # the computation. We need to use the caches table. Note, that + # zoom level 0 contains *entire world*, so we don't have to use + # any WHERE condition in the following query. - # This can be done a little faster (without the use of internal requests), - # but there is *no need* to - this query is run seldom and is cached. + # This can be done a little faster (without the use of internal requests), + # but there is *no need* to - this query is run seldom and is cached. - $params = array(); - $params['status'] = "Available|Temporarily unavailable|Archived"; # we want them all - $params['limit'] = "10000000"; # no limit + $params = array(); + $params['status'] = "Available|Temporarily unavailable|Archived"; # we want them all + $params['limit'] = "10000000"; # no limit - $internal_request = new OkapiInternalRequest(new OkapiInternalConsumer(), null, $params); - $internal_request->skip_limits = true; - $response = OkapiServiceRunner::call("services/caches/search/all", $internal_request); - $cache_codes = $response['results']; + $internal_request = new OkapiInternalRequest(new OkapiInternalConsumer(), null, $params); + $internal_request->skip_limits = true; + $response = OkapiServiceRunner::call("services/caches/search/all", $internal_request); + $cache_codes = $response['results']; - $internal_request = new OkapiInternalRequest(new OkapiInternalConsumer(), null, array( - 'cache_codes' => implode('|', $cache_codes), - 'fields' => 'internal_id|code|name|location|type|status|rating|recommendations|founds|trackables_count' - )); - $internal_request->skip_limits = true; - $caches = OkapiServiceRunner::call("services/caches/geocaches", $internal_request); + $internal_request = new OkapiInternalRequest(new OkapiInternalConsumer(), null, array( + 'cache_codes' => implode('|', $cache_codes), + 'fields' => 'internal_id|code|name|location|type|status|rating|recommendations|founds|trackables_count' + )); + $internal_request->skip_limits = true; + $caches = OkapiServiceRunner::call("services/caches/geocaches", $internal_request); - foreach ($caches as $cache) - { - $row = self::generate_short_row($cache); - Db::execute(" - replace into okapi_tile_caches ( - z, x, y, cache_id, z21x, z21y, status, type, rating, flags - ) values ( - 0, 0, 0, - '".mysql_real_escape_string($row[0])."', - '".mysql_real_escape_string($row[1])."', - '".mysql_real_escape_string($row[2])."', - '".mysql_real_escape_string($row[3])."', - '".mysql_real_escape_string($row[4])."', - ".(($row[5] === null) ? "null" : "'".mysql_real_escape_string($row[5])."'").", - '".mysql_real_escape_string($row[6])."' - ); - "); - } - $status = 2; - } - else - { - # We will use the parent tile to compute the contents of this tile. + foreach ($caches as $cache) + { + $row = self::generate_short_row($cache); + if (!$row) { + /* Some caches cannot be included, e.g. the ones near the poles. */ + continue; + } + Db::execute(" + replace into okapi_tile_caches ( + z, x, y, cache_id, z21x, z21y, status, type, rating, flags + ) values ( + 0, 0, 0, + '".mysql_real_escape_string($row[0])."', + '".mysql_real_escape_string($row[1])."', + '".mysql_real_escape_string($row[2])."', + '".mysql_real_escape_string($row[3])."', + '".mysql_real_escape_string($row[4])."', + ".(($row[5] === null) ? "null" : "'".mysql_real_escape_string($row[5])."'").", + '".mysql_real_escape_string($row[6])."' + ); + "); + } + $status = 2; + } + else + { + # We will use the parent tile to compute the contents of this tile. - $parent_zoom = $zoom - 1; - $parent_x = $x >> 1; - $parent_y = $y >> 1; + $parent_zoom = $zoom - 1; + $parent_x = $x >> 1; + $parent_y = $y >> 1; - $status = self::get_tile_status($parent_zoom, $parent_x, $parent_y); - if ($status === null) # Not computed. - { - $time_started = microtime(true); - $status = self::compute_tile($parent_zoom, $parent_x, $parent_y); - } + $status = self::get_tile_status($parent_zoom, $parent_x, $parent_y); + if ($status === null) # Not computed. + { + $time_started = microtime(true); + $status = self::compute_tile($parent_zoom, $parent_x, $parent_y); + } - if ($status === 1) # Computed and empty. - { - # No need to check. - } - else # Computed, not empty. - { - $scale = 8 + 21 - $zoom; - $parentcenter_z21x = (($parent_x << 1) | 1) << $scale; - $parentcenter_z21y = (($parent_y << 1) | 1) << $scale; - $margin = 1 << ($scale - 2); - $left_z21x = (($parent_x << 1) << $scale) - $margin; - $right_z21x = ((($parent_x + 1) << 1) << $scale) + $margin; - $top_z21y = (($parent_y << 1) << $scale) - $margin; - $bottom_z21y = ((($parent_y + 1) << 1) << $scale) + $margin; + if ($status === 1) # Computed and empty. + { + # No need to check. + } + else # Computed, not empty. + { + $scale = 8 + 21 - $zoom; + $parentcenter_z21x = (($parent_x << 1) | 1) << $scale; + $parentcenter_z21y = (($parent_y << 1) | 1) << $scale; + $margin = 1 << ($scale - 2); + $left_z21x = (($parent_x << 1) << $scale) - $margin; + $right_z21x = ((($parent_x + 1) << 1) << $scale) + $margin; + $top_z21y = (($parent_y << 1) << $scale) - $margin; + $bottom_z21y = ((($parent_y + 1) << 1) << $scale) + $margin; - # Choose the right quarter. - # |1 2| - # |3 4| + # Choose the right quarter. + # |1 2| + # |3 4| - if ($x & 1) # 2 or 4 - $left_z21x = $parentcenter_z21x - $margin; - else # 1 or 3 - $right_z21x = $parentcenter_z21x + $margin; - if ($y & 1) # 3 or 4 - $top_z21y = $parentcenter_z21y - $margin; - else # 1 or 2 - $bottom_z21y = $parentcenter_z21y + $margin; + if ($x & 1) # 2 or 4 + $left_z21x = $parentcenter_z21x - $margin; + else # 1 or 3 + $right_z21x = $parentcenter_z21x + $margin; + if ($y & 1) # 3 or 4 + $top_z21y = $parentcenter_z21y - $margin; + else # 1 or 2 + $bottom_z21y = $parentcenter_z21y + $margin; - # Cache the result. + # Cache the result. - Db::execute(" - replace into okapi_tile_caches ( - z, x, y, cache_id, z21x, z21y, status, type, rating, flags - ) - select - '".mysql_real_escape_string($zoom)."', - '".mysql_real_escape_string($x)."', - '".mysql_real_escape_string($y)."', - cache_id, z21x, z21y, status, type, rating, flags - from okapi_tile_caches - where - z = '".mysql_real_escape_string($parent_zoom)."' - and x = '".mysql_real_escape_string($parent_x)."' - and y = '".mysql_real_escape_string($parent_y)."' - and z21x between $left_z21x and $right_z21x - and z21y between $top_z21y and $bottom_z21y - "); - $test = Db::select_value(" - select 1 - from okapi_tile_caches - where - z = '".mysql_real_escape_string($zoom)."' - and x = '".mysql_real_escape_string($x)."' - and y = '".mysql_real_escape_string($y)."' - limit 1; - "); - if ($test) - $status = 2; - else - $status = 1; - } - } + Db::execute(" + replace into okapi_tile_caches ( + z, x, y, cache_id, z21x, z21y, status, type, rating, flags + ) + select + '".mysql_real_escape_string($zoom)."', + '".mysql_real_escape_string($x)."', + '".mysql_real_escape_string($y)."', + cache_id, z21x, z21y, status, type, rating, flags + from okapi_tile_caches + where + z = '".mysql_real_escape_string($parent_zoom)."' + and x = '".mysql_real_escape_string($parent_x)."' + and y = '".mysql_real_escape_string($parent_y)."' + and z21x between $left_z21x and $right_z21x + and z21y between $top_z21y and $bottom_z21y + "); + $test = Db::select_value(" + select 1 + from okapi_tile_caches + where + z = '".mysql_real_escape_string($zoom)."' + and x = '".mysql_real_escape_string($x)."' + and y = '".mysql_real_escape_string($y)."' + limit 1; + "); + if ($test) + $status = 2; + else + $status = 1; + } + } - # Mark tile as computed. + # Mark tile as computed. - Db::execute(" - replace into okapi_tile_status (z, x, y, status) - values ( - '".mysql_real_escape_string($zoom)."', - '".mysql_real_escape_string($x)."', - '".mysql_real_escape_string($y)."', - '".mysql_real_escape_string($status)."' - ); - "); + Db::execute(" + replace into okapi_tile_status (z, x, y, status) + values ( + '".mysql_real_escape_string($zoom)."', + '".mysql_real_escape_string($x)."', + '".mysql_real_escape_string($y)."', + '".mysql_real_escape_string($status)."' + ); + "); - return $status; - } + return $status; + } - /** - * Convert OKAPI's cache object to a short database row to be inserted - * into okapi_tile_caches table. Returns the list of the following attributes: - * cache_id, z21x, z21y, status, type, rating, flags (rating might be null!). - */ - public static function generate_short_row($cache) - { - list($lat, $lon) = explode("|", $cache['location']); - list($z21x, $z21y) = self::latlon_to_z21xy($lat, $lon); - $flags = 0; - if (($cache['founds'] > 6) && (($cache['recommendations'] / $cache['founds']) > 0.3)) - $flags |= self::$FLAG_STAR; - if ($cache['trackables_count'] > 0) - $flags |= self::$FLAG_HAS_TRACKABLES; - if ($cache['founds'] == 0) - $flags |= self::$FLAG_NOT_YET_FOUND; - return array($cache['internal_id'], $z21x, $z21y, Okapi::cache_status_name2id($cache['status']), - Okapi::cache_type_name2id($cache['type']), $cache['rating'], $flags); - } + /** + * Convert OKAPI's cache object to a short database row to be inserted + * into okapi_tile_caches table. Returns the list of the following attributes: + * cache_id, z21x, z21y, status, type, rating, flags (rating might be null!). + */ + public static function generate_short_row($cache) + { + list($lat, $lon) = explode("|", $cache['location']); + try { + list($z21x, $z21y) = self::latlon_to_z21xy($lat, $lon); + } catch (Exception $e) { + /* E.g. division by zero, if the cache is placed at the north pole. */ + return false; + } + $flags = 0; + if (($cache['founds'] > 6) && (($cache['recommendations'] / $cache['founds']) > 0.3)) + $flags |= self::$FLAG_STAR; + if ($cache['trackables_count'] > 0) + $flags |= self::$FLAG_HAS_TRACKABLES; + if ($cache['founds'] == 0) + $flags |= self::$FLAG_NOT_YET_FOUND; + return array($cache['internal_id'], $z21x, $z21y, Okapi::cache_status_name2id($cache['status']), + Okapi::cache_type_name2id($cache['type']), $cache['rating'], $flags); + } - private static function latlon_to_z21xy($lat, $lon) - { - $offset = 128 << 21; - $x = round($offset + ($offset * $lon / 180)); - $y = round($offset - $offset/pi() * log((1 + sin($lat * pi() / 180)) / (1 - sin($lat * pi() / 180))) / 2); - return array($x, $y); - } + private static function latlon_to_z21xy($lat, $lon) + { + $offset = 128 << 21; + $x = round($offset + ($offset * $lon / 180)); + $y = round($offset - $offset/pi() * log((1 + sin($lat * pi() / 180)) / (1 - sin($lat * pi() / 180))) / 2); + return array($x, $y); + } } \ No newline at end of file diff --git a/htdocs/okapi/services/caches/mark.php b/htdocs/okapi/services/caches/mark.php index 846c2372..d48e45cd 100644 --- a/htdocs/okapi/services/caches/mark.php +++ b/htdocs/okapi/services/caches/mark.php @@ -17,74 +17,74 @@ use okapi\Settings; class WebService { - public static function options() - { - return array( - 'min_auth_level' => 3 - ); - } + public static function options() + { + return array( + 'min_auth_level' => 3 + ); + } - public static function call(OkapiRequest $request) - { - # 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 - # on it - this will also throw a proper exception if it doesn't exist. + public static function call(OkapiRequest $request) + { + # 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 + # on it - this will also throw a proper exception if it doesn't exist. - $cache_code = $request->get_parameter('cache_code'); - if ($cache_code == null) - throw new ParamMissing('cache_code'); - $geocache = OkapiServiceRunner::call('services/caches/geocache', new OkapiInternalRequest( - $request->consumer, $request->token, array('cache_code' => $cache_code, 'fields' => 'internal_id'))); + $cache_code = $request->get_parameter('cache_code'); + if ($cache_code == null) + throw new ParamMissing('cache_code'); + $geocache = OkapiServiceRunner::call('services/caches/geocache', new OkapiInternalRequest( + $request->consumer, $request->token, array('cache_code' => $cache_code, 'fields' => 'internal_id'))); - # watched + # watched - if ($tmp = $request->get_parameter('watched')) - { - if (!in_array($tmp, array('true', 'false', 'unchanged'))) - throw new InvalidParam('watched', $tmp); - if ($tmp == 'true') - Db::execute(" - insert ignore into cache_watches (cache_id, user_id) - values ( - '".mysql_real_escape_string($geocache['internal_id'])."', - '".mysql_real_escape_string($request->token->user_id)."' - ); - "); - elseif ($tmp == 'false') - Db::execute(" - delete from cache_watches - where - cache_id = '".mysql_real_escape_string($geocache['internal_id'])."' - and user_id = '".mysql_real_escape_string($request->token->user_id)."'; - "); - } + if ($tmp = $request->get_parameter('watched')) + { + if (!in_array($tmp, array('true', 'false', 'unchanged'))) + throw new InvalidParam('watched', $tmp); + if ($tmp == 'true') + Db::execute(" + insert ignore into cache_watches (cache_id, user_id) + values ( + '".mysql_real_escape_string($geocache['internal_id'])."', + '".mysql_real_escape_string($request->token->user_id)."' + ); + "); + elseif ($tmp == 'false') + Db::execute(" + delete from cache_watches + where + cache_id = '".mysql_real_escape_string($geocache['internal_id'])."' + and user_id = '".mysql_real_escape_string($request->token->user_id)."'; + "); + } - # ignored + # ignored - if ($tmp = $request->get_parameter('ignored')) - { - if (!in_array($tmp, array('true', 'false', 'unchanged'))) - throw new InvalidParam('ignored', $tmp); - if ($tmp == 'true') - Db::execute(" - insert ignore into cache_ignore (cache_id, user_id) - values ( - '".mysql_real_escape_string($geocache['internal_id'])."', - '".mysql_real_escape_string($request->token->user_id)."' - ); - "); - elseif ($tmp == 'false') - Db::execute(" - delete from cache_ignore - where - cache_id = '".mysql_real_escape_string($geocache['internal_id'])."' - and user_id = '".mysql_real_escape_string($request->token->user_id)."' - "); - } + if ($tmp = $request->get_parameter('ignored')) + { + if (!in_array($tmp, array('true', 'false', 'unchanged'))) + throw new InvalidParam('ignored', $tmp); + if ($tmp == 'true') + Db::execute(" + insert ignore into cache_ignore (cache_id, user_id) + values ( + '".mysql_real_escape_string($geocache['internal_id'])."', + '".mysql_real_escape_string($request->token->user_id)."' + ); + "); + elseif ($tmp == 'false') + Db::execute(" + delete from cache_ignore + where + cache_id = '".mysql_real_escape_string($geocache['internal_id'])."' + and user_id = '".mysql_real_escape_string($request->token->user_id)."' + "); + } - $result = array( - 'success' => true, - ); - return Okapi::formatted_response($request, $result); - } + $result = array( + 'success' => true, + ); + return Okapi::formatted_response($request, $result); + } } diff --git a/htdocs/okapi/services/caches/mark.xml b/htdocs/okapi/services/caches/mark.xml index ff918770..452d860a 100644 --- a/htdocs/okapi/services/caches/mark.xml +++ b/htdocs/okapi/services/caches/mark.xml @@ -1,33 +1,33 @@ - Mark cache as watched or ignored - 166 - -

    This method allows your users to mark the geocache as watched or - ignored. - Read the docs on separate parameters for details.

    -
    - -

    Code of the geocache.

    -
    - -

    Mark (or unmark) the cache as watched. This should be true, - false or unchanged. You may access the current state of this - flag with the is_watched field of the geocache method.

    -
    - -

    Mark (or unmark) the cache as ignored. This should be true, - false or unchanged. You may access the current state of this - flag with the is_ignored field of the geocache method.

    -
    - - -

    A dictionary of the following structure:

    -
      -
    • success - true, if all went well.
    • -
    -

    Please note, that currently this will always be true! Nothing can go - 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, - then you may assume that all went well.

    -
    + Mark cache as watched or ignored + 166 + +

    This method allows your users to mark the geocache as watched or + ignored. + Read the docs on separate parameters for details.

    +
    + +

    Code of the geocache.

    +
    + +

    Mark (or unmark) the cache as watched. This should be true, + false or unchanged. You may access the current state of this + flag with the is_watched field of the geocache method.

    +
    + +

    Mark (or unmark) the cache as ignored. This should be true, + false or unchanged. You may access the current state of this + flag with the is_ignored field of the geocache method.

    +
    + + +

    A dictionary of the following structure:

    +
      +
    • success - true, if all went well.
    • +
    +

    Please note, that currently this will always be true! Nothing can go + 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, + then you may assume that all went well.

    +
    \ No newline at end of file diff --git a/htdocs/okapi/services/caches/search/all.php b/htdocs/okapi/services/caches/search/all.php index 454bebab..1d4bc497 100644 --- a/htdocs/okapi/services/caches/search/all.php +++ b/htdocs/okapi/services/caches/search/all.php @@ -20,17 +20,18 @@ require_once('searching.inc.php'); class WebService { - public static function options() - { - return array( - 'min_auth_level' => 1 - ); - } + public static function options() + { + return array( + 'min_auth_level' => 1 + ); + } - public static function call(OkapiRequest $request) - { - $search_params = SearchAssistant::get_common_search_params($request); - $result = SearchAssistant::get_common_search_result($search_params); - return Okapi::formatted_response($request, $result); - } + public static function call(OkapiRequest $request) + { + $search_assistant = new SearchAssistant($request); + $search_assistant->prepare_common_search_params(); + $result = $search_assistant->get_common_search_result(); + return Okapi::formatted_response($request, $result); + } } diff --git a/htdocs/okapi/services/caches/search/all.xml b/htdocs/okapi/services/caches/search/all.xml index 5b93d179..18d134dd 100644 --- a/htdocs/okapi/services/caches/search/all.xml +++ b/htdocs/okapi/services/caches/search/all.xml @@ -1,209 +1,209 @@ - Search for geocaches - 15 - -

    Search for geocaches using some simple filters. - All of them are optional, but you surely want to use at least some of them.

    -
      -
    • If you're looking for a way to keep your geocaching database in sync with ours, - have a peek at the replicate module.
    • -
    • Usually you will want to use search_and_retrieve method instead of this one. - This way, you can get much more data in one request.
    • -
    -
    - -

    Pipe-separated list of cache type codes. If given, the results will be limited - to the given cache types. For a list of cache type codes, see services/caches/geocache - method.

    -

    Notice: If you want to include all cache types except - given ones, prepend your list with the "-" sign.

    -
    - -

    Pipe-separated list of status codes. Only caches matching any of the given - statuses will included in the response. For a list of cache status codes, see - services/caches/geocache method.

    -
    - -

    Pipe-separated list of user IDs. If given, the list of returned caches - will be limited to those owned by the given users.

    -

    Notice: User ID and Username are two different things!

    -

    Notice: If you want to include all caches except the ones - owned by the given users, prepend your list with the "-" sign.

    -
    - -

    UTF-8 encoded string - the name (or part of the name) of the cache.

    -

    Allowed wildcard characters:

    -
      -
    • asterisk ("*") will match any string of characters,
    • -
    • underscore ("_") will match any single character.
    • -
    -

    Name matching is case-insensitive. Maximum length for the name parameter - is 100 characters.

    -

    Examples:

    -
      -
    • "name=the *" returns caches which name starts with "the " (or "The ", etc.),
    • -
    • "name=water tower" returns caches which have exactly that name (case insensitive),
    • -
    • "name=*water tower*" will match caches like "Beautiful Water Tower II",
    • -
    • "name=*tower_" will match "Water Towers" but will not match "Water Tower".
    • -
    -

    Notice: Cache code and name are two different things - if you want to - find a cache by its code, use the services/caches/geocache method.

    -
    - -

    A string "X-Y", where X and Y are integers between 1 and 5, and X <= Y. - Only caches with terrain rating between these numbers (inclusive) will be returned.

    -
    - -

    A string "X-Y", where X and Y are integers between 1 and 5, and X <= Y. - Only caches with difficulty rating between these numbers (inclusive) will be returned.

    -
    - -

    Deprecated. Please use size2 instead - this will allow you to - differentiate "nano" vs. "micro" and "none" vs. "other" sizes - (details).

    -

    A string "X-Y", where X and Y are integers between 1 and 5, and X <= Y. - Only caches with size attribute between these numbers (inclusive) will be returned - (1 - micro, 5 - very big).

    -

    Notice: 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 - with no container included, append "|X" suffix to the value of this parameter - (e.g. "3-5|X").

    -
    - -

    Pipe-separated list of "size2 codes". Only matching caches will be - included. The codes are: 'none', 'nano', 'micro', 'small', 'regular', - 'large', 'xlarge', 'other'.

    -

    Note: 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).

    -
    - -

    A string "X-Y", where X and Y are integers between 1 and 5, and X <= Y. - Only caches with an overall rating between these numbers (inclusive) will be returned - (1 - poor, 5 - excellent).

    -

    Notice: 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 - to the value of this parameter (e.g. "3-5|X").

    -

    Notice: Some OC installations do not provide ratings for their geocaches. - On such installation, this parameter will be ignored.

    -
    - -

    There are two possible value-types for this argument:

    -
      -
    • Integer N. If N given, the result will contain only those caches which have - at least N recommendations.
    • -
    • Integer N with a percent sign appended (e.g. "20%"). The result will contain - only those caches which have at last N% ratio of recommendations/founds. - Please note, that this is useful only with conjunction with min_founds - parameter.
    • -
    -
    - -

    Integer N. If N given, the result will contain only those caches which have - been found at least N times. Useful if you want to skip "unverified" caches.

    -
    - -

    Integer N. If N given, the result will contain only those caches which have - been found at most N times. Useful for FTF hunters.

    -
    - -

    A date and time string. This should be in ISO 8601 format (currently any - format acceptable by PHP's strtotime - function also will do, but most of them don't handle time zones properly, - try to use ISO 8601!).

    -

    If given, the list of returned geocaches will be limited to those which were - created or modified since that date. (Note: currently caches do not get - "modified" when user makes a log entry.)

    -
    - -

    Notice: This parameter may be used only for requests signed with an Access Token - (you will need to use Level 3 Authentication).

    -

    Should be one of the following:

    -
      -
    • found_only - only caches found by the user will be returned,
    • -
    • notfound_only - only caches not found by the user will be returned,
    • -
    • either - all caches will be returned.
    • -
    -
    - -

    User UUID. If given, the response will only include geocaches found by - the given user.

    -
    - -

    User UUID. If given, the response will only include geocaches not found by - the given user.

    -
    - -

    Notice: This parameter may be used only for requests signed with an Access Token - (you will need to use Level 3 Authentication).

    -

    Boolean. If set to true, only caches which the user has marked as watched - 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").

    -
    - -

    Notice: This parameter may be used only for requests signed - with an Access Token (you will need to use Level 3 Authentication).

    -

    Boolean. If set to true, caches which the user has marked as ignored - will not be included in the result.

    -
    - -

    Notice: This parameter may be used only for requests signed - with an Access Token (you will need to use Level 3 Authentication). - See owner_uuid parameter if you don't want to use OAuth.

    -

    Boolean. If set to true, caches which the user is an owner of will - not be included in the result.

    -
    - - Boolean. If set to true, only caches with at least one trackable - will be included in the result. - - - Boolean. If set to true, only caches which have not yet been - found by anyone will be included. - - -

    ID of a set previously created with the search/save method. - If given, the results are ANDed - together with this set.

    -

    If you want to list the set contents only, please note the default - value of the status parameter! You may want to override it - in order to include unavailable and/or archived geocaches within the set.

    -
    - -

    Integer in range 1..500. Maximum number of cache codes returned.

    -
    - -

    An offset, the amount of items to skip at the beginning of the result. Combined with limit - allows pagination of the result set. Please note, that the sum of offset and limit still cannot - exceed 500. You MAY NOT use this method to list all the caches, it is for searching only. - Have a peek at the replicate module if you need all the caches.

    -
    - -

    Pipe separated list of fields to order the results by. Prefix the field name with - a '-' sign to indicate a descending order.

    -

    Currently, fields which you can order by include: code, name, - founds, rcmds, rcmds% (tell us if you want more).

    -

    Examples:

    -
      -
    • to order by cache name use "order_by=name" or "order_by=+name",
    • -
    • to have the most recommended caches caches in front, use "order_by=-rcmds%",
    • -
    • multicolumn sorting is also allowed, ex. "order_by=-founds|name"
    • -
    -

    Note: Try to avoid executing separate OKAPI request every time you - want to sort the data which you already have. Consider caching and - sorting on client side. This will speed up things for both sides.

    -

    Note: For most other methods (like bbox and nearest), - the default order is by the distance from the center. If you supply - custom order_by parameter, then we will try to order by your preference - first, and then by the distance later.

    -
    - - -

    A dictionary of the following structure:

    -
      -
    • results - a list of cache codes,
    • -
    • more - boolean, true means that there were more - results for your query, but they were not returned because of the - limit parameter.
    • -
    -
    + Search for geocaches + 15 + +

    Search for geocaches using some simple filters. + All of them are optional, but you surely want to use at least some of them.

    +
      +
    • If you're looking for a way to keep your geocaching database in sync with ours, + have a peek at the replicate module.
    • +
    • Usually you will want to use search_and_retrieve method instead of this one. + This way, you can get much more data in one request.
    • +
    +
    + +

    Pipe-separated list of cache type codes. If given, the results will be limited + to the given cache types. For a list of cache type codes, see services/caches/geocache + method.

    +

    Notice: If you want to include all cache types except + given ones, prepend your list with the "-" sign.

    +
    + +

    Pipe-separated list of status codes. Only caches matching any of the given + statuses will included in the response. For a list of cache status codes, see + services/caches/geocache method.

    +
    + +

    Pipe-separated list of user IDs. If given, the list of returned caches + will be limited to those owned by the given users.

    +

    Notice: User ID and Username are two different things!

    +

    Notice: If you want to include all caches except the ones + owned by the given users, prepend your list with the "-" sign.

    +
    + +

    UTF-8 encoded string - the name (or part of the name) of the cache.

    +

    Allowed wildcard characters:

    +
      +
    • asterisk ("*") will match any string of characters,
    • +
    • underscore ("_") will match any single character.
    • +
    +

    Name matching is case-insensitive. Maximum length for the name parameter + is 100 characters.

    +

    Examples:

    +
      +
    • "name=the *" returns caches which name starts with "the " (or "The ", etc.),
    • +
    • "name=water tower" returns caches which have exactly that name (case insensitive),
    • +
    • "name=*water tower*" will match caches like "Beautiful Water Tower II",
    • +
    • "name=*tower_" will match "Water Towers" but will not match "Water Tower".
    • +
    +

    Notice: Cache code and name are two different things - if you want to + find a cache by its code, use the services/caches/geocache method.

    +
    + +

    A string "X-Y", where X and Y are integers between 1 and 5, and X <= Y. + Only caches with terrain rating between these numbers (inclusive) will be returned.

    +
    + +

    A string "X-Y", where X and Y are integers between 1 and 5, and X <= Y. + Only caches with difficulty rating between these numbers (inclusive) will be returned.

    +
    + +

    Deprecated. Please use size2 instead - this will allow you to + differentiate "nano" vs. "micro" and "none" vs. "other" sizes + (details).

    +

    A string "X-Y", where X and Y are integers between 1 and 5, and X <= Y. + Only caches with size attribute between these numbers (inclusive) will be returned + (1 - micro, 5 - very big).

    +

    Notice: 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 + with no container included, append "|X" suffix to the value of this parameter + (e.g. "3-5|X").

    +
    + +

    Pipe-separated list of "size2 codes". Only matching caches will be + included. The codes are: 'none', 'nano', 'micro', 'small', 'regular', + 'large', 'xlarge', 'other'.

    +

    Note: 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).

    +
    + +

    A string "X-Y", where X and Y are integers between 1 and 5, and X <= Y. + Only caches with an overall rating between these numbers (inclusive) will be returned + (1 - poor, 5 - excellent).

    +

    Notice: 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 + to the value of this parameter (e.g. "3-5|X").

    +

    Notice: Some OC installations do not provide ratings for their geocaches. + On such installation, this parameter will be ignored.

    +
    + +

    There are two possible value-types for this argument:

    +
      +
    • Integer N. If N given, the result will contain only those caches which have + at least N recommendations.
    • +
    • Integer N with a percent sign appended (e.g. "20%"). The result will contain + only those caches which have at last N% ratio of recommendations/founds. + Please note, that this is useful only with conjunction with min_founds + parameter.
    • +
    +
    + +

    Integer N. If N given, the result will contain only those caches which have + been found at least N times. Useful if you want to skip "unverified" caches.

    +
    + +

    Integer N. If N given, the result will contain only those caches which have + been found at most N times. Useful for FTF hunters.

    +
    + +

    A date and time string. This should be in ISO 8601 format (currently any + format acceptable by PHP's strtotime + function also will do, but most of them don't handle time zones properly, + try to use ISO 8601!).

    +

    If given, the list of returned geocaches will be limited to those which were + created or modified since that date. (Note: currently caches do not get + "modified" when user makes a log entry.)

    +
    + +

    Notice: This parameter may be used only for requests signed with an Access Token + (you will need to use Level 3 Authentication).

    +

    Should be one of the following:

    +
      +
    • found_only - only caches found by the user will be returned,
    • +
    • notfound_only - only caches not found by the user will be returned,
    • +
    • either - all caches will be returned.
    • +
    +
    + +

    User UUID. If given, the response will only include geocaches found by + the given user.

    +
    + +

    User UUID. If given, the response will only include geocaches not found by + the given user.

    +
    + +

    Notice: This parameter may be used only for requests signed with an Access Token + (you will need to use Level 3 Authentication).

    +

    Boolean. If set to true, only caches which the user has marked as watched + 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").

    +
    + +

    Notice: This parameter may be used only for requests signed + with an Access Token (you will need to use Level 3 Authentication).

    +

    Boolean. If set to true, caches which the user has marked as ignored + will not be included in the result.

    +
    + +

    Notice: This parameter may be used only for requests signed + with an Access Token (you will need to use Level 3 Authentication). + See owner_uuid parameter if you don't want to use OAuth.

    +

    Boolean. If set to true, caches which the user is an owner of will + not be included in the result.

    +
    + + Boolean. If set to true, only caches with at least one trackable + will be included in the result. + + + Boolean. If set to true, only caches which have not yet been + found by anyone will be included. + + +

    ID of a set previously created with the search/save method. + If given, the results are ANDed + together with this set.

    +

    If you want to list the set contents only, please note the default + value of the status parameter! You may want to override it + in order to include unavailable and/or archived geocaches within the set.

    +
    + +

    Integer in range 1..500. Maximum number of cache codes returned.

    +
    + +

    An offset, the amount of items to skip at the beginning of the result. Combined with limit + allows pagination of the result set. Please note, that the sum of offset and limit still cannot + exceed 500. You MAY NOT use this method to list all the caches, it is for searching only. + Have a peek at the replicate module if you need all the caches.

    +
    + +

    Pipe separated list of fields to order the results by. Prefix the field name with + a '-' sign to indicate a descending order.

    +

    Currently, fields which you can order by include: code, name, + founds, rcmds, rcmds% (tell us if you want more).

    +

    Examples:

    +
      +
    • to order by cache name use "order_by=name" or "order_by=+name",
    • +
    • to have the most recommended caches in front, use "order_by=-rcmds%",
    • +
    • multicolumn sorting is also allowed, ex. "order_by=-founds|name"
    • +
    +

    Note: Try to avoid executing separate OKAPI request every time you + want to sort the data which you already have. Consider caching and + sorting on client side. This will speed up things for both sides.

    +

    Note: For most other methods (like bbox and nearest), + the default order is by the distance from the center. If you supply + custom order_by parameter, then we will try to order by your preference + first, and then by the distance later.

    +
    + + +

    A dictionary of the following structure:

    +
      +
    • results - a list of cache codes,
    • +
    • more - boolean, true means that there were more + results for your query, but they were not returned because of the + limit parameter.
    • +
    +
    \ No newline at end of file diff --git a/htdocs/okapi/services/caches/search/bbox.php b/htdocs/okapi/services/caches/search/bbox.php index dfee1ff7..3658484a 100644 --- a/htdocs/okapi/services/caches/search/bbox.php +++ b/htdocs/okapi/services/caches/search/bbox.php @@ -12,76 +12,83 @@ use okapi\services\caches\search\SearchAssistant; class WebService { - public static function options() - { - return array( - 'min_auth_level' => 1 - ); - } + public static function options() + { + return array( + 'min_auth_level' => 1 + ); + } - public static function call(OkapiRequest $request) - { - # You may wonder, why there are no parameters like "bbox" or "center" in the - # "search/all" method. This is *intentional* and should be kept this way. - # Such parameters would fall in conflict with each other and - in result - - # make the documentation very fuzzy. That's why they were intentionally - # left out of the "search/all" method, and put in separate (individual) ones. - # It's much easier to grasp their meaning this way. + public static function call(OkapiRequest $request) + { + # You may wonder, why there are no parameters like "bbox" or "center" in the + # "search/all" method. This is *intentional* and should be kept this way. + # Such parameters would fall in conflict with each other and - in result - + # make the documentation very fuzzy. That's why they were intentionally + # left out of the "search/all" method, and put in separate (individual) ones. + # It's much easier to grasp their meaning this way. - $tmp = $request->get_parameter('bbox'); - if (!$tmp) - throw new ParamMissing('bbox'); - $parts = explode('|', $tmp); - if (count($parts) != 4) - throw new InvalidParam('bbox', "Expecting 4 pipe-separated parts, got ".count($parts)."."); - foreach ($parts as &$part_ref) - { - if (!preg_match("/^-?[0-9]+(\.?[0-9]*)$/", $part_ref)) - throw new InvalidParam('bbox', "'$part_ref' is not a valid float number."); - $part_ref = floatval($part_ref); - } - list($bbsouth, $bbwest, $bbnorth, $bbeast) = $parts; - if ($bbnorth <= $bbsouth) - throw new InvalidParam('bbox', "Northern edge must be situated to the north of the southern edge."); - if ($bbeast == $bbwest) - throw new InvalidParam('bbox', "Eastern edge longitude is the same as the western one."); - if ($bbnorth > 90 || $bbnorth < -90 || $bbsouth > 90 || $bbsouth < -90) - throw new InvalidParam('bbox', "Latitudes have to be within -90..90 range."); - if ($bbeast > 180 || $bbeast < -180 || $bbwest > 180 || $bbwest < -180) - throw new InvalidParam('bbox', "Longitudes have to be within -180..180 range."); + $tmp = $request->get_parameter('bbox'); + if (!$tmp) + throw new ParamMissing('bbox'); + $parts = explode('|', $tmp); + if (count($parts) != 4) + throw new InvalidParam('bbox', "Expecting 4 pipe-separated parts, got ".count($parts)."."); + foreach ($parts as &$part_ref) + { + if (!preg_match("/^-?[0-9]+(\.?[0-9]*)$/", $part_ref)) + throw new InvalidParam('bbox', "'$part_ref' is not a valid float number."); + $part_ref = floatval($part_ref); + } + list($bbsouth, $bbwest, $bbnorth, $bbeast) = $parts; + if ($bbnorth <= $bbsouth) + throw new InvalidParam('bbox', "Northern edge must be situated to the north of the southern edge."); + if ($bbeast == $bbwest) + throw new InvalidParam('bbox', "Eastern edge longitude is the same as the western one."); + if ($bbnorth > 90 || $bbnorth < -90 || $bbsouth > 90 || $bbsouth < -90) + throw new InvalidParam('bbox', "Latitudes have to be within -90..90 range."); + if ($bbeast > 180 || $bbeast < -180 || $bbwest > 180 || $bbwest < -180) + throw new InvalidParam('bbox', "Longitudes have to be within -180..180 range."); - # Construct SQL conditions for the specified bounding box. + # Construct SQL conditions for the specified bounding box. - $where_conds = array(); - $where_conds[] = "caches.latitude between '".mysql_real_escape_string($bbsouth)."' and '".mysql_real_escape_string($bbnorth)."'"; - if ($bbeast > $bbwest) - { - # Easy one. - $where_conds[] = "caches.longitude between '".mysql_real_escape_string($bbwest)."' and '".mysql_real_escape_string($bbeast)."'"; - } - else - { - # We'll have to assume that this box goes through the 180-degree meridian. - # For example, $bbwest = 179 and $bbeast = -179. - $where_conds[] = "(caches.longitude > '".mysql_real_escape_string($bbwest)."' or caches.longitude < '".mysql_real_escape_string($bbeast)."')"; - } + $search_assistant = new SearchAssistant($request); + $search_assistant->prepare_common_search_params(); + $search_assistant->prepare_location_search_params(); - # - # 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. - # + $where_conds = array(); + $where_conds[] = $search_assistant->get_latitude_expr()." between '".mysql_real_escape_string($bbsouth)."' and '".mysql_real_escape_string($bbnorth)."'"; + if ($bbeast > $bbwest) + { + # 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); - $search_params['where_conds'] = array_merge($where_conds, $search_params['where_conds']); - $search_params['order_by'][] = Okapi::get_distance_sql($center_lat, $center_lon, - "caches.latitude", "caches.longitude"); # not replaced; added to the end! + $center_lat = ($bbsouth + $bbnorth) / 2.0; + $center_lon = ($bbwest + $bbeast) / 2.0; - $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); + } } diff --git a/htdocs/okapi/services/caches/search/bbox.xml b/htdocs/okapi/services/caches/search/bbox.xml index 2bcde5bb..0c8c33a7 100644 --- a/htdocs/okapi/services/caches/search/bbox.xml +++ b/htdocs/okapi/services/caches/search/bbox.xml @@ -1,34 +1,38 @@ - Search for caches within specified bounding box - 16 - -

    This method is similar to the search/all method, but the results - are restricted to the caches situated within a given bounding box - (a rectangle on the map).

    -

    Unless overriden, results are ordered by the distance from the center of the bounding - box. This means, that if you hit the limit of geocaches - returned, you will receive the ones that are in the middle of your - box, and miss the ones on the edges.

    -

    Usually you will want to use search_and_retrieve method instead of this one. - This way, you can get much more data in one request.

    -
    - -

    The bounding box within to search for caches. The box is defined - by a string in "S|W|N|E" format, where:

    -
      -
    • S stands for southern edge latitude of the box,
    • -
    • W stands for western edge longitude of the box,
    • -
    • N stands for northern edge latitude of the box,
    • -
    • E stands for eastern edge longitude of the box.
    • -
    -

    Use positive numbers for latitudes in the northern hemisphere and - longitudes in the eastern hemisphere (and negative for southern and - western hemispheres accordingly). These are full degrees with a dot - as a decimal point (ex. "48.7|15.8|54|24.9").

    -
    - - - -

    Same format as in the search/all method.

    -
    + Search for caches within specified bounding box + 16 + +

    This method is similar to the search/all method, but the results + are restricted to the caches situated within a given bounding box + (a rectangle on the map).

    +

    Unless overriden, results are ordered by the distance from the center of the bounding + box. This means, that if you hit the limit of geocaches + returned, you will receive the ones that are in the middle of your + box, and miss the ones on the edges.

    +

    Usually you will want to use search_and_retrieve method instead of this one. + This way, you can get much more data in one request.

    +
    + +

    The bounding box within to search for caches. The box is defined + by a string in "S|W|N|E" format, where:

    +
      +
    • S stands for southern edge latitude of the box,
    • +
    • W stands for western edge longitude of the box,
    • +
    • N stands for northern edge latitude of the box,
    • +
    • E stands for eastern edge longitude of the box.
    • +
    +

    Use positive numbers for latitudes in the northern hemisphere and + longitudes in the eastern hemisphere (and negative for southern and + western hemispheres accordingly). These are full degrees with a dot + as a decimal point (ex. "48.7|15.8|54|24.9").

    +
    + + Same as in the + services/caches/search/nearest method. + + + + +

    Same format as in the search/all method.

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

    Given a set of URLs, determine codes of geocaches referenced within them.

    -

    This might be useful if you have a link to an Opencaching geocache page, but you're - not able to extract cache code from such link (and you need the cache code to operate - on a cache using OKAPI). Geocache pages may be referenced using various types of URLs, - only some of them contain the cache code.

    -
    - -

    Pipe-separated list of URLs. No more than 500.

    -
    - -

    If false, then the result of this method will be compatible with - search/all method (and therefore can be used with search_and_retrieve).

    -

    If true, then the result will be a dictionary. Your URLs will be - mapped to the keys of this dictionary. Each value will be either a string - (cache code) or null (if no cache code was found for the given URL).

    -
    - - -

    As described in the as_dict parameter.

    -

    Examples:

    -

    For by_urls?urls=url1|url2|url3 - query, the result might look something link this:

    -
    {"results": ["OP28F9"]}
    -

    For by_urls?urls=url1|url2|url3&as_dict=true - query, the result might look something link this:

    -
    {"url1": "OP28F9", "url2": null, "url3": "OP28F9"}
    -
    + Resolve cache references from given URLs + 116 + +

    Given a set of URLs, determine codes of geocaches referenced within them.

    +

    This might be useful if you have a link to an Opencaching geocache page, but you're + not able to extract cache code from such link (and you need the cache code to operate + on a cache using OKAPI). Geocache pages may be referenced using various types of URLs, + only some of them contain the cache code.

    +
    + +

    Pipe-separated list of URLs. No more than 500.

    +
    + +

    If false, then the result of this method will be compatible with + search/all method (and therefore can be used with search_and_retrieve).

    +

    If true, then the result will be a dictionary. Your URLs will be + mapped to the keys of this dictionary. Each value will be either a string + (cache code) or null (if no cache code was found for the given URL).

    +
    + + +

    As described in the as_dict parameter.

    +

    Examples:

    +

    For by_urls?urls=url1|url2|url3 + query, the result might look something link this:

    +
    {"results": ["OP28F9"]}
    +

    For by_urls?urls=url1|url2|url3&as_dict=true + query, the result might look something link this:

    +
    {"url1": "OP28F9", "url2": null, "url3": "OP28F9"}
    +
    diff --git a/htdocs/okapi/services/caches/search/nearest.php b/htdocs/okapi/services/caches/search/nearest.php index c04ebd05..64b5f4bf 100644 --- a/htdocs/okapi/services/caches/search/nearest.php +++ b/htdocs/okapi/services/caches/search/nearest.php @@ -12,75 +12,82 @@ use okapi\services\caches\search\SearchAssistant; class WebService { - public static function options() - { - return array( - 'min_auth_level' => 1 - ); - } + public static function options() + { + return array( + 'min_auth_level' => 1 + ); + } - public static function call(OkapiRequest $request) - { - # You may wonder, why there are no parameters like "bbox" or "center" in the - # "search/all" method. This is *intentional* and should be kept this way. - # Such parameters would fall in conflict with each other and - in result - - # make the documentation very fuzzy. That's why they were intentionally - # left out of the "search/all" method, and put in separate (individual) ones. - # It's much easier to grasp their meaning this way. + public static function call(OkapiRequest $request) + { + # You may wonder, why there are no parameters like "bbox" or "center" in the + # "search/all" method. This is *intentional* and should be kept this way. + # Such parameters would fall in conflict with each other and - in result - + # make the documentation very fuzzy. That's why they were intentionally + # left out of the "search/all" method, and put in separate (individual) ones. + # It's much easier to grasp their meaning this way. - $tmp = $request->get_parameter('center'); - if (!$tmp) - throw new ParamMissing('center'); - $parts = explode('|', $tmp); - if (count($parts) != 2) - throw new InvalidParam('center', "Expecting 2 pipe-separated parts, got ".count($parts)."."); - foreach ($parts as &$part_ref) - { - if (!preg_match("/^-?[0-9]+(\.?[0-9]*)$/", $part_ref)) - throw new InvalidParam('center', "'$part_ref' is not a valid float number."); - $part_ref = floatval($part_ref); - } - list($center_lat, $center_lon) = $parts; - if ($center_lat > 90 || $center_lat < -90) - throw new InvalidParam('center', "Latitudes have to be within -90..90 range."); - if ($center_lon > 180 || $center_lon < -180) - throw new InvalidParam('center', "Longitudes have to be within -180..180 range."); + $tmp = $request->get_parameter('center'); + if (!$tmp) + throw new ParamMissing('center'); + $parts = explode('|', $tmp); + if (count($parts) != 2) + throw new InvalidParam('center', "Expecting 2 pipe-separated parts, got ".count($parts)."."); + foreach ($parts as &$part_ref) + { + if (!preg_match("/^-?[0-9]+(\.?[0-9]*)$/", $part_ref)) + throw new InvalidParam('center', "'$part_ref' is not a valid float number."); + $part_ref = floatval($part_ref); + } + list($center_lat, $center_lon) = $parts; + if ($center_lat > 90 || $center_lat < -90) + throw new InvalidParam('center', "Latitudes have to be within -90..90 range."); + if ($center_lon > 180 || $center_lon < -180) + throw new InvalidParam('center', "Longitudes have to be within -180..180 range."); - # - # In the method description, we promised to return caches ordered by the *rough* - # distance from the center point. We'll use ORDER BY with a simplified distance - # formula and combine it with the LIMIT clause to get the best results. - # + # + # In the method description, we promised to return caches ordered by the *rough* + # distance from the center point. We'll use ORDER BY with a simplified distance + # formula and combine it with the LIMIT clause to get the best results. + # - $distance_formula = Okapi::get_distance_sql($center_lat, $center_lon, "caches.latitude", "caches.longitude"); + $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 - # distance for every cache in the database. + # 'radius' parameter is optional. If not given, we'll have to calculate the + # distance for every cache in the database. - $where_conds = array(); - $radius = null; - if ($tmp = $request->get_parameter('radius')) - { - if (!preg_match("/^-?[0-9]+(\.?[0-9]*)$/", $tmp)) - throw new InvalidParam('radius', "'$tmp' is not a valid float number."); - $radius = floatval($tmp); - if ($radius <= 0) - throw new InvalidParam('radius', "Has to be a positive number."); - $radius *= 1000; # this one is given in kilemeters, converting to meters! - $where_conds[] = "$distance_formula <= '".mysql_real_escape_string($radius)."'"; - } + $where_conds = array(); + $radius = null; + if ($tmp = $request->get_parameter('radius')) + { + if (!preg_match("/^-?[0-9]+(\.?[0-9]*)$/", $tmp)) + throw new InvalidParam('radius', "'$tmp' is not a valid float number."); + $radius = floatval($tmp); + if ($radius <= 0) + throw new InvalidParam('radius', "Has to be a positive number."); + $radius *= 1000; # this one is given in kilemeters, converting to meters! + $where_conds[] = "$distance_formula <= '".mysql_real_escape_string($radius)."'"; + } - $search_params = SearchAssistant::get_common_search_params($request); - $search_params['where_conds'] = array_merge($where_conds, $search_params['where_conds']); - $search_params['order_by'][] = $distance_formula; # not replaced; added to the end! + $search_params = $search_assistant->get_search_params(); + $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_assistant->set_search_params($search_params); - $result = SearchAssistant::get_common_search_result($search_params); - if ($radius == null) - { - # 'more' is meaningless in this case, we'll remove it. - unset($result['more']); - } + $result = $search_assistant->get_common_search_result(); + if ($radius == null) + { + # 'more' is meaningless in this case, we'll remove it. + unset($result['more']); + } - return Okapi::formatted_response($request, $result); - } + return Okapi::formatted_response($request, $result); + } } diff --git a/htdocs/okapi/services/caches/search/nearest.xml b/htdocs/okapi/services/caches/search/nearest.xml index fe5e83c7..ba14cf42 100644 --- a/htdocs/okapi/services/caches/search/nearest.xml +++ b/htdocs/okapi/services/caches/search/nearest.xml @@ -1,30 +1,44 @@ - Search for nearest geocaches - 17 - -

    Find the nearest geocaches. Unless overriden, results are ordered by the distance from - the given center point.

    -

    Usually you will want to use search_and_retrieve method instead of this one. - This way, you can get much more data in one request.

    -
    - -

    The center point (typically - the user's location), in the - "lat|lon" format.

    -

    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").

    -
    - -

    Maximal distance (from the center point) for the cache to - be included in the results. Unlike in most other places, this - distance is given in kilometers instead of meters - (it can contain a floating point though).

    -
    - - - -

    Same format as in the search/all method. The more key will - be skipped when no radius argument is given.

    -
    + Search for nearest geocaches + 17 + +

    Find the nearest geocaches. Unless overriden, results are ordered by the distance from + the given center point.

    +

    Usually you will want to use search_and_retrieve method instead of this one. + This way, you can get much more data in one request.

    +
    + +

    The center point (typically - the user's location), in the + "lat|lon" format.

    +

    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").

    +
    + +

    Maximal distance (from the center point) for the cache to + be included in the results. Unlike in most other places, this + distance is given in kilometers instead of meters + (it can contain a floating point though).

    +
    + +

    In general, this parameter should take the same value as in the + + services/caches/formatters/gpx method, but currently only two values are + supported: default-coords and alt_wpt:user-coords.

    + +

    Allows you to search among alternate locations of the geocache, + instead of the default one. Particularily useful with alt_wpt:user-coords + alternate waypoint.

    + +

    Please note, that if you plan on using this option in conjunction + with search_and_retrieve method, then you'd probably want to use + the same option in your retr_method too (if available).

    +
    + + + +

    Same format as in the search/all method. The more key will + be skipped when no radius argument is given.

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

    This works similar to the search/all method, but the returned - set of geocaches is temporarilly stored, instead of being directly - returned to you.

    -

    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. - To view a portion of a saved search, use the search/all - method with proper set_and and status (!) - parameters.

    -
    - - The amount of time (in seconds) after which you allow OKAPI to delete - the set (OKAPI may remove it, but doesn't have to). - The maximum allowed value is 64800 (18 hours). - - -

    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 - extend its lifetime so it fits your min_store). What is the - maximum age of the existing result set which you are willing to accept?

    -

    This should be an integer (in seconds) or a special - nolimit value. It must be greater or equal to 300 (5 minutes).

    -
    - - - -

    A dictionary of the following structure:

    -
      -
    • set_id - string, the identifier of your saved set, - for future reference.
    • -
    -
    + Save a search result set + 163 + +

    This works similar to the search/all method, but the returned + set of geocaches is temporarilly stored, instead of being directly + returned to you.

    +

    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. + To view a portion of a saved search, use the search/all + method with proper set_and and status (!) + parameters.

    +
    + + The amount of time (in seconds) after which you allow OKAPI to delete + the set (OKAPI may remove it, but doesn't have to). + The maximum allowed value is 64800 (18 hours). + + +

    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 + extend its lifetime so it fits your min_store). What is the + maximum age of the existing result set which you are willing to accept?

    +

    This should be an integer (in seconds) or a special + nolimit value. It must be greater or equal to 300 (5 minutes).

    +
    + + + +

    A dictionary of the following structure:

    +
      +
    • set_id - string, the identifier of your saved set, + for future reference.
    • +
    +
    diff --git a/htdocs/okapi/services/caches/search/searching.inc.php b/htdocs/okapi/services/caches/search/searching.inc.php index f1e5729a..32f20ab2 100644 --- a/htdocs/okapi/services/caches/search/searching.inc.php +++ b/htdocs/okapi/services/caches/search/searching.inc.php @@ -14,641 +14,783 @@ use Exception; class SearchAssistant { - /** - * Load, parse and check common geocache search parameters from the - * given OKAPI request. Most cache search methods share a common set - * of filtering parameters recognized by this method. It returns - * a dictionary of the following structure: - * - * - "where_conds" - list of additional WHERE conditions to be ANDed - * to the rest of your SQL query, - * - "offset" - value of the offset parameter to be used in the LIMIT clause, - * - "limit" - value of the limit parameter to be used in the LIMIT clause, - * - "order_by" - list of order by clauses to be included in the "order by" - * SQL clause, - * - "extra_tables" - extra tables to be included in the FROM clause. - * - "extra_joins" - extra join statements to be included - * - * Important: YOU HAVE TO make sure that all data returned by this function - * are properly sanitized for SQL queries! I.e. they cannot contain unescaped - * user-supplied data. All user-suppied data which is returned by this function - * MUST be escaped! - */ - public static function get_common_search_params(OkapiRequest $request) - { - $where_conds = array('true'); - $extra_tables = array(); - $extra_joins = array(); + /** + * Current request issued by the client. + */ + private $request; /* @var OkapiRequest */ - # At the beginning we have to set up some "magic e$Xpressions". - # We will use them to make our query run on both OCPL and OCDE databases. + /** + * Initializes an object with a content of the client request. + * (The request should contain common geocache search parameters.) + */ + public function __construct(OkapiRequest $request) + { + $this->request = $request; + $this->longitude_expr = NULL; + $this->latitude_expr = NULL; + $this->location_extra_sql = NULL; + $this->search_params = NULL; + } - if (Settings::get('OC_BRANCH') == 'oc.pl') - { - # OCPL's 'caches' table contains some fields which OCDE's does not - # (topratings, founds, notfounds, last_found, votes, score). If - # we're being run on OCPL installation, we will simply use them. + /** + * This member holds a dictionary, which is used to build SQL query. For details, + * see documentation of get_search_params() and prepare_common_search_params() + */ + private $search_params; - $X_TOPRATINGS = 'caches.topratings'; - $X_FOUNDS = 'caches.founds'; - $X_NOTFOUNDS = 'caches.notfounds'; - $X_LAST_FOUND = 'caches.last_found'; - $X_VOTES = 'caches.votes'; - $X_SCORE = 'caches.score'; - } - else - { - # OCDE holds this data in a separate table. Additionally, OCDE - # does not provide a rating system (votes and score fields). - # If we're being run on OCDE database, we will include this - # additional table in our query and we will map the field - # expressions to approriate places. + /** + * This function returns a dictionary of the following structure: + * + * - "where_conds" - list of additional WHERE conditions to be ANDed + * to the rest of your SQL query, + * - "offset" - value of the offset parameter to be used in the LIMIT clause, + * - "limit" - value of the limit parameter to be used in the LIMIT clause, + * - "order_by" - list of order by clauses to be included in the "order by" + * SQL clause, + * - "extra_tables" - extra tables to be included in the FROM clause. + * - "extra_joins" - extra join statements to be included + * + * The dictionary is initalized by the call to prepare_common_search_params(), + * and may be further altered before an actual SQL execution, performed usually + * by get_common_search_result(). + * + * If you alter the results, make sure to save them back to this class by calling + * set_search_params(). + * + * Important: YOU HAVE TO make sure that all options are properly sanitized + * for SQL queries! I.e. they cannot contain unescaped user-supplied data. + */ + public function get_search_params() + { + return $this->search_params; + } - # stat_caches entries are optional, therefore we must do a left join: - $extra_joins[] = 'left join stat_caches on stat_caches.cache_id = caches.cache_id'; + /** + * Set search params, a dictionary of the structure described in get_search_params(). + * + * Important: YOU HAVE TO make sure that all options are properly sanitized + * for SQL queries! I.e. they cannot contain unescaped user-supplied data. + */ + public function set_search_params($search_params) + { + $this->search_params = $search_params; + } - $X_TOPRATINGS = 'ifnull(stat_caches.toprating,0)'; - $X_FOUNDS = 'ifnull(stat_caches.found,0)'; - $X_NOTFOUNDS = 'ifnull(stat_caches.notfound,0)'; - $X_LAST_FOUND = 'ifnull(stat_caches.last_found,0)'; - $X_VOTES = '0'; // no support for ratings - $X_SCORE = '0'; // no support for ratings - } + /** + * Load, parse and check common geocache search parameters (the ones + * described in services/caches/search/all method) from $this->request. + * Most cache search methods share a common set + * of filtering parameters recognized by this method. It initalizes + * search params, which can be further altered by calls to other methods + * of this class, or outside of this class by a call to get_search_params(); + * + * This method doesn't return anything. See get_search_params method. + */ + public function prepare_common_search_params() + { + $where_conds = array('true'); + $extra_tables = array(); + $extra_joins = array(); - # - # type - # + # At the beginning we have to set up some "magic e$Xpressions". + # We will use them to make our query run on both OCPL and OCDE databases. - if ($tmp = $request->get_parameter('type')) - { - $operator = "in"; - if ($tmp[0] == '-') - { - $tmp = substr($tmp, 1); - $operator = "not in"; - } - $types = array(); - foreach (explode("|", $tmp) as $name) - { - try - { - $id = Okapi::cache_type_name2id($name); - $types[] = $id; - } - catch (Exception $e) - { - throw new InvalidParam('type', "'$name' is not a valid cache type."); - } - } - if (count($types) > 0) - $where_conds[] = "caches.type $operator ('".implode("','", array_map('mysql_real_escape_string', $types))."')"; - else if ($operator == "in") - $where_conds[] = "false"; - } + if (Settings::get('OC_BRANCH') == 'oc.pl') + { + # OCPL's 'caches' table contains some fields which OCDE's does not + # (topratings, founds, notfounds, last_found, votes, score). If + # we're being run on OCPL installation, we will simply use them. - # - # size2 - # + $X_TOPRATINGS = 'caches.topratings'; + $X_FOUNDS = 'caches.founds'; + $X_NOTFOUNDS = 'caches.notfounds'; + $X_LAST_FOUND = 'caches.last_found'; + $X_VOTES = 'caches.votes'; + $X_SCORE = 'caches.score'; + } + else + { + # OCDE holds this data in a separate table. Additionally, OCDE + # does not provide a rating system (votes and score fields). + # If we're being run on OCDE database, we will include this + # additional table in our query and we will map the field + # expressions to approriate places. - if ($tmp = $request->get_parameter('size2')) - { - $operator = "in"; - if ($tmp[0] == '-') - { - $tmp = substr($tmp, 1); - $operator = "not in"; - } - $types = array(); - foreach (explode("|", $tmp) as $name) - { - try - { - $id = Okapi::cache_size2_to_sizeid($name); - $types[] = $id; - } - catch (Exception $e) - { - throw new InvalidParam('size2', "'$name' is not a valid cache size."); - } - } - $where_conds[] = "caches.size $operator ('".implode("','", array_map('mysql_real_escape_string', $types))."')"; - } + # stat_caches entries are optional, therefore we must do a left join: + $extra_joins[] = 'left join stat_caches on stat_caches.cache_id = caches.cache_id'; - # - # status - filter by status codes - # + $X_TOPRATINGS = 'ifnull(stat_caches.toprating,0)'; + $X_FOUNDS = 'ifnull(stat_caches.found,0)'; + $X_NOTFOUNDS = 'ifnull(stat_caches.notfound,0)'; + $X_LAST_FOUND = 'ifnull(stat_caches.last_found,0)'; + $X_VOTES = '0'; // no support for ratings + $X_SCORE = '0'; // no support for ratings + } - $tmp = $request->get_parameter('status'); - if ($tmp == null) $tmp = "Available"; - $codes = array(); - foreach (explode("|", $tmp) as $name) - { - try - { - $codes[] = Okapi::cache_status_name2id($name); - } - catch (Exception $e) - { - throw new InvalidParam('status', "'$name' is not a valid cache status."); - } - } - $where_conds[] = "caches.status in ('".implode("','", array_map('mysql_real_escape_string', $codes))."')"; + # + # type + # - # - # owner_uuid - # + if ($tmp = $this->request->get_parameter('type')) + { + $operator = "in"; + if ($tmp[0] == '-') + { + $tmp = substr($tmp, 1); + $operator = "not in"; + } + $types = array(); + foreach (explode("|", $tmp) as $name) + { + try + { + $id = Okapi::cache_type_name2id($name); + $types[] = $id; + } + catch (Exception $e) + { + throw new InvalidParam('type', "'$name' is not a valid cache type."); + } + } + if (count($types) > 0) + $where_conds[] = "caches.type $operator ('".implode("','", array_map('mysql_real_escape_string', $types))."')"; + else if ($operator == "in") + $where_conds[] = "false"; + } - if ($tmp = $request->get_parameter('owner_uuid')) - { - $operator = "in"; - if ($tmp[0] == '-') - { - $tmp = substr($tmp, 1); - $operator = "not in"; - } - try - { - $users = OkapiServiceRunner::call("services/users/users", new OkapiInternalRequest( - $request->consumer, null, array('user_uuids' => $tmp, 'fields' => 'internal_id'))); - } - catch (InvalidParam $e) # invalid uuid - { - throw new InvalidParam('owner_uuid', $e->whats_wrong_about_it); - } - $user_ids = array(); - foreach ($users as $user) - $user_ids[] = $user['internal_id']; - $where_conds[] = "caches.user_id $operator ('".implode("','", array_map('mysql_real_escape_string', $user_ids))."')"; - } + # + # size2 + # - # - # terrain, difficulty, size, rating - these are similar, we'll do them in a loop - # + if ($tmp = $this->request->get_parameter('size2')) + { + $operator = "in"; + if ($tmp[0] == '-') + { + $tmp = substr($tmp, 1); + $operator = "not in"; + } + $types = array(); + foreach (explode("|", $tmp) as $name) + { + try + { + $id = Okapi::cache_size2_to_sizeid($name); + $types[] = $id; + } + catch (Exception $e) + { + throw new InvalidParam('size2', "'$name' is not a valid cache size."); + } + } + $where_conds[] = "caches.size $operator ('".implode("','", array_map('mysql_real_escape_string', $types))."')"; + } - foreach (array('terrain', 'difficulty', 'size', 'rating') as $param_name) - { - if ($tmp = $request->get_parameter($param_name)) - { - if (!preg_match("/^[1-5]-[1-5](\|X)?$/", $tmp)) - throw new InvalidParam($param_name, "'$tmp'"); - list($min, $max) = explode("-", $tmp); - if (strpos($max, "|X") !== false) - { - $max = $max[0]; - $allow_null = true; - } else { - $allow_null = false; - } - if ($min > $max) - throw new InvalidParam($param_name, "'$tmp'"); - switch ($param_name) - { - case 'terrain': - if ($allow_null) - throw new InvalidParam($param_name, "The '|X' suffix is not allowed here."); - if (($min == 1) && ($max == 5)) { - /* no extra condition necessary */ - } else { - $where_conds[] = "caches.terrain between 2*$min and 2*$max"; - } - break; - case 'difficulty': - if ($allow_null) - throw new InvalidParam($param_name, "The '|X' suffix is not allowed here."); - if (($min == 1) && ($max == 5)) { - /* no extra condition necessary */ - } else { - $where_conds[] = "caches.difficulty between 2*$min and 2*$max"; - } - break; - case 'size': - # Deprecated. Leave it for backward-compatibility. See issue 155. - if (($min == 1) && ($max == 5) && $allow_null) { - # No extra condition necessary ('other' caches will be - # included). - } else { - # 'other' size caches will NOT be included (user must use the - # 'size2' parameter to search these). 'nano' caches will be - # included whenever 'micro' caches are included ($min=1). - $where_conds[] = "(caches.size between $min+1 and $max+1)". - ($allow_null ? " or caches.size=7" : ""). - (($min == 1) ? " or caches.size=8" : ""); - } - break; - case 'rating': - if (Settings::get('OC_BRANCH') == 'oc.pl') - { - if (($min == 1) && ($max == 5) && $allow_null) { - /* no extra condition necessary */ - } else { - $divisors = array(-999, -1.0, 0.1, 1.4, 2.2, 999); - $min = $divisors[$min - 1]; - $max = $divisors[$max]; - $where_conds[] = "($X_SCORE >= $min and $X_SCORE < $max and $X_VOTES >= 3)". - ($allow_null ? " or ($X_VOTES < 3)" : ""); - } - } - else - { - # OCDE does not support rating. We will ignore this parameter. - } - break; - } - } - } + # + # status - filter by status codes + # - # - # min_rcmds - # + $tmp = $this->request->get_parameter('status'); + if ($tmp == null) $tmp = "Available"; + $codes = array(); + foreach (explode("|", $tmp) as $name) + { + try + { + $codes[] = Okapi::cache_status_name2id($name); + } + catch (Exception $e) + { + throw new InvalidParam('status', "'$name' is not a valid cache status."); + } + } + $where_conds[] = "caches.status in ('".implode("','", array_map('mysql_real_escape_string', $codes))."')"; - if ($tmp = $request->get_parameter('min_rcmds')) - { - if ($tmp[strlen($tmp) - 1] == '%') - { - $tmp = substr($tmp, 0, strlen($tmp) - 1); - if (!is_numeric($tmp)) - throw new InvalidParam('min_rcmds', "'$tmp'"); - $tmp = intval($tmp); - if ($tmp > 100 || $tmp < 0) - throw new InvalidParam('min_rcmds', "'$tmp'"); - $tmp = floatval($tmp) / 100.0; - $where_conds[] = "$X_TOPRATINGS >= $X_FOUNDS * '".mysql_real_escape_string($tmp)."'"; - $where_conds[] = "$X_FOUNDS > 0"; - } - if (!is_numeric($tmp)) - throw new InvalidParam('min_rcmds', "'$tmp'"); - $where_conds[] = "$X_TOPRATINGS >= '".mysql_real_escape_string($tmp)."'"; - } + # + # owner_uuid + # - # - # min_founds - # + if ($tmp = $this->request->get_parameter('owner_uuid')) + { + $operator = "in"; + if ($tmp[0] == '-') + { + $tmp = substr($tmp, 1); + $operator = "not in"; + } + try + { + $users = OkapiServiceRunner::call("services/users/users", new OkapiInternalRequest( + $this->request->consumer, null, array('user_uuids' => $tmp, 'fields' => 'internal_id'))); + } + catch (InvalidParam $e) # invalid uuid + { + throw new InvalidParam('owner_uuid', $e->whats_wrong_about_it); + } + $user_ids = array(); + foreach ($users as $user) + $user_ids[] = $user['internal_id']; + $where_conds[] = "caches.user_id $operator ('".implode("','", array_map('mysql_real_escape_string', $user_ids))."')"; + } - if ($tmp = $request->get_parameter('min_founds')) - { - if (!is_numeric($tmp)) - throw new InvalidParam('min_founds', "'$tmp'"); - $where_conds[] = "$X_FOUNDS >= '".mysql_real_escape_string($tmp)."'"; - } + # + # terrain, difficulty, size, rating - these are similar, we'll do them in a loop + # - # - # max_founds - # may be '0' for FTF hunts - # + foreach (array('terrain', 'difficulty', 'size', 'rating') as $param_name) + { + if ($tmp = $this->request->get_parameter($param_name)) + { + if (!preg_match("/^[1-5]-[1-5](\|X)?$/", $tmp)) + throw new InvalidParam($param_name, "'$tmp'"); + list($min, $max) = explode("-", $tmp); + if (strpos($max, "|X") !== false) + { + $max = $max[0]; + $allow_null = true; + } else { + $allow_null = false; + } + if ($min > $max) + throw new InvalidParam($param_name, "'$tmp'"); + switch ($param_name) + { + case 'terrain': + if ($allow_null) + throw new InvalidParam($param_name, "The '|X' suffix is not allowed here."); + if (($min == 1) && ($max == 5)) { + /* no extra condition necessary */ + } else { + $where_conds[] = "caches.terrain between 2*$min and 2*$max"; + } + break; + case 'difficulty': + if ($allow_null) + throw new InvalidParam($param_name, "The '|X' suffix is not allowed here."); + if (($min == 1) && ($max == 5)) { + /* no extra condition necessary */ + } else { + $where_conds[] = "caches.difficulty between 2*$min and 2*$max"; + } + break; + case 'size': + # Deprecated. Leave it for backward-compatibility. See issue 155. + if (($min == 1) && ($max == 5) && $allow_null) { + # No extra condition necessary ('other' caches will be + # included). + } else { + # 'other' size caches will NOT be included (user must use the + # 'size2' parameter to search these). 'nano' caches will be + # included whenever 'micro' caches are included ($min=1). + $where_conds[] = "(caches.size between $min+1 and $max+1)". + ($allow_null ? " or caches.size=7" : ""). + (($min == 1) ? " or caches.size=8" : ""); + } + break; + case 'rating': + if (Settings::get('OC_BRANCH') == 'oc.pl') + { + if (($min == 1) && ($max == 5) && $allow_null) { + /* no extra condition necessary */ + } else { + $divisors = array(-999, -1.0, 0.1, 1.4, 2.2, 999); + $min = $divisors[$min - 1]; + $max = $divisors[$max]; + $where_conds[] = "($X_SCORE >= $min and $X_SCORE < $max and $X_VOTES >= 3)". + ($allow_null ? " or ($X_VOTES < 3)" : ""); + } + } + else + { + # OCDE does not support rating. We will ignore this parameter. + } + break; + } + } + } - if (!is_null($tmp = $request->get_parameter('max_founds'))) - { - if (!is_numeric($tmp)) - throw new InvalidParam('max_founds', "'$tmp'"); - $where_conds[] = "$X_FOUNDS <= '".mysql_real_escape_string($tmp)."'"; - } + # + # min_rcmds + # - # - # modified_since - # + if ($tmp = $this->request->get_parameter('min_rcmds')) + { + if ($tmp[strlen($tmp) - 1] == '%') + { + $tmp = substr($tmp, 0, strlen($tmp) - 1); + if (!is_numeric($tmp)) + throw new InvalidParam('min_rcmds', "'$tmp'"); + $tmp = intval($tmp); + if ($tmp > 100 || $tmp < 0) + throw new InvalidParam('min_rcmds', "'$tmp'"); + $tmp = floatval($tmp) / 100.0; + $where_conds[] = "$X_TOPRATINGS >= $X_FOUNDS * '".mysql_real_escape_string($tmp)."'"; + $where_conds[] = "$X_FOUNDS > 0"; + } + if (!is_numeric($tmp)) + throw new InvalidParam('min_rcmds', "'$tmp'"); + $where_conds[] = "$X_TOPRATINGS >= '".mysql_real_escape_string($tmp)."'"; + } - if ($tmp = $request->get_parameter('modified_since')) - { - $timestamp = strtotime($tmp); - if ($timestamp) - $where_conds[] = "unix_timestamp(caches.last_modified) > '".mysql_real_escape_string($timestamp)."'"; - else - throw new InvalidParam('modified_since', "'$tmp' is not in a valid format or is not a valid date."); - } + # + # min_founds + # - # - # found_status - # + if ($tmp = $this->request->get_parameter('min_founds')) + { + if (!is_numeric($tmp)) + throw new InvalidParam('min_founds', "'$tmp'"); + $where_conds[] = "$X_FOUNDS >= '".mysql_real_escape_string($tmp)."'"; + } - if ($tmp = $request->get_parameter('found_status')) - { - if ($request->token == null) - throw new InvalidParam('found_status', "Might be used only for requests signed with an Access Token."); - if (!in_array($tmp, array('found_only', 'notfound_only', 'either'))) - throw new InvalidParam('found_status', "'$tmp'"); - if ($tmp != 'either') - { - $found_cache_ids = self::get_found_cache_ids($request->token->user_id); - $operator = ($tmp == 'found_only') ? "in" : "not in"; - $where_conds[] = "caches.cache_id $operator ('".implode("','", array_map('mysql_real_escape_string', $found_cache_ids))."')"; - } - } + # + # max_founds + # may be '0' for FTF hunts + # - # - # found_by - # + if (!is_null($tmp = $this->request->get_parameter('max_founds'))) + { + if (!is_numeric($tmp)) + throw new InvalidParam('max_founds', "'$tmp'"); + $where_conds[] = "$X_FOUNDS <= '".mysql_real_escape_string($tmp)."'"; + } - if ($tmp = $request->get_parameter('found_by')) - { - try { - $user = OkapiServiceRunner::call("services/users/user", new OkapiInternalRequest( - $request->consumer, null, array('user_uuid' => $tmp, 'fields' => 'internal_id'))); - } catch (InvalidParam $e) { # invalid uuid - throw new InvalidParam('found_by', $e->whats_wrong_about_it); - } - $found_cache_ids = self::get_found_cache_ids($user['internal_id']); - $where_conds[] = "caches.cache_id in ('".implode("','", array_map('mysql_real_escape_string', $found_cache_ids))."')"; - } + # + # modified_since + # - # - # not_found_by - # + if ($tmp = $this->request->get_parameter('modified_since')) + { + $timestamp = strtotime($tmp); + if ($timestamp) + $where_conds[] = "unix_timestamp(caches.last_modified) > '".mysql_real_escape_string($timestamp)."'"; + else + throw new InvalidParam('modified_since', "'$tmp' is not in a valid format or is not a valid date."); + } - if ($tmp = $request->get_parameter('not_found_by')) - { - try { - $user = OkapiServiceRunner::call("services/users/user", new OkapiInternalRequest( - $request->consumer, null, array('user_uuid' => $tmp, 'fields' => 'internal_id'))); - } catch (InvalidParam $e) { # invalid uuid - throw new InvalidParam('not_found_by', $e->whats_wrong_about_it); - } - $found_cache_ids = self::get_found_cache_ids($user['internal_id']); - $where_conds[] = "caches.cache_id not in ('".implode("','", array_map('mysql_real_escape_string', $found_cache_ids))."')"; - } + # + # found_status + # - # - # watched_only - # + if ($tmp = $this->request->get_parameter('found_status')) + { + if ($this->request->token == null) + throw new InvalidParam('found_status', "Might be used only for requests signed with an Access Token."); + if (!in_array($tmp, array('found_only', 'notfound_only', 'either'))) + throw new InvalidParam('found_status', "'$tmp'"); + if ($tmp != 'either') + { + $found_cache_ids = self::get_found_cache_ids($this->request->token->user_id); + $operator = ($tmp == 'found_only') ? "in" : "not in"; + $where_conds[] = "caches.cache_id $operator ('".implode("','", array_map('mysql_real_escape_string', $found_cache_ids))."')"; + } + } - if ($tmp = $request->get_parameter('watched_only')) - { - if ($request->token == null) - throw new InvalidParam('watched_only', "Might be used only for requests signed with an Access Token."); - if (!in_array($tmp, array('true', 'false'))) - throw new InvalidParam('watched_only', "'$tmp'"); - if ($tmp == 'true') - { - $watched_cache_ids = Db::select_column(" - select cache_id - from cache_watches - where user_id = '".mysql_real_escape_string($request->token->user_id)."' - "); - $where_conds[] = "caches.cache_id in ('".implode("','", array_map('mysql_real_escape_string', $watched_cache_ids))."')"; - } - } + # + # found_by + # - # - # exclude_ignored - # + if ($tmp = $this->request->get_parameter('found_by')) + { + try { + $user = OkapiServiceRunner::call("services/users/user", new OkapiInternalRequest( + $this->request->consumer, null, array('user_uuid' => $tmp, 'fields' => 'internal_id'))); + } catch (InvalidParam $e) { # invalid uuid + throw new InvalidParam('found_by', $e->whats_wrong_about_it); + } + $found_cache_ids = self::get_found_cache_ids($user['internal_id']); + $where_conds[] = "caches.cache_id in ('".implode("','", array_map('mysql_real_escape_string', $found_cache_ids))."')"; + } - if ($tmp = $request->get_parameter('exclude_ignored')) - { - if ($request->token == null) - throw new InvalidParam('exclude_ignored', "Might be used only for requests signed with an Access Token."); - if (!in_array($tmp, array('true', 'false'))) - throw new InvalidParam('exclude_ignored', "'$tmp'"); - if ($tmp == 'true') { - $ignored_cache_ids = Db::select_column(" - select cache_id - from cache_ignore - where user_id = '".mysql_real_escape_string($request->token->user_id)."' - "); - $where_conds[] = "caches.cache_id not in ('".implode("','", array_map('mysql_real_escape_string', $ignored_cache_ids))."')"; - } - } + # + # not_found_by + # - # - # exclude_my_own - # + if ($tmp = $this->request->get_parameter('not_found_by')) + { + try { + $user = OkapiServiceRunner::call("services/users/user", new OkapiInternalRequest( + $this->request->consumer, null, array('user_uuid' => $tmp, 'fields' => 'internal_id'))); + } catch (InvalidParam $e) { # invalid uuid + throw new InvalidParam('not_found_by', $e->whats_wrong_about_it); + } + $found_cache_ids = self::get_found_cache_ids($user['internal_id']); + $where_conds[] = "caches.cache_id not in ('".implode("','", array_map('mysql_real_escape_string', $found_cache_ids))."')"; + } - if ($tmp = $request->get_parameter('exclude_my_own')) - { - if ($request->token == null) - throw new InvalidParam('exclude_my_own', "Might be used only for requests signed with an Access Token."); - if (!in_array($tmp, array('true', 'false'))) - throw new InvalidParam('exclude_my_own', "'$tmp'"); - if ($tmp == 'true') - $where_conds[] = "caches.user_id != '".mysql_real_escape_string($request->token->user_id)."'"; - } + # + # watched_only + # - # - # name - # + if ($tmp = $this->request->get_parameter('watched_only')) + { + if ($this->request->token == null) + throw new InvalidParam('watched_only', "Might be used only for requests signed with an Access Token."); + if (!in_array($tmp, array('true', 'false'))) + throw new InvalidParam('watched_only', "'$tmp'"); + if ($tmp == 'true') + { + $watched_cache_ids = Db::select_column(" + select cache_id + from cache_watches + where user_id = '".mysql_real_escape_string($this->request->token->user_id)."' + "); + $where_conds[] = "caches.cache_id in ('".implode("','", array_map('mysql_real_escape_string', $watched_cache_ids))."')"; + } + } - if ($tmp = $request->get_parameter('name')) - { - # WRTODO: Make this more user-friendly. See: - # http://code.google.com/p/opencaching-api/issues/detail?id=121 + # + # exclude_ignored + # - if (strlen($tmp) > 100) - throw new InvalidParam('name', "Maximum length of 'name' parameter is 100 characters"); - $tmp = str_replace("*", "%", str_replace("%", "%%", $tmp)); - $where_conds[] = "caches.name LIKE '".mysql_real_escape_string($tmp)."'"; - } + if ($tmp = $this->request->get_parameter('exclude_ignored')) + { + if ($this->request->token == null) + throw new InvalidParam('exclude_ignored', "Might be used only for requests signed with an Access Token."); + if (!in_array($tmp, array('true', 'false'))) + throw new InvalidParam('exclude_ignored', "'$tmp'"); + if ($tmp == 'true') { + $ignored_cache_ids = Db::select_column(" + select cache_id + from cache_ignore + where user_id = '".mysql_real_escape_string($this->request->token->user_id)."' + "); + $where_conds[] = "caches.cache_id not in ('".implode("','", array_map('mysql_real_escape_string', $ignored_cache_ids))."')"; + } + } - # - # with_trackables_only - # + # + # exclude_my_own + # - if ($tmp = $request->get_parameter('with_trackables_only')) - { - if (!in_array($tmp, array('true', 'false'), 1)) - throw new InvalidParam('with_trackables_only', "'$tmp'"); - if ($tmp == 'true') - { - $where_conds[] = " - caches.wp_oc in ( - select distinct wp - from gk_item_waypoint - ) - "; - } - } + if ($tmp = $this->request->get_parameter('exclude_my_own')) + { + if ($this->request->token == null) + throw new InvalidParam('exclude_my_own', "Might be used only for requests signed with an Access Token."); + if (!in_array($tmp, array('true', 'false'))) + throw new InvalidParam('exclude_my_own', "'$tmp'"); + if ($tmp == 'true') + $where_conds[] = "caches.user_id != '".mysql_real_escape_string($this->request->token->user_id)."'"; + } - # - # ftf_hunter - # + # + # name + # - if ($tmp = $request->get_parameter('ftf_hunter')) - { - if (!in_array($tmp, array('true', 'false'), 1)) - throw new InvalidParam('not_yet_found_only', "'$tmp'"); - if ($tmp == 'true') - { - $where_conds[] = "$X_FOUNDS = 0"; - } - } + if ($tmp = $this->request->get_parameter('name')) + { + # WRTODO: Make this more user-friendly. See: + # http://code.google.com/p/opencaching-api/issues/detail?id=121 - # - # set_and - # + if (strlen($tmp) > 100) + throw new InvalidParam('name', "Maximum length of 'name' parameter is 100 characters"); + $tmp = str_replace("*", "%", str_replace("%", "%%", $tmp)); + $where_conds[] = "caches.name LIKE '".mysql_real_escape_string($tmp)."'"; + } - if ($tmp = $request->get_parameter('set_and')) - { - # Check if the set exists. + # + # with_trackables_only + # - $exists = Db::select_value(" - select 1 - from okapi_search_sets - where id = '".mysql_real_escape_string($tmp)."' - "); - if (!$exists) - throw new InvalidParam('set_and', "Couldn't find a set by given ID."); - $extra_tables[] = "okapi_search_results osr_and"; - $where_conds[] = "osr_and.cache_id = caches.cache_id"; - $where_conds[] = "osr_and.set_id = '".mysql_real_escape_string($tmp)."'"; - } + if ($tmp = $this->request->get_parameter('with_trackables_only')) + { + if (!in_array($tmp, array('true', 'false'), 1)) + throw new InvalidParam('with_trackables_only', "'$tmp'"); + if ($tmp == 'true') + { + $where_conds[] = " + caches.wp_oc in ( + select distinct wp + from gk_item_waypoint + ) + "; + } + } - # - # limit - # + # + # ftf_hunter + # - $limit = $request->get_parameter('limit'); - if ($limit == null) $limit = "100"; - if (!is_numeric($limit)) - throw new InvalidParam('limit', "'$limit'"); - if ($limit < 1 || (($limit > 500) && (!$request->skip_limits))) - throw new InvalidParam( - 'limit', - $request->skip_limits - ? "Cannot be lower than 1." - : "Has to be between 1 and 500." - ); + if ($tmp = $this->request->get_parameter('ftf_hunter')) + { + if (!in_array($tmp, array('true', 'false'), 1)) + throw new InvalidParam('not_yet_found_only', "'$tmp'"); + if ($tmp == 'true') + { + $where_conds[] = "$X_FOUNDS = 0"; + } + } - # - # offset - # + # + # set_and + # - $offset = $request->get_parameter('offset'); - if ($offset == null) $offset = "0"; - if (!is_numeric($offset)) - throw new InvalidParam('offset', "'$offset'"); - if (($offset + $limit > 500) && (!$request->skip_limits)) - throw new BadRequest("The sum of offset and limit may not exceed 500."); - if ($offset < 0 || (($offset > 499) && (!$request->skip_limits))) - throw new InvalidParam( - 'offset', - $request->skip_limits - ? "Cannot be lower than 0." - : "Has to be between 0 and 499." - ); + if ($tmp = $this->request->get_parameter('set_and')) + { + # Check if the set exists. - # - # order_by - # + $exists = Db::select_value(" + select 1 + from okapi_search_sets + where id = '".mysql_real_escape_string($tmp)."' + "); + if (!$exists) + throw new InvalidParam('set_and', "Couldn't find a set by given ID."); + $extra_tables[] = "okapi_search_results osr_and"; + $where_conds[] = "osr_and.cache_id = caches.cache_id"; + $where_conds[] = "osr_and.set_id = '".mysql_real_escape_string($tmp)."'"; + } - $order_clauses = array(); - $order_by = $request->get_parameter('order_by'); - if ($order_by != null) - { - $order_by = explode('|', $order_by); - foreach ($order_by as $field) - { - $dir = 'asc'; - if ($field[0] == '-') - { - $dir = 'desc'; - $field = substr($field, 1); - } - elseif ($field[0] == '+') - $field = substr($field, 1); # ignore leading "+" - switch ($field) - { - case 'code': $cl = "caches.wp_oc"; break; - case 'name': $cl = "caches.name"; break; - case 'founds': $cl = "$X_FOUNDS"; break; - case 'rcmds': $cl = "$X_TOPRATINGS"; break; - case 'rcmds%': - $cl = "$X_TOPRATINGS / if($X_FOUNDS = 0, 1, $X_FOUNDS)"; - break; - default: - throw new InvalidParam('order_by', "Unsupported field '$field'"); - } - $order_clauses[] = "($cl) $dir"; - } - } + # + # limit + # - # To avoid join errors, put each of the $where_conds in extra paranthesis. + $limit = $this->request->get_parameter('limit'); + if ($limit == null) $limit = "100"; + if (!is_numeric($limit)) + throw new InvalidParam('limit', "'$limit'"); + if ($limit < 1 || (($limit > 500) && (!$this->request->skip_limits))) + throw new InvalidParam( + 'limit', + $this->request->skip_limits + ? "Cannot be lower than 1." + : "Has to be between 1 and 500." + ); - $tmp = array(); - foreach($where_conds as $cond) - $tmp[] = "(".$cond.")"; - $where_conds = $tmp; - unset($tmp); + # + # offset + # - $ret_array = array( - 'where_conds' => $where_conds, - 'offset' => (int)$offset, - 'limit' => (int)$limit, - 'order_by' => $order_clauses, - 'extra_tables' => $extra_tables, - 'extra_joins' => $extra_joins, - ); + $offset = $this->request->get_parameter('offset'); + if ($offset == null) $offset = "0"; + if (!is_numeric($offset)) + throw new InvalidParam('offset', "'$offset'"); + if (($offset + $limit > 500) && (!$this->request->skip_limits)) + throw new BadRequest("The sum of offset and limit may not exceed 500."); + if ($offset < 0 || (($offset > 499) && (!$this->request->skip_limits))) + throw new InvalidParam( + 'offset', + $this->request->skip_limits + ? "Cannot be lower than 0." + : "Has to be between 0 and 499." + ); - return $ret_array; - } + # + # order_by + # - /** - * Search for caches using given conditions and options. Return - * an array in a "standard" format of array('results' => list of - * cache codes, 'more' => boolean). This method takes care of the - * 'more' variable in an appropriate way. - * - * The $options parameter include: - * - where_conds - list of additional WHERE conditions to be ANDed - * to the rest of your SQL query, - * - extra_tables - list of additional tables to be joined within - * the query, - * - order_by - list or SQL clauses to be used with ORDER BY, - * - limit - maximum number of cache codes to be returned. - * - * Important: YOU HAVE TO make sure that all options are properly sanitized - * for SQL queries! I.e. they cannot contain unescaped user-supplied data. - */ - public static function get_common_search_result($options) - { - $tables = array_merge( - array('caches'), - $options['extra_tables'] - ); - $where_conds = array_merge( - array('caches.wp_oc is not null'), - $options['where_conds'] - ); + $order_clauses = array(); + $order_by = $this->request->get_parameter('order_by'); + if ($order_by != null) + { + $order_by = explode('|', $order_by); + foreach ($order_by as $field) + { + $dir = 'asc'; + if ($field[0] == '-') + { + $dir = 'desc'; + $field = substr($field, 1); + } + elseif ($field[0] == '+') + $field = substr($field, 1); # ignore leading "+" + switch ($field) + { + case 'code': $cl = "caches.wp_oc"; break; + case 'name': $cl = "caches.name"; break; + case 'founds': $cl = "$X_FOUNDS"; break; + case 'rcmds': $cl = "$X_TOPRATINGS"; break; + case 'rcmds%': + $cl = "$X_TOPRATINGS / if($X_FOUNDS = 0, 1, $X_FOUNDS)"; + break; + default: + throw new InvalidParam('order_by', "Unsupported field '$field'"); + } + $order_clauses[] = "($cl) $dir"; + } + } - # We need to pull limit+1 items, in order to properly determine the - # value of "more" variable. + # To avoid join errors, put each of the $where_conds in extra paranthesis. - $cache_codes = Db::select_column(" - select caches.wp_oc - from ".implode(", ", $tables)." ". - implode(" ", $options['extra_joins'])." - where ".implode(" and ", $where_conds)." - ".((count($options['order_by']) > 0) ? "order by ".implode(", ", $options['order_by']) : "")." - limit ".($options['offset']).", ".($options['limit'] + 1)."; - "); + $tmp = array(); + foreach($where_conds as $cond) + $tmp[] = "(".$cond.")"; + $where_conds = $tmp; + unset($tmp); - if (count($cache_codes) > $options['limit']) - { - $more = true; - array_pop($cache_codes); # get rid of the one above the limit - } else { - $more = false; - } + $ret_array = array( + 'where_conds' => $where_conds, + 'offset' => (int)$offset, + 'limit' => (int)$limit, + 'order_by' => $order_clauses, + 'extra_tables' => $extra_tables, + 'extra_joins' => $extra_joins, + ); - $result = array( - 'results' => $cache_codes, - 'more' => $more, - ); - return $result; - } + if ($this->search_params === NULL) + { + $this->search_params = $ret_array; + } else { + $this->search_params = array_merge_recursive($this->search_params, $ret_array); + } + } - /** - * Get the list of cache IDs which were found by given user. - * Parameter needs to be *internal* user id, not uuid. - */ - private static function get_found_cache_ids($internal_user_id) - { - return Db::select_column(" - select cache_id - from cache_logs - where - user_id = '".mysql_real_escape_string($internal_user_id)."' - and type in ( - '".mysql_real_escape_string(Okapi::logtypename2id("Found it"))."', - '".mysql_real_escape_string(Okapi::logtypename2id("Attended"))."' - ) - and ".((Settings::get('OC_BRANCH') == 'oc.pl') ? "deleted = 0" : "true")." - "); - } + /** + * Search for caches using conditions and options stored in the instance + * of this class. These conditions are usually initialized by the call + * to prepare_common_search_params(), and may be further altered by the + * client of this call by calling get_search_params() and set_search_params(). + * + * Returns an array in a "standard" format of array('results' => list of + * cache codes, 'more' => boolean). This method takes care of the + * 'more' variable in an appropriate way. + */ + public function get_common_search_result() + { + $tables = array_merge( + array('caches'), + $this->search_params['extra_tables'] + ); + $where_conds = array_merge( + array('caches.wp_oc is not null'), + $this->search_params['where_conds'] + ); + + # We need to pull limit+1 items, in order to properly determine the + # value of "more" variable. + + $cache_codes = Db::select_column(" + select caches.wp_oc + from ".implode(", ", $tables)." ". + implode(" ", $this->search_params['extra_joins'])." + where ".implode(" and ", $where_conds)." + ".((count($this->search_params['order_by']) > 0) ? "order by ".implode(", ", $this->search_params['order_by']) : "")." + limit ".($this->search_params['offset']).", ".($this->search_params['limit'] + 1)."; + "); + + if (count($cache_codes) > $this->search_params['limit']) + { + $more = true; + array_pop($cache_codes); # get rid of the one above the limit + } else { + $more = false; + } + + $result = array( + 'results' => $cache_codes, + 'more' => $more, + ); + return $result; + } + + # Issue #298 - user coordinates implemented in oc.pl + private $longitude_expr; + private $latitude_expr; + + /** + * This method extends search params in case you would like to search + * using the geocache location, i.e. search for the geocaches nearest + * to the given location. When you search for such geocaches, you must + * use expressions returned by get_longitude_expr() and get_latitude_expr() + * to query for the actual location of geocaches. + */ + public function prepare_location_search_params() + { + $location_source = $this->request->get_parameter('location_source'); + if (!$location_source) + $location_source = 'default-coords'; + + # Make sure location_source has prefix alt_wpt: + if ($location_source != 'default-coords' && strncmp($location_source, 'alt_wpt:', 8) != 0) + { + throw new InvalidParam('location_source', '\''.$location_source.'\''); + } + + # Make sure we have sufficient authorization + if ($location_source == 'alt_wpt:user-coords' && $this->request->token == null) + { + throw new BadRequest("Level 3 Authentication is required to access 'alt_wpt:user-coords'."); + } + + if ($location_source != 'alt_wpt:user-coords') { + # unsupported waypoint type - use default geocache coordinates + $location_source = 'default-coords'; + } + + if ($location_source == 'default-coords') + { + $this->longitude_expr = 'caches.longitude'; + $this->latitude_expr = 'caches.latitude'; + } else { + $extra_joins = null; + if (Settings::get('OC_BRANCH') == 'oc.pl') + { + $this->longitude_expr = 'ifnull(cache_mod_cords.longitude, caches.longitude)'; + $this->latitude_expr = 'ifnull(cache_mod_cords.latitude, caches.latitude)'; + $extra_joins = array(" + left join cache_mod_cords + on cache_mod_cords.cache_id = caches.cache_id + and cache_mod_cords.user_id = '".mysql_real_escape_string($this->request->token->user_id)."' + "); + } else { + # oc.de + $this->longitude_expr = 'ifnull(coordinates.longitude, caches.longitude)'; + $this->latitude_expr = 'ifnull(coordinates.latitude, caches.latitude)'; + $extra_joins = array(" + left join coordinates + on coordinates.cache_id = caches.cache_id + and coordinates.user_id = '".mysql_real_escape_string($this->request->token->user_id)."' + and coordinates.type = 2 + and coordinates.longitude != 0 + and coordinates.latitude != 0 + "); + } + $location_extra_sql = array( + 'extra_joins' => $extra_joins + ); + if ($this->search_params === NULL) + { + $this->search_params = $location_extra_sql; + } else { + $this->search_params = array_merge_recursive($this->search_params, $location_extra_sql); + } + } + } + + /** + * Returns the expression used as cache's longitude source. You may use this + * method only after prepare_search_params_for_location() invocation. + */ + public function get_longitude_expr() + { + return $this->longitude_expr; + } + + /** + * Returns the expression used as cache's latitude source. You may use this + * method only after prepare_search_params_for_location() invocation. + */ + public function get_latitude_expr() + { + return $this->latitude_expr; + } + + /** + * Get the list of cache IDs which were found by given user. + * Parameter needs to be *internal* user id, not uuid. + */ + private static function get_found_cache_ids($internal_user_id) + { + return Db::select_column(" + select cache_id + from cache_logs + where + user_id = '".mysql_real_escape_string($internal_user_id)."' + and type in ( + '".mysql_real_escape_string(Okapi::logtypename2id("Found it"))."', + '".mysql_real_escape_string(Okapi::logtypename2id("Attended"))."' + ) + and ".((Settings::get('OC_BRANCH') == 'oc.pl') ? "deleted = 0" : "true")." + "); + } } diff --git a/htdocs/okapi/services/caches/shortcuts/search_and_retrieve.php b/htdocs/okapi/services/caches/shortcuts/search_and_retrieve.php index 1cc9d7cb..17522824 100644 --- a/htdocs/okapi/services/caches/shortcuts/search_and_retrieve.php +++ b/htdocs/okapi/services/caches/shortcuts/search_and_retrieve.php @@ -13,109 +13,109 @@ use okapi\BadRequest; class WebService { - public static function options() - { - return array( - 'min_auth_level' => 1 - ); - } + public static function options() + { + return array( + 'min_auth_level' => 1 + ); + } - public static function call(OkapiRequest $request) - { - # Check search method - $search_method = $request->get_parameter('search_method'); - if (!$search_method) - throw new ParamMissing('search_method'); - if (strpos($search_method, "services/caches/search/") !== 0) - throw new InvalidParam('search_method', "Should begin with 'services/caches/search/'."); - if (!OkapiServiceRunner::exists($search_method)) - throw new InvalidParam('search_method', "Method does not exist: '$search_method'"); - $search_params = $request->get_parameter('search_params'); - if (!$search_params) - throw new ParamMissing('search_params'); - $search_params = json_decode($search_params, true); - if (!is_array($search_params)) - throw new InvalidParam('search_params', "Should be a JSON-encoded dictionary"); + public static function call(OkapiRequest $request) + { + # Check search method + $search_method = $request->get_parameter('search_method'); + if (!$search_method) + throw new ParamMissing('search_method'); + if (strpos($search_method, "services/caches/search/") !== 0) + throw new InvalidParam('search_method', "Should begin with 'services/caches/search/'."); + if (!OkapiServiceRunner::exists($search_method)) + throw new InvalidParam('search_method', "Method does not exist: '$search_method'"); + $search_params = $request->get_parameter('search_params'); + if (!$search_params) + throw new ParamMissing('search_params'); + $search_params = json_decode($search_params, true); + if (!is_array($search_params)) + throw new InvalidParam('search_params', "Should be a JSON-encoded dictionary"); - # Check retrieval method - $retr_method = $request->get_parameter('retr_method'); - if (!$retr_method) - throw new ParamMissing('retr_method'); - if (!OkapiServiceRunner::exists($retr_method)) - throw new InvalidParam('retr_method', "Method does not exist: '$retr_method'"); - $retr_params = $request->get_parameter('retr_params'); - if (!$retr_params) - throw new ParamMissing('retr_params'); - $retr_params = json_decode($retr_params, true); - if (!is_array($retr_params)) - throw new InvalidParam('retr_params', "Should be a JSON-encoded dictionary"); + # Check retrieval method + $retr_method = $request->get_parameter('retr_method'); + if (!$retr_method) + throw new ParamMissing('retr_method'); + if (!OkapiServiceRunner::exists($retr_method)) + throw new InvalidParam('retr_method', "Method does not exist: '$retr_method'"); + $retr_params = $request->get_parameter('retr_params'); + if (!$retr_params) + throw new ParamMissing('retr_params'); + $retr_params = json_decode($retr_params, true); + if (!is_array($retr_params)) + throw new InvalidParam('retr_params', "Should be a JSON-encoded dictionary"); - self::map_values_to_strings($search_params); - self::map_values_to_strings($retr_params); + self::map_values_to_strings($search_params); + self::map_values_to_strings($retr_params); - # Wrapped? - $wrap = $request->get_parameter('wrap'); - if ($wrap == null) throw new ParamMissing('wrap'); - if (!in_array($wrap, array('true', 'false'))) - throw new InvalidParam('wrap'); - $wrap = ($wrap == 'true'); + # Wrapped? + $wrap = $request->get_parameter('wrap'); + if ($wrap == null) throw new ParamMissing('wrap'); + if (!in_array($wrap, array('true', 'false'))) + throw new InvalidParam('wrap'); + $wrap = ($wrap == 'true'); - # Run search method - try - { - $search_result = OkapiServiceRunner::call($search_method, new OkapiInternalRequest( - $request->consumer, $request->token, $search_params)); - } - catch (BadRequest $e) - { - throw new InvalidParam('search_params', "Search method responded with the ". - "following error message: ".$e->getMessage()); - } + # Run search method + try + { + $search_result = OkapiServiceRunner::call($search_method, new OkapiInternalRequest( + $request->consumer, $request->token, $search_params)); + } + catch (BadRequest $e) + { + throw new InvalidParam('search_params', "Search method responded with the ". + "following error message: ".$e->getMessage()); + } - # Run retrieval method - try - { - $retr_result = OkapiServiceRunner::call($retr_method, new OkapiInternalRequest( - $request->consumer, $request->token, array_merge($retr_params, - array('cache_codes' => implode("|", $search_result['results']))))); - } - catch (BadRequest $e) - { - throw new InvalidParam('retr_params', "Retrieval method responded with the ". - "following error message: ".$e->getMessage()); - } + # Run retrieval method + try + { + $retr_result = OkapiServiceRunner::call($retr_method, new OkapiInternalRequest( + $request->consumer, $request->token, array_merge($retr_params, + array('cache_codes' => implode("|", $search_result['results']))))); + } + catch (BadRequest $e) + { + throw new InvalidParam('retr_params', "Retrieval method responded with the ". + "following error message: ".$e->getMessage()); + } - if ($wrap) - { - # $retr_result might be a PHP object, but also might be a binary response - # (e.g. a GPX file). - if ($retr_result instanceof OkapiHttpResponse) - $result = array('results' => $retr_result->get_body()); - else - $result = array('results' => $retr_result); - foreach ($search_result as $key => &$value_ref) - if ($key != 'results') - $result[$key] = $value_ref; - return Okapi::formatted_response($request, $result); - } - else - { - if ($retr_result instanceof OkapiHttpResponse) - return $retr_result; - else - return Okapi::formatted_response($request, $retr_result); - } - } + if ($wrap) + { + # $retr_result might be a PHP object, but also might be a binary response + # (e.g. a GPX file). + if ($retr_result instanceof OkapiHttpResponse) + $result = array('results' => $retr_result->get_body()); + else + $result = array('results' => $retr_result); + foreach ($search_result as $key => &$value_ref) + if ($key != 'results') + $result[$key] = $value_ref; + return Okapi::formatted_response($request, $result); + } + else + { + if ($retr_result instanceof OkapiHttpResponse) + return $retr_result; + else + return Okapi::formatted_response($request, $retr_result); + } + } - private static function map_values_to_strings(&$dict) - { - foreach (array_keys($dict) as $key) - { - $val = $dict[$key]; - if (is_numeric($val) || is_string($val)) - $dict[$key] = (string)$val; - else - throw new BadRequest("Invalid value format for key: ".$key); - } - } + private static function map_values_to_strings(&$dict) + { + foreach (array_keys($dict) as $key) + { + $val = $dict[$key]; + if (is_numeric($val) || is_string($val)) + $dict[$key] = (string)$val; + else + throw new BadRequest("Invalid value format for key: ".$key); + } + } } diff --git a/htdocs/okapi/services/caches/shortcuts/search_and_retrieve.xml b/htdocs/okapi/services/caches/shortcuts/search_and_retrieve.xml index 3978a556..3fb374d4 100644 --- a/htdocs/okapi/services/caches/shortcuts/search_and_retrieve.xml +++ b/htdocs/okapi/services/caches/shortcuts/search_and_retrieve.xml @@ -1,65 +1,65 @@ - Search for caches and retrieve formatted results - 18 - -

    We think that searching and retrieving data are two different things - and that's why they were cleanly separated in the documentation (put in - separate methods). This was done primarily to keep the docs clean. - This method allows you to do a quick search+retrieve task in one request.

    -

    All services/caches/search/* methods respond with a list of cache codes. - Then you have to use other method (like services/caches/geocaches) to - retrieve the names and locations, based on those cache codes. The - search_and_retrieve method allows you to do these two steps in one - method call.

    -

    First, you have to choose both methods and their parameters - one method - which returns the cache codes, and the other one, that responds - with additional data for the given caches.

    -
    - -

    Name of the search method (begin with "services/").

    -

    E.g. services/caches/search/nearest.

    -
    - -

    JSON-formatted dictionary of parameters to be passed on - to the search method.

    -

    E.g. {"center": "49|19", "status": "Available"}.

    -
    - -

    Name of the retrieval method (begin with "services/").

    -

    E.g. services/caches/geocaches.

    -
    - -

    JSON-formatted dictionary of parameters to be passed on - to the retrieval method.

    -

    E.g. {"fields": "name|location|type"}

    -

    The method will be called with one additional parameter - cache_codes. - These will be the cache codes collected from the results of the search method.

    -
    - -

    Boolean.

    -
      -
    • If true, then results will be wrapped in an additional - object, in order to include any additional data received along with the - search_method response (i.e. the more value).
    • -
    • If false, then this method will return exactly what the - retr_method will respond with.
    • -
    -
    - - -

    If wrap is true, then the method will return a - dictionary of the following structure:

    -
      -
    • results - anything the retrival method - responds with (as long as it's not an error).
    • -
    • any extra keys and values received as a response of - the search_method (i.e. the more variable).
    • -
    -

    If wrap is false, then the method will return - anything the retrieval methods responds with (along with its HTTP headers).

    -

    Example:

    -
    search_and_retrieve
    ?search_method=services/caches/search/bbox
    &search_params={"bbox":"49|19|50|20","limit":"1"}
    &retr_method=services/caches/geocaches
    &retr_params={"fields":"location"}
    &wrap=false
    -

    Possible output:
    {"OP205A": {"location": "49.572417|19.525867"}}

    -

    The same example with wrap=true would return:
    {"results": {"OP205A": {"location": "49.572417|19.525867"}}, "more": true}

    -
    + Search for caches and retrieve formatted results + 18 + +

    We think that searching and retrieving data are two different things + and that's why they were cleanly separated in the documentation (put in + separate methods). This was done primarily to keep the docs clean. + This method allows you to do a quick search+retrieve task in one request.

    +

    All services/caches/search/* methods respond with a list of cache codes. + Then you have to use other method (like services/caches/geocaches) to + retrieve the names and locations, based on those cache codes. The + search_and_retrieve method allows you to do these two steps in one + method call.

    +

    First, you have to choose both methods and their parameters - one method + which returns the cache codes, and the other one, that responds + with additional data for the given caches.

    +
    + +

    Name of the search method (begin with "services/").

    +

    E.g. services/caches/search/nearest.

    +
    + +

    JSON-formatted dictionary of parameters to be passed on + to the search method.

    +

    E.g. {"center": "49|19", "status": "Available"}.

    +
    + +

    Name of the retrieval method (begin with "services/").

    +

    E.g. services/caches/geocaches.

    +
    + +

    JSON-formatted dictionary of parameters to be passed on + to the retrieval method.

    +

    E.g. {"fields": "name|location|type"}

    +

    The method will be called with one additional parameter - cache_codes. + These will be the cache codes collected from the results of the search method.

    +
    + +

    Boolean.

    +
      +
    • If true, then results will be wrapped in an additional + object, in order to include any additional data received along with the + search_method response (i.e. the more value).
    • +
    • If false, then this method will return exactly what the + retr_method will respond with.
    • +
    +
    + + +

    If wrap is true, then the method will return a + dictionary of the following structure:

    +
      +
    • results - anything the retrival method + responds with (as long as it's not an error).
    • +
    • any extra keys and values received as a response of + the search_method (i.e. the more variable).
    • +
    +

    If wrap is false, then the method will return + anything the retrieval methods responds with (along with its HTTP headers).

    +

    Example:

    +
    search_and_retrieve
    ?search_method=services/caches/search/bbox
    &search_params={"bbox":"49|19|50|20","limit":"1"}
    &retr_method=services/caches/geocaches
    &retr_params={"fields":"location"}
    &wrap=false
    +

    Possible output:
    {"OP205A": {"location": "49.572417|19.525867"}}

    +

    The same example with wrap=true would return:
    {"results": {"OP205A": {"location": "49.572417|19.525867"}}, "more": true}

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

    This method works like the services/logs/entry method, but works - with multiple log entries (instead of only one).

    -
    - -

    Pipe-separated list of UUIDs. These represent the log entries you - are interested in. No more than 500 codes are allowed. - Unlike in the "entry" method, this CAN be an empty string (it will - result in an empty, but valid, response).

    -
    - -

    Same as in the services/logs/entry method. Pipe-separated list - of field names which you are interested with. - See services/logs/entry method for a list of available values.

    -
    - - -

    A dictionary. UUIDs you provide will be mapped to dictionary keys, - and each value will be a dictionary of fields you have selected.

    -

    Value of null means that the given UUID haven't been found. - (This behavior is different than in the services/logs/entry method, which - responds with an HTTP 400 error in such case.)

    -
    + Retrieve information on multiple log entries + 107 + +

    This method works like the services/logs/entry method, but works + with multiple log entries (instead of only one).

    +
    + +

    Pipe-separated list of UUIDs. These represent the log entries you + are interested in. No more than 500 codes are allowed. + Unlike in the "entry" method, this CAN be an empty string (it will + result in an empty, but valid, response).

    +
    + +

    Same as in the services/logs/entry method. Pipe-separated list + of field names which you are interested with. + See services/logs/entry method for a list of available values.

    +
    + + +

    A dictionary. UUIDs you provide will be mapped to dictionary keys, + and each value will be a dictionary of fields you have selected.

    +

    Value of null means that the given UUID haven't been found. + (This behavior is different than in the services/logs/entry method, which + responds with an HTTP 400 error in such case.)

    +
    \ No newline at end of file diff --git a/htdocs/okapi/services/logs/entry.php b/htdocs/okapi/services/logs/entry.php index 8fb25bba..8f8cbf4d 100644 --- a/htdocs/okapi/services/logs/entry.php +++ b/htdocs/okapi/services/logs/entry.php @@ -11,26 +11,26 @@ use okapi\InvalidParam; class WebService { - public static function options() - { - return array( - 'min_auth_level' => 1 - ); - } + public static function options() + { + return array( + 'min_auth_level' => 1 + ); + } - public static function call(OkapiRequest $request) - { - $log_uuid = $request->get_parameter('log_uuid'); - if (!$log_uuid) throw new ParamMissing('log_uuid'); - $fields = $request->get_parameter('fields'); - if (!$fields) $fields = "date|user|type|comment"; + public static function call(OkapiRequest $request) + { + $log_uuid = $request->get_parameter('log_uuid'); + if (!$log_uuid) throw new ParamMissing('log_uuid'); + $fields = $request->get_parameter('fields'); + if (!$fields) $fields = "date|user|type|comment"; - $results = OkapiServiceRunner::call('services/logs/entries', new OkapiInternalRequest( - $request->consumer, $request->token, array('log_uuids' => $log_uuid, - 'fields' => $fields))); - $result = $results[$log_uuid]; - if ($result == null) - throw new InvalidParam('log_uuid', "This log entry does not exist."); - return Okapi::formatted_response($request, $result); - } + $results = OkapiServiceRunner::call('services/logs/entries', new OkapiInternalRequest( + $request->consumer, $request->token, array('log_uuids' => $log_uuid, + 'fields' => $fields))); + $result = $results[$log_uuid]; + if ($result == null) + throw new InvalidParam('log_uuid', "This log entry does not exist."); + return Okapi::formatted_response($request, $result); + } } diff --git a/htdocs/okapi/services/logs/entry.xml b/htdocs/okapi/services/logs/entry.xml index aea3426c..38fa8f8c 100644 --- a/htdocs/okapi/services/logs/entry.xml +++ b/htdocs/okapi/services/logs/entry.xml @@ -1,117 +1,117 @@ - Retrieve information on a single log entry - 108 - -

    Retrieve information on a single log entry.

    -
    - UUID of the log entry - -

    Pipe-separated list of field names which you are interested with. - Selected fields will be included in the response. See below fot the list - of available fields.

    -
    - - -

    A dictionary of fields you have selected. Currently available fields:

    - -
      -
    • uuid - unique ID of the log entry,
    • -
    • cache_code - code of the cache which the log entry refers to,
    • -
    • -

      date - date and time (ISO 8601) when the log entry was submitted.

      -

      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 - want to avoid displaying the time. You may assume that if the date - value contains the "00:00:00" string, then it is date-only.

      -
    • -
    • -

      user - a dictionary:

      -
        -
      • uuid - ID of the user (author of the log entry),
      • -
      • username - name of the user (who submitted the log entry),
      • -
      • profile_url - URL of the profile page of the user,
      • -
      -
    • -
    • -

      type - string; log type. One of the values documented - below.

      - -

      Primary types, commonly used by all Opencaching installations:

      - -
        -
      • "Found it" - a user found the cache (Non-Event caches).
      • -
      • "Didn't find it" - a user searched for, but couldn't find the cache (Non-Event caches).
      • -
      • "Comment".
      • -
      • "Will attend" - a user is planning to attend the event (for Event caches only).
      • -
      • "Attended" - a user has attended the event (for Event caches only).
      • -
      - -

      Types which indicate a change of state of the geocache or confirm the - current state (used only by some Opencaching installations):

      - -
        -
      • "Temporarily unavailable" - probably the cache cannot be found, - but it may be repaired (then, "Ready to search" will be submitted).
      • -
      • "Ready to search" - the cache can be found again.
      • -
      • "Archived" - the cache cannot be found and probably won't be - repaired.
      • -
      • "Locked" - the cache has been archived and can no longer be logged.
      • -
      - -

      Other types (used only by some Opencaching installations):

      - -
        -
      • "Needs maintenance" - a user states that the cache is - in need of maintenance.
      • -
      • "Maintenance performed" - the cache owner states that he - had performed the maintenance.
      • -
      • "Moved" - the cache has been moved to a different location.
      • -
      • "OC Team comment" - a comment made by the official OC Team - member.
      • -
      • (to be continued) - this list MAY expand in time! - Your application should accept unknown log types (you may - treat them as "Comment"s).
      • -
      -
    • -
    • -

      oc_team_entry - true if the log entry has been made by an - official OC team member and marked as administrative log; false if it has - not been marked.

      -

      Note: false does NOT mean that it is no administrative log, - because this flag can be missing for (mostly old) admin logs.

      -
    • -
    • -

      was_recommended - true, if the author included his recommendation - in this log entry,

      -
    • -
    • comment - HTML string, text entered - with the log entry,
    • -
    • -

      images - list of dictionaries, each dictionary represents one - image saved along with the log; each dictionary has the following - structure:

      -
        -
      • uuid - UUID of the image,
      • -
      • url - URL of the image,
      • -
      • thumb_url - URL of a small (thumb) version of the image,
      • -
      • caption - plain-text string, caption of the image,
      • -
      • is_spoiler - boolean, if true then the image is - a spoiler image and should not be displayed to the user unless - the user explicitly asks for it.
      • -
      -
    • -
    • -

      internal_id - undocumented, you should not use - this unless you really know you need to. Internal IDs are - not unique across various OKAPI installations. - Try to use UUIDs instead.

      -
    • -
    - -

    Note, that some fields can change in time (users can edit/delete - their log entries).

    - -

    If given log entry does not exist, the method will - respond with an HTTP 400 error.

    -
    + Retrieve information on a single log entry + 108 + +

    Retrieve information on a single log entry.

    +
    + UUID of the log entry + +

    Pipe-separated list of field names which you are interested with. + Selected fields will be included in the response. See below fot the list + of available fields.

    +
    + + +

    A dictionary of fields you have selected. Currently available fields:

    + +
      +
    • uuid - unique ID of the log entry,
    • +
    • cache_code - code of the cache which the log entry refers to,
    • +
    • +

      date - date and time (ISO 8601) when the log entry was submitted.

      +

      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 + want to avoid displaying the time. You may assume that if the date + value contains the "00:00:00" string, then it is date-only.

      +
    • +
    • +

      user - a dictionary:

      +
        +
      • uuid - ID of the user (author of the log entry),
      • +
      • username - name of the user (who submitted the log entry),
      • +
      • profile_url - URL of the profile page of the user,
      • +
      +
    • +
    • +

      type - string; log type. One of the values documented + below.

      + +

      Primary types, commonly used by all Opencaching installations:

      + +
        +
      • "Found it" - a user found the cache (Non-Event caches).
      • +
      • "Didn't find it" - a user searched for, but couldn't find the cache (Non-Event caches).
      • +
      • "Comment".
      • +
      • "Will attend" - a user is planning to attend the event (for Event caches only).
      • +
      • "Attended" - a user has attended the event (for Event caches only).
      • +
      + +

      Types which indicate a change of state of the geocache or confirm the + current state (used only by some Opencaching installations):

      + +
        +
      • "Temporarily unavailable" - probably the cache cannot be found, + but it may be repaired (then, "Ready to search" will be submitted).
      • +
      • "Ready to search" - the cache can be found again.
      • +
      • "Archived" - the cache cannot be found and probably won't be + repaired.
      • +
      • "Locked" - the cache has been archived and can no longer be logged.
      • +
      + +

      Other types (used only by some Opencaching installations):

      + +
        +
      • "Needs maintenance" - a user states that the cache is + in need of maintenance.
      • +
      • "Maintenance performed" - the cache owner states that he + had performed the maintenance.
      • +
      • "Moved" - the cache has been moved to a different location.
      • +
      • "OC Team comment" - a comment made by the official OC Team + member.
      • +
      • (to be continued) - this list MAY expand in time! + Your application should accept unknown log types (you may + treat them as "Comment"s).
      • +
      +
    • +
    • +

      oc_team_entry - true if the log entry has been made by an + official OC team member and marked as administrative log; false if it has + not been marked.

      +

      Note: false does NOT mean that it is no administrative log, + because this flag can be missing for (mostly old) admin logs.

      +
    • +
    • +

      was_recommended - true, if the author included his recommendation + in this log entry,

      +
    • +
    • comment - HTML string, text entered + with the log entry,
    • +
    • +

      images - list of dictionaries, each dictionary represents one + image saved along with the log; each dictionary has the following + structure:

      +
        +
      • uuid - UUID of the image,
      • +
      • url - URL of the image,
      • +
      • thumb_url - URL of a small (thumb) version of the image,
      • +
      • caption - plain-text string, caption of the image,
      • +
      • is_spoiler - boolean, if true then the image is + a spoiler image and should not be displayed to the user unless + the user explicitly asks for it.
      • +
      +
    • +
    • +

      internal_id - undocumented, you should not use + this unless you really know you need to. Internal IDs are + not unique across various OKAPI installations. + Try to use UUIDs instead.

      +
    • +
    + +

    Note, that some fields can change in time (users can edit/delete + their log entries).

    + +

    If given log entry does not exist, the method will + respond with an HTTP 400 error.

    +
    \ No newline at end of file diff --git a/htdocs/okapi/services/logs/logs.php b/htdocs/okapi/services/logs/logs.php index b9687f8e..d34340ad 100644 --- a/htdocs/okapi/services/logs/logs.php +++ b/htdocs/okapi/services/logs/logs.php @@ -15,58 +15,69 @@ use okapi\services\caches\search\SearchAssistant; class WebService { - public static function options() - { - return array( - 'min_auth_level' => 1 - ); - } - public static function call(OkapiRequest $request) - { - $cache_code = $request->get_parameter('cache_code'); - if (!$cache_code) throw new ParamMissing('cache_code'); - $fields = $request->get_parameter('fields'); - if (!$fields) $fields = "uuid|date|user|type|comment"; + public static function options() + { + return array( + 'min_auth_level' => 1 + ); + } + public static function call(OkapiRequest $request) + { + $cache_code = $request->get_parameter('cache_code'); + if (!$cache_code) throw new ParamMissing('cache_code'); + $fields = $request->get_parameter('fields'); + if (!$fields) $fields = "uuid|date|user|type|comment"; - $offset = $request->get_parameter('offset'); - if (!$offset) $offset = "0"; - if ((((int)$offset) != $offset) || ((int)$offset) < 0) - throw new InvalidParam('offset', "Expecting non-negative integer."); - $limit = $request->get_parameter('limit'); - if (!$limit) $limit = "none"; - if ($limit == "none") $limit = "999999999"; - if ((((int)$limit) != $limit) || ((int)$limit) < 0) - throw new InvalidParam('limit', "Expecting non-negative integer or 'none'."); + $offset = $request->get_parameter('offset'); + if (!$offset) $offset = "0"; + if ((((int)$offset) != $offset) || ((int)$offset) < 0) + throw new InvalidParam('offset', "Expecting non-negative integer."); + $limit = $request->get_parameter('limit'); + if (!$limit) $limit = "none"; + if ($limit == "none") $limit = "999999999"; + if ((((int)$limit) != $limit) || ((int)$limit) < 0) + throw new InvalidParam('limit', "Expecting non-negative integer or 'none'."); - # Check if code exists and retrieve cache ID (this will throw - # a proper exception on invalid code). + # Check if code exists and retrieve cache ID (this will throw + # a proper exception on invalid code). - $cache = OkapiServiceRunner::call('services/caches/geocache', new OkapiInternalRequest( - $request->consumer, null, array('cache_code' => $cache_code, 'fields' => 'internal_id'))); + $cache = OkapiServiceRunner::call('services/caches/geocache', new OkapiInternalRequest( + $request->consumer, null, array('cache_code' => $cache_code, 'fields' => 'internal_id'))); - # Cache exists. Getting the uuids of its logs. + # Cache exists. Getting the uuids of its logs. - $log_uuids = Db::select_column(" - select uuid - from cache_logs - where - cache_id = '".mysql_real_escape_string($cache['internal_id'])."' - and ".((Settings::get('OC_BRANCH') == 'oc.pl') ? "deleted = 0" : "true")." - order by date desc - limit $offset, $limit - "); + $log_uuids = Db::select_column(" + select uuid + from cache_logs + where + cache_id = '".mysql_real_escape_string($cache['internal_id'])."' + and ".((Settings::get('OC_BRANCH') == 'oc.pl') ? "deleted = 0" : "true")." + order by date desc + limit $offset, $limit + "); - # Getting the logs themselves. Formatting as an ordered list. + # Getting the logs themselves. Formatting as an ordered list. - $internal_request = new OkapiInternalRequest( - $request->consumer, $request->token, array('log_uuids' => implode("|", $log_uuids), - 'fields' => $fields)); - $internal_request->skip_limits = true; - $logs = OkapiServiceRunner::call('services/logs/entries', $internal_request); - $results = array(); - foreach ($log_uuids as $log_uuid) - $results[] = $logs[$log_uuid]; + $internal_request = new OkapiInternalRequest( + $request->consumer, $request->token, array('log_uuids' => implode("|", $log_uuids), + 'fields' => $fields)); + $internal_request->skip_limits = true; + $logs = OkapiServiceRunner::call('services/logs/entries', $internal_request); + $results = array(); + foreach ($log_uuids as $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); + } } diff --git a/htdocs/okapi/services/logs/logs.xml b/htdocs/okapi/services/logs/logs.xml index 01ab1f6a..74c937bd 100644 --- a/htdocs/okapi/services/logs/logs.xml +++ b/htdocs/okapi/services/logs/logs.xml @@ -1,31 +1,31 @@ - Retrieve all log entries for the specified geocache - 41 - -

    Retrieve the log entries for the specified geocache. Use the offset and - limit parameters for pagination. If you want only the latest entries, you - may also use the latest_logs field in the services/caches/geocache method.

    -

    Log entries are ordered by a descending date of the entry.

    -
    - -

    Code of the geocache.

    -
    - -

    Same as in the services/logs/entry method. Pipe-separated list - of field names which you are interested with. - See services/logs/entry method for a list of available values.

    -
    - -

    Number of entries to skip at the beginning. Use this along the limit parameter - for pagination.

    -
    - -

    Maximum number of entries to return or none - if you want all the entries.

    -
    - - -

    A list of log entries, ordered by date. Each log entry is a dictionary of a format - described in the "entry" method.

    -
    + Retrieve all log entries for the specified geocache + 41 + +

    Retrieve the log entries for the specified geocache. Use the offset and + limit parameters for pagination. If you want only the latest entries, you + may also use the latest_logs field in the services/caches/geocache method.

    +

    Log entries are ordered by a descending date of the entry.

    +
    + +

    Code of the geocache.

    +
    + +

    Same as in the services/logs/entry method. Pipe-separated list + of field names which you are interested with. + See services/logs/entry method for a list of available values.

    +
    + +

    Number of entries to skip at the beginning. Use this along the limit parameter + for pagination.

    +
    + +

    Maximum number of entries to return or none + if you want all the entries.

    +
    + + +

    A list of log entries, ordered by date. Each log entry is a dictionary of a format + described in the "entry" method.

    +
    \ No newline at end of file diff --git a/htdocs/okapi/services/logs/submit.php b/htdocs/okapi/services/logs/submit.php index af288bf8..937372a5 100644 --- a/htdocs/okapi/services/logs/submit.php +++ b/htdocs/okapi/services/logs/submit.php @@ -26,644 +26,644 @@ class CannotPublishException extends Exception {} class WebService { - public static function options() - { - return array( - 'min_auth_level' => 3 - ); - } - - /** - * Publish a new log entry and return log entry uuid. Throws - * CannotPublishException or BadRequest on errors. - */ - private static function _call(OkapiRequest $request) - { - # Developers! Please notice the fundamental difference between throwing - # CannotPublishException and standard BadRequest/InvalidParam exceptions! - # Notice, that this is "_call" method, not the usual "call" (see below - # for "call"). - - $cache_code = $request->get_parameter('cache_code'); - if (!$cache_code) throw new ParamMissing('cache_code'); - - $logtype = $request->get_parameter('logtype'); - if (!$logtype) throw new ParamMissing('logtype'); - if (!in_array($logtype, array('Found it', "Didn't find it", 'Comment', 'Will attend', 'Attended'))) - throw new InvalidParam('logtype', "'$logtype' in not a valid logtype code."); - - $comment = $request->get_parameter('comment'); - if (!$comment) $comment = ""; - - $comment_format = $request->get_parameter('comment_format'); - if (!$comment_format) $comment_format = "auto"; - if (!in_array($comment_format, array('auto', 'html', 'plaintext'))) - throw new InvalidParam('comment_format', $comment_format); - - $tmp = $request->get_parameter('when'); - if ($tmp) - { - $when = strtotime($tmp); - if (!$when) - throw new InvalidParam('when', "'$tmp' is not in a valid format or is not a valid date."); - if ($when > time() + 5*60) - throw new CannotPublishException(_("You are trying to publish a log entry with a date in future. ". - "Cache log entries are allowed to be published in the past, but NOT in the future.")); - } - else - $when = time(); - - $on_duplicate = $request->get_parameter('on_duplicate'); - if (!$on_duplicate) $on_duplicate = "silent_success"; - if (!in_array($on_duplicate, array('silent_success', 'user_error', 'continue'))) - throw new InvalidParam('on_duplicate', "Unknown option: '$on_duplicate'."); - - $rating = $request->get_parameter('rating'); - if ($rating !== null && (!in_array($rating, array(1,2,3,4,5)))) - throw new InvalidParam('rating', "If present, it must be an integer in the 1..5 scale."); - if ($rating && $logtype != 'Found it' && $logtype != 'Attended') - throw new BadRequest("Rating is allowed only for 'Found it' and 'Attended' logtypes."); - if ($rating !== null && (Settings::get('OC_BRANCH') == 'oc.de')) - { - # We will remove the rating request and change the success message - # (which will be returned IF the rest of the query will meet all the - # requirements). - - self::$success_message .= " ".sprintf(_("However, your cache rating was ignored, because %s does not have a rating system."), - Okapi::get_normalized_site_name()); - $rating = null; - } - - $recommend = $request->get_parameter('recommend'); - if (!$recommend) $recommend = 'false'; - if (!in_array($recommend, array('true', 'false'))) - throw new InvalidParam('recommend', "Unknown option: '$recommend'."); - $recommend = ($recommend == 'true'); - if ($recommend && $logtype != 'Found it') - { - if ($logtype != 'Attended') - throw new BadRequest("Recommending is allowed only for 'Found it' and 'Attended' logs."); - else if (Settings::get('OC_BRANCH') == 'oc.pl') - { - # We will remove the recommendation request and change the success message - # (which will be returned IF the rest of the query will meet all the - # requirements). - self::$success_message .= " ".sprintf(_("However, your cache recommendation was ignored, because %s does not allow recommending event caches."), - Okapi::get_normalized_site_name()); - $recommend = null; - } - } - - $needs_maintenance = $request->get_parameter('needs_maintenance'); - if (!$needs_maintenance) $needs_maintenance = 'false'; - if (!in_array($needs_maintenance, array('true', 'false'))) - throw new InvalidParam('needs_maintenance', "Unknown option: '$needs_maintenance'."); - $needs_maintenance = ($needs_maintenance == 'true'); - if ($needs_maintenance && (!Settings::get('SUPPORTS_LOGTYPE_NEEDS_MAINTENANCE'))) - { - # If not supported, just ignore it. - self::$success_message .= " ".sprintf(_("However, your \"needs maintenance\" flag was ignored, because %s does not support this feature."), - Okapi::get_normalized_site_name()); - $needs_maintenance = false; - } - - # Check if cache exists and retrieve cache internal ID (this will throw - # a proper exception on invalid cache_code). Also, get the user object. - - $cache = OkapiServiceRunner::call('services/caches/geocache', new OkapiInternalRequest( - $request->consumer, null, array('cache_code' => $cache_code, - 'fields' => 'internal_id|status|owner|type|req_passwd'))); - $user = OkapiServiceRunner::call('services/users/by_internal_id', new OkapiInternalRequest( - $request->consumer, $request->token, array('internal_id' => $request->token->user_id, - 'fields' => 'is_admin|uuid|internal_id|caches_found|rcmds_given'))); - - # Various integrity checks. - - if ($cache['type'] == 'Event') - { - if (!in_array($logtype, array('Will attend', 'Attended', 'Comment'))) - throw new CannotPublishException(_('This cache is an Event cache. You cannot "Find" it (but you can attend it, or comment on it)!')); - } - else # type != event - { - if (in_array($logtype, array('Will attend', 'Attended'))) - throw new CannotPublishException(_('This cache is NOT an Event cache. You cannot "Attend" it (but you can find it, or comment on it)!')); - else if (!in_array($logtype, array('Found it', "Didn't find it", 'Comment'))) - throw new Exception("Unknown log entry - should be documented here."); - } - if ($logtype == 'Comment' && strlen(trim($comment)) == 0) - throw new CannotPublishException(_("Your have to supply some text for your comment.")); - - # Password check. - - if (($logtype == 'Found it' || $logtype == 'Attended') && $cache['req_passwd']) - { - $valid_password = Db::select_value(" - select logpw - from caches - where cache_id = '".mysql_real_escape_string($cache['internal_id'])."' - "); - $supplied_password = $request->get_parameter('password'); - if (!$supplied_password) - throw new CannotPublishException(_("This cache requires a password. You didn't provide one!")); - if (strtolower($supplied_password) != strtolower($valid_password)) - throw new CannotPublishException(_("Invalid password!")); - } - - # Prepare our comment to be inserted into the database. This may require - # some reformatting which depends on the current OC installation. - - if (Settings::get('OC_BRANCH') == 'oc.de') - { - # OCDE stores all comments in HTML format, while the 'text_html' field - # indicates their *original* format as delivered by the user. This - # allows processing the 'text' field contents without caring about the - # original format, while still being able to re-create the comment in - # its original form. It requires us to HTML-encode plaintext comments - # and to indicate this by setting 'html_text' to FALSE. - # - # For user-supplied HTML comments, OCDE requires us to do additional - # HTML purification prior to the insertion into the database. - - if ($comment_format == 'plaintext') - { - $formatted_comment = htmlspecialchars($comment, ENT_QUOTES); - $formatted_comment = nl2br($formatted_comment); - $value_for_text_html_field = 0; - } - else - { - if ($comment_format == 'auto') - { - # 'Auto' is for backward compatibility. Before the "comment_format" - # was introduced, OKAPI used a weird format in between (it allowed - # HTML, but applied nl2br too). - - $formatted_comment = nl2br($comment); - } - else - $formatted_comment = $comment; - - # NOTICE: We are including EXTERNAL OCDE library here! This - # code does not belong to OKAPI! - - $opt['rootpath'] = $GLOBALS['rootpath']; - $opt['html_purifier'] = Settings::get('OCDE_HTML_PURIFIER_SETTINGS'); - require_once $GLOBALS['rootpath'] . 'lib2/OcHTMLPurifier.class.php'; - - $purifier = new \OcHTMLPurifier($opt); - $formatted_comment = $purifier->purify($formatted_comment); - $value_for_text_html_field = 1; - } - } - else - { - # OCPL is even weirder. It also stores HTML-lized comments in the database - # (it doesn't really matter if 'text_html' field is set to FALSE). OKAPI must - # save it in HTML either way. However, escaping plain-text doesn't work! - # If we put "<b>" in, it still gets converted to "" before display! - # NONE of this process is documented within OCPL code. OKAPI uses a dirty - # "hack" to save PLAINTEXT comments (let us hope the hack will remain valid). - # - # OCPL doesn't require HTML purification prior to the database insertion. - # HTML seems to be purified dynamically, before it is displayed. - - if ($comment_format == 'plaintext') - { - $formatted_comment = htmlspecialchars($comment, ENT_QUOTES); - $formatted_comment = nl2br($formatted_comment); - $formatted_comment = str_replace("&", "&#38;", $formatted_comment); - $formatted_comment = str_replace("<", "&#60;", $formatted_comment); - $formatted_comment = str_replace(">", "&#62;", $formatted_comment); - $value_for_text_html_field = 0; // WRTODO: get rid of - } - elseif ($comment_format == 'auto') - { - $formatted_comment = nl2br($comment); - $value_for_text_html_field = 1; - } - else - { - $formatted_comment = $comment; - $value_for_text_html_field = 1; - } - } - unset($comment); - - # Duplicate detection. - - if ($on_duplicate != 'continue') - { - # Attempt to find a log entry made by the same user, for the same cache, with - # the same date, type, comment, etc. Note, that these are not ALL the fields - # we could check, but should work ok in most cases. Also note, that we - # DO NOT guarantee that duplicate detection will succeed. If it doesn't, - # nothing bad happens (user will just post two similar log entries). - # Keep this simple! - - $duplicate_uuid = Db::select_value(" - select uuid - from cache_logs - where - user_id = '".mysql_real_escape_string($request->token->user_id)."' - and cache_id = '".mysql_real_escape_string($cache['internal_id'])."' - and type = '".mysql_real_escape_string(Okapi::logtypename2id($logtype))."' - and date = from_unixtime('".mysql_real_escape_string($when)."') - and text = '".mysql_real_escape_string($formatted_comment)."' - ".((Settings::get('OC_BRANCH') == 'oc.pl') ? "and deleted = 0" : "")." - limit 1 - "); - if ($duplicate_uuid != null) - { - if ($on_duplicate == 'silent_success') - { - # Act as if the log has been submitted successfully. - return $duplicate_uuid; - } - elseif ($on_duplicate == 'user_error') - { - throw new CannotPublishException(_("You have already submitted a log entry with exactly the same contents.")); - } - } - } - - # Check if already found it (and make sure the user is not the owner). - # - # OCPL forbids logging 'Found it' or "Didn't find" for an already found cache, - # while OCDE allows all kinds of duplicate logs. - - if (Settings::get('OC_BRANCH') == 'oc.pl' - && (($logtype == 'Found it') || ($logtype == "Didn't find it"))) - { - $has_already_found_it = Db::select_value(" - select 1 - from cache_logs - where - user_id = '".mysql_real_escape_string($user['internal_id'])."' - and cache_id = '".mysql_real_escape_string($cache['internal_id'])."' - and type = '".mysql_real_escape_string(Okapi::logtypename2id("Found it"))."' - and ".((Settings::get('OC_BRANCH') == 'oc.pl') ? "deleted = 0" : "true")." - "); - if ($has_already_found_it) - throw new CannotPublishException(_("You have already submitted a \"Found it\" log entry once. Now you may submit \"Comments\" only!")); - if ($user['uuid'] == $cache['owner']['uuid']) - throw new CannotPublishException(_("You are the owner of this cache. You may submit \"Comments\" only!")); - } - - # Check if the user has already rated the cache. BTW: I don't get this one. - # If we already know, that the cache was NOT found yet, then HOW could the - # user submit a rating for it? Anyway, I will stick to the procedure - # found in log.php. On the bright side, it's fail-safe. - - if ($rating) - { - $has_already_rated = Db::select_value(" - select 1 - from scores - where - user_id = '".mysql_real_escape_string($user['internal_id'])."' - and cache_id = '".mysql_real_escape_string($cache['internal_id'])."' - "); - if ($has_already_rated) - throw new CannotPublishException(_("You have already rated this cache once. Your rating cannot be changed.")); - } - - # If user wants to recommend... - - if ($recommend) - { - # Do the same "fail-safety" check as we did for the rating. - - $already_recommended = Db::select_value(" - select 1 - from cache_rating - where - user_id = '".mysql_real_escape_string($user['internal_id'])."' - and cache_id = '".mysql_real_escape_string($cache['internal_id'])."' - "); - if ($already_recommended) - throw new CannotPublishException(_("You have already recommended this cache once.")); - - # Check the number of recommendations. - - $founds = $user['caches_found'] + 1; // +1, because he'll find THIS ONE in a moment, right? - # Note: caches_found includes event attendance on both, OCDE and OCPL. - # Though OCPL does not allow recommending events, for each 10 event - # attendances the user may recommend a non-event cache. - $rcmds_left = floor($founds / 10.0) - $user['rcmds_given']; - if ($rcmds_left <= 0) - throw new CannotPublishException(_("You don't have any recommendations to give. Find more caches first!")); - } - - # If user checked the "needs_maintenance" flag, we will shuffle things a little... - - if ($needs_maintenance) - { - # If we're here, then we also know that the "Needs maintenance" log type is supported - # by this OC site. However, it's a separate log type, so we might have to submit - # two log types together: - - if ($logtype == 'Comment') - { - # If user submits a "Comment", we'll just change its type to "Needs maintenance". - # Only one log entry will be issued. - - $logtype = 'Needs maintenance'; - $second_logtype = null; - $second_formatted_comment = null; - } - elseif ($logtype == 'Found it') - { - # If "Found it", then we'll issue two log entries: one "Found it" with the - # original comment, and second one "Needs maintenance" with empty comment. - - $second_logtype = 'Needs maintenance'; - $second_formatted_comment = ""; - } - elseif ($logtype == "Didn't find it") - { - # If "Didn't find it", then we'll issue two log entries, but this time - # we'll do this the other way around. The first "Didn't find it" entry - # will have an empty comment. We will move the comment to the second - # "Needs maintenance" log entry. (It's okay for this behavior to change - # in the future, but it seems natural to me.) - - $second_logtype = 'Needs maintenance'; - $second_formatted_comment = $formatted_comment; - $formatted_comment = ""; - } - else if ($logtype == 'Will attend' || $logtype == 'Attended') - { - # OC branches which know maintenance logs do not allow them on event caches. - throw new CannotPublishException(_("Event caches cannot \"need maintenance\".")); - } - else - throw new Exception(); - } - else - { - # User didn't check the "Needs maintenance" flag OR "Needs maintenance" log type - # isn't supported by this server. - - $second_logtype = null; - $second_formatted_comment = null; - } - - # Finally! Insert the rows into the log entries table. Update - # cache stats and user stats. - - $log_uuid = self::insert_log_row( - $request->consumer->key, $cache['internal_id'], $user['internal_id'], $logtype, - $when, $formatted_comment, $value_for_text_html_field); - self::increment_cache_stats($cache['internal_id'], $when, $logtype); - self::increment_user_stats($user['internal_id'], $logtype); - if ($second_logtype != null) - { - # Reminder: This will never be called while SUPPORTS_LOGTYPE_NEEDS_MAINTENANCE is off. - - self::insert_log_row( - $request->consumer->key, $cache['internal_id'], $user['internal_id'], $second_logtype, - $when + 1, $second_formatted_comment, $value_for_text_html_field); - self::increment_cache_stats($cache['internal_id'], $when + 1, $second_logtype); - self::increment_user_stats($user['internal_id'], $second_logtype); - } - - # Save the rating. - - if ($rating) - { - # This code will be called for OCPL branch only. Earlier, we made sure, - # to set $rating to null, if we're running on OCDE. - - # OCPL has a little strange way of storing cumulative rating. Instead - # of storing the sum of all ratings, OCPL stores the computed average - # and update it using multiple floating-point operations. Moreover, - # the "score" field in the database is on the -3..3 scale (NOT 1..5), - # and the translation made at retrieval time is DIFFERENT than the - # one made here (both of them are non-linear). Also, once submitted, - # the rating can never be changed. It surely feels quite inconsistent, - # but presumably has some deep logic into it. See also here (Polish): - # http://wiki.opencaching.pl/index.php/Oceny_skrzynek - - switch ($rating) - { - case 1: $db_score = -2.0; break; - case 2: $db_score = -0.5; break; - case 3: $db_score = 0.7; break; - case 4: $db_score = 1.7; break; - case 5: $db_score = 3.0; break; - default: throw new Exception(); - } - Db::execute(" - update caches - set - score = (score*votes + '".mysql_real_escape_string($db_score)."')/(votes + 1), - votes = votes + 1 - where cache_id = '".mysql_real_escape_string($cache['internal_id'])."' - "); - Db::execute(" - insert into scores (user_id, cache_id, score) - values ( - '".mysql_real_escape_string($user['internal_id'])."', - '".mysql_real_escape_string($cache['internal_id'])."', - '".mysql_real_escape_string($db_score)."' - ); - "); - } - - # Save recommendation. - - if ($recommend) - { - if (Db::field_exists('cache_rating', 'rating_date')) - { - Db::execute(" - insert into cache_rating (user_id, cache_id, rating_date) - values ( - '".mysql_real_escape_string($user['internal_id'])."', - '".mysql_real_escape_string($cache['internal_id'])."', - from_unixtime('".mysql_real_escape_string($when)."') - ); - "); - } - else - { - Db::execute(" - insert into cache_rating (user_id, cache_id) - values ( - '".mysql_real_escape_string($user['internal_id'])."', - '".mysql_real_escape_string($cache['internal_id'])."' - ); - "); - } - } - - # We need to delete the copy of stats-picture for this user. Otherwise, - # the legacy OC code won't detect that the picture needs to be refreshed. - - $filepath = Okapi::get_var_dir().'/images/statpics/statpic'.$user['internal_id'].'.jpg'; - if (file_exists($filepath)) - unlink($filepath); - - # Success. Return the uuid. - - return $log_uuid; - } - - private static $success_message = null; - public static function call(OkapiRequest $request) - { - # This is the "real" entry point. A wrapper for the _call method. - - $langpref = $request->get_parameter('langpref'); - if (!$langpref) $langpref = "en"; - - # Error messages thrown via CannotPublishException exceptions should be localized. - # They will be delivered for end user to display in his language. - - Okapi::gettext_domain_init(explode("|", $langpref)); - try - { - # If appropriate, $success_message might be changed inside the _call. - self::$success_message = _("Your cache log entry was posted successfully."); - $log_uuid = self::_call($request); - $result = array( - 'success' => true, - 'message' => self::$success_message, - 'log_uuid' => $log_uuid - ); - Okapi::gettext_domain_restore(); - } - catch (CannotPublishException $e) - { - Okapi::gettext_domain_restore(); - $result = array( - 'success' => false, - 'message' => $e->getMessage(), - 'log_uuid' => null - ); - } - - return Okapi::formatted_response($request, $result); - } - - private static function increment_cache_stats($cache_internal_id, $when, $logtype) - { - if (Settings::get('OC_BRANCH') == 'oc.de') - { - # OCDE handles cache stats updates using triggers. So, they are already - # incremented properly. - } - else - { - # OCPL doesn't use triggers for this. We need to update manually. - - if ($logtype == 'Found it') - { - Db::execute(" - update caches - set - founds = founds + 1, - last_found = greatest(last_found, from_unixtime('".mysql_real_escape_string($when)."')) - where cache_id = '".mysql_real_escape_string($cache_internal_id)."' - "); - } - elseif ($logtype == "Didn't find it") - { - Db::execute(" - update caches - set notfounds = notfounds + 1 - where cache_id = '".mysql_real_escape_string($cache_internal_id)."' - "); - } - elseif ($logtype == 'Comment') - { - Db::execute(" - update caches - set notes = notes + 1 - where cache_id = '".mysql_real_escape_string($cache_internal_id)."' - "); - } - else - { - # This log type is not represented in cache stats. - } - } - } - - private static function increment_user_stats($user_internal_id, $logtype) - { - if (Settings::get('OC_BRANCH') == 'oc.de') - { - # OCDE handles cache stats updates using triggers. So, they are already - # incremented properly. - } - else - { - # OCPL doesn't have triggers for this. We need to update manually. - - switch ($logtype) - { - case 'Found it': $field_to_increment = 'founds_count'; break; - case "Didn't find it": $field_to_increment = 'notfounds_count'; break; - case 'Comment': $field_to_increment = 'log_notes_count'; break; - default: - # This log type is not represented in user stats. - return; - } - Db::execute(" - update user - set $field_to_increment = $field_to_increment + 1 - where user_id = '".mysql_real_escape_string($user_internal_id)."' - "); - } - } - - private static function create_uuid() - { - return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', - mt_rand(0, 0xffff), mt_rand(0, 0xffff), - mt_rand(0, 0xffff), - mt_rand(0, 0x0fff) | 0x4000, - mt_rand(0, 0x3fff) | 0x8000, - mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff) - ); - } - - private static function insert_log_row( - $consumer_key, $cache_internal_id, $user_internal_id, $logtype, $when, - $formatted_comment, $text_html - ) - { - $log_uuid = self::create_uuid(); - Db::execute(" - insert into cache_logs (uuid, cache_id, user_id, type, date, text, text_html, last_modified, date_created, node) - values ( - '".mysql_real_escape_string($log_uuid)."', - '".mysql_real_escape_string($cache_internal_id)."', - '".mysql_real_escape_string($user_internal_id)."', - '".mysql_real_escape_string(Okapi::logtypename2id($logtype))."', - from_unixtime('".mysql_real_escape_string($when)."'), - '".mysql_real_escape_string($formatted_comment)."', - '".mysql_real_escape_string($text_html)."', - now(), - now(), - '".mysql_real_escape_string(Settings::get('OC_NODE_ID'))."' - ); - "); - $log_internal_id = Db::last_insert_id(); - - # Store additional information on consumer_key which have created this log entry. - # (Maybe we'll want to display this somewhere later.) - - Db::execute(" - insert into okapi_cache_logs (log_id, consumer_key) - values ( - '".mysql_real_escape_string($log_internal_id)."', - '".mysql_real_escape_string($consumer_key)."' - ); - "); - - return $log_uuid; - } + public static function options() + { + return array( + 'min_auth_level' => 3 + ); + } + + /** + * Publish a new log entry and return log entry uuid. Throws + * CannotPublishException or BadRequest on errors. + */ + private static function _call(OkapiRequest $request) + { + # Developers! Please notice the fundamental difference between throwing + # CannotPublishException and standard BadRequest/InvalidParam exceptions! + # Notice, that this is "_call" method, not the usual "call" (see below + # for "call"). + + $cache_code = $request->get_parameter('cache_code'); + if (!$cache_code) throw new ParamMissing('cache_code'); + + $logtype = $request->get_parameter('logtype'); + if (!$logtype) throw new ParamMissing('logtype'); + if (!in_array($logtype, array('Found it', "Didn't find it", 'Comment', 'Will attend', 'Attended'))) + throw new InvalidParam('logtype', "'$logtype' in not a valid logtype code."); + + $comment = $request->get_parameter('comment'); + if (!$comment) $comment = ""; + + $comment_format = $request->get_parameter('comment_format'); + if (!$comment_format) $comment_format = "auto"; + if (!in_array($comment_format, array('auto', 'html', 'plaintext'))) + throw new InvalidParam('comment_format', $comment_format); + + $tmp = $request->get_parameter('when'); + if ($tmp) + { + $when = strtotime($tmp); + if (!$when) + throw new InvalidParam('when', "'$tmp' is not in a valid format or is not a valid date."); + if ($when > time() + 5*60) + throw new CannotPublishException(_("You are trying to publish a log entry with a date in future. ". + "Cache log entries are allowed to be published in the past, but NOT in the future.")); + } + else + $when = time(); + + $on_duplicate = $request->get_parameter('on_duplicate'); + if (!$on_duplicate) $on_duplicate = "silent_success"; + if (!in_array($on_duplicate, array('silent_success', 'user_error', 'continue'))) + throw new InvalidParam('on_duplicate', "Unknown option: '$on_duplicate'."); + + $rating = $request->get_parameter('rating'); + if ($rating !== null && (!in_array($rating, array(1,2,3,4,5)))) + throw new InvalidParam('rating', "If present, it must be an integer in the 1..5 scale."); + if ($rating && $logtype != 'Found it' && $logtype != 'Attended') + throw new BadRequest("Rating is allowed only for 'Found it' and 'Attended' logtypes."); + if ($rating !== null && (Settings::get('OC_BRANCH') == 'oc.de')) + { + # We will remove the rating request and change the success message + # (which will be returned IF the rest of the query will meet all the + # requirements). + + self::$success_message .= " ".sprintf(_("However, your cache rating was ignored, because %s does not have a rating system."), + Okapi::get_normalized_site_name()); + $rating = null; + } + + $recommend = $request->get_parameter('recommend'); + if (!$recommend) $recommend = 'false'; + if (!in_array($recommend, array('true', 'false'))) + throw new InvalidParam('recommend', "Unknown option: '$recommend'."); + $recommend = ($recommend == 'true'); + if ($recommend && $logtype != 'Found it') + { + if ($logtype != 'Attended') + throw new BadRequest("Recommending is allowed only for 'Found it' and 'Attended' logs."); + else if (Settings::get('OC_BRANCH') == 'oc.pl') + { + # We will remove the recommendation request and change the success message + # (which will be returned IF the rest of the query will meet all the + # requirements). + self::$success_message .= " ".sprintf(_("However, your cache recommendation was ignored, because %s does not allow recommending event caches."), + Okapi::get_normalized_site_name()); + $recommend = null; + } + } + + $needs_maintenance = $request->get_parameter('needs_maintenance'); + if (!$needs_maintenance) $needs_maintenance = 'false'; + if (!in_array($needs_maintenance, array('true', 'false'))) + throw new InvalidParam('needs_maintenance', "Unknown option: '$needs_maintenance'."); + $needs_maintenance = ($needs_maintenance == 'true'); + if ($needs_maintenance && (!Settings::get('SUPPORTS_LOGTYPE_NEEDS_MAINTENANCE'))) + { + # If not supported, just ignore it. + self::$success_message .= " ".sprintf(_("However, your \"needs maintenance\" flag was ignored, because %s does not support this feature."), + Okapi::get_normalized_site_name()); + $needs_maintenance = false; + } + + # Check if cache exists and retrieve cache internal ID (this will throw + # a proper exception on invalid cache_code). Also, get the user object. + + $cache = OkapiServiceRunner::call('services/caches/geocache', new OkapiInternalRequest( + $request->consumer, null, array('cache_code' => $cache_code, + 'fields' => 'internal_id|status|owner|type|req_passwd'))); + $user = OkapiServiceRunner::call('services/users/by_internal_id', new OkapiInternalRequest( + $request->consumer, $request->token, array('internal_id' => $request->token->user_id, + 'fields' => 'is_admin|uuid|internal_id|caches_found|rcmds_given'))); + + # Various integrity checks. + + if ($cache['type'] == 'Event') + { + if (!in_array($logtype, array('Will attend', 'Attended', 'Comment'))) + throw new CannotPublishException(_('This cache is an Event cache. You cannot "Find" it (but you can attend it, or comment on it)!')); + } + else # type != event + { + if (in_array($logtype, array('Will attend', 'Attended'))) + throw new CannotPublishException(_('This cache is NOT an Event cache. You cannot "Attend" it (but you can find it, or comment on it)!')); + else if (!in_array($logtype, array('Found it', "Didn't find it", 'Comment'))) + throw new Exception("Unknown log entry - should be documented here."); + } + if ($logtype == 'Comment' && strlen(trim($comment)) == 0) + throw new CannotPublishException(_("Your have to supply some text for your comment.")); + + # Password check. + + if (($logtype == 'Found it' || $logtype == 'Attended') && $cache['req_passwd']) + { + $valid_password = Db::select_value(" + select logpw + from caches + where cache_id = '".mysql_real_escape_string($cache['internal_id'])."' + "); + $supplied_password = $request->get_parameter('password'); + if (!$supplied_password) + throw new CannotPublishException(_("This cache requires a password. You didn't provide one!")); + if (strtolower($supplied_password) != strtolower($valid_password)) + throw new CannotPublishException(_("Invalid password!")); + } + + # Prepare our comment to be inserted into the database. This may require + # some reformatting which depends on the current OC installation. + + if (Settings::get('OC_BRANCH') == 'oc.de') + { + # OCDE stores all comments in HTML format, while the 'text_html' field + # indicates their *original* format as delivered by the user. This + # allows processing the 'text' field contents without caring about the + # original format, while still being able to re-create the comment in + # its original form. It requires us to HTML-encode plaintext comments + # and to indicate this by setting 'html_text' to FALSE. + # + # For user-supplied HTML comments, OCDE requires us to do additional + # HTML purification prior to the insertion into the database. + + if ($comment_format == 'plaintext') + { + $formatted_comment = htmlspecialchars($comment, ENT_QUOTES); + $formatted_comment = nl2br($formatted_comment); + $value_for_text_html_field = 0; + } + else + { + if ($comment_format == 'auto') + { + # 'Auto' is for backward compatibility. Before the "comment_format" + # was introduced, OKAPI used a weird format in between (it allowed + # HTML, but applied nl2br too). + + $formatted_comment = nl2br($comment); + } + else + $formatted_comment = $comment; + + # NOTICE: We are including EXTERNAL OCDE library here! This + # code does not belong to OKAPI! + + $opt['rootpath'] = $GLOBALS['rootpath']; + $opt['html_purifier'] = Settings::get('OCDE_HTML_PURIFIER_SETTINGS'); + require_once($GLOBALS['rootpath'] . 'lib2/OcHTMLPurifier.class.php'); + + $purifier = new \OcHTMLPurifier($opt); + $formatted_comment = $purifier->purify($formatted_comment); + $value_for_text_html_field = 1; + } + } + else + { + # OCPL is even weirder. It also stores HTML-lized comments in the database + # (it doesn't really matter if 'text_html' field is set to FALSE). OKAPI must + # save it in HTML either way. However, escaping plain-text doesn't work! + # If we put "<b>" in, it still gets converted to "" before display! + # NONE of this process is documented within OCPL code. OKAPI uses a dirty + # "hack" to save PLAINTEXT comments (let us hope the hack will remain valid). + # + # OCPL doesn't require HTML purification prior to the database insertion. + # HTML seems to be purified dynamically, before it is displayed. + + if ($comment_format == 'plaintext') + { + $formatted_comment = htmlspecialchars($comment, ENT_QUOTES); + $formatted_comment = nl2br($formatted_comment); + $formatted_comment = str_replace("&", "&#38;", $formatted_comment); + $formatted_comment = str_replace("<", "&#60;", $formatted_comment); + $formatted_comment = str_replace(">", "&#62;", $formatted_comment); + $value_for_text_html_field = 0; // WRTODO: get rid of + } + elseif ($comment_format == 'auto') + { + $formatted_comment = nl2br($comment); + $value_for_text_html_field = 1; + } + else + { + $formatted_comment = $comment; + $value_for_text_html_field = 1; + } + } + unset($comment); + + # Duplicate detection. + + if ($on_duplicate != 'continue') + { + # Attempt to find a log entry made by the same user, for the same cache, with + # the same date, type, comment, etc. Note, that these are not ALL the fields + # we could check, but should work ok in most cases. Also note, that we + # DO NOT guarantee that duplicate detection will succeed. If it doesn't, + # nothing bad happens (user will just post two similar log entries). + # Keep this simple! + + $duplicate_uuid = Db::select_value(" + select uuid + from cache_logs + where + user_id = '".mysql_real_escape_string($request->token->user_id)."' + and cache_id = '".mysql_real_escape_string($cache['internal_id'])."' + and type = '".mysql_real_escape_string(Okapi::logtypename2id($logtype))."' + and date = from_unixtime('".mysql_real_escape_string($when)."') + and text = '".mysql_real_escape_string($formatted_comment)."' + ".((Settings::get('OC_BRANCH') == 'oc.pl') ? "and deleted = 0" : "")." + limit 1 + "); + if ($duplicate_uuid != null) + { + if ($on_duplicate == 'silent_success') + { + # Act as if the log has been submitted successfully. + return $duplicate_uuid; + } + elseif ($on_duplicate == 'user_error') + { + throw new CannotPublishException(_("You have already submitted a log entry with exactly the same contents.")); + } + } + } + + # Check if already found it (and make sure the user is not the owner). + # + # OCPL forbids logging 'Found it' or "Didn't find" for an already found cache, + # while OCDE allows all kinds of duplicate logs. + + if (Settings::get('OC_BRANCH') == 'oc.pl' + && (($logtype == 'Found it') || ($logtype == "Didn't find it"))) + { + $has_already_found_it = Db::select_value(" + select 1 + from cache_logs + where + user_id = '".mysql_real_escape_string($user['internal_id'])."' + and cache_id = '".mysql_real_escape_string($cache['internal_id'])."' + and type = '".mysql_real_escape_string(Okapi::logtypename2id("Found it"))."' + and ".((Settings::get('OC_BRANCH') == 'oc.pl') ? "deleted = 0" : "true")." + "); + if ($has_already_found_it) + throw new CannotPublishException(_("You have already submitted a \"Found it\" log entry once. Now you may submit \"Comments\" only!")); + if ($user['uuid'] == $cache['owner']['uuid']) + throw new CannotPublishException(_("You are the owner of this cache. You may submit \"Comments\" only!")); + } + + # Check if the user has already rated the cache. BTW: I don't get this one. + # If we already know, that the cache was NOT found yet, then HOW could the + # user submit a rating for it? Anyway, I will stick to the procedure + # found in log.php. On the bright side, it's fail-safe. + + if ($rating) + { + $has_already_rated = Db::select_value(" + select 1 + from scores + where + user_id = '".mysql_real_escape_string($user['internal_id'])."' + and cache_id = '".mysql_real_escape_string($cache['internal_id'])."' + "); + if ($has_already_rated) + throw new CannotPublishException(_("You have already rated this cache once. Your rating cannot be changed.")); + } + + # If user wants to recommend... + + if ($recommend) + { + # Do the same "fail-safety" check as we did for the rating. + + $already_recommended = Db::select_value(" + select 1 + from cache_rating + where + user_id = '".mysql_real_escape_string($user['internal_id'])."' + and cache_id = '".mysql_real_escape_string($cache['internal_id'])."' + "); + if ($already_recommended) + throw new CannotPublishException(_("You have already recommended this cache once.")); + + # Check the number of recommendations. + + $founds = $user['caches_found'] + 1; // +1, because he'll find THIS ONE in a moment, right? + # Note: caches_found includes event attendance on both, OCDE and OCPL. + # Though OCPL does not allow recommending events, for each 10 event + # attendances the user may recommend a non-event cache. + $rcmds_left = floor($founds / 10.0) - $user['rcmds_given']; + if ($rcmds_left <= 0) + throw new CannotPublishException(_("You don't have any recommendations to give. Find more caches first!")); + } + + # If user checked the "needs_maintenance" flag, we will shuffle things a little... + + if ($needs_maintenance) + { + # If we're here, then we also know that the "Needs maintenance" log type is supported + # by this OC site. However, it's a separate log type, so we might have to submit + # two log types together: + + if ($logtype == 'Comment') + { + # If user submits a "Comment", we'll just change its type to "Needs maintenance". + # Only one log entry will be issued. + + $logtype = 'Needs maintenance'; + $second_logtype = null; + $second_formatted_comment = null; + } + elseif ($logtype == 'Found it') + { + # If "Found it", then we'll issue two log entries: one "Found it" with the + # original comment, and second one "Needs maintenance" with empty comment. + + $second_logtype = 'Needs maintenance'; + $second_formatted_comment = ""; + } + elseif ($logtype == "Didn't find it") + { + # If "Didn't find it", then we'll issue two log entries, but this time + # we'll do this the other way around. The first "Didn't find it" entry + # will have an empty comment. We will move the comment to the second + # "Needs maintenance" log entry. (It's okay for this behavior to change + # in the future, but it seems natural to me.) + + $second_logtype = 'Needs maintenance'; + $second_formatted_comment = $formatted_comment; + $formatted_comment = ""; + } + else if ($logtype == 'Will attend' || $logtype == 'Attended') + { + # OC branches which know maintenance logs do not allow them on event caches. + throw new CannotPublishException(_("Event caches cannot \"need maintenance\".")); + } + else + throw new Exception(); + } + else + { + # User didn't check the "Needs maintenance" flag OR "Needs maintenance" log type + # isn't supported by this server. + + $second_logtype = null; + $second_formatted_comment = null; + } + + # Finally! Insert the rows into the log entries table. Update + # cache stats and user stats. + + $log_uuid = self::insert_log_row( + $request->consumer->key, $cache['internal_id'], $user['internal_id'], $logtype, + $when, $formatted_comment, $value_for_text_html_field); + self::increment_cache_stats($cache['internal_id'], $when, $logtype); + self::increment_user_stats($user['internal_id'], $logtype); + if ($second_logtype != null) + { + # Reminder: This will never be called while SUPPORTS_LOGTYPE_NEEDS_MAINTENANCE is off. + + self::insert_log_row( + $request->consumer->key, $cache['internal_id'], $user['internal_id'], $second_logtype, + $when + 1, $second_formatted_comment, $value_for_text_html_field); + self::increment_cache_stats($cache['internal_id'], $when + 1, $second_logtype); + self::increment_user_stats($user['internal_id'], $second_logtype); + } + + # Save the rating. + + if ($rating) + { + # This code will be called for OCPL branch only. Earlier, we made sure, + # to set $rating to null, if we're running on OCDE. + + # OCPL has a little strange way of storing cumulative rating. Instead + # of storing the sum of all ratings, OCPL stores the computed average + # and update it using multiple floating-point operations. Moreover, + # the "score" field in the database is on the -3..3 scale (NOT 1..5), + # and the translation made at retrieval time is DIFFERENT than the + # one made here (both of them are non-linear). Also, once submitted, + # the rating can never be changed. It surely feels quite inconsistent, + # but presumably has some deep logic into it. See also here (Polish): + # http://wiki.opencaching.pl/index.php/Oceny_skrzynek + + switch ($rating) + { + case 1: $db_score = -2.0; break; + case 2: $db_score = -0.5; break; + case 3: $db_score = 0.7; break; + case 4: $db_score = 1.7; break; + case 5: $db_score = 3.0; break; + default: throw new Exception(); + } + Db::execute(" + update caches + set + score = (score*votes + '".mysql_real_escape_string($db_score)."')/(votes + 1), + votes = votes + 1 + where cache_id = '".mysql_real_escape_string($cache['internal_id'])."' + "); + Db::execute(" + insert into scores (user_id, cache_id, score) + values ( + '".mysql_real_escape_string($user['internal_id'])."', + '".mysql_real_escape_string($cache['internal_id'])."', + '".mysql_real_escape_string($db_score)."' + ); + "); + } + + # Save recommendation. + + if ($recommend) + { + if (Db::field_exists('cache_rating', 'rating_date')) + { + Db::execute(" + insert into cache_rating (user_id, cache_id, rating_date) + values ( + '".mysql_real_escape_string($user['internal_id'])."', + '".mysql_real_escape_string($cache['internal_id'])."', + from_unixtime('".mysql_real_escape_string($when)."') + ); + "); + } + else + { + Db::execute(" + insert into cache_rating (user_id, cache_id) + values ( + '".mysql_real_escape_string($user['internal_id'])."', + '".mysql_real_escape_string($cache['internal_id'])."' + ); + "); + } + } + + # We need to delete the copy of stats-picture for this user. Otherwise, + # the legacy OC code won't detect that the picture needs to be refreshed. + + $filepath = Okapi::get_var_dir().'/images/statpics/statpic'.$user['internal_id'].'.jpg'; + if (file_exists($filepath)) + unlink($filepath); + + # Success. Return the uuid. + + return $log_uuid; + } + + private static $success_message = null; + public static function call(OkapiRequest $request) + { + # This is the "real" entry point. A wrapper for the _call method. + + $langpref = $request->get_parameter('langpref'); + if (!$langpref) $langpref = "en"; + + # Error messages thrown via CannotPublishException exceptions should be localized. + # They will be delivered for end user to display in his language. + + Okapi::gettext_domain_init(explode("|", $langpref)); + try + { + # If appropriate, $success_message might be changed inside the _call. + self::$success_message = _("Your cache log entry was posted successfully."); + $log_uuid = self::_call($request); + $result = array( + 'success' => true, + 'message' => self::$success_message, + 'log_uuid' => $log_uuid + ); + Okapi::gettext_domain_restore(); + } + catch (CannotPublishException $e) + { + Okapi::gettext_domain_restore(); + $result = array( + 'success' => false, + 'message' => $e->getMessage(), + 'log_uuid' => null + ); + } + + return Okapi::formatted_response($request, $result); + } + + private static function increment_cache_stats($cache_internal_id, $when, $logtype) + { + if (Settings::get('OC_BRANCH') == 'oc.de') + { + # OCDE handles cache stats updates using triggers. So, they are already + # incremented properly. + } + else + { + # OCPL doesn't use triggers for this. We need to update manually. + + if ($logtype == 'Found it') + { + Db::execute(" + update caches + set + founds = founds + 1, + last_found = greatest(ifnull(last_found, 0), from_unixtime('".mysql_real_escape_string($when)."')) + where cache_id = '".mysql_real_escape_string($cache_internal_id)."' + "); + } + elseif ($logtype == "Didn't find it") + { + Db::execute(" + update caches + set notfounds = notfounds + 1 + where cache_id = '".mysql_real_escape_string($cache_internal_id)."' + "); + } + elseif ($logtype == 'Comment') + { + Db::execute(" + update caches + set notes = notes + 1 + where cache_id = '".mysql_real_escape_string($cache_internal_id)."' + "); + } + else + { + # This log type is not represented in cache stats. + } + } + } + + private static function increment_user_stats($user_internal_id, $logtype) + { + if (Settings::get('OC_BRANCH') == 'oc.de') + { + # OCDE handles cache stats updates using triggers. So, they are already + # incremented properly. + } + else + { + # OCPL doesn't have triggers for this. We need to update manually. + + switch ($logtype) + { + case 'Found it': $field_to_increment = 'founds_count'; break; + case "Didn't find it": $field_to_increment = 'notfounds_count'; break; + case 'Comment': $field_to_increment = 'log_notes_count'; break; + default: + # This log type is not represented in user stats. + return; + } + Db::execute(" + update user + set $field_to_increment = $field_to_increment + 1 + where user_id = '".mysql_real_escape_string($user_internal_id)."' + "); + } + } + + private static function create_uuid() + { + return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', + mt_rand(0, 0xffff), mt_rand(0, 0xffff), + mt_rand(0, 0xffff), + mt_rand(0, 0x0fff) | 0x4000, + mt_rand(0, 0x3fff) | 0x8000, + mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff) + ); + } + + private static function insert_log_row( + $consumer_key, $cache_internal_id, $user_internal_id, $logtype, $when, + $formatted_comment, $text_html + ) + { + $log_uuid = self::create_uuid(); + Db::execute(" + insert into cache_logs (uuid, cache_id, user_id, type, date, text, text_html, last_modified, date_created, node) + values ( + '".mysql_real_escape_string($log_uuid)."', + '".mysql_real_escape_string($cache_internal_id)."', + '".mysql_real_escape_string($user_internal_id)."', + '".mysql_real_escape_string(Okapi::logtypename2id($logtype))."', + from_unixtime('".mysql_real_escape_string($when)."'), + '".mysql_real_escape_string($formatted_comment)."', + '".mysql_real_escape_string($text_html)."', + now(), + now(), + '".mysql_real_escape_string(Settings::get('OC_NODE_ID'))."' + ); + "); + $log_internal_id = Db::last_insert_id(); + + # Store additional information on consumer_key which have created this log entry. + # (Maybe we'll want to display this somewhere later.) + + Db::execute(" + insert into okapi_cache_logs (log_id, consumer_key) + values ( + '".mysql_real_escape_string($log_internal_id)."', + '".mysql_real_escape_string($consumer_key)."' + ); + "); + + return $log_uuid; + } } diff --git a/htdocs/okapi/services/logs/submit.xml b/htdocs/okapi/services/logs/submit.xml index b7a7373e..63d9f587 100644 --- a/htdocs/okapi/services/logs/submit.xml +++ b/htdocs/okapi/services/logs/submit.xml @@ -1,110 +1,110 @@ - Submit a log entry - 42 - -

    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.

    -
    - -

    Code of the geocache.

    -
    - -

    Type of an entry. This should be one of:

    -
      -
    • - Will attend, Attended or Comment for Event caches; -
    • -
    • - Found it, Didn't find it or Comment for all other - cache types. -
    • -
    -
    - -

    Text to be submitted with the log entry.

    -
    - -

    Indicates the format of your comment. Three values allowed: - auto, html or plaintext. Usually, you should not - use the auto option, because its exact behavior is unspecified - and may depend on the installation - (more info).

    + Submit a log entry + 42 + +

    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.

    +
    + +

    Code of the geocache.

    +
    + +

    Type of an entry. This should be one of:

    +
      +
    • + Will attend, Attended or Comment for Event caches; +
    • +
    • + Found it, Didn't find it or Comment for all other + cache types. +
    • +
    +
    + +

    Text to be submitted with the log entry.

    +
    + +

    Indicates the format of your comment. Three values allowed: + auto, html or plaintext. Usually, you should not + use the auto option, because its exact behavior is unspecified + and may depend on the installation + (more info).

    -

    Important note: The subset of allowed HTML elements is left undefined - and may change in the future. For future-compatibility, you should use only - basic formatting tags.

    -
    - -

    A date and time string. This should be in ISO 8601 format (currently any - format acceptable by PHP's strtotime - function also will do, but most of them don't handle time zones properly, - try to use ISO 8601!).

    -

    Indicates when the cache was found. If given, the log will be published - with the given date and time. If not, log will be published using the current - date and time.

    -
    - -

    Some caches require a password in order to submit a "Found it" log entry. - You may check if this cache requires password with req_passwd field - of the services/caches/geocache method.

    -
    - -

    Pipe-separated list of ISO 639-1 language codes. This indicates the - order of preference in which language will be chosen for error messages.

    -
    - -

    How should OKAPI react when you are trying to submit a duplicate entry? - One of the following values:

    -
      -
    • silent_success - try to respond with success=true, but don't - add a new log entry (existing log_uuid will be returned),
    • -
    • user_error - respond with success=false and a proper user message,
    • -
    • continue - don't detect duplicates (note, that this will still - fail in some cases, i.e. when you're trying to submit a "Found it" entry for an - already found cache).
    • -
    -

    Note, that duplicate detection may take the when parameter into account. - When you don't supply it, "when" is generated for you. This means that you may - have to supply the "when" parameter if you want duplicate detection to work.

    -
    - -

    An integer in range between 1 and 5 - user's optional rating of a found cache.

    -

    Important: logtype has to be "Found it" in order to use this argument.

    -

    Note: You should allow your user to not rate a found cache.

    -

    Note: Currently, some OC installations do not support cache ratings. On such installations - user's rating will be ignored (if you include the rating, log entry will be posted - successfully, but rating will be ignored).

    -
    - -

    Set to true if the user wants to recommend this cache.

    -

    Important: logtype has to be "Found it" or "Attended" in order to - use this argument. However, some installations do not support recommending - event caches (if you include a recommendation, the log entry will be posted - successfully, but the recommendation will be ignored).

    -

    Recommending may only succeed when the user meets certain criteria - set on him by the OC site. If criteria are not met, the request will - end with user error (HTTP 200, success=false).

    -
    - -

    Set to true if the user thinks that the cache needs some special attension - of its owner. Users should describe the reason for maintenance in their comments.

    -

    Note: Depending on OC installation and user's log entry type, OKAPI may actually - publish two separate log entries when you check this flag (one of them with empty - comment). Even then, you will still receive only one UUID reference (pointing to - only one of those entries). Moreover, on some OC servers this flag might be - completely ignored (not all OC servers support this feature).

    -

    Note: Currently, this is not allowed for Event Caches (you will get a HTTP 200 - "user friendly" response).

    -
    - - -

    A dictionary of the following structure:

    -
      -
    • success - true, if the log entry was submitted successfully,
    • -
    • message - 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),
    • -
    • log_uuid - ID of the newly created log entry, or null - in case of an error.
    • -
    -
    +

    Important note: The subset of allowed HTML elements is left undefined + and may change in the future. For future-compatibility, you should use only + basic formatting tags.

    +
    + +

    A date and time string. This should be in ISO 8601 format (currently any + format acceptable by PHP's strtotime + function also will do, but most of them don't handle time zones properly, + try to use ISO 8601!).

    +

    Indicates when the cache was found. If given, the log will be published + with the given date and time. If not, log will be published using the current + date and time.

    +
    + +

    Some caches require a password in order to submit a "Found it" log entry. + You may check if this cache requires password with req_passwd field + of the services/caches/geocache method.

    +
    + +

    Pipe-separated list of ISO 639-1 language codes. This indicates the + order of preference in which language will be chosen for error messages.

    +
    + +

    How should OKAPI react when you are trying to submit a duplicate entry? + One of the following values:

    +
      +
    • silent_success - try to respond with success=true, but don't + add a new log entry (existing log_uuid will be returned),
    • +
    • user_error - respond with success=false and a proper user message,
    • +
    • continue - don't detect duplicates (note, that this will still + fail in some cases, i.e. when you're trying to submit a "Found it" entry for an + already found cache).
    • +
    +

    Note, that duplicate detection may take the when parameter into account. + When you don't supply it, "when" is generated for you. This means that you may + have to supply the "when" parameter if you want duplicate detection to work.

    +
    + +

    An integer in range between 1 and 5 - user's optional rating of a found cache.

    +

    Important: logtype has to be "Found it" in order to use this argument.

    +

    Note: You should allow your user to not rate a found cache.

    +

    Note: Currently, some OC installations do not support cache ratings. On such installations + user's rating will be ignored (if you include the rating, log entry will be posted + successfully, but rating will be ignored).

    +
    + +

    Set to true if the user wants to recommend this cache.

    +

    Important: logtype has to be "Found it" or "Attended" in order to + use this argument. However, some installations do not support recommending + event caches (if you include a recommendation, the log entry will be posted + successfully, but the recommendation will be ignored).

    +

    Recommending may only succeed when the user meets certain criteria + set on him by the OC site. If criteria are not met, the request will + end with user error (HTTP 200, success=false).

    +
    + +

    Set to true if the user thinks that the cache needs some special attension + of its owner. Users should describe the reason for maintenance in their comments.

    +

    Note: Depending on OC installation and user's log entry type, OKAPI may actually + publish two separate log entries when you check this flag (one of them with empty + comment). Even then, you will still receive only one UUID reference (pointing to + only one of those entries). Moreover, on some OC servers this flag might be + completely ignored (not all OC servers support this feature).

    +

    Note: Currently, this is not allowed for Event Caches (you will get a HTTP 200 + "user friendly" response).

    +
    + + +

    A dictionary of the following structure:

    +
      +
    • success - true, if the log entry was submitted successfully,
    • +
    • message - 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),
    • +
    • log_uuid - ID of the newly created log entry, or null + in case of an error.
    • +
    +
    \ No newline at end of file diff --git a/htdocs/okapi/services/logs/userlogs.php b/htdocs/okapi/services/logs/userlogs.php index d2e35bf9..ad0884b2 100644 --- a/htdocs/okapi/services/logs/userlogs.php +++ b/htdocs/okapi/services/logs/userlogs.php @@ -15,62 +15,62 @@ use okapi\services\caches\search\SearchAssistant; class WebService { - public static function options() - { - return array( - 'min_auth_level' => 1 - ); - } - public static function call(OkapiRequest $request) - { - $user_uuid = $request->get_parameter('user_uuid'); - if (!$user_uuid) throw new ParamMissing('user_uuid'); - $limit = $request->get_parameter('limit'); - if (!$limit) $limit = "20"; - if (!is_numeric($limit)) - throw new InvalidParam('limit', "'$limit'"); - $limit = intval($limit); - if (($limit < 1) || ($limit > 1000)) - throw new InvalidParam('limit', "Has to be in range 1..1000."); - $offset = $request->get_parameter('offset'); - if (!$offset) $offset = "0"; - if (!is_numeric($offset)) - throw new InvalidParam('offset', "'$offset'"); - $offset = intval($offset); - if ($offset < 0) - throw new InvalidParam('offset', "'$offset'"); + public static function options() + { + return array( + 'min_auth_level' => 1 + ); + } + public static function call(OkapiRequest $request) + { + $user_uuid = $request->get_parameter('user_uuid'); + if (!$user_uuid) throw new ParamMissing('user_uuid'); + $limit = $request->get_parameter('limit'); + if (!$limit) $limit = "20"; + if (!is_numeric($limit)) + throw new InvalidParam('limit', "'$limit'"); + $limit = intval($limit); + if (($limit < 1) || ($limit > 1000)) + throw new InvalidParam('limit', "Has to be in range 1..1000."); + $offset = $request->get_parameter('offset'); + if (!$offset) $offset = "0"; + if (!is_numeric($offset)) + throw new InvalidParam('offset', "'$offset'"); + $offset = intval($offset); + if ($offset < 0) + throw new InvalidParam('offset', "'$offset'"); - # Check if user exists and retrieve user's ID (this will throw - # a proper exception on invalid UUID). - $user = OkapiServiceRunner::call('services/users/user', new OkapiInternalRequest( - $request->consumer, null, array('user_uuid' => $user_uuid, 'fields' => 'internal_id'))); + # Check if user exists and retrieve user's ID (this will throw + # a proper exception on invalid UUID). + $user = OkapiServiceRunner::call('services/users/user', new OkapiInternalRequest( + $request->consumer, null, array('user_uuid' => $user_uuid, 'fields' => 'internal_id'))); - # User exists. Retrieving logs. + # User exists. Retrieving logs. - $rs = Db::query(" - select cl.id, cl.uuid, cl.type, unix_timestamp(cl.date) as date, cl.text, - c.wp_oc as cache_code - from cache_logs cl, caches c - where - cl.user_id = '".mysql_real_escape_string($user['internal_id'])."' - and ".((Settings::get('OC_BRANCH') == 'oc.pl') ? "cl.deleted = 0" : "true")." - and c.status in (1,2,3) - and cl.cache_id = c.cache_id - order by cl.date desc - limit $offset, $limit - "); - $results = array(); - while ($row = mysql_fetch_assoc($rs)) - { - $results[] = array( - 'uuid' => $row['uuid'], - 'date' => date('c', $row['date']), - 'cache_code' => $row['cache_code'], - 'type' => Okapi::logtypeid2name($row['type']), - 'comment' => $row['text'] - ); - } + $rs = Db::query(" + select cl.id, cl.uuid, cl.type, unix_timestamp(cl.date) as date, cl.text, + c.wp_oc as cache_code + from cache_logs cl, caches c + where + cl.user_id = '".mysql_real_escape_string($user['internal_id'])."' + and ".((Settings::get('OC_BRANCH') == 'oc.pl') ? "cl.deleted = 0" : "true")." + and c.status in (1,2,3) + and cl.cache_id = c.cache_id + order by cl.date desc + limit $offset, $limit + "); + $results = array(); + while ($row = mysql_fetch_assoc($rs)) + { + $results[] = array( + 'uuid' => $row['uuid'], + 'date' => date('c', $row['date']), + 'cache_code' => $row['cache_code'], + 'type' => Okapi::logtypeid2name($row['type']), + 'comment' => $row['text'] + ); + } - return Okapi::formatted_response($request, $results); - } + return Okapi::formatted_response($request, $results); + } } diff --git a/htdocs/okapi/services/logs/userlogs.xml b/htdocs/okapi/services/logs/userlogs.xml index 63633a08..4a63aab8 100644 --- a/htdocs/okapi/services/logs/userlogs.xml +++ b/htdocs/okapi/services/logs/userlogs.xml @@ -1,41 +1,45 @@ - Retrieve log entries of a specified user - 80 - -

    Retrieve log entries of a specified user.

    -
    - -

    ID of the user. (Use services/users/by_username to get it.)

    -
    - -

    Integer N. If given, no more than N logs will be returned (the most recent ones). - Maximum allowed value is 1000.

    -

    Note: Some users have thousands of log entries!

    -
    - -

    Combined with the limit argument, this gives you an abbility to get - all log entries of the user (with multiple requests).

    -
    - - -

    Log entries. A dictionary of the following format:

    -
      -
    • uuid - ID of the log entry,
    • -
    • -

      date - date and time (ISO 8601) when the log entry was submitted.

      -

      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 - want to avoid displaying the time. You may assume that if the date - value contains the "00:00:00" string, then it is date-only.

      -
    • -
    • cache_code - code of the geocache,
    • -
    • -

      type - string; log type. This could be pretty much - everything, but there are some primary types (see logs/entry - method for more info).

      -
    • -
    • comment - HTML string, text entered - with the log entry.
    • -
    -
    + Retrieve log entries of a specified user + 80 + +

    Retrieve log entries of a specified user.

    +
    + +

    ID of the user. (Use services/users/by_username to get it.)

    +
    + +

    Integer N. If given, no more than N logs will be returned (the most recent ones). + Maximum allowed value is 1000.

    +

    Note: Some users have thousands of log entries!

    +
    + +

    Combined with the limit argument, this gives you an abbility to get + all log entries of the user (with multiple requests).

    +
    + + +

    A list of log entries, ordered by descending date. Each entry is a + dictionary of the following format:

    + +
      +
    • uuid - ID of the log entry,
    • +
    • +

      date - date and time (ISO 8601) when the log entry was submitted.

      +

      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 + want to avoid displaying the time. You may assume that if the date + value contains the "00:00:00" string, then it is date-only.

      +
    • +
    • cache_code - code of the geocache,
    • +
    • +

      type - string; log type. This could be pretty much + everything, but there are some primary types (see logs/entry + method for more info).

      +
    • +
    • + comment - HTML string, text entered + with the log entry. +
    • +
    +
    \ No newline at end of file diff --git a/htdocs/okapi/services/oauth/access_token.php b/htdocs/okapi/services/oauth/access_token.php index 4ef461f4..5223f8b5 100644 --- a/htdocs/okapi/services/oauth/access_token.php +++ b/htdocs/okapi/services/oauth/access_token.php @@ -10,28 +10,28 @@ use okapi\InvalidParam; class WebService { - public static function options() - { - return array( - 'min_auth_level' => 3, - 'token_type' => 'request' - ); - } + public static function options() + { + return array( + 'min_auth_level' => 3, + 'token_type' => 'request' + ); + } - public static function call(OkapiRequest $request) - { - $verifier = $request->get_parameter('oauth_verifier'); - if (!$verifier) - { - # We require the 1.0a flow (throw an error when there is no oauth_verifier). - throw new ParamMissing("oauth_verifier"); - } + public static function call(OkapiRequest $request) + { + $verifier = $request->get_parameter('oauth_verifier'); + if (!$verifier) + { + # We require the 1.0a flow (throw an error when there is no oauth_verifier). + throw new ParamMissing("oauth_verifier"); + } - $new_token = Okapi::$data_store->new_access_token($request->token, $request->consumer, $verifier); + $new_token = Okapi::$data_store->new_access_token($request->token, $request->consumer, $verifier); - $response = new OkapiHttpResponse(); - $response->content_type = "text/plain; charset=utf-8"; - $response->body = $new_token; - return $response; - } + $response = new OkapiHttpResponse(); + $response->content_type = "text/plain; charset=utf-8"; + $response->body = $new_token; + return $response; + } } diff --git a/htdocs/okapi/services/oauth/access_token.xml b/htdocs/okapi/services/oauth/access_token.xml index 2977b5c7..63eeee84 100644 --- a/htdocs/okapi/services/oauth/access_token.xml +++ b/htdocs/okapi/services/oauth/access_token.xml @@ -1,20 +1,20 @@ - Exchange authorized Request Token for an Access Token - 21 - -

    Exchange authorized Request Token for an Access Token. - Access Token can be used to access resources of a user who - authorized the Request Token.

    -

    Standard OAuth "Consumer & Request Token" Authorization arguments - are required.

    -
    - - Consult OAuth documentation for details. - - -

    Standard OAuth 1.0a Token response - a string in a form-encoded format:

    -
    oauth_token=...&oauth_token_secret=...
    -

    You must be prepared that there might be more parameters returned - in the future (you should ignore them gracefully).

    -
    + Exchange authorized Request Token for an Access Token + 21 + +

    Exchange authorized Request Token for an Access Token. + Access Token can be used to access resources of a user who + authorized the Request Token.

    +

    Standard OAuth "Consumer & Request Token" Authorization arguments + are required.

    +
    + + Consult OAuth documentation for details. + + +

    Standard OAuth 1.0a Token response - a string in a form-encoded format:

    +
    oauth_token=...&oauth_token_secret=...
    +

    You must be prepared that there might be more parameters returned + in the future (you should ignore them gracefully).

    +
    diff --git a/htdocs/okapi/services/oauth/authorize.php b/htdocs/okapi/services/oauth/authorize.php index 55ee782b..5298d311 100644 --- a/htdocs/okapi/services/oauth/authorize.php +++ b/htdocs/okapi/services/oauth/authorize.php @@ -11,31 +11,31 @@ use okapi\InvalidParam; class WebService { - public static function options() - { - return array( - 'min_auth_level' => 0 - ); - } + public static function options() + { + return array( + 'min_auth_level' => 0 + ); + } - public static function call(OkapiRequest $request) - { - $token_key = $request->get_parameter('oauth_token'); - if (!$token_key) - throw new ParamMissing("oauth_token"); - $langpref = $request->get_parameter('langpref'); - $interactivity = $request->get_parameter('interactivity'); - if (!$interactivity) $interactivity = 'minimal'; - if (!in_array($interactivity, array('minimal', 'confirm_user'))) - throw new InvalidParam('interactivity', $interactivity); + public static function call(OkapiRequest $request) + { + $token_key = $request->get_parameter('oauth_token'); + if (!$token_key) + throw new ParamMissing("oauth_token"); + $langpref = $request->get_parameter('langpref'); + $interactivity = $request->get_parameter('interactivity'); + if (!$interactivity) $interactivity = 'minimal'; + if (!in_array($interactivity, array('minimal', 'confirm_user'))) + throw new InvalidParam('interactivity', $interactivity); - # Redirect to the "apps" folder. This is done there (not here) - # because: 1) we don't want any cookie or session-handling - # done in the "services" folder. 2) "services" don't display - # any interactive webpages, they just return the result. + # Redirect to the "apps" folder. This is done there (not here) + # because: 1) we don't want any cookie or session-handling + # done in the "services" folder. 2) "services" don't display + # any interactive webpages, they just return the result. - return new OkapiRedirectResponse(Settings::get('SITE_URL')."okapi/apps/authorize". - "?oauth_token=".$token_key.(($langpref != null) ? "&langpref=".$langpref : ""). - "&interactivity=".$interactivity); - } + return new OkapiRedirectResponse(Settings::get('SITE_URL')."okapi/apps/authorize". + "?oauth_token=".$token_key.(($langpref != null) ? "&langpref=".$langpref : ""). + "&interactivity=".$interactivity); + } } diff --git a/htdocs/okapi/services/oauth/authorize.xml b/htdocs/okapi/services/oauth/authorize.xml index 495c2bef..12f5068b 100644 --- a/htdocs/okapi/services/oauth/authorize.xml +++ b/htdocs/okapi/services/oauth/authorize.xml @@ -1,66 +1,66 @@ - Authorize the Request Token - 22 - -

    Unlike other methods, the authorize method is to be executed - inside the User's browser. Consumer's role is to redirect the User to - this URL, then wait if he ever comes back with a callback request.

    -

    Once the User is redirected to this URL, several things will happen:

    -
      -
    • If he's not already logged in, he will be asked to do so.
    • -
    • OKAPI will check if the User haven't previously granted your - application access to his Opencaching account.
    • -
    • If User did not previously authorize your application, OKAPI - will display an "Authorization Request" form to the User. User - will be presented with a choice to allow or not to allow your - application access to his account.
    • -
    • If the User clicks one of these two options ("allow" or "don't allow"), - he's browser will be redirected to the callback_url you defined - while getting your Request Token. - If you did not provide a callback (in other word, provided "oob"), - user will be redirected to a default "authorized" page, where he - will be presented with an oauth_verifier (user will know it - by name of a PIN code) and asked to type it into your application.
    • -
    -
    - - Consult OAuth documentation for details. - - -

    Currently, one of the following values:

    -
      -
    • minimal - OKAPI will use as little interactivity as it can. - It will assume that currently logged in user is the user which you - want to authorize. If the user has already authorized your application, - he will not be asked to do this again.
    • -
    • confirm_user - even if a user is logged in, OKAPI will NOT - assume that this is the user who wants to be authorized. OKAPI will - offer to authorize a different user (e.g. by automatically logging out - the user who is currently logged in).
    • -
    -
    - -

    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.

    -

    By default, OKAPI will display the page in the primary native language of local - Opencaching installation.

    -
    - -

    Technically, an HTTP 302 Redirect - it will direct user's browser to the OKAPI apps - authorization page.

    -

    Whether with callback_url or with a manual user entry - you will get - your oauth_verifier, which allows you to continue the 3-legged - authentication dance.

    -

    If you used callback_url, you should wait for an HTTP GET request, - with one additional GET parameter appended:

    -
      -
    • oauth_token - the Request Token that has been just authorized,
    • -
    • oauth_verifier - the PIN code required to get an Access Token.
    • -
    -

    OR, in case when user denied the request:

    -
      -
    • oauth_token - the Request Token,
    • -
    • error - codename of an error - access_denied.
    • -
    -
    + Authorize the Request Token + 22 + +

    Unlike other methods, the authorize method is to be executed + inside the User's browser. Consumer's role is to redirect the User to + this URL, then wait if he ever comes back with a callback request.

    +

    Once the User is redirected to this URL, several things will happen:

    +
      +
    • If he's not already logged in, he will be asked to do so.
    • +
    • OKAPI will check if the User haven't previously granted your + application access to his Opencaching account.
    • +
    • If User did not previously authorize your application, OKAPI + will display an "Authorization Request" form to the User. User + will be presented with a choice to allow or not to allow your + application access to his account.
    • +
    • If the User clicks one of these two options ("allow" or "don't allow"), + he's browser will be redirected to the callback_url you defined + while getting your Request Token. + If you did not provide a callback (in other word, provided "oob"), + user will be redirected to a default "authorized" page, where he + will be presented with an oauth_verifier (user will know it + by name of a PIN code) and asked to type it into your application.
    • +
    +
    + + Consult OAuth documentation for details. + + +

    Currently, one of the following values:

    +
      +
    • minimal - OKAPI will use as little interactivity as it can. + It will assume that currently logged in user is the user which you + want to authorize. If the user has already authorized your application, + he will not be asked to do this again.
    • +
    • confirm_user - even if a user is logged in, OKAPI will NOT + assume that this is the user who wants to be authorized. OKAPI will + offer to authorize a different user (e.g. by automatically logging out + the user who is currently logged in).
    • +
    +
    + +

    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.

    +

    By default, OKAPI will display the page in the primary native language of local + Opencaching installation.

    +
    + +

    Technically, an HTTP 302 Redirect - it will direct user's browser to the OKAPI apps + authorization page.

    +

    Whether with callback_url or with a manual user entry - you will get + your oauth_verifier, which allows you to continue the 3-legged + authentication dance.

    +

    If you used callback_url, you should wait for an HTTP GET request, + with one additional GET parameter appended:

    +
      +
    • oauth_token - the Request Token that has been just authorized,
    • +
    • oauth_verifier - the PIN code required to get an Access Token.
    • +
    +

    OR, in case when user denied the request:

    +
      +
    • oauth_token - the Request Token,
    • +
    • error - codename of an error - access_denied.
    • +
    +
    diff --git a/htdocs/okapi/services/oauth/request_token.php b/htdocs/okapi/services/oauth/request_token.php index 5f9c6850..a8344e88 100644 --- a/htdocs/okapi/services/oauth/request_token.php +++ b/htdocs/okapi/services/oauth/request_token.php @@ -10,27 +10,27 @@ use okapi\InvalidParam; class WebService { - public static function options() - { - return array( - 'min_auth_level' => 2 - ); - } + public static function options() + { + return array( + 'min_auth_level' => 2 + ); + } - public static function call(OkapiRequest $request) - { - $callback = $request->get_parameter('oauth_callback'); - if (!$callback) - { - # We require the 1.0a flow (throw an error when there is no oauth_callback). - throw new ParamMissing("oauth_callback"); - } + public static function call(OkapiRequest $request) + { + $callback = $request->get_parameter('oauth_callback'); + if (!$callback) + { + # We require the 1.0a flow (throw an error when there is no oauth_callback). + throw new ParamMissing("oauth_callback"); + } - $new_token = Okapi::$data_store->new_request_token($request->consumer, $callback); + $new_token = Okapi::$data_store->new_request_token($request->consumer, $callback); - $response = new OkapiHttpResponse(); - $response->content_type = "text/plain; charset=utf-8"; - $response->body = $new_token."&oauth_callback_confirmed=true"; - return $response; - } + $response = new OkapiHttpResponse(); + $response->content_type = "text/plain; charset=utf-8"; + $response->body = $new_token."&oauth_callback_confirmed=true"; + return $response; + } } diff --git a/htdocs/okapi/services/oauth/request_token.xml b/htdocs/okapi/services/oauth/request_token.xml index ae99ca22..7b68a40c 100644 --- a/htdocs/okapi/services/oauth/request_token.xml +++ b/htdocs/okapi/services/oauth/request_token.xml @@ -1,24 +1,24 @@ - Get a new unauthorized OAuth Request Token - 23 - -

    Get a new unauthorized OAuth Request Token. This token is bound to - the Consumer it was issued to. It has a short expiration date and - can be used in one way only - first it has to get authorized by - the user, then it has to be exchanged for an Access Token.

    -
    - -

    URL which you want a User to be redirected to after a - successful Request Token Authorization (see "authorize" method). - If the client is unable to receive callbacks, the parameter - must be set to "oob", OKAPI will provide a user with a - PIN code (oauth_verifier) in this case.

    - Consult OAuth documentation for details. -
    - -

    Standard OAuth 1.0a Token response - a string in a form-encoded format:

    -
    oauth_token=...&oauth_token_secret=...&oauth_callback_confirmed=true
    -

    You must be prepared that there might be more parameters returned - in the future (you should ignore them gracefully).

    -
    + Get a new unauthorized OAuth Request Token + 23 + +

    Get a new unauthorized OAuth Request Token. This token is bound to + the Consumer it was issued to. It has a short expiration date and + can be used in one way only - first it has to get authorized by + the user, then it has to be exchanged for an Access Token.

    +
    + +

    URL which you want a User to be redirected to after a + successful Request Token Authorization (see "authorize" method). + If the client is unable to receive callbacks, the parameter + must be set to "oob", OKAPI will provide a user with a + PIN code (oauth_verifier) in this case.

    + Consult OAuth documentation for details. +
    + +

    Standard OAuth 1.0a Token response - a string in a form-encoded format:

    +
    oauth_token=...&oauth_token_secret=...&oauth_callback_confirmed=true
    +

    You must be prepared that there might be more parameters returned + in the future (you should ignore them gracefully).

    +
    diff --git a/htdocs/okapi/services/replicate/changelog.php b/htdocs/okapi/services/replicate/changelog.php index 7cdd5d23..78f7be78 100644 --- a/htdocs/okapi/services/replicate/changelog.php +++ b/htdocs/okapi/services/replicate/changelog.php @@ -17,37 +17,37 @@ use okapi\services\replicate\ReplicateCommon; class WebService { - public static function options() - { - return array( - 'min_auth_level' => 1 - ); - } + public static function options() + { + return array( + 'min_auth_level' => 1 + ); + } - public static function call(OkapiRequest $request) - { - require_once('replicate_common.inc.php'); + public static function call(OkapiRequest $request) + { + require_once('replicate_common.inc.php'); - $since = $request->get_parameter('since'); - if ($since === null) throw new ParamMissing('since'); - if ((int)$since != $since) throw new InvalidParam('since'); + $since = $request->get_parameter('since'); + if ($since === null) throw new ParamMissing('since'); + if ((int)$since != $since) throw new InvalidParam('since'); - # Let's check the $since parameter. + # Let's check the $since parameter. - if (!ReplicateCommon::check_since_param($since)) - throw new BadRequest("The 'since' parameter is too old. You must update your database more frequently."); + if (!ReplicateCommon::check_since_param($since)) + throw new BadRequest("The 'since' parameter is too old. You must update your database more frequently."); - # Select a best chunk for the given $since, get the chunk from the database (or cache). + # Select a best chunk for the given $since, get the chunk from the database (or cache). - list($from, $to) = ReplicateCommon::select_best_chunk($since); - $clog_entries = ReplicateCommon::get_chunk($from, $to); + list($from, $to) = ReplicateCommon::select_best_chunk($since); + $clog_entries = ReplicateCommon::get_chunk($from, $to); - $result = array( - 'changelog' => &$clog_entries, - 'revision' => $to + 0, - 'more' => $to < ReplicateCommon::get_revision(), - ); + $result = array( + 'changelog' => &$clog_entries, + 'revision' => $to + 0, + 'more' => $to < ReplicateCommon::get_revision(), + ); - return Okapi::formatted_response($request, $result); - } + return Okapi::formatted_response($request, $result); + } } diff --git a/htdocs/okapi/services/replicate/changelog.xml b/htdocs/okapi/services/replicate/changelog.xml index 99e4be6e..5088fe6e 100644 --- a/htdocs/okapi/services/replicate/changelog.xml +++ b/htdocs/okapi/services/replicate/changelog.xml @@ -1,133 +1,140 @@ - Get the list of changes for your database - 109 - -

    Beta status. Get the list of changes to be replayed on your own copy - of Opencaching database. Use this method periodically (ex. once per hour) to - keep your database in sync with ours.

    - -

    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 replicate module to achive this effect. The changelog 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 fulldump method.

    - -

    A couple of things for you to remember:

    -
      -
    • You must update your database frequently for this method to work. - We don't keep the changelog indefinitelly. You must update at least once every week.
    • -
    • You should not update your database more frequently than once every - 5 minutes. This won't do you any good, since we update the changelog only once - every 5 minutes anyway.
    • -
    - -

    Let's assume that you already have a copy of OKAPI database, but it's - already 2 days old. You want to use the changelog method to update - your copy to the most current state.

    - -

    Changelog is the list of all changes which appeared in the OKAPI database - since the last time you downloaded it. What you have to do is to download this - list of changes and to replay them on your copy of our database. After you do - this, your database is up-to-date.

    - -

    We use revision numbers to keep track of all the versions of the - database. Every time you update a database, you receive the revision - number along with it. You must keep this number carefully, because you need - it in order for us to generate a proper changelog for you next time you ask - for it.

    - -

    Example. This is a valid list of requests you should issue and their - responses:

    -
      -
    • fulldump - you receive a fulldump of our database with the - revision number 238004. You will call this method only once, never again.
    • -
    • changelog?since=238004 - OKAPI checks if there were any changes - recorded since revision 238004. It responds with the list of changes and the - new revision number 238017. You replay the changes on your database.
    • -
    • You wait for some time between requesting the changelog again.
    • -
    • changelog?since=238017 - etc.
    • -
    -
    - -

    Current revision of your database. This should be the same as the value - of revision attribute, which you received with your previous update.

    - -

    Old revisions are deleted, the since argument MUST referer to a revision no older - than 10 days. You will have to download a fulldump if you have an older copy - (shame on you!).

    -
    - - -

    A dictionary of the following structure:

    -
      -
    • -

      changelog - a list of dictionaries. Each dictionary has the following structure:

      -
        -
      • -

        object_type - string, object type to which the change refers to. One of the following values:

        -
          -
        • geocache - this change refers to a geocache object,
        • -
        • log_entry - this change refers to a log entry.
        • -
        -

        More object types will come in the future. You should ignore all changelog - entries with an unknown object_type.

        -
      • -
      • -

        object_key - a dictionary of fields which compose the primary key for the object. - This will be the code field for the geocache object, and uuid field - for the log_entry object.

        -
      • -
      • -

        change_type - string, the type of the change. One of the following values:

        -
          -
        • replace - the object was inserted or updated. You should check if you - already have the object in your database. If you have it, you should update its - fields accordingly. If you don't, you should create it.
        • -
        • delete - the object was deleted. You should check if you already have - the object in your database. If you do, you should delete it.
        • -
        -
      • -
      • -

        fields - a dictionary of fields associated with the object (present only - if change_type equals replace).

        -
          -
        • For geocache objects, this might be any subset of fields described - in the services/caches/geocache method. Note that not all of these fields will - be included here (i.e. latest_logs will not).
        • -
        • For log_entry objects, this might be any subset of fields described - in the services/logs/logs method, plus additional cache_code field, - the code of the geocache to which the log entry refers to.
        • -
        -

        In theory this dictionary should contain only these fields which actually changed. - In truth, it MAY contain all the other fields too. This behavior may change in future.

        -
      • -
      -

      You should iterate through this changelog sequentially. All the changes should be applied - in the same order as they were listed in the changelog. Single object may appear multiple times - inside the changelog. Changelog will contain all the changes which occured since the time you - specified in the since parameter and it MAY contain some more, which were submitted - before this date (see below). The changes which are unnecessary MAY be skipped (ex. when - cache description changes multiple times, we may want to include only the last change).

      -
    • -
    • -

      revision - this is the revision number which you should use in the since parameter - when you call this method next time.

      -
    • -
    • -

      more - boolean. If false, then it means that the entire changelog had been - pulled. If true, then there are more items waiting to be pulled - you should rerun this - method again (with the value of revision inserted in the since parameter).

      -

      The changelog is usually tiny, but it also might be huge at times. It may even - contain all the caches in the database (i.e. if we decide to do some changes on all - caches in a bulk). This is the reason why we introduced the more attribute. It makes it - easier to parse the changelog sequentially.

      -
    • -
    -

    The response MAY contain changes recorded before the revision which you stated in your since - 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 revision and more attributes in a correct way, which - in turn will make you query us again, if you need to.

    -
    + Get the list of changes for your database + 109 + +

    Get the list of changes to be replayed on your own copy + of Opencaching database. Use this method periodically (e.g. every 5 minutes) to + keep your database in sync with ours.

    + +

    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 replicate module to achive this effect. The changelog 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 fulldump method.

    + +

    A couple of things for you to remember:

    +
      +
    • You must update your database frequently for this method to work. + We don't keep the changelog indefinitelly. You must update at least once a week!
    • + +
    • You should not update your database more frequently than once per + 5 minutes. This could kill our servers, and it wouldn't do you any good, since + we update the changelog only once every 5 minutes anyway.
    • +
    + +

    Let's assume that you already have a copy of OKAPI database, but it's + already 2 days old. You want to use the changelog method to update + your copy to the most current state.

    + +

    Changelog is the list of all changes which appeared in the OKAPI database + since the last time you downloaded it. What you have to do is to download this + list of changes and to replay them on your copy of our database. After you do + this, your database is up-to-date.

    + +

    We use revision numbers to keep track of all the versions of the + database. Every time we update a database, the revision is increased. + You will also receive this number every time you fetch a changelog or a fulldump + 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 + for it!

    + +

    Example. This is a valid list of requests you should issue and their + responses:

    + +
      +
    • fulldump - you receive a fulldump of our database with the + revision number 238004. You will call this method only once (to + initiate your copy of the database).
    • + +
    • changelog?since=238004 - OKAPI checks if there were any changes + recorded since revision 238004. It responds with the list of changes and the + new revision number 238017. You replay the changes on your database.
    • + +
    • You wait for some time between requesting the changelog again.
    • + +
    • Upon your next update, you'll ask for changelog?since=238017, etc.
    • +
    +
    + +

    Current revision of your database. This should be the same as the value + of revision attribute, which you received with your previous update.

    + +

    Old revisions are deleted, the since argument MUST referer to a revision no older + than 10 days. You will have to download a fulldump if you have an older copy + (shame on you!).

    +
    + + +

    A dictionary of the following structure:

    +
      +
    • +

      changelog - a list of dictionaries. Each dictionary has the following structure:

      +
        +
      • +

        object_type - string, object type to which the change refers to. One of the following values:

        +
          +
        • geocache - this change refers to a geocache object,
        • +
        • log_entry - this change refers to a log entry.
        • +
        +

        More object types will come in the future. You should ignore all changelog + entries with an unknown object_type.

        +
      • +
      • +

        object_key - a dictionary of fields which compose the primary key for the object. + This will be the code field for the geocache object, and uuid field + for the log_entry object.

        +
      • +
      • +

        change_type - string, the type of the change. One of the following values:

        +
          +
        • replace - the object was inserted or updated. You should check if you + already have the object in your database. If you have it, you should update its + fields accordingly. If you don't, you should create it.
        • +
        • delete - the object was deleted. You should check if you already have + the object in your database. If you do, you should delete it.
        • +
        +
      • +
      • +

        data - a dictionary of fields associated with the object (present only + if change_type equals replace).

        +
          +
        • For geocache objects, this might be any subset of fields described + in the services/caches/geocache method. Note that not all of these fields will + be included here (i.e. latest_logs will not).
        • +
        • For log_entry objects, this might be any subset of fields described + in the services/logs/logs method, plus additional cache_code field, + the code of the geocache to which the log entry refers to.
        • +
        +

        In theory this dictionary should contain only these fields which actually changed. + In truth, it MAY contain all the other fields too. This behavior may change in future.

        +
      • +
      +

      You should iterate through this changelog sequentially. All the changes should be applied + in the same order as they were listed in the changelog. Single object may appear multiple times + inside the changelog. Changelog will contain all the changes which occured since the time you + specified in the since parameter and it MAY contain some more, which were submitted + before this date (see below). The changes which are unnecessary MAY be skipped (ex. when + cache description changes multiple times, we may want to include only the last change).

      +
    • +
    • +

      revision - this is the revision number which you should use in the since parameter + when you call this method next time.

      +
    • +
    • +

      more - boolean. If false, then it means that the entire changelog had been + pulled. If true, then there are more items waiting to be pulled - you should rerun this + method again (with the value of revision inserted in the since parameter).

      +

      The changelog is usually tiny, but it also might be huge at times. It may even + contain all the caches in the database (i.e. if we decide to do some changes on all + caches in a bulk). This is the reason why we introduced the more attribute. It makes it + easier to parse the changelog sequentially.

      +
    • +
    +

    The response MAY contain changes recorded before the revision which you stated in your since + 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 revision and more attributes in a correct way, which + in turn will make you query us again, if you need to.

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

    Download the latest snapshot of OKAPI database. You should call this method - only once.

    - -

    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 replicate module to achive this condition. The changelog 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 fulldump method.

    + Download OKAPI database snapshot + 110 + +

    Download the latest snapshot of OKAPI database. You should call this method + only once.

    -

    Important: There is a remote possibility that this method MAY change in - a non-backward-compatible way or it might even get removed. We don't plan on doing - this, but we might be forced to (i.e. to prevent abuse).

    - -

    Note: The cache descriptions will be generated using the attribution_append=static - parameter (see the geocache method). Full attributions are not always suitable for replication, - since they may contain dates on some installations - (why?). - To make sure that static attributions are enough, consult the local Data - Licence (the Sign Up page).

    - -

    A couple of things for you to remember:

    - -
      -
    • Currently, this functionality is available for developers only, - NOT for the individual users. We don't want users to download a fresh snapshot of - the entire database every time they want, this could kill our servers. - If you want to enable such features for your users, you can do it, but you - must use your own server for data traffic (especially, fulldump requests).
    • - -
    • Fulldump is a copy of the entire database. We generate such copy once every couple of - days. This copy if intended for you to start only, later you must use the changelog to - keep it up-to-date.
    • - -
    • Every time our database is extended (new fields or new object types), and you want to - make use of these new fields, you are of course allowed to download a fulldump copy again.
    • - -
    • There is no XMLMAP version of this file.
    • -
    -
    - -

    Archive with JSON-encoded files. File named index.json will contain a dictionary - of the following structure:

    -
      -
    • revision - revision of the database snapshot contained in the archive,
    • -
    • data_files - list of filenames which contain the changelog entries for - you to parse. Each file contains a JSON-encoded list of changelog entries, in format - described in the changelog method ("replace" only).
    • -
    • meta - a dictionary of other meta data, not important.
    • -
    -

    Note: We use TGZ or TBZ2 format to encode this archive:

    -
      -
    • TGZ archive (more commonly known as .tar.gz archive) is a TAR - archive compressed with GZIP.
    • -
    • TBZ2 archive (more commonly known as .tar.bz2 archive) is a TAR - archive compressed with BZIP2.
    • -
    -

    There are many tools available for handling these archives. In Linux, try "tar -xf filename".

    -
    +

    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 replicate module to achive this state. The changelog 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 fulldump method.

    + +

    A couple of things for you to remember:

    + +
      +
    • Currently, this method is available for developers only, + not for the individual users. We don't want users to download a fresh snapshot of + the entire database every time they want, this could kill our servers. + If you want to enable such features for your users, you can do it, but you + must use your own server for serving fulldump requests. (You don't + have to use your server to relay changelog requests.)
    • + +
    • Fulldump is a copy of the entire database. We generate such copy once every couple of + days. This copy if intended for you to make a fresh start only. Later, you + must use the changelog method to keep your data up-to-date.
    • + +
    • Every time our database is extended (new fields or new object types), and you want to + make use of these new fields, you are of course allowed to download a fulldump copy again.
    • + +
    • There is no XMLMAP version of this file. JSON only.
    • +
    + +

    Additional notes on data attribution: Cache descriptions will be + generated using the attribution_append=static parameter (see the + geocache method). This is because the full attributions are not always suitable + for replication, since they may contain dynamically changing dates on some + installations (why?). + To make sure that static attributions are enough, consult the local Data + Licence (the Sign Up page).

    +
    + +

    Compressed archive with JSON-encoded files. File named index.json will + contain a dictionary of the following structure:

    + +
      +
    • revision - revision of the database snapshot contained in the archive,
    • + +
    • data_files - list of filenames which contain the changelog entries for + you to parse. Each file contains a JSON-encoded list of changelog entries, in format + described in the changelog method ("replace" only).
    • + +
    • meta - a dictionary of other meta data, not important.
    • +
    + +

    Note: We use TGZ or TBZ2 format to encode this archive:

    + +
      +
    • TGZ archive (also known as .tar.gz) is a TAR + archive compressed with GZIP.
    • + +
    • TBZ2 archive (also known as .tar.bz2) is a TAR + archive compressed with BZIP2.
    • +
    + +

    There are many tools available for handling these archives. In Linux, try "tar -xf filename".

    +
    \ No newline at end of file diff --git a/htdocs/okapi/services/replicate/info.php b/htdocs/okapi/services/replicate/info.php index e9e3c73b..dc0f4961 100644 --- a/htdocs/okapi/services/replicate/info.php +++ b/htdocs/okapi/services/replicate/info.php @@ -18,35 +18,35 @@ use okapi\services\replicate\ReplicateCommon; class WebService { - public static function options() - { - return array( - 'min_auth_level' => 1 - ); - } + public static function options() + { + return array( + 'min_auth_level' => 1 + ); + } - public static function call(OkapiRequest $request) - { - require_once('replicate_common.inc.php'); + public static function call(OkapiRequest $request) + { + require_once('replicate_common.inc.php'); - $result = array(); - $result['changelog'] = array( - 'min_since' => ReplicateCommon::get_min_since(), - 'revision' => ReplicateCommon::get_revision(), - ); - $dump = Cache::get("last_fulldump"); - if ($dump) - { - $result['latest_fulldump'] = array( - 'revision' => $dump['revision'], - 'generated_at' => $dump['meta']['generated_at'], - 'size' => $dump['meta']['compressed_size'], - 'size_uncompressed' => $dump['meta']['uncompressed_size'], - ); - } else { - $result['latest_fulldump'] = null; - } + $result = array(); + $result['changelog'] = array( + 'min_since' => ReplicateCommon::get_min_since(), + 'revision' => ReplicateCommon::get_revision(), + ); + $dump = Cache::get("last_fulldump"); + if ($dump) + { + $result['latest_fulldump'] = array( + 'revision' => $dump['revision'], + 'generated_at' => $dump['meta']['generated_at'], + 'size' => $dump['meta']['compressed_size'], + 'size_uncompressed' => $dump['meta']['uncompressed_size'], + ); + } else { + $result['latest_fulldump'] = null; + } - return Okapi::formatted_response($request, $result); - } + return Okapi::formatted_response($request, $result); + } } diff --git a/htdocs/okapi/services/replicate/info.xml b/htdocs/okapi/services/replicate/info.xml index 16c423e2..7f207983 100644 --- a/htdocs/okapi/services/replicate/info.xml +++ b/htdocs/okapi/services/replicate/info.xml @@ -1,31 +1,31 @@ - Get information on current changelog and fulldump state - 111 - -

    Beta status. Get information on current changelog and fulldump state.

    -
    - - -

    A dictionary of the following structure:

    -
      -
    • -

      changelog - a dictionary of the following structure:

      -
        -
      • min_since - the lowest allowed value for the since attribute - (the ID of the last changelog entry was already removed from our database),
      • -
      • revision - the current revision of the database (the ID of the - newest changelog entry kept in our database),
      • -
      -
    • -
    • -

      latest_fulldump - a dictionary of the following structure:

      -
        -
      • revision - revision of the database saved in the latest fulldump,
      • -
      • generated_at - date and time (ISO 8601) when the fulldump was generated,
      • -
      • size - size of the file, in bytes,
      • -
      • size_uncompressed - approximate size of the uncompressed contents, in bytes.
      • -
      -
    • -
    -
    + Get information on current changelog and fulldump state + 111 + +

    Beta status. Get information on current changelog and fulldump state.

    +
    + + +

    A dictionary of the following structure:

    +
      +
    • +

      changelog - a dictionary of the following structure:

      +
        +
      • min_since - the lowest allowed value for the since attribute + (the ID of the last changelog entry was already removed from our database),
      • +
      • revision - the current revision of the database (the ID of the + newest changelog entry kept in our database),
      • +
      +
    • +
    • +

      latest_fulldump - a dictionary of the following structure:

      +
        +
      • revision - revision of the database saved in the latest fulldump,
      • +
      • generated_at - date and time (ISO 8601) when the fulldump was generated,
      • +
      • size - size of the file, in bytes,
      • +
      • size_uncompressed - approximate size of the uncompressed contents, in bytes.
      • +
      +
    • +
    +
    \ No newline at end of file diff --git a/htdocs/okapi/services/replicate/replicate_common.inc.php b/htdocs/okapi/services/replicate/replicate_common.inc.php index 44d6c02a..6f47075e 100644 --- a/htdocs/okapi/services/replicate/replicate_common.inc.php +++ b/htdocs/okapi/services/replicate/replicate_common.inc.php @@ -18,556 +18,579 @@ use okapi\Settings; class ReplicateCommon { - private static $chunk_size = 50; - private static $logged_cache_fields = 'code|names|location|type|status|url|owner|founds|notfounds|size|size2|oxsize|difficulty|terrain|rating|rating_votes|recommendations|req_passwd|descriptions|hints|images|trackables_count|trackables|alt_wpts|last_found|last_modified|date_created|date_hidden'; - private static $logged_log_entry_fields = 'uuid|cache_code|date|user|type|was_recommended|comment'; + private static $chunk_size = 50; + private static $logged_cache_fields = 'code|names|location|type|status|url|owner|founds|notfounds|size|size2|oxsize|difficulty|terrain|rating|rating_votes|recommendations|req_passwd|descriptions|hints|images|trackables_count|trackables|alt_wpts|last_found|last_modified|date_created|date_hidden|attr_acodes|willattends|country|state|preview_image|trip_time|trip_distance|gc_code|hints2|protection_areas'; + private static $logged_log_entry_fields = 'uuid|cache_code|date|user|type|was_recommended|comment'; - /** Return current (greatest) changelog revision number. */ - public static function get_revision() - { - return Okapi::get_var('clog_revision', 0) + 0; - } + /** Return current (greatest) changelog revision number. */ + public static function get_revision() + { + return Okapi::get_var('clog_revision', 0) + 0; + } - /** Return the number of the oldest changelog revision kept in database. */ - public static function get_min_since() - { - static $cache = null; - if ($cache == null) - { - $cache = Db::select_value("select min(id) from okapi_clog"); - if ($cache === null) - $cache = 1; - $cache -= 1; - } - return $cache; - } + /** Return the number of the oldest changelog revision kept in database. */ + public static function get_min_since() + { + static $cache = null; + if ($cache == null) + { + $cache = Db::select_value("select min(id) from okapi_clog"); + if ($cache === null) + $cache = 1; + $cache -= 1; + } + return $cache; + } - /** - * Compare two dictionaries. Return the $new dictionary with all unchanged - * keys removed. Only the changed ones will remain. - */ - private static function get_diff($old, $new) - { - if (!$old) - return $new; - $changed_keys = array(); - foreach ($new as $key => $value) - { - if (!array_key_exists($key, $old)) - $changed_keys[] = $key; - elseif ($old[$key] != $new[$key]) - $changed_keys[] = $key; - } - $changed = array(); - foreach ($changed_keys as $key) - $changed[$key] = $new[$key]; - return $changed; - } + /** + * Compare two dictionaries. Return the $new dictionary with all unchanged + * keys removed. Only the changed ones will remain. + */ + private static function get_diff($old, $new) + { + if (!$old) + return $new; + $changed_keys = array(); + foreach ($new as $key => $value) + { + if (!array_key_exists($key, $old)) + $changed_keys[] = $key; + elseif ($old[$key] != $new[$key]) + $changed_keys[] = $key; + } + $changed = array(); + foreach ($changed_keys as $key) + $changed[$key] = $new[$key]; + return $changed; + } - /** Check for modifications in the database and update the changelog table accordingly. */ - public static function update_clog_table() - { - $now = Db::select_value("select date_add(now(), interval -1 minute)"); # See issue 157. - $last_update = Okapi::get_var('last_clog_update'); - if ($last_update === null) - $last_update = Db::select_value("select date_add(now(), interval -1 day)"); + /** Check for modifications in the database and update the changelog table accordingly. */ + public static function update_clog_table() + { + $now = Db::select_value("select date_add(now(), interval -1 minute)"); # See issue 157. + $last_update = Okapi::get_var('last_clog_update'); + if ($last_update === null) + $last_update = Db::select_value("select date_add(now(), interval -1 day)"); - # Usually this will be fast. But, for example, if admin changes ALL the - # caches, this will take forever. But we still want it to finish properly - # without interruption. + # Usually this will be fast. But, for example, if admin changes ALL the + # caches, this will take forever. But we still want it to finish properly + # without interruption. - set_time_limit(0); - ignore_user_abort(true); + set_time_limit(0); + ignore_user_abort(true); - # Get the list of modified cache codes. Split it into groups of N cache codes. - # Note that we should include ALL cache codes in this particular query, not - # only "status in (1,2,3)". This way, when the cache changes its status, e.g. - # from 3 to 6, changelog will include a proper "delete" statement. + # Get the list of modified cache codes. Split it into groups of N cache codes. + # Note that we should include ALL cache codes in this particular query, not + # only "status in (1,2,3)". This way, when the cache changes its status, e.g. + # from 3 to 6, changelog will include a proper "delete" statement. - $cache_codes = Db::select_column(" - select wp_oc - from caches - where okapi_syncbase > '".mysql_real_escape_string($last_update)."'; - "); - $cache_code_groups = Okapi::make_groups($cache_codes, 50); - unset($cache_codes); + $cache_codes = Db::select_column(" + select wp_oc + from caches + where okapi_syncbase > '".mysql_real_escape_string($last_update)."'; + "); + $cache_code_groups = Okapi::make_groups($cache_codes, 50); + unset($cache_codes); - # For each group, update the changelog table accordingly. + # For each group, update the changelog table accordingly. - foreach ($cache_code_groups as $cache_codes) - { - self::generate_changelog_entries('services/caches/geocaches', 'geocache', 'cache_codes', - 'code', $cache_codes, self::$logged_cache_fields, false, true, null); - } + foreach ($cache_code_groups as $cache_codes) + { + self::generate_changelog_entries('services/caches/geocaches', 'geocache', 'cache_codes', + 'code', $cache_codes, self::$logged_cache_fields, false, true, null); + } - # Same as above, for log entries. + # Same as above, for log entries. - $offset = 0; - while (true) - { - $log_uuids = Db::select_column(" - select uuid - from cache_logs - where okapi_syncbase > '".mysql_real_escape_string($last_update)."' - limit $offset, 10000; - "); - if (count($log_uuids) == 0) - break; - $offset += 10000; - $log_uuid_groups = Okapi::make_groups($log_uuids, 100); - unset($log_uuids); - foreach ($log_uuid_groups as $log_uuids) - { - self::generate_changelog_entries('services/logs/entries', 'log', 'log_uuids', - 'uuid', $log_uuids, self::$logged_log_entry_fields, false, true, 3600); - } - } - if (Settings::get('OC_BRANCH') == 'oc.de') - { - # On OCDE branch, deleted log entries are MOVED to another table. - # So the above queries won't detect them. We need to run one more. - # We will assume there are not so many of them and we don't have to - # split them in groups as we did above. + $offset = 0; + while (true) + { + $log_uuids = Db::select_column(" + select uuid + from cache_logs + where okapi_syncbase > '".mysql_real_escape_string($last_update)."' + limit $offset, 10000; + "); + if (count($log_uuids) == 0) + break; + $offset += 10000; + $log_uuid_groups = Okapi::make_groups($log_uuids, 100); + unset($log_uuids); + foreach ($log_uuid_groups as $log_uuids) + { + self::generate_changelog_entries('services/logs/entries', 'log', 'log_uuids', + 'uuid', $log_uuids, self::$logged_log_entry_fields, false, true, 3600); + } + } + if (Settings::get('OC_BRANCH') == 'oc.de') + { + # On OCDE branch, deleted log entries are MOVED to another table. + # So the above queries won't detect them. We need to run one more. + # We will assume there are not so many of them and we don't have to + # split them in groups as we did above. - $DELETED_uuids = Db::select_column(" - select uuid - from cache_logs_archived - where okapi_syncbase > '".mysql_real_escape_string($last_update)."' - "); - self::generate_changelog_entries('services/logs/entries', 'log', 'log_uuids', - 'uuid', $DELETED_uuids, self::$logged_log_entry_fields, false, true, 3600); - } + $DELETED_uuids = Db::select_column(" + select uuid + from cache_logs_archived + where okapi_syncbase > '".mysql_real_escape_string($last_update)."' + "); + self::generate_changelog_entries('services/logs/entries', 'log', 'log_uuids', + 'uuid', $DELETED_uuids, self::$logged_log_entry_fields, false, true, 3600); + } - # Update state variables. + # Update state variables. - Okapi::set_var("last_clog_update", $now); - $revision = Db::select_value("select max(id) from okapi_clog"); - Okapi::set_var("clog_revision", $revision); - } + Okapi::set_var("last_clog_update", $now); + $revision = Db::select_value("select max(id) from okapi_clog"); + Okapi::set_var("clog_revision", $revision); + } - /** - * Scan the database and compare the current values of old entries to - * the cached values of the same entries. If differences found, update - * okapi_syncbase accordingly, and email the OKAPI developers. - * - * Currently, only caches are checked (log entries are not). - */ - public static function verify_clog_consistency() - { - set_time_limit(0); - ignore_user_abort(true); + /** + * Scan the database and compare the current values of old entries to + * the cached values of the same entries. If differences found, update + * okapi_syncbase accordingly, and email the OKAPI developers. + * + * Currently, only caches are checked (log entries are not). + */ + public static function verify_clog_consistency( + $force_all=false, $geocache_ignored_fields = null + ) + { + set_time_limit(0); + ignore_user_abort(true); - # We will SKIP the entries which have been modified SINCE one day ago. - # Such entries might have not been seen by the update_clog_table() yet - # (e.g. other long-running cronjob is preventing update_clog_table from - # running). + # We will SKIP the entries which have been modified SINCE one day ago. + # Such entries might have not been seen by the update_clog_table() yet + # (e.g. other long-running cronjob is preventing update_clog_table from + # running). + # + # If $force_all is true, then all caches will be verified. This is + # quite important when used in conjunction with ignored_fields. - $cache_codes = Db::select_column(" - select wp_oc - from caches - where okapi_syncbase < date_add(now(), interval -1 day); - "); - $cache_code_groups = Okapi::make_groups($cache_codes, 50); - unset($cache_codes); + $cache_codes = Db::select_column(" + select wp_oc + from caches + ".($force_all ? "" : "where okapi_syncbase < date_add(now(), interval -1 day)")." + "); + $cache_code_groups = Okapi::make_groups($cache_codes, 50); + unset($cache_codes); - # For each group, get the changelog entries, but don't store them - # (the "fulldump" mode). Instead, just update the okapi_syncbase column. + # For each group, get the changelog entries, but don't store them + # (the "fulldump" mode). Instead, just update the okapi_syncbase column. - $sum = 0; - $two_examples = array(); - foreach ($cache_code_groups as $cache_codes) - { - $entries = self::generate_changelog_entries( - 'services/caches/geocaches', 'geocache', 'cache_codes', - 'code', $cache_codes, self::$logged_cache_fields, true, true, null - ); - foreach ($entries as $entry) - { - if ($entry['object_type'] != 'geocache') - continue; - $cache_code = $entry['object_key']['code']; + $sum = 0; + $two_examples = array(); + foreach ($cache_code_groups as $cache_codes) + { + $entries = self::generate_changelog_entries( + 'services/caches/geocaches', 'geocache', 'cache_codes', + 'code', $cache_codes, self::$logged_cache_fields, true, true, null + ); + foreach ($entries as $entry) + { + if ($entry['object_type'] != 'geocache') + continue; + $cache_code = $entry['object_key']['code']; - # We will story the first and the last entry in the $two_examples - # vars which is to be emailed to OKAPI developers. + if (($entry['change_type'] == 'replace') && ($geocache_ignored_fields != null)) { - if (count($two_examples) == 0) - $two_examples[0] = $entry; /* The first entry */ - $two_examples[1] = $entry; /* The last entry */ + # We were called with a non-null ignored fields. Probably + # this call originated from the database update script + # and new fields have been added to the replicate module. + # We will ignore such new fields - this way no unnecessary + # clog entries will be created. - Db::execute(" - update caches - set okapi_syncbase = now() - where wp_oc = '".mysql_real_escape_string($cache_code)."' - "); - $sum += 1; - } - } - if ($sum > 0) - { - $message = ( - "Number of invalid entries scheduled to be fixed: $sum\n". - "Approx revision of the first one: ".Okapi::get_var('clog_revision')."\n\n". - "Two examples:\n\n".print_r($two_examples, true) - ); - Okapi::mail_from_okapi( - "rygielski@mimuw.edu.pl", - "verify_clog_consistency - ".Okapi::get_normalized_site_name(), - $message, true - ); - } - } + foreach ($geocache_ignored_fields as $field) { + unset($entry['data'][$field]); + } + if (count($entry['data']) == 0) { + # Skip this geocache. Nothing was changed here, only + # new fields have been added. + continue; + } + } + + # We will story the first and the last entry in the $two_examples + # vars which is to be emailed to OKAPI developers. + + if (count($two_examples) == 0) + $two_examples[0] = $entry; /* The first entry */ + $two_examples[1] = $entry; /* The last entry */ + + Db::execute(" + update caches + set okapi_syncbase = now() + where wp_oc = '".mysql_real_escape_string($cache_code)."' + "); + $sum += 1; + } + } + if ($sum > 0) + { + $message = ( + "Number of invalid entries scheduled to be fixed: $sum\n". + "Approx revision of the first one: ".Okapi::get_var('clog_revision')."\n\n". + "Two examples:\n\n".print_r($two_examples, true) + ); + Okapi::mail_from_okapi( + "rygielski@mimuw.edu.pl", + "verify_clog_consistency - ".Okapi::get_normalized_site_name(), + $message, true + ); + } + } - /** - * Generate OKAPI changelog entries. This method will call $feeder_method OKAPI - * service with the following parameters: array($feeder_keys_param => implode('|', $key_values), - * 'fields' => $fields). Then it will generate the changelog, based on the result. - * This looks pretty much the same for various object types, that's why it's here. - * - * If $use_cache is true, then all the dictionaries from $feeder_method will be also - * kept in OKAPI cache, for future comparison. - * - * In normal mode, update the changelog and don't return anything. - * In fulldump mode, return the generated changelog entries *instead* of - * updating it. - */ - private static function generate_changelog_entries($feeder_method, $object_type, $feeder_keys_param, - $key_name, $key_values, $fields, $fulldump_mode, $use_cache, $cache_timeout = 86400) - { - # Retrieve the previous versions of all objects from OKAPI cache. + /** + * Generate OKAPI changelog entries. This method will call $feeder_method OKAPI + * service with the following parameters: array($feeder_keys_param => implode('|', $key_values), + * 'fields' => $fields). Then it will generate the changelog, based on the result. + * This looks pretty much the same for various object types, that's why it's here. + * + * If $use_cache is true, then all the dictionaries from $feeder_method will be also + * kept in OKAPI cache, for future comparison. + * + * In normal mode, update the changelog and don't return anything. + * In fulldump mode, return the generated changelog entries *instead* of + * updating it. + */ + private static function generate_changelog_entries($feeder_method, $object_type, $feeder_keys_param, + $key_name, $key_values, $fields, $fulldump_mode, $use_cache, $cache_timeout = 86400) + { + # Retrieve the previous versions of all objects from OKAPI cache. - if ($use_cache) - { - $cache_keys1 = array(); - $cache_keys2 = array(); - foreach ($key_values as $key) - $cache_keys1[] = 'clog#'.$object_type.'#'.$key; - foreach ($key_values as $key) - $cache_keys2[] = 'clogmd5#'.$object_type.'#'.$key; - $cached_values1 = Cache::get_many($cache_keys1); - $cached_values2 = Cache::get_many($cache_keys2); - if (!$fulldump_mode) - { - Cache::delete_many($cache_keys1); - Cache::delete_many($cache_keys2); - } - unset($cache_keys1); - unset($cache_keys2); - } + if ($use_cache) + { + $cache_keys1 = array(); + $cache_keys2 = array(); + foreach ($key_values as $key) + $cache_keys1[] = 'clog#'.$object_type.'#'.$key; + foreach ($key_values as $key) + $cache_keys2[] = 'clogmd5#'.$object_type.'#'.$key; + $cached_values1 = Cache::get_many($cache_keys1); + $cached_values2 = Cache::get_many($cache_keys2); + if (!$fulldump_mode) + { + Cache::delete_many($cache_keys1); + Cache::delete_many($cache_keys2); + } + unset($cache_keys1); + unset($cache_keys2); + } - # Get the current values for objects. Compare them with their previous versions - # and generate changelog entries. + # Get the current values for objects. Compare them with their previous versions + # and generate changelog entries. - require_once($GLOBALS['rootpath'].'okapi/service_runner.php'); - $current_values = OkapiServiceRunner::call($feeder_method, new OkapiInternalRequest( - new OkapiInternalConsumer(), null, array( - $feeder_keys_param => implode("|", $key_values), - 'fields' => $fields, - 'attribution_append' => 'static' # currently, this is for the "geocaches" method only - ))); - $entries = array(); - foreach ($current_values as $key => $object) - { - if ($object !== null) - { - # Currently, the object exists. - if ($use_cache) - { - # First, compare the cached hash. The hash has much longer lifetime - # than the actual cached object. - $cached_md5 = $cached_values2['clogmd5#'.$object_type.'#'.$key]; - $current_md5 = md5(serialize($object)); - if ($cached_md5 == $current_md5) - { - # The object was not changed since it was last replaced. - continue; - } - $diff = self::get_diff($cached_values1['clog#'.$object_type.'#'.$key], $object); - if (count($diff) == 0) - { - # Md5 differs, but diff does not. Weird, but it can happen - # (e.g. just after the md5 extension was introduced, or if - # md5 somehow expired before the actual object did). - continue; - } - } - $entries[] = array( - 'object_type' => $object_type, - 'object_key' => array($key_name => $key), - 'change_type' => 'replace', - 'data' => ($use_cache ? $diff : $object), - ); - if ($use_cache) - { - # Save the last-published state of the object, for future comparison. - $cached_values2['clogmd5#'.$object_type.'#'.$key] = $current_md5; - $cached_values1['clog#'.$object_type.'#'.$key] = $object; - } - } - else - { - # Currently, the object does not exist. - if ($use_cache && ($cached_values1['clog#'.$object_type.'#'.$key] === false)) - { - # No need to delete, we have already published its deletion. - continue; - } - $entries[] = array( - 'object_type' => $object_type, - 'object_key' => array($key_name => $key), - 'change_type' => 'delete', - ); - if ($use_cache) - { - # Cache the fact, that the object was deleted. - $cached_values2['clogmd5#'.$object_type.'#'.$key] = false; - $cached_values1['clog#'.$object_type.'#'.$key] = false; - } - } - } + require_once($GLOBALS['rootpath'].'okapi/service_runner.php'); + $current_values = OkapiServiceRunner::call($feeder_method, new OkapiInternalRequest( + new OkapiInternalConsumer(), null, array( + $feeder_keys_param => implode("|", $key_values), + 'fields' => $fields, + 'attribution_append' => 'static' # currently, this is for the "geocaches" method only + ))); + $entries = array(); + foreach ($current_values as $key => $object) + { + if ($object !== null) + { + # Currently, the object exists. + if ($use_cache) + { + # First, compare the cached hash. The hash has much longer lifetime + # than the actual cached object. + $cached_md5 = $cached_values2['clogmd5#'.$object_type.'#'.$key]; + $current_md5 = md5(serialize($object)); + if ($cached_md5 == $current_md5) + { + # The object was not changed since it was last replaced. + continue; + } + $diff = self::get_diff($cached_values1['clog#'.$object_type.'#'.$key], $object); + if (count($diff) == 0) + { + # Md5 differs, but diff does not. Weird, but it can happen + # (e.g. just after the md5 extension was introduced, or if + # md5 somehow expired before the actual object did). + continue; + } + } + $entries[] = array( + 'object_type' => $object_type, + 'object_key' => array($key_name => $key), + 'change_type' => 'replace', + 'data' => ($use_cache ? $diff : $object), + ); + if ($use_cache) + { + # Save the last-published state of the object, for future comparison. + $cached_values2['clogmd5#'.$object_type.'#'.$key] = $current_md5; + $cached_values1['clog#'.$object_type.'#'.$key] = $object; + } + } + else + { + # Currently, the object does not exist. + if ($use_cache && ($cached_values1['clog#'.$object_type.'#'.$key] === false)) + { + # No need to delete, we have already published its deletion. + continue; + } + $entries[] = array( + 'object_type' => $object_type, + 'object_key' => array($key_name => $key), + 'change_type' => 'delete', + ); + if ($use_cache) + { + # Cache the fact, that the object was deleted. + $cached_values2['clogmd5#'.$object_type.'#'.$key] = false; + $cached_values1['clog#'.$object_type.'#'.$key] = false; + } + } + } - if ($fulldump_mode) - { - return $entries; - } - else - { - # Save the entries to the clog table. + if ($fulldump_mode) + { + return $entries; + } + else + { + # Save the entries to the clog table. - if (count($entries) > 0) - { - $data_values = array(); - foreach ($entries as $entry) - $data_values[] = gzdeflate(serialize($entry)); - Db::execute(" - insert into okapi_clog (data) - values ('".implode("'),('", array_map('mysql_real_escape_string', $data_values))."'); - "); - } + if (count($entries) > 0) + { + $data_values = array(); + foreach ($entries as $entry) + $data_values[] = gzdeflate(serialize($entry)); + Db::execute(" + insert into okapi_clog (data) + values ('".implode("'),('", array_map('mysql_real_escape_string', $data_values))."'); + "); + } - # Update the values kept in OKAPI cache. + # Update the values kept in OKAPI cache. - if ($use_cache) - { - Cache::set_many($cached_values1, $cache_timeout); - Cache::set_many($cached_values2, null); # make it persistent - } - } - } + if ($use_cache) + { + Cache::set_many($cached_values1, $cache_timeout); + Cache::set_many($cached_values2, null); # make it persistent + } + } + } - /** - * Check if the 'since' parameter is up-do-date. If it is not, then it means - * that the user waited too long and he has to download the fulldump again. - */ - public static function check_since_param($since) - { - $first_id = Db::select_value(" - select id from okapi_clog where id > '".mysql_real_escape_string($since)."' limit 1 - "); - if ($first_id === null) - return true; # okay, since points to the newest revision - if ($first_id == $since + 1) - return true; # okay, revision $since + 1 is present + /** + * Check if the 'since' parameter is up-do-date. If it is not, then it means + * that the user waited too long and he has to download the fulldump again. + */ + public static function check_since_param($since) + { + $first_id = Db::select_value(" + select id from okapi_clog where id > '".mysql_real_escape_string($since)."' limit 1 + "); + if ($first_id === null) + return true; # okay, since points to the newest revision + if ($first_id == $since + 1) + return true; # okay, revision $since + 1 is present - # If we're here, then this means that $first_id > $since + 1. - # Revision $since + 1 is already deleted, $since must be too old! + # If we're here, then this means that $first_id > $since + 1. + # Revision $since + 1 is already deleted, $since must be too old! - return false; - } + return false; + } - /** - * Select best chunk for a given $since parameter. This function will try to select - * one chunk for different values of $since parameter, this is done in order to - * allow more optimal caching. Returns: list($from, $to). NOTICE: If $since is - * at the newest revision, then this will return list($since + 1, $since) - an - * empty chunk. - */ - public static function select_best_chunk($since) - { - $current_revision = self::get_revision(); - $last_chunk_cut = $current_revision - ($current_revision % self::$chunk_size); - if ($since >= $last_chunk_cut) - { - # If, for example, we have a choice to give user 50 items he wants, or 80 items - # which we probably already have in cache (and this includes the 50 which the - # user wants), then we'll give him 80. If user wants less than half of what we - # have (ex. 30), then we'll give him only his 30. + /** + * Select best chunk for a given $since parameter. This function will try to select + * one chunk for different values of $since parameter, this is done in order to + * allow more optimal caching. Returns: list($from, $to). NOTICE: If $since is + * at the newest revision, then this will return list($since + 1, $since) - an + * empty chunk. + */ + public static function select_best_chunk($since) + { + $current_revision = self::get_revision(); + $last_chunk_cut = $current_revision - ($current_revision % self::$chunk_size); + if ($since >= $last_chunk_cut) + { + # If, for example, we have a choice to give user 50 items he wants, or 80 items + # which we probably already have in cache (and this includes the 50 which the + # user wants), then we'll give him 80. If user wants less than half of what we + # have (ex. 30), then we'll give him only his 30. - if ($current_revision - $since > $since - $last_chunk_cut) - return array($last_chunk_cut + 1, $current_revision); - else - return array($since + 1, $current_revision); - } - $prev_chunk_cut = $since - ($since % self::$chunk_size); - return array($prev_chunk_cut + 1, $prev_chunk_cut + self::$chunk_size); - } + if ($current_revision - $since > $since - $last_chunk_cut) + return array($last_chunk_cut + 1, $current_revision); + else + return array($since + 1, $current_revision); + } + $prev_chunk_cut = $since - ($since % self::$chunk_size); + return array($prev_chunk_cut + 1, $prev_chunk_cut + self::$chunk_size); + } - /** - * Return changelog chunk, starting at $from, ending as $to. - */ - public static function get_chunk($from, $to) - { - if ($to < $from) - return array(); - if ($to - $from > self::$chunk_size) - throw new Exception("You should not get chunksize bigger than ".self::$chunk_size." entries at one time."); + /** + * Return changelog chunk, starting at $from, ending as $to. + */ + public static function get_chunk($from, $to) + { + if ($to < $from) + return array(); + if ($to - $from > self::$chunk_size) + throw new Exception("You should not get chunksize bigger than ".self::$chunk_size." entries at one time."); - # Check if we already have this chunk in cache. + # Check if we already have this chunk in cache. - $cache_key = 'clog_chunk#'.$from.'-'.$to; - $chunk = Cache::get($cache_key); - if ($chunk === null) - { - $rs = Db::query(" - select id, data - from okapi_clog - where id between '".mysql_real_escape_string($from)."' and '".mysql_real_escape_string($to)."' - order by id - "); - $chunk = array(); - while ($row = mysql_fetch_assoc($rs)) - { - $chunk[] = unserialize(gzinflate($row['data'])); - } + $cache_key = 'clog_chunk#'.$from.'-'.$to; + $chunk = Cache::get($cache_key); + if ($chunk === null) + { + $rs = Db::query(" + select id, data + from okapi_clog + where id between '".mysql_real_escape_string($from)."' and '".mysql_real_escape_string($to)."' + order by id + "); + $chunk = array(); + while ($row = mysql_fetch_assoc($rs)) + { + $chunk[] = unserialize(gzinflate($row['data'])); + } - # Cache timeout depends on the chunk starting and ending point. Chunks - # which start and end on the boundries of chunk_size should be cached - # longer (they can be accessed even after 10 days). Other chunks won't - # be ever accessed after the next revision appears, so there is not point - # in storing them that long. + # Cache timeout depends on the chunk starting and ending point. Chunks + # which start and end on the boundries of chunk_size should be cached + # longer (they can be accessed even after 10 days). Other chunks won't + # be ever accessed after the next revision appears, so there is not point + # in storing them that long. - if (($from % self::$chunk_size === 0) && ($to % self::$chunk_size === 0)) - $timeout = 10 * 86400; - else - $timeout = 86400; - Cache::set($cache_key, $chunk, $timeout); - } + if (($from % self::$chunk_size === 0) && ($to % self::$chunk_size === 0)) + $timeout = 10 * 86400; + else + $timeout = 86400; + Cache::set($cache_key, $chunk, $timeout); + } - return $chunk; - } + return $chunk; + } - /** - * Generate a new fulldump file and put it into the OKAPI cache table. - * Return the cache key. - */ - public static function generate_fulldump() - { - # First we will create temporary files, then compress them in the end. + /** + * Generate a new fulldump file and put it into the OKAPI cache table. + * Return the cache key. + */ + public static function generate_fulldump() + { + # First we will create temporary files, then compress them in the end. - $revision = self::get_revision(); - $generated_at = date('c', time()); - $dir = Okapi::get_var_dir()."/okapi-db-dump"; - $i = 1; - $json_files = array(); + $revision = self::get_revision(); + $generated_at = date('c', time()); + $dir = Okapi::get_var_dir()."/okapi-db-dump"; + $i = 1; + $json_files = array(); - # Cleanup (from a previous, possibly unsuccessful, execution) + # Cleanup (from a previous, possibly unsuccessful, execution) - shell_exec("rm -f $dir/*"); - shell_exec("rmdir $dir"); - shell_exec("mkdir $dir"); - shell_exec("chmod 777 $dir"); + shell_exec("rm -f $dir/*"); + shell_exec("rmdir $dir"); + shell_exec("mkdir $dir"); + shell_exec("chmod 777 $dir"); - # Geocaches + # Geocaches - $cache_codes = Db::select_column("select wp_oc from caches"); - $cache_code_groups = Okapi::make_groups($cache_codes, self::$chunk_size); - unset($cache_codes); - foreach ($cache_code_groups as $cache_codes) - { - $basename = "part".str_pad($i, 5, "0", STR_PAD_LEFT); - $json_files[] = $basename.".json"; - $entries = self::generate_changelog_entries('services/caches/geocaches', 'geocache', 'cache_codes', - 'code', $cache_codes, self::$logged_cache_fields, true, false); - $filtered = array(); - foreach ($entries as $entry) - if ($entry['change_type'] == 'replace') - $filtered[] = $entry; - unset($entries); - file_put_contents("$dir/$basename.json", json_encode($filtered)); - unset($filtered); - $i++; - } - unset($cache_code_groups); + $cache_codes = Db::select_column("select wp_oc from caches"); + $cache_code_groups = Okapi::make_groups($cache_codes, self::$chunk_size); + unset($cache_codes); + foreach ($cache_code_groups as $cache_codes) + { + $basename = "part".str_pad($i, 5, "0", STR_PAD_LEFT); + $json_files[] = $basename.".json"; + $entries = self::generate_changelog_entries('services/caches/geocaches', 'geocache', 'cache_codes', + 'code', $cache_codes, self::$logged_cache_fields, true, false); + $filtered = array(); + foreach ($entries as $entry) + if ($entry['change_type'] == 'replace') + $filtered[] = $entry; + unset($entries); + file_put_contents("$dir/$basename.json", json_encode($filtered)); + unset($filtered); + $i++; + } + unset($cache_code_groups); - # Log entries. We cannot load all the uuids at one time, this would take - # too much memory. Hence the offset/limit loop. + # Log entries. We cannot load all the uuids at one time, this would take + # too much memory. Hence the offset/limit loop. - $offset = 0; - while (true) - { - $log_uuids = Db::select_column(" - select uuid - from cache_logs - where ".((Settings::get('OC_BRANCH') == 'oc.pl') ? "deleted = 0" : "true")." - order by uuid - limit $offset, 10000 - "); - if (count($log_uuids) == 0) - break; - $offset += 10000; - $log_uuid_groups = Okapi::make_groups($log_uuids, 500); - unset($log_uuids); - foreach ($log_uuid_groups as $log_uuids) - { - $basename = "part".str_pad($i, 5, "0", STR_PAD_LEFT); - $json_files[] = $basename.".json"; - $entries = self::generate_changelog_entries('services/logs/entries', 'log', 'log_uuids', - 'uuid', $log_uuids, self::$logged_log_entry_fields, true, false); - $filtered = array(); - foreach ($entries as $entry) - if ($entry['change_type'] == 'replace') - $filtered[] = $entry; - unset($entries); - file_put_contents("$dir/$basename.json", json_encode($filtered)); - unset($filtered); - $i++; - } - } + $offset = 0; + while (true) + { + $log_uuids = Db::select_column(" + select uuid + from cache_logs + where ".((Settings::get('OC_BRANCH') == 'oc.pl') ? "deleted = 0" : "true")." + order by uuid + limit $offset, 10000 + "); + if (count($log_uuids) == 0) + break; + $offset += 10000; + $log_uuid_groups = Okapi::make_groups($log_uuids, 500); + unset($log_uuids); + foreach ($log_uuid_groups as $log_uuids) + { + $basename = "part".str_pad($i, 5, "0", STR_PAD_LEFT); + $json_files[] = $basename.".json"; + $entries = self::generate_changelog_entries('services/logs/entries', 'log', 'log_uuids', + 'uuid', $log_uuids, self::$logged_log_entry_fields, true, false); + $filtered = array(); + foreach ($entries as $entry) + if ($entry['change_type'] == 'replace') + $filtered[] = $entry; + unset($entries); + file_put_contents("$dir/$basename.json", json_encode($filtered)); + unset($filtered); + $i++; + } + } - # Package data. + # Package data. - $metadata = array( - 'revision' => $revision, - 'data_files' => $json_files, - 'meta' => array( - 'site_name' => Okapi::get_normalized_site_name(), - 'okapi_revision' => Okapi::$revision, - 'generated_at' => $generated_at, - ), - ); - file_put_contents("$dir/index.json", json_encode($metadata)); + $metadata = array( + 'revision' => $revision, + 'data_files' => $json_files, + 'meta' => array( + 'site_name' => Okapi::get_normalized_site_name(), + 'okapi_revision' => Okapi::$revision, + 'generated_at' => $generated_at, + ), + ); + file_put_contents("$dir/index.json", json_encode($metadata)); - # Compute uncompressed size. + # Compute uncompressed size. - $size = filesize("$dir/index.json"); - foreach ($json_files as $filename) - $size += filesize("$dir/$filename"); + $size = filesize("$dir/index.json"); + foreach ($json_files as $filename) + $size += filesize("$dir/$filename"); - # Create JSON archive. We use tar options: -j for bzip2, -z for gzip - # (bzip2 is MUCH slower). + # Create JSON archive. We use tar options: -j for bzip2, -z for gzip + # (bzip2 is MUCH slower). - $use_bzip2 = true; - $dumpfilename = "okapi-dump.tar.".($use_bzip2 ? "bz2" : "gz"); - shell_exec("tar --directory $dir -c".($use_bzip2 ? "j" : "z")."f $dir/$dumpfilename index.json ".implode(" ", $json_files). " 2>&1"); + $use_bzip2 = true; + $dumpfilename = "okapi-dump.tar.".($use_bzip2 ? "bz2" : "gz"); + shell_exec("tar --directory $dir -c".($use_bzip2 ? "j" : "z")."f $dir/$dumpfilename index.json ".implode(" ", $json_files). " 2>&1"); - # Delete temporary files. + # Delete temporary files. - shell_exec("rm -f $dir/*.json"); + shell_exec("rm -f $dir/*.json"); - # Move the archive one directory upwards, replacing the previous one. - # Remove the temporary directory. + # Move the archive one directory upwards, replacing the previous one. + # Remove the temporary directory. - shell_exec("mv -f $dir/$dumpfilename ".Okapi::get_var_dir()); - shell_exec("rmdir $dir"); + shell_exec("mv -f $dir/$dumpfilename ".Okapi::get_var_dir()); + shell_exec("rmdir $dir"); - # Update the database info. + # Update the database info. - $metadata['meta']['filepath'] = Okapi::get_var_dir().'/'.$dumpfilename; - $metadata['meta']['content_type'] = ($use_bzip2 ? "application/octet-stream" : "application/x-gzip"); - $metadata['meta']['public_filename'] = 'okapi-dump-r'.$metadata['revision'].'.tar.'.($use_bzip2 ? "bz2" : "gz"); - $metadata['meta']['uncompressed_size'] = $size; - $metadata['meta']['compressed_size'] = filesize($metadata['meta']['filepath']); - Cache::set("last_fulldump", $metadata, 10 * 86400); - } + $metadata['meta']['filepath'] = Okapi::get_var_dir().'/'.$dumpfilename; + $metadata['meta']['content_type'] = ($use_bzip2 ? "application/octet-stream" : "application/x-gzip"); + $metadata['meta']['public_filename'] = 'okapi-dump-r'.$metadata['revision'].'.tar.'.($use_bzip2 ? "bz2" : "gz"); + $metadata['meta']['uncompressed_size'] = $size; + $metadata['meta']['compressed_size'] = filesize($metadata['meta']['filepath']); + Cache::set("last_fulldump", $metadata, 10 * 86400); + } } diff --git a/htdocs/okapi/services/users/by_internal_id.php b/htdocs/okapi/services/users/by_internal_id.php index 70f09419..e5e444da 100644 --- a/htdocs/okapi/services/users/by_internal_id.php +++ b/htdocs/okapi/services/users/by_internal_id.php @@ -13,27 +13,27 @@ use okapi\services\caches\search\SearchAssistant; class WebService { - public static function options() - { - return array( - 'min_auth_level' => 1 - ); - } + public static function options() + { + return array( + 'min_auth_level' => 1 + ); + } - public static function call(OkapiRequest $request) - { - $internal_id = $request->get_parameter('internal_id'); - if (!$internal_id) throw new ParamMissing('internal_id'); - $fields = $request->get_parameter('fields'); + public static function call(OkapiRequest $request) + { + $internal_id = $request->get_parameter('internal_id'); + if (!$internal_id) throw new ParamMissing('internal_id'); + $fields = $request->get_parameter('fields'); - # There's no need to validate the fields parameter. + # There's no need to validate the fields parameter. - $results = OkapiServiceRunner::call('services/users/by_internal_ids', new OkapiInternalRequest( - $request->consumer, $request->token, array('internal_ids' => $internal_id, - 'fields' => $fields))); - $result = $results[$internal_id]; - if ($result == null) - throw new InvalidParam('internal_id', "There is no user by this internal_id."); - return Okapi::formatted_response($request, $result); - } + $results = OkapiServiceRunner::call('services/users/by_internal_ids', new OkapiInternalRequest( + $request->consumer, $request->token, array('internal_ids' => $internal_id, + 'fields' => $fields))); + $result = $results[$internal_id]; + if ($result == null) + throw new InvalidParam('internal_id', "There is no user by this internal_id."); + return Okapi::formatted_response($request, $result); + } } diff --git a/htdocs/okapi/services/users/by_internal_id.xml b/htdocs/okapi/services/users/by_internal_id.xml index 4bb1928c..c65ffe6a 100644 --- a/htdocs/okapi/services/users/by_internal_id.xml +++ b/htdocs/okapi/services/users/by_internal_id.xml @@ -1,18 +1,18 @@ - Find single user, by his internal_id - 44 - -

    Retrieve information on a single user, referencing him by his internal ID.

    -

    In general, you should not use this method. Reference your users by their - normal ID - user_uuid. Also, internal IDs are not universally unique and are - a poor choice for a key.

    -
    - Internal ID - -

    See services/users/user method.

    -
    - - -

    See services/users/user method.

    -
    + Find single user, by his internal_id + 44 + +

    Retrieve information on a single user, referencing him by his internal ID.

    +

    In general, you should not use this method. Reference your users by their + normal ID - user_uuid. Also, internal IDs are not universally unique and are + a poor choice for a key.

    +
    + Internal ID + +

    See services/users/user method.

    +
    + + +

    See services/users/user method.

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

    This method works like the services/users/by_internal_id method, but works - with multiple users (instead of only one).

    -
    - -

    Pipe-separated list of internal user IDs. No more than 500 are allowed.

    -
    - -

    See services/users/user method.

    -
    - - -

    A dictionary. Internal IDs you provide will be mapped to dictionary keys, - and each value will be a dictionary of fields you have selected.

    -

    Value of null means that the given internal ID haven't been found.

    -
    + Find multiple users, by their internal IDs + 45 + +

    This method works like the services/users/by_internal_id method, but works + with multiple users (instead of only one).

    +
    + +

    Pipe-separated list of internal user IDs. No more than 500 are allowed.

    +
    + +

    See services/users/user method.

    +
    + + +

    A dictionary. Internal IDs you provide will be mapped to dictionary keys, + and each value will be a dictionary of fields you have selected.

    +

    Value of null means that the given internal ID haven't been found.

    +
    \ No newline at end of file diff --git a/htdocs/okapi/services/users/by_username.php b/htdocs/okapi/services/users/by_username.php index debe85de..43a72127 100644 --- a/htdocs/okapi/services/users/by_username.php +++ b/htdocs/okapi/services/users/by_username.php @@ -16,27 +16,27 @@ use okapi\services\caches\search\SearchAssistant; class WebService { - public static function options() - { - return array( - 'min_auth_level' => 1 - ); - } + public static function options() + { + return array( + 'min_auth_level' => 1 + ); + } - public static function call(OkapiRequest $request) - { - $username = $request->get_parameter('username'); - if (!$username) throw new ParamMissing('username'); - $fields = $request->get_parameter('fields'); + public static function call(OkapiRequest $request) + { + $username = $request->get_parameter('username'); + if (!$username) throw new ParamMissing('username'); + $fields = $request->get_parameter('fields'); - # There's no need to validate the fields parameter. + # There's no need to validate the fields parameter. - $results = OkapiServiceRunner::call('services/users/by_usernames', new OkapiInternalRequest( - $request->consumer, $request->token, array('usernames' => $username, - 'fields' => $fields))); - $result = $results[$username]; - if ($result == null) - throw new InvalidParam('username', "There is no user by this username."); - return Okapi::formatted_response($request, $result); - } + $results = OkapiServiceRunner::call('services/users/by_usernames', new OkapiInternalRequest( + $request->consumer, $request->token, array('usernames' => $username, + 'fields' => $fields))); + $result = $results[$username]; + if ($result == null) + throw new InvalidParam('username', "There is no user by this username."); + return Okapi::formatted_response($request, $result); + } } diff --git a/htdocs/okapi/services/users/by_username.xml b/htdocs/okapi/services/users/by_username.xml index a30f883d..4425cee1 100644 --- a/htdocs/okapi/services/users/by_username.xml +++ b/htdocs/okapi/services/users/by_username.xml @@ -1,22 +1,22 @@ - Find single user, by his/her username - 24 - -

    Retrieve information on a single user, referencing him by his username.

    -

    IMPORTANT: user IDs don't change, but usernames may change.

    -

    Usually, you'll want to run this method only once, in order to get the IDs - of referenced users. You should not store usernames in your database, it is - much safer to reference them by their IDs.

    -
    - Name of the user (case insensitive). - -

    Same as in the services/users/user method. Pipe-separated list - of field names which you are interested with. - See services/users/user method for a list available values.

    -
    - - -

    A dictionary of fields you have selected.

    -

    If given user does not exist, the method will respond with an HTTP 400 error.

    -
    + Find single user, by his/her username + 24 + +

    Retrieve information on a single user, referencing him by his username.

    +

    IMPORTANT: user IDs don't change, but usernames may change.

    +

    Usually, you'll want to run this method only once, in order to get the IDs + of referenced users. You should not store usernames in your database, it is + much safer to reference them by their IDs.

    +
    + Name of the user (case insensitive). + +

    Same as in the services/users/user method. Pipe-separated list + of field names which you are interested with. + See services/users/user method for a list available values.

    +
    + + +

    A dictionary of fields you have selected.

    +

    If given user does not exist, the method will respond with an HTTP 400 error.

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

    This method works like the services/users/by_username method, but works - with multiple users (instead of only one).

    -
    - -

    Pipe-separated list of usernames. No more than 500 are allowed.

    -
    - -

    Same as in the services/users/user method. Pipe-separated list - of field names which you are interested with. - See services/users/user method for a list available values.

    -
    - - -

    A dictionary. Usernames you provide will be mapped to dictionary keys, - and each value will be a dictionary of fields you have selected.

    -

    Value of null means that the given user haven't been found. - (This behavior is different than in the services/users/by_username method, which - responds with an HTTP 400 error in such case.)

    -
    + Find multiple users, by their usernames + 25 + +

    This method works like the services/users/by_username method, but works + with multiple users (instead of only one).

    +
    + +

    Pipe-separated list of usernames. No more than 500 are allowed.

    +
    + +

    Same as in the services/users/user method. Pipe-separated list + of field names which you are interested with. + See services/users/user method for a list available values.

    +
    + + +

    A dictionary. Usernames you provide will be mapped to dictionary keys, + and each value will be a dictionary of fields you have selected.

    +

    Value of null means that the given user haven't been found. + (This behavior is different than in the services/users/by_username method, which + responds with an HTTP 400 error in such case.)

    +
    \ No newline at end of file diff --git a/htdocs/okapi/services/users/user.php b/htdocs/okapi/services/users/user.php index 18d6e7be..21a75e9a 100644 --- a/htdocs/okapi/services/users/user.php +++ b/htdocs/okapi/services/users/user.php @@ -16,41 +16,41 @@ use okapi\services\caches\search\SearchAssistant; class WebService { - public static function options() - { - return array( - 'min_auth_level' => 1 - ); - } + public static function options() + { + return array( + 'min_auth_level' => 1 + ); + } - public static function call(OkapiRequest $request) - { - $user_uuid = $request->get_parameter('user_uuid'); - if (!$user_uuid) - { - if ($request->token) - { - $tmp = OkapiServiceRunner::call('services/users/by_internal_id', new OkapiInternalRequest( - $request->consumer, null, array('internal_id' => $request->token->user_id, 'fields' => 'uuid'))); - $user_uuid = $tmp['uuid']; - } - else - { - throw new BadRequest("You must either: 1. supply the user_uuid argument, or " - ."2. sign your request with an Access Token."); - } - } - $fields = $request->get_parameter('fields'); + public static function call(OkapiRequest $request) + { + $user_uuid = $request->get_parameter('user_uuid'); + if (!$user_uuid) + { + if ($request->token) + { + $tmp = OkapiServiceRunner::call('services/users/by_internal_id', new OkapiInternalRequest( + $request->consumer, null, array('internal_id' => $request->token->user_id, 'fields' => 'uuid'))); + $user_uuid = $tmp['uuid']; + } + else + { + throw new BadRequest("You must either: 1. supply the user_uuid argument, or " + ."2. sign your request with an Access Token."); + } + } + $fields = $request->get_parameter('fields'); - # There's no need to validate the fields parameter as the 'users' - # method does this (it will raise a proper exception on invalid values). + # There's no need to validate the fields parameter as the 'users' + # method does this (it will raise a proper exception on invalid values). - $results = OkapiServiceRunner::call('services/users/users', new OkapiInternalRequest( - $request->consumer, $request->token, array('user_uuids' => $user_uuid, - 'fields' => $fields))); - $result = $results[$user_uuid]; - if ($result == null) - throw new InvalidParam('user_uuid', "There is no user by this ID."); - return Okapi::formatted_response($request, $result); - } + $results = OkapiServiceRunner::call('services/users/users', new OkapiInternalRequest( + $request->consumer, $request->token, array('user_uuids' => $user_uuid, + 'fields' => $fields))); + $result = $results[$user_uuid]; + if ($result == null) + throw new InvalidParam('user_uuid', "There is no user by this ID."); + return Okapi::formatted_response($request, $result); + } } diff --git a/htdocs/okapi/services/users/user.xml b/htdocs/okapi/services/users/user.xml index 46ad6e14..57b67480 100644 --- a/htdocs/okapi/services/users/user.xml +++ b/htdocs/okapi/services/users/user.xml @@ -1,45 +1,45 @@ - Retrieve information on a single user - 26 - - Retrieve information on a single user. This method might be also used - to retrieve data of the user in who's name the Access Token have been - issued. To do this, include the Access Token in your request, and don't - include the user_uuid argument. - - -

    Pipe-separated list of field names which you are interested with. - Selected fields will be included in the response. See below for the - list of available fields.

    -
    - -

    ID of the user.

    -

    This parameter is optional only when you sign your - request with an Access Token (Level 3 Authentication). Otherwise, - it is required.

    -
    - - -

    A dictionary of fields you have selected. Currently available fields:

    - -
      -
    • uuid - ID of the user,
    • -
    • username - username (login) of the user,
    • -
    • profile_url - URL of the user's Opencaching profile page,
    • -
    • -

      is_admin - boolean; true is user has admin privileges.

      -

      This value can be accessed only with Level 3 Authentication - and only for the user of your Access Token. For all other reads, is_admin - will equal null.

      -
    • -
    • internal_id - internal ID of the user (DO NOT use this! - use the uuid as the user identifier),
    • -
    • caches_found - number of "Found it" and "Attended" log entries,
    • -
    • caches_notfound - number of "Didn't find it" log entries,
    • -
    • caches_hidden - number of caches owned,
    • -
    • rcmds_given - number of recommendations given.
    • -
    - -

    If given user does not exist, the method will respond with an HTTP 400 error.

    -
    + Retrieve information on a single user + 26 + + Retrieve information on a single user. This method might be also used + to retrieve data of the user in who's name the Access Token have been + issued. To do this, include the Access Token in your request, and don't + include the user_uuid argument. + + +

    Pipe-separated list of field names which you are interested with. + Selected fields will be included in the response. See below for the + list of available fields.

    +
    + +

    ID of the user.

    +

    This parameter is optional only when you sign your + request with an Access Token (Level 3 Authentication). Otherwise, + it is required.

    +
    + + +

    A dictionary of fields you have selected. Currently available fields:

    + +
      +
    • uuid - ID of the user,
    • +
    • username - username (login) of the user,
    • +
    • profile_url - URL of the user's Opencaching profile page,
    • +
    • +

      is_admin - boolean; true is user has admin privileges.

      +

      This value can be accessed only with Level 3 Authentication + and only for the user of your Access Token. For all other reads, is_admin + will equal null.

      +
    • +
    • internal_id - internal ID of the user (DO NOT use this! + use the uuid as the user identifier),
    • +
    • caches_found - number of "Found it" and "Attended" log entries,
    • +
    • caches_notfound - number of "Didn't find it" log entries,
    • +
    • caches_hidden - number of caches owned,
    • +
    • rcmds_given - number of recommendations given.
    • +
    + +

    If given user does not exist, the method will respond with an HTTP 400 error.

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

    This method works like the services/users/user method, but works - with multiple users (instead of only one).

    -
    - -

    Pipe-separated list of user IDs. No more than 500 IDs are allowed.

    -
    - -

    Same as in the services/users/user method. Pipe-separated list - of field names which you are interested with. - See services/users/user method for a list available values.

    -
    - - -

    A dictionary. User IDs you provide will be mapped to dictionary keys, - and each value will be a dictionary of fields you have selected.

    - -

    Value of null means that the given user haven't been found. - (This behavior is different than in the services/users/user method, which - responds with an HTTP 400 error in such case.)

    -
    + Retrieve information on multiple users + 27 + +

    This method works like the services/users/user method, but works + with multiple users (instead of only one).

    +
    + +

    Pipe-separated list of user IDs. No more than 500 IDs are allowed.

    +
    + +

    Same as in the services/users/user method. Pipe-separated list + of field names which you are interested with. + See services/users/user method for a list available values.

    +
    + + +

    A dictionary. User IDs you provide will be mapped to dictionary keys, + and each value will be a dictionary of fields you have selected.

    + +

    Value of null means that the given user haven't been found. + (This behavior is different than in the services/users/user method, which + responds with an HTTP 400 error in such case.)

    +
    \ No newline at end of file diff --git a/htdocs/okapi/settings.php b/htdocs/okapi/settings.php index 0713af33..f9a6473d 100644 --- a/htdocs/okapi/settings.php +++ b/htdocs/okapi/settings.php @@ -19,260 +19,268 @@ use okapi\Locales; final class Settings { - /** Default values for setting keys. */ - private static $DEFAULT_SETTINGS = array( + /** Default values for setting keys. */ + private static $DEFAULT_SETTINGS = array( - /** - * List of administrator email addresses. OKAPI will send important messages - * to this addresses. You should replace this with your true email address. - */ - 'ADMINS' => array(), + /** + * List of administrator email addresses. OKAPI will send important messages + * to this addresses. You should replace this with your true email address. + */ + 'ADMINS' => array(), - /** Set this to true on development machines. */ - 'DEBUG' => false, + /** Set this to true on development machines. */ + 'DEBUG' => false, - /** - * Currently there are two mainstream branches of Opencaching code. - * Which branch is you installation using? - * - * Possible values: "oc.pl" or "oc.de". (As far as we know, oc.us and - * oc.org.uk use "oc.pl" branch, the rest uses "oc.de" branch.) - */ - 'OC_BRANCH' => "oc.pl", + /** + * Currently there are two mainstream branches of Opencaching code. + * Which branch is you installation using? + * + * Possible values: "oc.pl" or "oc.de". (As far as we know, oc.us and + * oc.org.uk use "oc.pl" branch, the rest uses "oc.de" branch.) + */ + 'OC_BRANCH' => "oc.pl", - /** - * Each Opencaching site has a default language. I.e. the language in - * which all the names of caches are entered. What is the ISO 639-1 code - * of this language? Note: ISO 639-1 codes are always lowercase. - * - * E.g. "pl", "en", "de". - */ - 'SITELANG' => "en", + /** + * Each Opencaching site has a default language. I.e. the language in + * which all the names of caches are entered. What is the ISO 639-1 code + * of this language? Note: ISO 639-1 codes are always lowercase. + * + * E.g. "pl", "en", "de". + */ + 'SITELANG' => "en", - /** - * If set, it will be passed to date_default_timezone_set. OKAPI may - * refuse to start if this value is unset (more information here: - * http://code.google.com/p/opencaching-api/issues/detail?id=177) - * You should set it to the timezone used in your country (the one you'd - * use for your database inserts etc.). Choose one of the values listed - * here: http://www.php.net/manual/en/timezones.php - * - * E.g. "Europe/Berlin", "America/New_York". - */ - 'TIMEZONE' => null, + /** + * If set, it will be passed to date_default_timezone_set. OKAPI may + * refuse to start if this value is unset (more information here: + * http://code.google.com/p/opencaching-api/issues/detail?id=177) + * You should set it to the timezone used in your country (the one you'd + * use for your database inserts etc.). Choose one of the values listed + * here: http://www.php.net/manual/en/timezones.php + * + * E.g. "Europe/Berlin", "America/New_York". + */ + 'TIMEZONE' => null, - /** Email address to use in the "From:" when sending messages. */ - 'FROM_FIELD' => 'root@localhost', + /** Email address to use in the "From:" when sending messages. */ + 'FROM_FIELD' => 'root@localhost', - /** - * All OKAPI documentation pages should remain English-only, but some - * other pages (and results) might be translated to their localized - * versions. We try to catch up to all OKAPI instances and - * fill our default translation tables with all the languages of all - * OKAPI installations. But we also give you an option to use your own - * translation table if you really need to. Use this variable to pass your - * own gettext initialization function/method. See default_gettext_init - * function below for details. - */ - 'GETTEXT_INIT' => array('\okapi\Settings', 'default_gettext_init'), + /** + * All OKAPI documentation pages should remain English-only, but some + * other pages (and results) might be translated to their localized + * versions. We try to catch up to all OKAPI instances and + * fill our default translation tables with all the languages of all + * OKAPI installations. But we also give you an option to use your own + * translation table if you really need to. Use this variable to pass your + * own gettext initialization function/method. See default_gettext_init + * function below for details. + */ + 'GETTEXT_INIT' => array('\okapi\Settings', 'default_gettext_init'), - /** - * By default, OKAPI uses "okapi_messages" domain file for translations. - * Use this variable when you want it to use your own domain. - */ - 'GETTEXT_DOMAIN' => 'okapi_messages', + /** + * By default, OKAPI uses "okapi_messages" domain file for translations. + * Use this variable when you want it to use your own domain. + */ + 'GETTEXT_DOMAIN' => 'okapi_messages', - /** - * Where should OKAPI store dynamically generated cache files? If you leave it at null, - * OKAPI will try to guess (not recommended). If you move this directory, it's better - * if you also move all the files which were inside. - */ - 'VAR_DIR' => null, + /** + * Where should OKAPI store dynamically generated cache files? If you leave it at null, + * OKAPI will try to guess (not recommended). If you move this directory, it's better + * if you also move all the files which were inside. + */ + 'VAR_DIR' => null, - /** - * Where to store uploaded images? This directory needs to be shared among - * both OKAPI and OC code (see $picdir in your settings.inc.php). - */ - 'IMAGES_DIR' => null, + /** + * Where to store uploaded images? This directory needs to be shared among + * both OKAPI and OC code (see $picdir in your settings.inc.php). + */ + 'IMAGES_DIR' => null, - /** - * Name of the cookie within which OC stores serialized session id, etc. - * OKAPI requires to access this in order to make sure which user is logged - * in. - */ - 'OC_COOKIE_NAME' => null, + /** + * Name of the cookie within which OC stores serialized session id, etc. + * OKAPI requires to access this in order to make sure which user is logged + * in. + */ + 'OC_COOKIE_NAME' => null, - /** - * Set to true, if your installation supports "Needs maintenance" log type (with - * log type id == 5). If your users are not allowed to submit "Needs maintenance" - * log entries, leave it at false. - */ - 'SUPPORTS_LOGTYPE_NEEDS_MAINTENANCE' => false, + /** + * Set to true, if your installation supports "Needs maintenance" log type (with + * log type id == 5). If your users are not allowed to submit "Needs maintenance" + * log entries, leave it at false. + */ + 'SUPPORTS_LOGTYPE_NEEDS_MAINTENANCE' => false, - /** - * Set to true, to prevent OKAPI from sending email messages. ALLOWED ONLY ON - * DEVELOPMENT ENVIRONMENT! Sending emails is vital for OKAPI administration and - * usage! (I.e. users need this to receive their tokens upon registration.) - */ - 'DEBUG_PREVENT_EMAILS' => false, + /** + * Set to true, to prevent OKAPI from sending email messages. ALLOWED ONLY ON + * DEVELOPMENT ENVIRONMENT! Sending emails is vital for OKAPI administration and + * usage! (I.e. users need this to receive their tokens upon registration.) + */ + 'DEBUG_PREVENT_EMAILS' => false, - /** - * Set to true, to prevent OKAPI from using sem_get family of functions. - * ALLOWED ONLY ON DEVELOPMENT ENVIRONMENT! Semaphores are vital for OKAPI's - * performance and data integrity! - */ - 'DEBUG_PREVENT_SEMAPHORES' => false, + /** + * Set to true, to prevent OKAPI from using sem_get family of functions. + * ALLOWED ONLY ON DEVELOPMENT ENVIRONMENT! Semaphores are vital for OKAPI's + * performance and data integrity! + */ + 'DEBUG_PREVENT_SEMAPHORES' => false, - /* Database settings */ + /* Database settings */ - 'DB_SERVER' => 'localhost', - 'DB_NAME' => null, - 'DB_USERNAME' => null, - 'DB_PASSWORD' => null, + 'DB_SERVER' => 'localhost', + 'DB_NAME' => null, + 'DB_USERNAME' => null, + 'DB_PASSWORD' => null, - /** - * URL of this site (with slash, and without "/okapi"). If this is a - * development installation, it should point to the local URL (e.g. - * "http://localhost/". - */ - 'SITE_URL' => null, + /** + * URL of this site (with slash, and without "/okapi"). If this is a + * development installation, it should point to the local URL (e.g. + * "http://localhost/". + */ + 'SITE_URL' => null, - /** - * URL of the official OC site. For production sites, it should equal - * SITE_URL (this is also the default). For development installations, - * it should point to the official OC site from which you got your - * database dump (e.g. "http://opencaching.pl/"). - */ - 'ORIGIN_URL' => null, + /** + * URL of the official OC site. For production sites, it should equal + * SITE_URL (this is also the default). For development installations, + * it should point to the official OC site from which you got your + * database dump (e.g. "http://opencaching.pl/"). + */ + 'ORIGIN_URL' => null, - /** OKAPI needs this when inserting new data to cache_logs table. */ - 'OC_NODE_ID' => null, + /** OKAPI needs this when inserting new data to cache_logs table. */ + 'OC_NODE_ID' => null, - /** - * Your OC sites data licencing document. All OKAPI Consumers will be - * required to accept this. - */ - 'DATA_LICENSE_URL' => null, + /** + * Your OC sites data licencing document. All OKAPI Consumers will be + * required to accept this. + */ + 'DATA_LICENSE_URL' => null, - /** - * URL of the site's logo image. - * Minimum size is 64x64 pixels, maximum size 72x72 pixels. - */ - 'SITE_LOGO' => null, + /** + * URL of the site's logo image. + * Minimum size is 64x64 pixels, maximum size 72x72 pixels. + */ + 'SITE_LOGO' => null, - /** - * Settings for the OCDE HTML purifier which is used by services/logs/submit. - */ - 'OCDE_HTML_PURIFIER_SETTINGS' => array(), - ); + /** + * Settings for the OCDE HTML purifier which is used by services/logs/submit. + */ + 'OCDE_HTML_PURIFIER_SETTINGS' => array(), - /** - * Final values for settings keys (defaults + local overrides). - * (Loaded upon first access.) - */ - private static $SETTINGS = null; + /** + * This is a BETA feature, and it works on OCPL branch only (it + * requires some undocumented OCPL tables). This feature may be removed + * at any moment. OCPL admins temporarily use feature to track + * suspicious activity of some certain users. + */ + 'OCPL_ENABLE_GEOCACHE_ACCESS_LOGS' => false, + ); - /** - * Initialize self::$SETTINGS. - */ - private static function load_settings() - { - try { - # This is an external code and it MAY generate E_NOTICEs. - # We have to temporarilly disable our default error handler. + /** + * Final values for settings keys (defaults + local overrides). + * (Loaded upon first access.) + */ + private static $SETTINGS = null; - OkapiErrorHandler::disable(); - require_once($GLOBALS['rootpath']."okapi_settings.php"); - $ref = get_okapi_settings(); - OkapiErrorHandler::reenable(); + /** + * Initialize self::$SETTINGS. + */ + private static function load_settings() + { + try { + # This is an external code and it MAY generate E_NOTICEs. + # We have to temporarilly disable our default error handler. - } catch (Exception $e) { - throw new Exception("Could not import /okapi_settings.php:\n".$e->getMessage()); - } - self::$SETTINGS = self::$DEFAULT_SETTINGS; - foreach (self::$SETTINGS as $key => $_) - { - if (isset($ref[$key])) - { - self::$SETTINGS[$key] = $ref[$key]; - } - } - self::verify(self::$SETTINGS); - } + OkapiErrorHandler::disable(); + require_once($GLOBALS['rootpath']."okapi_settings.php"); + $ref = get_okapi_settings(); + OkapiErrorHandler::reenable(); - private static function verify(&$dict) - { - if (!in_array($dict['OC_BRANCH'], array('oc.pl', 'oc.de'))) - throw new Exception("Currently, OC_BRANCH has to be either 'oc.pl' or 'oc.de'. Hint: Whom did you get your code from?"); - $boolean_keys = array('SUPPORTS_LOGTYPE_NEEDS_MAINTENANCE', 'DEBUG', 'DEBUG_PREVENT_EMAILS', 'DEBUG_PREVENT_SEMAPHORES'); - foreach ($boolean_keys as $key) - if (!in_array($dict[$key], array(true, false))) - throw new Exception("Invalid value for $key."); - if (count($dict['ADMINS']) == 0) - throw new Exception("ADMINS array has to filled (e.g. array('root@localhost'))."); - if ($dict['DEBUG'] == false) - foreach ($dict as $k => $v) - if ((strpos($k, 'DEBUG_') === 0) && $v == true) - throw new Exception("When DEBUG is false, $k has to be false too."); - if ($dict['VAR_DIR'] == null) - throw new Exception("VAR_DIR cannot be null. Please provide a valid directory."); - if ($dict['IMAGES_DIR'] == null) - throw new Exception("IMAGES_DIR cannot be null. Please provide a valid directory."); - foreach ($dict as $k => $v) - if ((strpos($k, '_DIR') !== false) && ($k[strlen($k) - 1] == '/')) - throw new Exception("None of the *_DIR settings may end with a slash. Check $k."); - $notnull = array('OC_COOKIE_NAME', 'DB_SERVER', 'DB_NAME', 'DB_USERNAME', 'SITE_URL', 'OC_NODE_ID'); - foreach ($notnull as $k) - if ($dict[$k] === null) - throw new Exception("$k cannot be null."); - if ($dict['ORIGIN_URL'] === null) - $dict['ORIGIN_URL'] = $dict['SITE_URL']; - $slash_keys = array('SITE_URL', 'ORIGIN_URL'); - foreach ($slash_keys as $key) - if ($dict[$key][strlen($dict[$key]) - 1] != '/') - throw new Exception("$key must end with a slash."); - if ($dict['SITE_LOGO'] === null) - $dict['SITE_LOGO'] = $dict['SITE_URL'] . 'okapi/static/oc_logo.png'; - } + } catch (Exception $e) { + throw new Exception("Could not import /okapi_settings.php:\n".$e->getMessage()); + } + self::$SETTINGS = self::$DEFAULT_SETTINGS; + foreach (self::$SETTINGS as $key => $_) + { + if (isset($ref[$key])) + { + self::$SETTINGS[$key] = $ref[$key]; + } + } + self::verify(self::$SETTINGS); + } - /** - * Get the value for the $key setting. - */ - public static function get($key) - { - if (self::$SETTINGS == null) - self::load_settings(); + private static function verify(&$dict) + { + if (!in_array($dict['OC_BRANCH'], array('oc.pl', 'oc.de'))) + throw new Exception("Currently, OC_BRANCH has to be either 'oc.pl' or 'oc.de'. Hint: Whom did you get your code from?"); + $boolean_keys = array('SUPPORTS_LOGTYPE_NEEDS_MAINTENANCE', 'DEBUG', 'DEBUG_PREVENT_EMAILS', 'DEBUG_PREVENT_SEMAPHORES'); + foreach ($boolean_keys as $key) + if (!in_array($dict[$key], array(true, false))) + throw new Exception("Invalid value for $key."); + if (count($dict['ADMINS']) == 0) + throw new Exception("ADMINS array has to filled (e.g. array('root@localhost'))."); + if ($dict['DEBUG'] == false) + foreach ($dict as $k => $v) + if ((strpos($k, 'DEBUG_') === 0) && $v == true) + throw new Exception("When DEBUG is false, $k has to be false too."); + if ($dict['VAR_DIR'] == null) + throw new Exception("VAR_DIR cannot be null. Please provide a valid directory."); + if ($dict['IMAGES_DIR'] == null) + throw new Exception("IMAGES_DIR cannot be null. Please provide a valid directory."); + foreach ($dict as $k => $v) + if ((strpos($k, '_DIR') !== false) && ($k[strlen($k) - 1] == '/')) + throw new Exception("None of the *_DIR settings may end with a slash. Check $k."); + $notnull = array('OC_COOKIE_NAME', 'DB_SERVER', 'DB_NAME', 'DB_USERNAME', 'SITE_URL', 'OC_NODE_ID'); + foreach ($notnull as $k) + if ($dict[$k] === null) + throw new Exception("$k cannot be null."); + if ($dict['ORIGIN_URL'] === null) + $dict['ORIGIN_URL'] = $dict['SITE_URL']; + $slash_keys = array('SITE_URL', 'ORIGIN_URL'); + foreach ($slash_keys as $key) + if ($dict[$key][strlen($dict[$key]) - 1] != '/') + throw new Exception("$key must end with a slash."); + if ($dict['SITE_LOGO'] === null) + $dict['SITE_LOGO'] = $dict['SITE_URL'] . 'okapi/static/oc_logo.png'; + } - if (!array_key_exists($key, self::$SETTINGS)) - throw new Exception("Tried to access an invalid settings key: '$key'"); + /** + * Get the value for the $key setting. + */ + public static function get($key) + { + if (self::$SETTINGS == null) + self::load_settings(); - return self::$SETTINGS[$key]; - } + if (!array_key_exists($key, self::$SETTINGS)) + throw new Exception("Tried to access an invalid settings key: '$key'"); - /** - * Bind "okapi_messages" with our local i18n database. Set proper locale - * based on the language codes passed and return the locale code. - * $langprefs is a list of language codes in order of preference. - * - * Please note, that OKAPI consumers may ask OKAPI to return contents - * in a specified language. (For example, consumers from Germany may ask - * Polish OKAPI server to return GPX file in German.) If you insist on using - * your own translation tables, you should still fallback to the default - * OKAPI translations table in case of other languages! - */ - public static function default_gettext_init($langprefs) - { - require_once($GLOBALS['rootpath']."okapi/locale/locales.php"); - $locale = Locales::get_best_locale($langprefs); - putenv("LC_ALL=$locale"); - setlocale(LC_ALL, $locale); - setlocale(LC_NUMERIC, "POSIX"); # We don't want *this one* to get out of control. - bindtextdomain("okapi_messages", $GLOBALS['rootpath'].'okapi/locale'); - return $locale; - } + return self::$SETTINGS[$key]; + } - public static function describe_settings() - { - return print_r(self::$SETTINGS, true); - } + /** + * Bind "okapi_messages" with our local i18n database. Set proper locale + * based on the language codes passed and return the locale code. + * $langprefs is a list of language codes in order of preference. + * + * Please note, that OKAPI consumers may ask OKAPI to return contents + * in a specified language. (For example, consumers from Germany may ask + * Polish OKAPI server to return GPX file in German.) If you insist on using + * your own translation tables, you should still fallback to the default + * OKAPI translations table in case of other languages! + */ + public static function default_gettext_init($langprefs) + { + require_once($GLOBALS['rootpath']."okapi/locale/locales.php"); + $locale = Locales::get_best_locale($langprefs); + putenv("LC_ALL=$locale"); + setlocale(LC_ALL, $locale); + setlocale(LC_NUMERIC, "POSIX"); # We don't want *this one* to get out of control. + bindtextdomain("okapi_messages", $GLOBALS['rootpath'].'okapi/locale'); + return $locale; + } + + public static function describe_settings() + { + return Okapi::removeSensitiveData(print_r(self::$SETTINGS, true)); + } } \ No newline at end of file diff --git a/htdocs/okapi/static/common.js b/htdocs/okapi/static/common.js index ba38243a..44bc11c3 100644 --- a/htdocs/okapi/static/common.js +++ b/htdocs/okapi/static/common.js @@ -1,35 +1,35 @@ $(function() { - $('.issue-comments').each(function() { - var div = $(this); - var issue_id = div.attr('issue_id'); - $.ajax({ - type: 'GET', - dataType: 'json', - url: okapi_base_url + 'services/apiref/issue', - data: { - 'issue_id': issue_id - }, - success: function(issue) - { - var comments, link; - if (issue.comment_count === null) { - link = $("Comments"); - } else { - comments = (issue.comment_count == 1) ? "comment" : "comments"; - link = $("" + issue.comment_count + " " + comments + ""); - } - link.attr('href', issue.url); - div.append(link); - var notice = $("Notice: comments are shared between all OKAPI installations."); - div.append(notice); - } - }); - }); - $('#switcher').change(function() { - var current_base_url = $('#switcher option[current]').attr('value'); - var new_base_url = $('#switcher option:selected').attr('value'); - if (current_base_url != new_base_url) - window.location.href = window.location.href.replace(current_base_url, new_base_url); - }); - $('#switcher option[current]').attr('selected', true); + $('.issue-comments').each(function() { + var div = $(this); + var issue_id = div.attr('issue_id'); + $.ajax({ + type: 'GET', + dataType: 'json', + url: okapi_base_url + 'services/apiref/issue', + data: { + 'issue_id': issue_id + }, + success: function(issue) + { + var comments, link; + if (issue.comment_count === null) { + link = $("Comments"); + } else { + comments = (issue.comment_count == 1) ? "comment" : "comments"; + link = $("" + issue.comment_count + " " + comments + ""); + } + link.attr('href', issue.url); + div.append(link); + var notice = $("Notice: comments are shared between all OKAPI installations."); + div.append(notice); + } + }); + }); + $('#switcher').change(function() { + var current_base_url = $('#switcher option[current]').attr('value'); + var new_base_url = $('#switcher option:selected').attr('value'); + if (current_base_url != new_base_url) + window.location.href = window.location.href.replace(current_base_url, new_base_url); + }); + $('#switcher option[current]').attr('selected', true); }); \ No newline at end of file diff --git a/htdocs/okapi/static/examples/javascript_nearest.html b/htdocs/okapi/static/examples/javascript_nearest.html index dc68bff7..c5ecba66 100644 --- a/htdocs/okapi/static/examples/javascript_nearest.html +++ b/htdocs/okapi/static/examples/javascript_nearest.html @@ -1,83 +1,83 @@ - - - OKAPI JavaScript Example - - + - - - -

    Select OKAPI installation:

    -

    Enter your Consumer Key:

    -

    Click here to view the nearest caches

    -
    - + // Fill the OKAPI installations select-box with dynamic installation list. + + $.ajax({ + url: 'http://opencaching.pl/okapi/services/apisrv/installations', + dataType: 'json', + success: function(installations) { + for (var i in installations) { + var inst = installations[i]; + $('#installations').append( + $("") + .attr('value', inst.okapi_base_url) + .text(inst.okapi_base_url) + ); + } + } + }); + + $('#get_nearest').click(function() { + $('#get_nearest').hide(); + $('#results').text("Attempting to access your location..."); + if (navigator.geolocation) + { + navigator.geolocation.getCurrentPosition( + function(position) { + $('#results').text("Location acquired! Search for geocaches..."); + $.ajax({ + url: $('#installations option:selected').attr('value') + 'services/caches/shortcuts/search_and_retrieve', + dataType: 'json', + data: { + 'search_method': 'services/caches/search/nearest', + 'search_params': '{"center": "' + position.coords.latitude + "|" + + position.coords.longitude + '", "limit": 5}', + 'retr_method': 'services/caches/geocaches', + 'retr_params': '{"fields": "code|name|url"}', + 'wrap': 'false', + 'consumer_key': $('#consumer_key').attr('value') + }, + success: function(response) { + var ul = $("
      "); + for (var cache_code in response) { + var cache = response[cache_code]; + var li = $("
    • "); + li.find('a').attr('href', cache.url).text(cache.name); + ul.append(li); + } + $('#results').html("

      Nearest geocaches:

      "); + $('#results').append(ul); + }, + error: function() { + $('#results').text("Error :( Have you entered a valid Consumer Key?"); + } + }); + }, + function (error) { + $('#results').text("Your browser refused to give me your location."); + } + ); + } else { + $('#results').text("Your browser does not support geolocation."); + } + }); + }); + + + + +

      Select OKAPI installation:

      +

      Enter your Consumer Key:

      +

      Click here to view the nearest caches

      +
      + diff --git a/htdocs/okapi/urls.php b/htdocs/okapi/urls.php index 8eac542f..761b76f0 100644 --- a/htdocs/okapi/urls.php +++ b/htdocs/okapi/urls.php @@ -9,28 +9,28 @@ namespace okapi; class OkapiUrls { - public static $mapping = array( - '^(services/.*)\.html$' => 'method_doc', - '^(services/.*)$' => 'method_call', - '^introduction\.html$' => 'introduction', - '^signup\.html$' => 'signup', - '^examples\.html$' => 'examples', - '^$' => 'index', - '^apps/$' => 'apps/index', - '^apps/authorize$' => 'apps/authorize', - '^apps/authorized$' => 'apps/authorized', - '^apps/revoke_access$' => 'apps/revoke_access', - '^update/?$' => 'update', - '^cron5$' => 'cron5', - '^devel/attrlist$' => 'devel/attrlist', - '^devel/dbstruct$' => 'devel/dbstruct', - '^devel/cronreport$' => 'devel/cronreport', - '^devel/tilereport$' => 'devel/tilereport', - '^devel/clogentry$' => 'devel/clogentry', + public static $mapping = array( + '^(services/.*)\.html$' => 'method_doc', + '^(services/.*)$' => 'method_call', + '^introduction\.html$' => 'introduction', + '^signup\.html$' => 'signup', + '^examples\.html$' => 'examples', + '^$' => 'index', + '^apps/$' => 'apps/index', + '^apps/authorize$' => 'apps/authorize', + '^apps/authorized$' => 'apps/authorized', + '^apps/revoke_access$' => 'apps/revoke_access', + '^update/?$' => 'update', + '^cron5$' => 'cron5', + '^devel/attrlist$' => 'devel/attrlist', + '^devel/dbstruct$' => 'devel/dbstruct', + '^devel/cronreport$' => 'devel/cronreport', + '^devel/tilereport$' => 'devel/tilereport', + '^devel/clogentry$' => 'devel/clogentry', - # For debugging TileMap performance only. - // '^tilestress$' => 'tilestress', - ); + # For debugging TileMap performance only. + // '^tilestress$' => 'tilestress', + ); } # This line is used for commit-hooks testing: ..........> diff --git a/htdocs/okapi/views/apps/authorize.php b/htdocs/okapi/views/apps/authorize.php index 2aa587af..86706aad 100644 --- a/htdocs/okapi/views/apps/authorize.php +++ b/htdocs/okapi/views/apps/authorize.php @@ -14,176 +14,176 @@ use okapi\OCSession; class View { - public static function call() - { - $token_key = isset($_GET['oauth_token']) ? $_GET['oauth_token'] : ''; - $langpref = isset($_GET['langpref']) ? $_GET['langpref'] : Settings::get('SITELANG'); - $langprefs = explode("|", $langpref); - $locales = array(); - foreach (Locales::$languages as $lang => $attrs) - $locales[$attrs['locale']] = $attrs; + public static function call() + { + $token_key = isset($_GET['oauth_token']) ? $_GET['oauth_token'] : ''; + $langpref = isset($_GET['langpref']) ? $_GET['langpref'] : Settings::get('SITELANG'); + $langprefs = explode("|", $langpref); + $locales = array(); + foreach (Locales::$languages as $lang => $attrs) + $locales[$attrs['locale']] = $attrs; - # Current implementation of the "interactivity" parameter is: If developer - # wants to "confirm_user", then just log out the current user before we - # continue. + # Current implementation of the "interactivity" parameter is: If developer + # wants to "confirm_user", then just log out the current user before we + # continue. - $force_relogin = (isset($_GET['interactivity']) && $_GET['interactivity'] == 'confirm_user'); + $force_relogin = (isset($_GET['interactivity']) && $_GET['interactivity'] == 'confirm_user'); - $token = Db::select_row(" - select - t.`key` as `key`, - c.`key` as consumer_key, - c.name as consumer_name, - c.url as consumer_url, - t.callback, - t.verifier - from - okapi_consumers c, - okapi_tokens t - where - t.`key` = '".mysql_real_escape_string($token_key)."' - and t.consumer_key = c.`key` - and t.user_id is null - "); + $token = Db::select_row(" + select + t.`key` as `key`, + c.`key` as consumer_key, + c.name as consumer_name, + c.url as consumer_url, + t.callback, + t.verifier + from + okapi_consumers c, + okapi_tokens t + where + t.`key` = '".mysql_real_escape_string($token_key)."' + and t.consumer_key = c.`key` + and t.user_id is null + "); - $callback_concat_char = (strpos($token['callback'], '?') === false) ? "?" : "&"; + $callback_concat_char = (strpos($token['callback'], '?') === false) ? "?" : "&"; - if (!$token) - { - # Probably Request Token has expired. This will be usually viewed - # by the user, who knows nothing on tokens and OAuth. Let's be nice then! + if (!$token) + { + # Probably Request Token has expired. This will be usually viewed + # by the user, who knows nothing on tokens and OAuth. Let's be nice then! - $vars = array( - 'okapi_base_url' => Settings::get('SITE_URL')."okapi/", - 'token' => $token, - 'token_expired' => true, - 'site_name' => Okapi::get_normalized_site_name(), - 'site_url' => Settings::get('SITE_URL'), - 'site_logo' => Settings::get('SITE_LOGO'), - 'locales' => $locales, - ); - $response = new OkapiHttpResponse(); - $response->content_type = "text/html; charset=utf-8"; - ob_start(); - $vars['locale_displayed'] = Okapi::gettext_domain_init($langprefs); - include 'authorize.tpl.php'; - $response->body = ob_get_clean(); - Okapi::gettext_domain_restore(); - return $response; - } + $vars = array( + 'okapi_base_url' => Settings::get('SITE_URL')."okapi/", + 'token' => $token, + 'token_expired' => true, + 'site_name' => Okapi::get_normalized_site_name(), + 'site_url' => Settings::get('SITE_URL'), + 'site_logo' => Settings::get('SITE_LOGO'), + 'locales' => $locales, + ); + $response = new OkapiHttpResponse(); + $response->content_type = "text/html; charset=utf-8"; + ob_start(); + $vars['locale_displayed'] = Okapi::gettext_domain_init($langprefs); + include 'authorize.tpl.php'; + $response->body = ob_get_clean(); + Okapi::gettext_domain_restore(); + return $response; + } - # Determine which user is logged in to OC. + # Determine which user is logged in to OC. - require_once($GLOBALS['rootpath']."okapi/lib/oc_session.php"); - $OC_user_id = OCSession::get_user_id(); + require_once($GLOBALS['rootpath']."okapi/lib/oc_session.php"); + $OC_user_id = OCSession::get_user_id(); - # Ensure a user is logged in (or force re-login). + # Ensure a user is logged in (or force re-login). - if ($force_relogin || ($OC_user_id == null)) - { - if ($force_relogin) - { - # OC uses REAL MAGIC for session handling. I don't get ANY of it. - # The logout.php DOES NOT support the "target" parameter, so we - # can't just call it. The only thing that comes to mind is... - # Destroy EVERYTHING. + if ($force_relogin || ($OC_user_id == null)) + { + if ($force_relogin) + { + # OC uses REAL MAGIC for session handling. I don't get ANY of it. + # The logout.php DOES NOT support the "target" parameter, so we + # can't just call it. The only thing that comes to mind is... + # Destroy EVERYTHING. - $past = time() - 86400; - foreach ($_COOKIE as $key => $value) - setcookie($key, $value, $past, '/'); - } + $past = time() - 86400; + foreach ($_COOKIE as $key => $value) + setcookie($key, $value, $past, '/'); + } - # We should be logged out now. Let's login again. + # We should be logged out now. Let's login again. - $after_login = "okapi/apps/authorize?oauth_token=$token_key".(($langpref != Settings::get('SITELANG'))?"&langpref=".$langpref:""); - $login_url = Settings::get('SITE_URL')."login.php?target=".urlencode($after_login) - ."&langpref=".$langpref; - return new OkapiRedirectResponse($login_url); - } + $after_login = "okapi/apps/authorize?oauth_token=$token_key".(($langpref != Settings::get('SITELANG'))?"&langpref=".$langpref:""); + $login_url = Settings::get('SITE_URL')."login.php?target=".urlencode($after_login) + ."&langpref=".$langpref; + return new OkapiRedirectResponse($login_url); + } - # Check if this user has already authorized this Consumer. If he did, - # then we will automatically authorize all subsequent Request Tokens - # from this Consumer. + # Check if this user has already authorized this Consumer. If he did, + # then we will automatically authorize all subsequent Request Tokens + # from this Consumer. - $authorized = Db::select_value(" - select 1 - from okapi_authorizations - where - user_id = '".mysql_real_escape_string($OC_user_id)."' - and consumer_key = '".mysql_real_escape_string($token['consumer_key'])."' - ", 0); + $authorized = Db::select_value(" + select 1 + from okapi_authorizations + where + user_id = '".mysql_real_escape_string($OC_user_id)."' + and consumer_key = '".mysql_real_escape_string($token['consumer_key'])."' + ", 0); - if (!$authorized) - { - if (isset($_POST['authorization_result'])) - { - # Not yet authorized, but user have just submitted the authorization form. - # WRTODO: CSRF protection + if (!$authorized) + { + if (isset($_POST['authorization_result'])) + { + # Not yet authorized, but user have just submitted the authorization form. + # WRTODO: CSRF protection - if ($_POST['authorization_result'] == 'granted') - { - Db::execute(" - insert ignore into okapi_authorizations (consumer_key, user_id) - values ( - '".mysql_real_escape_string($token['consumer_key'])."', - '".mysql_real_escape_string($OC_user_id)."' - ); - "); - $authorized = true; - } - else - { - # User denied access. Nothing sensible to do now. Will try to report - # back to the Consumer application with an error. + if ($_POST['authorization_result'] == 'granted') + { + Db::execute(" + insert ignore into okapi_authorizations (consumer_key, user_id) + values ( + '".mysql_real_escape_string($token['consumer_key'])."', + '".mysql_real_escape_string($OC_user_id)."' + ); + "); + $authorized = true; + } + else + { + # User denied access. Nothing sensible to do now. Will try to report + # back to the Consumer application with an error. - if ($token['callback']) { - return new OkapiRedirectResponse($token['callback'].$callback_concat_char."error=access_denied"); - } else { - # Consumer did not provide a callback URL (oauth_callback=oob). - # We'll have to redirect to the Opencaching main page then... - return new OkapiRedirectResponse(Settings::get('SITE_URL')."index.php"); - } - } - } - else - { - # Not yet authorized. Display an authorization request. - $vars = array( - 'okapi_base_url' => Settings::get('SITE_URL')."okapi/", - 'token' => $token, - 'site_name' => Okapi::get_normalized_site_name(), - 'site_url' => Settings::get('SITE_URL'), - 'site_logo' => Settings::get('SITE_LOGO'), - 'locales' => $locales, - ); - $response = new OkapiHttpResponse(); - $response->content_type = "text/html; charset=utf-8"; - ob_start(); - $vars['locale_displayed'] = Okapi::gettext_domain_init($langprefs); - include 'authorize.tpl.php'; - $response->body = ob_get_clean(); - Okapi::gettext_domain_restore(); - return $response; - } - } + if ($token['callback']) { + return new OkapiRedirectResponse($token['callback'].$callback_concat_char."error=access_denied"); + } else { + # Consumer did not provide a callback URL (oauth_callback=oob). + # We'll have to redirect to the Opencaching main page then... + return new OkapiRedirectResponse(Settings::get('SITE_URL')."index.php"); + } + } + } + else + { + # Not yet authorized. Display an authorization request. + $vars = array( + 'okapi_base_url' => Settings::get('SITE_URL')."okapi/", + 'token' => $token, + 'site_name' => Okapi::get_normalized_site_name(), + 'site_url' => Settings::get('SITE_URL'), + 'site_logo' => Settings::get('SITE_LOGO'), + 'locales' => $locales, + ); + $response = new OkapiHttpResponse(); + $response->content_type = "text/html; charset=utf-8"; + ob_start(); + $vars['locale_displayed'] = Okapi::gettext_domain_init($langprefs); + include 'authorize.tpl.php'; + $response->body = ob_get_clean(); + Okapi::gettext_domain_restore(); + return $response; + } + } - # User granted access. Now we can authorize the Request Token. + # User granted access. Now we can authorize the Request Token. - Db::execute(" - update okapi_tokens - set user_id = '".mysql_real_escape_string($OC_user_id)."' - where `key` = '".mysql_real_escape_string($token_key)."'; - "); + Db::execute(" + update okapi_tokens + set user_id = '".mysql_real_escape_string($OC_user_id)."' + where `key` = '".mysql_real_escape_string($token_key)."'; + "); - # Redirect to the callback_url. + # Redirect to the callback_url. - if ($token['callback']) { - return new OkapiRedirectResponse($token['callback'].$callback_concat_char."oauth_token=".$token_key."&oauth_verifier=".$token['verifier']); - } else { - # Consumer did not provide a callback URL (probably the user is using a desktop - # or mobile application). We'll just have to display the verifier to the user. - return new OkapiRedirectResponse(Settings::get('SITE_URL')."okapi/apps/authorized?oauth_token=".$token_key - ."&oauth_verifier=".$token['verifier']."&langpref=".$langpref); - } - } + if ($token['callback']) { + return new OkapiRedirectResponse($token['callback'].$callback_concat_char."oauth_token=".$token_key."&oauth_verifier=".$token['verifier']); + } else { + # Consumer did not provide a callback URL (probably the user is using a desktop + # or mobile application). We'll just have to display the verifier to the user. + return new OkapiRedirectResponse(Settings::get('SITE_URL')."okapi/apps/authorized?oauth_token=".$token_key + ."&oauth_verifier=".$token['verifier']."&langpref=".$langpref); + } + } } diff --git a/htdocs/okapi/views/apps/authorize.tpl.php b/htdocs/okapi/views/apps/authorize.tpl.php index d3b7dca6..7d357874 100644 --- a/htdocs/okapi/views/apps/authorize.tpl.php +++ b/htdocs/okapi/views/apps/authorize.tpl.php @@ -1,68 +1,68 @@ - - - <?= _("Authorization Form") ?> - - - - - + + + <?= _("Authorization Form") ?> + + + + + -
      - '>static/logo-xsmall.gif' alt='OKAPI' style='float: right; margin-left: 10px;'> - '>Opencaching - ' class='opencaching'> +
      + '>static/logo-xsmall.gif' alt='OKAPI' style='float: right; margin-left: 10px;'> + '>Opencaching + ' class='opencaching'> -
      - Choose your language: - -
      +
      + Choose your language: + +
      - -

      -

      - -

      -

      %s wants to access your %s account. Do you agree to grant access to this application?"), htmlentities($vars['token']['consumer_name']), $vars['site_name']) ?>

      -
      - - " onclick="document.getElementById('authform_result').setAttribute('value', 'granted'); document.forms['authform'].submit();"> - " onclick="document.forms['authform'].submit();"> -
      - Once permission is granted it is valid until its withdrawal on - the applications management page.

      -

      The application will access your acount via the OKAPI Framework. - If you allow this request application will be able to access all methods delivered - by the OKAPI Framework, i.e. post log entries on geocaches in your name. - You can revoke this permission at any moment.

      - "), $vars['okapi_base_url']."apps/", $vars['okapi_base_url']) ?> - -
      + +

      +

      + +

      +

      %s wants to access your %s account. Do you agree to grant access to this application?"), htmlentities($vars['token']['consumer_name']), $vars['site_name']) ?>

      +
      + + " onclick="document.getElementById('authform_result').setAttribute('value', 'granted'); document.forms['authform'].submit();"> + " onclick="document.forms['authform'].submit();"> +
      + Once permission is granted it is valid until its withdrawal on + the applications management page.

      +

      The application will access your acount via the OKAPI Framework. + If you allow this request application will be able to access all methods delivered + by the OKAPI Framework, i.e. post log entries on geocaches in your name. + You can revoke this permission at any moment.

      + "), $vars['okapi_base_url']."apps/", $vars['okapi_base_url']) ?> + +
      - + \ No newline at end of file diff --git a/htdocs/okapi/views/apps/authorized.php b/htdocs/okapi/views/apps/authorized.php index 1fee7570..9133f021 100644 --- a/htdocs/okapi/views/apps/authorized.php +++ b/htdocs/okapi/views/apps/authorized.php @@ -12,49 +12,49 @@ use okapi\Settings; class View { - public static function call() - { - $token_key = isset($_GET['oauth_token']) ? $_GET['oauth_token'] : ''; - $verifier = isset($_GET['oauth_verifier']) ? $_GET['oauth_verifier'] : ''; - $langpref = isset($_GET['langpref']) ? $_GET['langpref'] : Settings::get('SITELANG'); - $langprefs = explode("|", $langpref); + public static function call() + { + $token_key = isset($_GET['oauth_token']) ? $_GET['oauth_token'] : ''; + $verifier = isset($_GET['oauth_verifier']) ? $_GET['oauth_verifier'] : ''; + $langpref = isset($_GET['langpref']) ? $_GET['langpref'] : Settings::get('SITELANG'); + $langprefs = explode("|", $langpref); - $token = Db::select_row(" - select - c.`key` as consumer_key, - c.name as consumer_name, - c.url as consumer_url, - t.verifier - from - okapi_consumers c, - okapi_tokens t - where - t.`key` = '".mysql_real_escape_string($token_key)."' - and t.consumer_key = c.`key` - "); + $token = Db::select_row(" + select + c.`key` as consumer_key, + c.name as consumer_name, + c.url as consumer_url, + t.verifier + from + okapi_consumers c, + okapi_tokens t + where + t.`key` = '".mysql_real_escape_string($token_key)."' + and t.consumer_key = c.`key` + "); - if (!$token) - { - # Probably Request Token has expired or it was already used. We'll - # just redirect to the Opencaching main page. - return new OkapiRedirectResponse(Settings::get('SITE_URL')); - } + if (!$token) + { + # Probably Request Token has expired or it was already used. We'll + # just redirect to the Opencaching main page. + return new OkapiRedirectResponse(Settings::get('SITE_URL')); + } - $vars = array( - 'okapi_base_url' => Settings::get('SITE_URL')."okapi/", - 'token' => $token, - 'verifier' => $verifier, - 'site_name' => Okapi::get_normalized_site_name(), - 'site_url' => Settings::get('SITE_URL'), - 'site_logo' => Settings::get('SITE_LOGO'), - ); - $response = new OkapiHttpResponse(); - $response->content_type = "text/html; charset=utf-8"; - ob_start(); - Okapi::gettext_domain_init($langprefs); - include 'authorized.tpl.php'; - $response->body = ob_get_clean(); - Okapi::gettext_domain_restore(); - return $response; - } + $vars = array( + 'okapi_base_url' => Settings::get('SITE_URL')."okapi/", + 'token' => $token, + 'verifier' => $verifier, + 'site_name' => Okapi::get_normalized_site_name(), + 'site_url' => Settings::get('SITE_URL'), + 'site_logo' => Settings::get('SITE_LOGO'), + ); + $response = new OkapiHttpResponse(); + $response->content_type = "text/html; charset=utf-8"; + ob_start(); + Okapi::gettext_domain_init($langprefs); + include 'authorized.tpl.php'; + $response->body = ob_get_clean(); + Okapi::gettext_domain_restore(); + return $response; + } } diff --git a/htdocs/okapi/views/apps/authorized.tpl.php b/htdocs/okapi/views/apps/authorized.tpl.php index 2aacdd3f..c12b798b 100644 --- a/htdocs/okapi/views/apps/authorized.tpl.php +++ b/htdocs/okapi/views/apps/authorized.tpl.php @@ -1,40 +1,40 @@ - - - <?= _("Authorization Succeeded") ?> - - - + + + <?= _("Authorization Succeeded") ?> + + + -
      - '>static/logo-xsmall.gif' alt='OKAPI' style='float: right; margin-left: 10px;'> - '>Opencaching - ' class='opencaching'> +
      + '>static/logo-xsmall.gif' alt='OKAPI' style='float: right; margin-left: 10px;'> + '>Opencaching + ' class='opencaching'> -

      - You've just granted %s application access to your %s account. - To complete the operation, go back to %s and enter the following PIN code:

      - "), $vars['token']['consumer_name'], $vars['site_name'], $vars['token']['consumer_name']) ?> +

      + You've just granted %s application access to your %s account. + To complete the operation, go back to %s and enter the following PIN code:

      + "), $vars['token']['consumer_name'], $vars['site_name'], $vars['token']['consumer_name']) ?> -
      +
      -

      -
      +

      +
      - + diff --git a/htdocs/okapi/views/apps/index.php b/htdocs/okapi/views/apps/index.php index d940a7fe..907bac2f 100644 --- a/htdocs/okapi/views/apps/index.php +++ b/htdocs/okapi/views/apps/index.php @@ -13,52 +13,52 @@ use okapi\OCSession; class View { - public static function call() - { - $langpref = isset($_GET['langpref']) ? $_GET['langpref'] : Settings::get('SITELANG'); - $langprefs = explode("|", $langpref); + public static function call() + { + $langpref = isset($_GET['langpref']) ? $_GET['langpref'] : Settings::get('SITELANG'); + $langprefs = explode("|", $langpref); - # Determine which user is logged in to OC. + # Determine which user is logged in to OC. - require_once($GLOBALS['rootpath']."okapi/lib/oc_session.php"); - $OC_user_id = OCSession::get_user_id(); + require_once($GLOBALS['rootpath']."okapi/lib/oc_session.php"); + $OC_user_id = OCSession::get_user_id(); - if ($OC_user_id == null) - { - $after_login = "okapi/apps/".(($langpref != Settings::get('SITELANG'))?"?langpref=".$langpref:""); - $login_url = Settings::get('SITE_URL')."login.php?target=".urlencode($after_login); - return new OkapiRedirectResponse($login_url); - } + if ($OC_user_id == null) + { + $after_login = "okapi/apps/".(($langpref != Settings::get('SITELANG'))?"?langpref=".$langpref:""); + $login_url = Settings::get('SITE_URL')."login.php?target=".urlencode($after_login); + return new OkapiRedirectResponse($login_url); + } - # Get the list of authorized apps. + # Get the list of authorized apps. - $rs = Db::query(" - select c.`key`, c.name, c.url - from - okapi_consumers c, - okapi_authorizations a - where - a.user_id = '".mysql_real_escape_string($OC_user_id)."' - and c.`key` = a.consumer_key - order by c.name - "); - $vars = array(); - $vars['okapi_base_url'] = Settings::get('SITE_URL')."okapi/"; - $vars['site_url'] = Settings::get('SITE_URL'); - $vars['site_name'] = Okapi::get_normalized_site_name(); - $vars['site_logo'] = Settings::get('SITE_LOGO'); - $vars['apps'] = array(); - while ($row = mysql_fetch_assoc($rs)) - $vars['apps'][] = $row; - mysql_free_result($rs); + $rs = Db::query(" + select c.`key`, c.name, c.url + from + okapi_consumers c, + okapi_authorizations a + where + a.user_id = '".mysql_real_escape_string($OC_user_id)."' + and c.`key` = a.consumer_key + order by c.name + "); + $vars = array(); + $vars['okapi_base_url'] = Settings::get('SITE_URL')."okapi/"; + $vars['site_url'] = Settings::get('SITE_URL'); + $vars['site_name'] = Okapi::get_normalized_site_name(); + $vars['site_logo'] = Settings::get('SITE_LOGO'); + $vars['apps'] = array(); + while ($row = mysql_fetch_assoc($rs)) + $vars['apps'][] = $row; + mysql_free_result($rs); - $response = new OkapiHttpResponse(); - $response->content_type = "text/html; charset=utf-8"; - ob_start(); - Okapi::gettext_domain_init($langprefs); - include 'index.tpl.php'; - $response->body = ob_get_clean(); - Okapi::gettext_domain_restore(); - return $response; - } + $response = new OkapiHttpResponse(); + $response->content_type = "text/html; charset=utf-8"; + ob_start(); + Okapi::gettext_domain_init($langprefs); + include 'index.tpl.php'; + $response->body = ob_get_clean(); + Okapi::gettext_domain_restore(); + return $response; + } } diff --git a/htdocs/okapi/views/apps/index.tpl.php b/htdocs/okapi/views/apps/index.tpl.php index 79325499..87f27672 100644 --- a/htdocs/okapi/views/apps/index.tpl.php +++ b/htdocs/okapi/views/apps/index.tpl.php @@ -1,59 +1,59 @@ - - - <?= _("My Apps") ?> - - - + + + <?= _("My Apps") ?> + + + -
      - '>static/logo-xsmall.gif' alt='OKAPI' style='float: right; margin-left: 10px;'> - '>Opencaching - ' class='opencaching'> +
      + '>static/logo-xsmall.gif' alt='OKAPI' style='float: right; margin-left: 10px;'> + '>Opencaching + ' class='opencaching'> -

      - 0) { ?> - This is the list of applications which you granted access to your %s account. - This page gives you the abbility to revoke all previously granted privileges. - Once you click \"remove\" the application will no longer be able to perform any - actions on your behalf.

      - "), $vars['site_name']) ?> - - - Thanks to the OKAPI Framework you can grant external applications - access to your %s account. Currently no applications are authorized to act - on your behalf. Once you start using external Opencaching applications, they will appear here.

      - "), $vars['okapi_base_url'], $vars['site_name']) ?> - -
      +

      + 0) { ?> + This is the list of applications which you granted access to your %s account. + This page gives you the abbility to revoke all previously granted privileges. + Once you click \"remove\" the application will no longer be able to perform any + actions on your behalf.

      + "), $vars['site_name']) ?> + + + Thanks to the OKAPI Framework you can grant external applications + access to your %s account. Currently no applications are authorized to act + on your behalf. Once you start using external Opencaching applications, they will appear here.

      + "), $vars['okapi_base_url'], $vars['site_name']) ?> + +
      - + \ No newline at end of file diff --git a/htdocs/okapi/views/apps/revoke_access.php b/htdocs/okapi/views/apps/revoke_access.php index 6140cb6b..3cbf06f1 100644 --- a/htdocs/okapi/views/apps/revoke_access.php +++ b/htdocs/okapi/views/apps/revoke_access.php @@ -13,41 +13,41 @@ use okapi\OCSession; class View { - public static function call() - { - # Determine which user is logged in to OC. + public static function call() + { + # Determine which user is logged in to OC. - require_once($GLOBALS['rootpath']."okapi/lib/oc_session.php"); - $OC_user_id = OCSession::get_user_id(); + require_once($GLOBALS['rootpath']."okapi/lib/oc_session.php"); + $OC_user_id = OCSession::get_user_id(); - # Ensure a user is logged in. + # Ensure a user is logged in. - if ($OC_user_id == null) - { - $after_login = "okapi/apps/"; # it is correct, if you're wondering - $login_url = Settings::get('SITE_URL')."login.php?target=".urlencode($after_login); - return new OkapiRedirectResponse($login_url); - } + if ($OC_user_id == null) + { + $after_login = "okapi/apps/"; # it is correct, if you're wondering + $login_url = Settings::get('SITE_URL')."login.php?target=".urlencode($after_login); + return new OkapiRedirectResponse($login_url); + } - $consumer_key = isset($_REQUEST['consumer_key']) ? $_REQUEST['consumer_key'] : ''; + $consumer_key = isset($_REQUEST['consumer_key']) ? $_REQUEST['consumer_key'] : ''; - # Just remove app (if it doesn't exist - nothing wrong will happen anyway). + # Just remove app (if it doesn't exist - nothing wrong will happen anyway). - Db::execute(" - delete from okapi_tokens - where - user_id = '".mysql_real_escape_string($OC_user_id)."' - and consumer_key = '".mysql_real_escape_string($consumer_key)."' - "); - Db::execute(" - delete from okapi_authorizations - where - user_id = '".mysql_real_escape_string($OC_user_id)."' - and consumer_key = '".mysql_real_escape_string($consumer_key)."' - "); + Db::execute(" + delete from okapi_tokens + where + user_id = '".mysql_real_escape_string($OC_user_id)."' + and consumer_key = '".mysql_real_escape_string($consumer_key)."' + "); + Db::execute(" + delete from okapi_authorizations + where + user_id = '".mysql_real_escape_string($OC_user_id)."' + and consumer_key = '".mysql_real_escape_string($consumer_key)."' + "); - # Redirect back to the apps page. + # Redirect back to the apps page. - return new OkapiRedirectResponse(Settings::get('SITE_URL')."okapi/apps/"); - } + return new OkapiRedirectResponse(Settings::get('SITE_URL')."okapi/apps/"); + } } diff --git a/htdocs/okapi/views/cron5.php b/htdocs/okapi/views/cron5.php index 787eeae9..949ace3e 100644 --- a/htdocs/okapi/views/cron5.php +++ b/htdocs/okapi/views/cron5.php @@ -21,18 +21,18 @@ use okapi\cronjobs\CronJobController; */ class View { - public static function call() - { - ignore_user_abort(true); - set_time_limit(0); - header("Content-Type: text/plain; charset=utf-8"); + public static function call() + { + ignore_user_abort(true); + set_time_limit(0); + header("Content-Type: text/plain; charset=utf-8"); - # Uncomment the following if you want to debug a specific cronjob. It will be run - # every 5 minutes (run 'crontab -e' to change or disable it) AND additionally - # every time you visit http://yoursite/okapi/cron5 + # Uncomment the following if you want to debug a specific cronjob. It will be run + # every 5 minutes (run 'crontab -e' to change or disable it) AND additionally + # every time you visit http://yoursite/okapi/cron5 - # require_once($GLOBALS['rootpath']."okapi/cronjobs.php"); CronJobController::force_run("JOB_NAME"); die(); + # require_once($GLOBALS['rootpath']."okapi/cronjobs.php"); CronJobController::force_run("JOB_NAME"); die(); - Okapi::execute_cron5_cronjobs(); - } + Okapi::execute_cron5_cronjobs(); + } } diff --git a/htdocs/okapi/views/devel/attrlist.php b/htdocs/okapi/views/devel/attrlist.php index dfbc7bd4..e705d780 100644 --- a/htdocs/okapi/views/devel/attrlist.php +++ b/htdocs/okapi/views/devel/attrlist.php @@ -18,191 +18,191 @@ use okapi\services\attrs\AttrHelper; class View { - public static function call() - { - # This is a hidden page for OKAPI developers. It will list all - # attributes defined in this OC installation (and some other stuff). + public static function call() + { + # This is a hidden page for OKAPI developers. It will list all + # attributes defined in this OC installation (and some other stuff). - ob_start(); + ob_start(); - print "Cache Types:\n\n"; - foreach (self::get_all_cachetypes() as $id => $name) - print "$id: $name\n"; + print "Cache Types:\n\n"; + foreach (self::get_all_cachetypes() as $id => $name) + print "$id: $name\n"; - print "\nLog Types:\n\n"; - foreach (self::get_all_logtypes() as $id => $name) - print "$id: $name\n"; + print "\nLog Types:\n\n"; + foreach (self::get_all_logtypes() as $id => $name) + print "$id: $name\n"; - print "\nAttributes:\n\n"; - require_once($GLOBALS['rootpath'].'okapi/services/attrs/attr_helper.inc.php'); - $internal2acode = AttrHelper::get_internal_id_to_acode_mapping(); - $dict = self::get_all_atribute_names(); - foreach ($dict as $internal_id => $langs) - { - print $internal_id.": "; - $langkeys = array_keys($langs); - sort($langkeys); - if (in_array('en', $langkeys)) - print strtoupper($langs['en']); - else - print ">>>> ENGLISH NAME UNSET! <<<<"; - if (isset($internal2acode[$internal_id])) - print " - ".$internal2acode[$internal_id]; - else - print " - >>>> MISSING A-CODE MAPPING <<<<"; - print "\n"; - foreach ($langkeys as $langkey) - print " $langkey: ".$langs[$langkey]."\n"; - } + print "\nAttributes:\n\n"; + require_once($GLOBALS['rootpath'].'okapi/services/attrs/attr_helper.inc.php'); + $internal2acode = AttrHelper::get_internal_id_to_acode_mapping(); + $dict = self::get_all_atribute_names(); + foreach ($dict as $internal_id => $langs) + { + print $internal_id.": "; + $langkeys = array_keys($langs); + sort($langkeys); + if (in_array('en', $langkeys)) + print strtoupper($langs['en']); + else + print ">>>> ENGLISH NAME UNSET! <<<<"; + if (isset($internal2acode[$internal_id])) + print " - ".$internal2acode[$internal_id]; + else + print " - >>>> MISSING A-CODE MAPPING <<<<"; + print "\n"; + foreach ($langkeys as $langkey) + print " $langkey: ".$langs[$langkey]."\n"; + } - print "\nAttribute notices:\n\n"; - print "There are three priorities: (!), (-) and ( )\n"; - print "(the last one ( ) can be safely ignored)\n\n"; + print "\nAttribute notices:\n\n"; + print "There are three priorities: (!), (-) and ( )\n"; + print "(the last one ( ) can be safely ignored)\n\n"; - $attrdict = AttrHelper::get_attrdict(); - foreach ($dict as $internal_id => $langs) - { - if (!isset($internal2acode[$internal_id])) - { - print "(!) Attribute ".$internal_id." is not mapped to any A-code.\n"; - continue; - } - $acode = $internal2acode[$internal_id]; - $attr = $attrdict[$acode]; - foreach ($langs as $lang => $value) - { - if ($lang == 'en') - { - continue; - } - if (!isset($attr['names'][$lang])) - { - print "(-) Attribute $acode is missing a name in the '$lang' language.\n"; - print " Local name: $value\n"; - print " OKAPI name: >> none <<\n"; - continue; - } - if ($attr['names'][$lang] !== $value) - { - print "( ) Attribute $acode has a different name in the '$lang' language\n"; - print " Local name: $value\n"; - print " OKAPI name: ".$attr['names'][$lang]."\n"; - } - } - } + $attrdict = AttrHelper::get_attrdict(); + foreach ($dict as $internal_id => $langs) + { + if (!isset($internal2acode[$internal_id])) + { + print "(!) Attribute ".$internal_id." is not mapped to any A-code.\n"; + continue; + } + $acode = $internal2acode[$internal_id]; + $attr = $attrdict[$acode]; + foreach ($langs as $lang => $value) + { + if ($lang == 'en') + { + continue; + } + if (!isset($attr['names'][$lang])) + { + print "(-) Attribute $acode is missing a name in the '$lang' language.\n"; + print " Local name: $value\n"; + print " OKAPI name: >> none <<\n"; + continue; + } + if ($attr['names'][$lang] !== $value) + { + print "( ) Attribute $acode has a different name in the '$lang' language\n"; + print " Local name: $value\n"; + print " OKAPI name: ".$attr['names'][$lang]."\n"; + } + } + } - $response = new OkapiHttpResponse(); - $response->content_type = "text/plain; charset=utf-8"; - $response->body = ob_get_clean(); - return $response; - } + $response = new OkapiHttpResponse(); + $response->content_type = "text/plain; charset=utf-8"; + $response->body = ob_get_clean(); + return $response; + } - /** - * Get an array of all site-specific attributes in the following format: - * $arr[][] = . - */ - private static function get_all_atribute_names() - { - if (Settings::get('OC_BRANCH') == 'oc.pl') - { - # OCPL branch uses cache_attrib table to store attribute names. It has - # different structure than the OCDE cache_attrib table. OCPL does not - # have translation tables. + /** + * Get an array of all site-specific attributes in the following format: + * $arr[][] = . + */ + private static function get_all_atribute_names() + { + if (Settings::get('OC_BRANCH') == 'oc.pl') + { + # OCPL branch uses cache_attrib table to store attribute names. It has + # different structure than the OCDE cache_attrib table. OCPL does not + # have translation tables. - $rs = Db::query("select id, language, text_long from cache_attrib order by id"); - } - else - { - # OCDE branch uses translation tables. Let's make a select which will - # produce results compatible with the one above. + $rs = Db::query("select id, language, text_long from cache_attrib order by id"); + } + else + { + # OCDE branch uses translation tables. Let's make a select which will + # produce results compatible with the one above. - $rs = Db::query(" - select - ca.id, - stt.lang as language, - stt.text as text_long - from - cache_attrib ca, - sys_trans_text stt - where ca.trans_id = stt.trans_id - order by ca.id - "); - } + $rs = Db::query(" + select + ca.id, + stt.lang as language, + stt.text as text_long + from + cache_attrib ca, + sys_trans_text stt + where ca.trans_id = stt.trans_id + order by ca.id + "); + } - $dict = array(); - while ($row = mysql_fetch_assoc($rs)) { - $dict[$row['id']][strtolower($row['language'])] = $row['text_long']; - } - return $dict; - } + $dict = array(); + while ($row = mysql_fetch_assoc($rs)) { + $dict[$row['id']][strtolower($row['language'])] = $row['text_long']; + } + return $dict; + } - /** - * Get an array of all site-specific cache-types (id => name in English). - */ - private static function get_all_cachetypes() - { - if (Settings::get('OC_BRANCH') == 'oc.pl') - { - # OCPL branch does not store cache types in many languages (just two). + /** + * Get an array of all site-specific cache-types (id => name in English). + */ + private static function get_all_cachetypes() + { + if (Settings::get('OC_BRANCH') == 'oc.pl') + { + # OCPL branch does not store cache types in many languages (just two). - $rs = Db::query("select id, en from cache_type order by id"); - } - else - { - # OCDE branch uses translation tables. + $rs = Db::query("select id, en from cache_type order by id"); + } + else + { + # OCDE branch uses translation tables. - $rs = Db::query(" - select - ct.id, - stt.text as en - from - cache_type ct - left join sys_trans_text stt - on ct.trans_id = stt.trans_id - and stt.lang = 'EN' - order by ct.id - "); - } + $rs = Db::query(" + select + ct.id, + stt.text as en + from + cache_type ct + left join sys_trans_text stt + on ct.trans_id = stt.trans_id + and stt.lang = 'EN' + order by ct.id + "); + } - $dict = array(); - while ($row = mysql_fetch_assoc($rs)) { - $dict[$row['id']] = $row['en']; - } - return $dict; - } + $dict = array(); + while ($row = mysql_fetch_assoc($rs)) { + $dict[$row['id']] = $row['en']; + } + return $dict; + } - /** - * Get an array of all site-specific log-types (id => name in English). - */ - private static function get_all_logtypes() - { - if (Settings::get('OC_BRANCH') == 'oc.pl') - { - # OCPL branch does not store cache types in many languages (just two). + /** + * Get an array of all site-specific log-types (id => name in English). + */ + private static function get_all_logtypes() + { + if (Settings::get('OC_BRANCH') == 'oc.pl') + { + # OCPL branch does not store cache types in many languages (just two). - $rs = Db::query("select id, en from log_types order by id"); - } - else - { - # OCDE branch uses translation tables. + $rs = Db::query("select id, en from log_types order by id"); + } + else + { + # OCDE branch uses translation tables. - $rs = Db::query(" - select - lt.id, - stt.text as en - from - log_types lt - left join sys_trans_text stt - on lt.trans_id = stt.trans_id - and stt.lang = 'EN' - order by lt.id - "); - } + $rs = Db::query(" + select + lt.id, + stt.text as en + from + log_types lt + left join sys_trans_text stt + on lt.trans_id = stt.trans_id + and stt.lang = 'EN' + order by lt.id + "); + } - $dict = array(); - while ($row = mysql_fetch_assoc($rs)) { - $dict[$row['id']] = $row['en']; - } - return $dict; - } + $dict = array(); + while ($row = mysql_fetch_assoc($rs)) { + $dict[$row['id']] = $row['en']; + } + return $dict; + } } diff --git a/htdocs/okapi/views/devel/clogentry.php b/htdocs/okapi/views/devel/clogentry.php index 52589438..cf9220b9 100644 --- a/htdocs/okapi/views/devel/clogentry.php +++ b/htdocs/okapi/views/devel/clogentry.php @@ -17,21 +17,21 @@ use okapi\Settings; class View { - public static function call() - { - if (!isset($_GET['id'])) { - throw new ParamMissing("id"); - } - $tmp = Db::select_value(" - select data - from okapi_clog - where id='".mysql_real_escape_string($_GET['id'])."' - "); - $data = unserialize(gzinflate($tmp)); + public static function call() + { + if (!isset($_GET['id'])) { + throw new ParamMissing("id"); + } + $tmp = Db::select_value(" + select data + from okapi_clog + where id='".mysql_real_escape_string($_GET['id'])."' + "); + $data = unserialize(gzinflate($tmp)); - $response = new OkapiHttpResponse(); - $response->content_type = "application/json; charset=utf-8"; - $response->body = json_encode($data); - return $response; - } + $response = new OkapiHttpResponse(); + $response->content_type = "application/json; charset=utf-8"; + $response->body = json_encode($data); + return $response; + } } diff --git a/htdocs/okapi/views/devel/comparator.inc.php b/htdocs/okapi/views/devel/comparator.inc.php index 2adba4c4..5ca69294 100644 --- a/htdocs/okapi/views/devel/comparator.inc.php +++ b/htdocs/okapi/views/devel/comparator.inc.php @@ -46,723 +46,723 @@ DONE: move all options to $this->config $res = $updater->getUpdates($struct1, $struct2); ----- $res == array ( - [0]=>"ALTER TABLE `b` MODIFY `name` varchar(255) NOT NULL", - ... + [0]=>"ALTER TABLE `b` MODIFY `name` varchar(255) NOT NULL", + ... ) */ class dbStructUpdater { - var $sourceStruct = '';//structure dump of the reference database - var $destStruct = '';//structure dump of database to update - var $config = array();//updater configuration + var $sourceStruct = '';//structure dump of the reference database + var $destStruct = '';//structure dump of database to update + var $config = array();//updater configuration - /** - * Constructor - * @access public - */ - function dbStructUpdater() - { - $this->init(); - } + /** + * Constructor + * @access public + */ + function dbStructUpdater() + { + $this->init(); + } - function init() - { - //table operations: create, drop; field operations: add, remove, modify - $this->config['updateTypes'] = 'create, drop, add, remove, modify'; - //ignores default part in cases like (var)char NOT NULL default '' upon the comparison - $this->config['varcharDefaultIgnore'] = true; - //the same for int NOT NULL default 0 - $this->config['intDefaultIgnore'] = true; - //ignores table autoincrement field value, also remove AUTO_INCREMENT value from the create query if exists - $this->config['ignoreIncrement'] = true; - //add 'IF NOT EXIST' to each CREATE TABLE query - $this->config['forceIfNotExists'] = true; - //remove 'IF NOT EXIST' if already exists CREATE TABLE dump - $this->config['ingoreIfNotExists'] = false; - } + function init() + { + //table operations: create, drop; field operations: add, remove, modify + $this->config['updateTypes'] = 'create, drop, add, remove, modify'; + //ignores default part in cases like (var)char NOT NULL default '' upon the comparison + $this->config['varcharDefaultIgnore'] = true; + //the same for int NOT NULL default 0 + $this->config['intDefaultIgnore'] = true; + //ignores table autoincrement field value, also remove AUTO_INCREMENT value from the create query if exists + $this->config['ignoreIncrement'] = true; + //add 'IF NOT EXIST' to each CREATE TABLE query + $this->config['forceIfNotExists'] = true; + //remove 'IF NOT EXIST' if already exists CREATE TABLE dump + $this->config['ingoreIfNotExists'] = false; + } - /** - * merges current updater config with the given one - * @param assoc_array $config new configuration values - */ - function setConfig($config=array()) - { - if (is_array($config)) - { - $this->config = array_merge($this->config, $config); - } - } + /** + * merges current updater config with the given one + * @param assoc_array $config new configuration values + */ + function setConfig($config=array()) + { + if (is_array($config)) + { + $this->config = array_merge($this->config, $config); + } + } - /** - * Returns array of update SQL with default options, $source, $dest - database structures - * @access public - * @param string $source structure dump of database to update - * @param string $dest structure dump of the reference database - * @param bool $asString if true - result will be a string, otherwise - array - * @return array|string update sql statements - in array or string (separated with ';') - */ - function getUpdates($source, $dest, $asString=false) - { - $result = $asString?'':array(); - $compRes = $this->compare($source, $dest); - if (empty($compRes)) - { - return $result; - } - $compRes = $this->filterDiffs($compRes); - if (empty($compRes)) - { - return $result; - } - $result = $this->getDiffSql($compRes); - if ($asString) - { - $result = implode(";\r\n", $result).';'; - } - return $result; - } + /** + * Returns array of update SQL with default options, $source, $dest - database structures + * @access public + * @param string $source structure dump of database to update + * @param string $dest structure dump of the reference database + * @param bool $asString if true - result will be a string, otherwise - array + * @return array|string update sql statements - in array or string (separated with ';') + */ + function getUpdates($source, $dest, $asString=false) + { + $result = $asString?'':array(); + $compRes = $this->compare($source, $dest); + if (empty($compRes)) + { + return $result; + } + $compRes = $this->filterDiffs($compRes); + if (empty($compRes)) + { + return $result; + } + $result = $this->getDiffSql($compRes); + if ($asString) + { + $result = implode(";\r\n", $result).';'; + } + return $result; + } - /** - * Filters comparison result and lefts only sync actions allowed by 'updateTypes' option - */ - function filterDiffs($compRes) - { - $result = array(); - if (is_array($this->config['updateTypes'])) - { - $updateActions = $this->config['updateTypes']; - } - else - { - $updateActions = array_map('trim', explode(',', $this->config['updateTypes'])); - } - $allowedActions = array('create', 'drop', 'add', 'remove', 'modify'); - $updateActions = array_intersect($updateActions, $allowedActions); - foreach($compRes as $table=>$info) - { - if ($info['sourceOrphan']) - { - if (in_array('create', $updateActions)) - { - $result[$table] = $info; - } - } - elseif ($info['destOrphan']) - { - if (in_array('drop', $updateActions)) - { - $result[$table] = $info; - } - } - elseif($info['differs']) - { - $resultInfo = $info; - unset($resultInfo['differs']); - foreach ($info['differs'] as $diff) - { - if (empty($diff['dest']) && in_array('add', $updateActions)) - { - $resultInfo['differs'][] = $diff; - } - elseif (empty($diff['source']) && in_array('remove', $updateActions)) - { - $resultInfo['differs'][] = $diff; - } - elseif(in_array('modify', $updateActions)) - { - $resultInfo['differs'][] = $diff; - } - } - if (!empty($resultInfo['differs'])) - { - $result[$table] = $resultInfo; - } - } - } - return $result; - } + /** + * Filters comparison result and lefts only sync actions allowed by 'updateTypes' option + */ + function filterDiffs($compRes) + { + $result = array(); + if (is_array($this->config['updateTypes'])) + { + $updateActions = $this->config['updateTypes']; + } + else + { + $updateActions = array_map('trim', explode(',', $this->config['updateTypes'])); + } + $allowedActions = array('create', 'drop', 'add', 'remove', 'modify'); + $updateActions = array_intersect($updateActions, $allowedActions); + foreach($compRes as $table=>$info) + { + if ($info['sourceOrphan']) + { + if (in_array('create', $updateActions)) + { + $result[$table] = $info; + } + } + elseif ($info['destOrphan']) + { + if (in_array('drop', $updateActions)) + { + $result[$table] = $info; + } + } + elseif($info['differs']) + { + $resultInfo = $info; + unset($resultInfo['differs']); + foreach ($info['differs'] as $diff) + { + if (empty($diff['dest']) && in_array('add', $updateActions)) + { + $resultInfo['differs'][] = $diff; + } + elseif (empty($diff['source']) && in_array('remove', $updateActions)) + { + $resultInfo['differs'][] = $diff; + } + elseif(in_array('modify', $updateActions)) + { + $resultInfo['differs'][] = $diff; + } + } + if (!empty($resultInfo['differs'])) + { + $result[$table] = $resultInfo; + } + } + } + return $result; + } - /** - * Gets structured general info about the databases diff : - * array(sourceOrphans=>array(...), destOrphans=>array(...), different=>array(...)) - */ - function getDiffInfo($compRes) - { - if (!is_array($compRes)) - { - return false; - } - $result = array('sourceOrphans'=>array(), 'destOrphans'=>array(), 'different'=>array()); - foreach($compRes as $table=>$info) - { - if ($info['sourceOrphan']) - { - $result['sourceOrphans'][] = $table; - } - elseif ($info['destOrphan']) - { - $result['destOrphans'][] = $table; - } - else - { - $result['different'][] = $table; - } - } - return $result; - } + /** + * Gets structured general info about the databases diff : + * array(sourceOrphans=>array(...), destOrphans=>array(...), different=>array(...)) + */ + function getDiffInfo($compRes) + { + if (!is_array($compRes)) + { + return false; + } + $result = array('sourceOrphans'=>array(), 'destOrphans'=>array(), 'different'=>array()); + foreach($compRes as $table=>$info) + { + if ($info['sourceOrphan']) + { + $result['sourceOrphans'][] = $table; + } + elseif ($info['destOrphan']) + { + $result['destOrphans'][] = $table; + } + else + { + $result['different'][] = $table; + } + } + return $result; + } - /** - * Makes comparison of the given database structures, support some options - * @access private - * @param string $source and $dest are strings - database tables structures - * @return array - * - table (array) - * - destOrphan (boolean) - * - sourceOrphan (boolean) - * - differs (array) OR (boolean) false if no diffs - * - [0](array) - * - source (string) structure definition line in the out-of-date table - * - dest (string) structure definition line in the reference table - * - [1](array) ... - */ - function compare($source, $dest) - { - $this->sourceStruct = $source; - $this->destStruct = $dest; + /** + * Makes comparison of the given database structures, support some options + * @access private + * @param string $source and $dest are strings - database tables structures + * @return array + * - table (array) + * - destOrphan (boolean) + * - sourceOrphan (boolean) + * - differs (array) OR (boolean) false if no diffs + * - [0](array) + * - source (string) structure definition line in the out-of-date table + * - dest (string) structure definition line in the reference table + * - [1](array) ... + */ + function compare($source, $dest) + { + $this->sourceStruct = $source; + $this->destStruct = $dest; - $result = array(); - $destTabNames = $this->getTableList($this->destStruct); - $sourceTabNames = $this->getTableList($this->sourceStruct); + $result = array(); + $destTabNames = $this->getTableList($this->destStruct); + $sourceTabNames = $this->getTableList($this->sourceStruct); - $common = array_intersect($destTabNames, $sourceTabNames); - $destOrphans = array_diff($destTabNames, $common); - $sourceOrphans = array_diff($sourceTabNames, $common); - $all = array_unique(array_merge($destTabNames, $sourceTabNames)); - sort($all); - foreach ($all as $tab) - { - $info = array('destOrphan'=>false, 'sourceOrphan'=>false, 'differs'=>false); - if(in_array($tab, $destOrphans)) - { - $info['destOrphan'] = true; - } - elseif (in_array($tab, $sourceOrphans)) - { - $info['sourceOrphan'] = true; - } - else - { - $destSql = $this->getTabSql($this->destStruct, $tab, true); - $sourceSql = $this->getTabSql($this->sourceStruct, $tab, true); - $diffs = $this->compareSql($sourceSql, $destSql); - if ($diffs===false) - { - trigger_error('[WARNING] error parsing definition of table "'.$tab.'" - skipped'); - continue; - } - elseif (!empty($diffs))//not empty array - { - $info['differs'] = $diffs; - } - else continue;//empty array - } - $result[$tab] = $info; - } - return $result; - } + $common = array_intersect($destTabNames, $sourceTabNames); + $destOrphans = array_diff($destTabNames, $common); + $sourceOrphans = array_diff($sourceTabNames, $common); + $all = array_unique(array_merge($destTabNames, $sourceTabNames)); + sort($all); + foreach ($all as $tab) + { + $info = array('destOrphan'=>false, 'sourceOrphan'=>false, 'differs'=>false); + if(in_array($tab, $destOrphans)) + { + $info['destOrphan'] = true; + } + elseif (in_array($tab, $sourceOrphans)) + { + $info['sourceOrphan'] = true; + } + else + { + $destSql = $this->getTabSql($this->destStruct, $tab, true); + $sourceSql = $this->getTabSql($this->sourceStruct, $tab, true); + $diffs = $this->compareSql($sourceSql, $destSql); + if ($diffs===false) + { + trigger_error('[WARNING] error parsing definition of table "'.$tab.'" - skipped'); + continue; + } + elseif (!empty($diffs))//not empty array + { + $info['differs'] = $diffs; + } + else continue;//empty array + } + $result[$tab] = $info; + } + return $result; + } - /** - * Retrieves list of table names from the database structure dump - * @access private - * @param string $struct database structure listing - */ - function getTableList($struct) - { - $result = array(); - if (preg_match_all('/CREATE(?:\s*TEMPORARY)?\s*TABLE\s*(?:IF NOT EXISTS\s*)?(?:`?(\w+)`?\.)?`?(\w+)`?/i', $struct, $m)) - { - foreach($m[2] as $match)//m[1] is a database name if any - { - $result[] = $match; - } - } - return $result; - } + /** + * Retrieves list of table names from the database structure dump + * @access private + * @param string $struct database structure listing + */ + function getTableList($struct) + { + $result = array(); + if (preg_match_all('/CREATE(?:\s*TEMPORARY)?\s*TABLE\s*(?:IF NOT EXISTS\s*)?(?:`?(\w+)`?\.)?`?(\w+)`?/i', $struct, $m)) + { + foreach($m[2] as $match)//m[1] is a database name if any + { + $result[] = $match; + } + } + return $result; + } - /** - * Retrieves table structure definition from the database structure dump - * @access private - * @param string $struct database structure listing - * @param string $tab table name - * @param bool $removeDatabase - either to remove database name in "CREATE TABLE database.tab"-like declarations - * @return string table structure definition - */ - function getTabSql($struct, $tab, $removeDatabase=true) - { - $result = ''; - /* create table should be single line in this case*/ - //1 - part before database, 2-database name, 3 - part after database - if (preg_match('/(CREATE(?:\s*TEMPORARY)?\s*TABLE\s*(?:IF NOT EXISTS\s*)?)(?:`?(\w+)`?\.)?(`?('.$tab.')`?(\W|$))/i', $struct, $m, PREG_OFFSET_CAPTURE)) - { - $tableDef = $m[0][0]; - $start = $m[0][1]; - $database = $m[2][0]; - $offset = $start+strlen($m[0][0]); - $end = $this->getDelimPos($struct, $offset); - if ($end === false) - { - $result = substr($struct, $start); - } - else - { - $result = substr($struct, $start, $end-$start);//already without ';' - } - } - $result = trim($result); - if ($database && $removeDatabase) - { - $result = str_replace($tableDef, $m[1][0].$m[3][0], $result); - } - return $result; - } + /** + * Retrieves table structure definition from the database structure dump + * @access private + * @param string $struct database structure listing + * @param string $tab table name + * @param bool $removeDatabase - either to remove database name in "CREATE TABLE database.tab"-like declarations + * @return string table structure definition + */ + function getTabSql($struct, $tab, $removeDatabase=true) + { + $result = ''; + /* create table should be single line in this case*/ + //1 - part before database, 2-database name, 3 - part after database + if (preg_match('/(CREATE(?:\s*TEMPORARY)?\s*TABLE\s*(?:IF NOT EXISTS\s*)?)(?:`?(\w+)`?\.)?(`?('.$tab.')`?(\W|$))/i', $struct, $m, PREG_OFFSET_CAPTURE)) + { + $tableDef = $m[0][0]; + $start = $m[0][1]; + $database = $m[2][0]; + $offset = $start+strlen($m[0][0]); + $end = $this->getDelimPos($struct, $offset); + if ($end === false) + { + $result = substr($struct, $start); + } + else + { + $result = substr($struct, $start, $end-$start);//already without ';' + } + } + $result = trim($result); + if ($database && $removeDatabase) + { + $result = str_replace($tableDef, $m[1][0].$m[3][0], $result); + } + return $result; + } - /** - * Splits table sql into indexed array - * - */ - function splitTabSql($sql) - { - $result = array(); - //find opening bracket, get the prefix along with it - $openBracketPos = $this->getDelimPos($sql, 0, '('); - if ($openBracketPos===false) - { - trigger_error('[WARNING] can not find opening bracket in table definition'); - return false; - } - $prefix = substr($sql, 0, $openBracketPos+1);//prefix can not be empty, so do not check it, just trim - $result[] = trim($prefix); - $body = substr($sql, strlen($prefix));//fields, indexes and part after closing bracket - //split by commas, get part by part - while(($commaPos = $this->getDelimPos($body, 0, ',', true))!==false) - { - $part = trim(substr($body, 0, $commaPos+1));//read another part and shorten $body - if ($part) - { - $result[] = $part; - } - $body = substr($body, $commaPos+1); - } - //here we have last field (or index) definition + part after closing bracket (ENGINE, ect) - $closeBracketPos = $this->getDelimRpos($body, 0, ')'); - if ($closeBracketPos===false) - { - trigger_error('[WARNING] can not find closing bracket in table definition'); - return false; - } - //get last field / index definition before closing bracket - $part = substr($body, 0, $closeBracketPos); - $result[] = trim($part); - //get the suffix part along with the closing bracket - $suffix = substr($body, $closeBracketPos); - $suffix = trim($suffix); - if ($suffix) - { - $result[] = $suffix; - } - return $result; - } + /** + * Splits table sql into indexed array + * + */ + function splitTabSql($sql) + { + $result = array(); + //find opening bracket, get the prefix along with it + $openBracketPos = $this->getDelimPos($sql, 0, '('); + if ($openBracketPos===false) + { + trigger_error('[WARNING] can not find opening bracket in table definition'); + return false; + } + $prefix = substr($sql, 0, $openBracketPos+1);//prefix can not be empty, so do not check it, just trim + $result[] = trim($prefix); + $body = substr($sql, strlen($prefix));//fields, indexes and part after closing bracket + //split by commas, get part by part + while(($commaPos = $this->getDelimPos($body, 0, ',', true))!==false) + { + $part = trim(substr($body, 0, $commaPos+1));//read another part and shorten $body + if ($part) + { + $result[] = $part; + } + $body = substr($body, $commaPos+1); + } + //here we have last field (or index) definition + part after closing bracket (ENGINE, ect) + $closeBracketPos = $this->getDelimRpos($body, 0, ')'); + if ($closeBracketPos===false) + { + trigger_error('[WARNING] can not find closing bracket in table definition'); + return false; + } + //get last field / index definition before closing bracket + $part = substr($body, 0, $closeBracketPos); + $result[] = trim($part); + //get the suffix part along with the closing bracket + $suffix = substr($body, $closeBracketPos); + $suffix = trim($suffix); + if ($suffix) + { + $result[] = $suffix; + } + return $result; + } - /** - * returns array of fields or keys definitions that differs in the given tables structure - * @access private - * @param sring $sourceSql table structure - * @param sring $destSql right table structure - * supports some $options - * @return array - * - [0] - * - source (string) out-of-date table field definition - * - dest (string) reference table field definition - * - [1]... - */ - function compareSql($sourceSql, $destSql)//$sourceSql, $destSql - { - $result = array(); - //split with comma delimiter, not line breaks - $sourceParts = $this->splitTabSql($sourceSql); - if ($sourceParts===false)//error parsing sql - { - trigger_error('[WARNING] error parsing source sql'); - return false; - } - $destParts = $this->splitTabSql($destSql); - if ($destParts===false) - { - trigger_error('[WARNING] error parsing destination sql'); - return false; - } - $sourcePartsIndexed = array(); - $destPartsIndexed = array(); - foreach($sourceParts as $line) - { - $lineInfo = $this->processLine($line); - if (!$lineInfo) continue; - $sourcePartsIndexed[$lineInfo['key']] = $lineInfo['line']; - } - foreach($destParts as $line) - { - $lineInfo = $this->processLine($line); - if (!$lineInfo) continue; - $destPartsIndexed[$lineInfo['key']] = $lineInfo['line']; - } - $sourceKeys = array_keys($sourcePartsIndexed); - $destKeys = array_keys($destPartsIndexed); - $all = array_unique(array_merge($sourceKeys, $destKeys)); - sort($all);//fields first, then indexes - because fields are prefixed with '!' + /** + * returns array of fields or keys definitions that differs in the given tables structure + * @access private + * @param sring $sourceSql table structure + * @param sring $destSql right table structure + * supports some $options + * @return array + * - [0] + * - source (string) out-of-date table field definition + * - dest (string) reference table field definition + * - [1]... + */ + function compareSql($sourceSql, $destSql)//$sourceSql, $destSql + { + $result = array(); + //split with comma delimiter, not line breaks + $sourceParts = $this->splitTabSql($sourceSql); + if ($sourceParts===false)//error parsing sql + { + trigger_error('[WARNING] error parsing source sql'); + return false; + } + $destParts = $this->splitTabSql($destSql); + if ($destParts===false) + { + trigger_error('[WARNING] error parsing destination sql'); + return false; + } + $sourcePartsIndexed = array(); + $destPartsIndexed = array(); + foreach($sourceParts as $line) + { + $lineInfo = $this->processLine($line); + if (!$lineInfo) continue; + $sourcePartsIndexed[$lineInfo['key']] = $lineInfo['line']; + } + foreach($destParts as $line) + { + $lineInfo = $this->processLine($line); + if (!$lineInfo) continue; + $destPartsIndexed[$lineInfo['key']] = $lineInfo['line']; + } + $sourceKeys = array_keys($sourcePartsIndexed); + $destKeys = array_keys($destPartsIndexed); + $all = array_unique(array_merge($sourceKeys, $destKeys)); + sort($all);//fields first, then indexes - because fields are prefixed with '!' - foreach ($all as $key) - { - $info = array('source'=>'', 'dest'=>''); - $inSource= in_array($key, $sourceKeys); - $inDest= in_array($key, $destKeys); - $sourceOrphan = $inSource && !$inDest; - $destOrphan = $inDest && !$inSource; - $different = $inSource && $inDest && - strcasecmp($this->normalizeString($destPartsIndexed[$key]), $this->normalizeString($sourcePartsIndexed[$key])); - if ($sourceOrphan) - { - $info['source'] = $sourcePartsIndexed[$key]; - } - elseif ($destOrphan) - { - $info['dest'] = $destPartsIndexed[$key]; - } - elseif ($different) - { - $info['source'] = $sourcePartsIndexed[$key]; - $info['dest'] = $destPartsIndexed[$key]; - } - else continue; - $result[] = $info; - } - return $result; - } + foreach ($all as $key) + { + $info = array('source'=>'', 'dest'=>''); + $inSource= in_array($key, $sourceKeys); + $inDest= in_array($key, $destKeys); + $sourceOrphan = $inSource && !$inDest; + $destOrphan = $inDest && !$inSource; + $different = $inSource && $inDest && + strcasecmp($this->normalizeString($destPartsIndexed[$key]), $this->normalizeString($sourcePartsIndexed[$key])); + if ($sourceOrphan) + { + $info['source'] = $sourcePartsIndexed[$key]; + } + elseif ($destOrphan) + { + $info['dest'] = $destPartsIndexed[$key]; + } + elseif ($different) + { + $info['source'] = $sourcePartsIndexed[$key]; + $info['dest'] = $destPartsIndexed[$key]; + } + else continue; + $result[] = $info; + } + return $result; + } - /** - * Transforms table structure defnition line into key=>value pair where the key is a string that uniquely - * defines field or key desribed - * @access private - * @param string $line field definition string - * @return array array with single key=>value pair as described in the description - * implements some options - */ - function processLine($line) - { - $options = $this->config; - $result = array('key'=>'', 'line'=>''); - $line = rtrim(trim($line), ','); - if (preg_match('/^(CREATE\s+TABLE)|(\) ENGINE=)/i', $line))//first or last table definition line - { - return false; - } - //if (preg_match('/^(PRIMARY KEY)|(((UNIQUE )|(FULLTEXT ))?KEY `?\w+`?)/i', $line, $m))//key definition - if (preg_match('/^(PRIMARY\s+KEY)|(((UNIQUE\s+)|(FULLTEXT\s+))?KEY\s+`?\w+`?)/i', $line, $m))//key definition - { - $key = $m[0]; - } - elseif (preg_match('/^`?\w+`?/i', $line, $m))//field definition - { - $key = '!'.$m[0];//to make sure fields will be synchronised before the keys - } - else - { - return false;//line has no valuable info (empty or comment) - } - //$key = str_replace('`', '', $key); - if (!empty($options['varcharDefaultIgnore'])) - { - $line = preg_replace("/(var)?char\(([0-9]+)\)\s+NOT\s+NULL\s+default\s+''/i", '$1char($2) NOT NULL', $line); - } - if (!empty($options['intDefaultIgnore'])) - { - $line = preg_replace("/((?:big)|(?:tiny))?int\(([0-9]+)\)\s+NOT\s+NULL\s+default\s+'0'/i", '$1int($2) NOT NULL', $line); - } - if (!empty($options['ignoreIncrement'])) - { - $line = preg_replace("/ AUTO_INCREMENT=[0-9]+/i", '', $line); - } - $result['key'] = $this->normalizeString($key); - $result['line']= $line; - return $result; - } + /** + * Transforms table structure defnition line into key=>value pair where the key is a string that uniquely + * defines field or key desribed + * @access private + * @param string $line field definition string + * @return array array with single key=>value pair as described in the description + * implements some options + */ + function processLine($line) + { + $options = $this->config; + $result = array('key'=>'', 'line'=>''); + $line = rtrim(trim($line), ','); + if (preg_match('/^(CREATE\s+TABLE)|(\) ENGINE=)/i', $line))//first or last table definition line + { + return false; + } + //if (preg_match('/^(PRIMARY KEY)|(((UNIQUE )|(FULLTEXT ))?KEY `?\w+`?)/i', $line, $m))//key definition + if (preg_match('/^(PRIMARY\s+KEY)|(((UNIQUE\s+)|(FULLTEXT\s+))?KEY\s+`?\w+`?)/i', $line, $m))//key definition + { + $key = $m[0]; + } + elseif (preg_match('/^`?\w+`?/i', $line, $m))//field definition + { + $key = '!'.$m[0];//to make sure fields will be synchronised before the keys + } + else + { + return false;//line has no valuable info (empty or comment) + } + //$key = str_replace('`', '', $key); + if (!empty($options['varcharDefaultIgnore'])) + { + $line = preg_replace("/(var)?char\(([0-9]+)\)\s+NOT\s+NULL\s+default\s+''/i", '$1char($2) NOT NULL', $line); + } + if (!empty($options['intDefaultIgnore'])) + { + $line = preg_replace("/((?:big)|(?:tiny))?int\(([0-9]+)\)\s+NOT\s+NULL\s+default\s+'0'/i", '$1int($2) NOT NULL', $line); + } + if (!empty($options['ignoreIncrement'])) + { + $line = preg_replace("/ AUTO_INCREMENT=[0-9]+/i", '', $line); + } + $result['key'] = $this->normalizeString($key); + $result['line']= $line; + return $result; + } - /** - * Takes an output of compare() method to generate the set of sql needed to update source table to make it - * look as a destination one - * @access private - * @param array $diff compare() method output - * @return array list of sql statements - * supports query generation options - */ - function getDiffSql($diff)//maybe add option to ommit or force 'IF NOT EXISTS', skip autoincrement - { - $options = $this->config; - $sqls = array(); - if (!is_array($diff) || empty($diff)) - { - return $sqls; - } - foreach($diff as $tab=>$info) - { - if ($info['sourceOrphan'])//delete it - { - $sqls[] = 'DROP TABLE `'.$tab.'`'; - } - elseif ($info['destOrphan'])//create destination table in source - { - $database = ''; - $destSql = $this->getTabSql($this->destStruct, $tab, $database); - if (!empty($options['ignoreIncrement'])) - { - $destSql = preg_replace("/\s*AUTO_INCREMENT=[0-9]+/i", '', $destSql); - } - if (!empty($options['ingoreIfNotExists'])) - { - $destSql = preg_replace("/IF NOT EXISTS\s*/i", '', $destSql); - } - if (!empty($options['forceIfNotExists'])) - { - $destSql = preg_replace('/(CREATE(?:\s*TEMPORARY)?\s*TABLE\s*)(?:IF\sNOT\sEXISTS\s*)?(`?\w+`?)/i', '$1IF NOT EXISTS $2', $destSql); - } - $sqls[] = $destSql; - } - else - { - foreach($info['differs'] as $finfo) - { - $inDest = !empty($finfo['dest']); - $inSource = !empty($finfo['source']); - if ($inSource && !$inDest) - { - $sql = $finfo['source']; - $action = 'drop'; - } - elseif ($inDest && !$inSource) - { - $sql = $finfo['dest']; - $action = 'add'; - } - else - { - $sql = $finfo['dest']; - $action = 'modify'; - } - $sql = $this->getActionSql($action, $tab, $sql); - $sqls[] = $sql; - } - } - } - return $sqls; - } + /** + * Takes an output of compare() method to generate the set of sql needed to update source table to make it + * look as a destination one + * @access private + * @param array $diff compare() method output + * @return array list of sql statements + * supports query generation options + */ + function getDiffSql($diff)//maybe add option to ommit or force 'IF NOT EXISTS', skip autoincrement + { + $options = $this->config; + $sqls = array(); + if (!is_array($diff) || empty($diff)) + { + return $sqls; + } + foreach($diff as $tab=>$info) + { + if ($info['sourceOrphan'])//delete it + { + $sqls[] = 'DROP TABLE `'.$tab.'`'; + } + elseif ($info['destOrphan'])//create destination table in source + { + $database = ''; + $destSql = $this->getTabSql($this->destStruct, $tab, $database); + if (!empty($options['ignoreIncrement'])) + { + $destSql = preg_replace("/\s*AUTO_INCREMENT=[0-9]+/i", '', $destSql); + } + if (!empty($options['ingoreIfNotExists'])) + { + $destSql = preg_replace("/IF NOT EXISTS\s*/i", '', $destSql); + } + if (!empty($options['forceIfNotExists'])) + { + $destSql = preg_replace('/(CREATE(?:\s*TEMPORARY)?\s*TABLE\s*)(?:IF\sNOT\sEXISTS\s*)?(`?\w+`?)/i', '$1IF NOT EXISTS $2', $destSql); + } + $sqls[] = $destSql; + } + else + { + foreach($info['differs'] as $finfo) + { + $inDest = !empty($finfo['dest']); + $inSource = !empty($finfo['source']); + if ($inSource && !$inDest) + { + $sql = $finfo['source']; + $action = 'drop'; + } + elseif ($inDest && !$inSource) + { + $sql = $finfo['dest']; + $action = 'add'; + } + else + { + $sql = $finfo['dest']; + $action = 'modify'; + } + $sql = $this->getActionSql($action, $tab, $sql); + $sqls[] = $sql; + } + } + } + return $sqls; + } - /** - * Compiles update sql - * @access private - * @param string $action - 'drop', 'add' or 'modify' - * @param string $tab table name - * @param string $sql definition of the element to change - * @return string update sql - */ - function getActionSql($action, $tab, $sql) - { - $result = 'ALTER TABLE `'.$tab.'` '; - $action = strtolower($action); - $keyField = '`?\w`?(?:\(\d+\))?';//matches `name`(10) - $keyFieldList = '(?:'.$keyField.'(?:,\s?)?)+';//matches `name`(10),`desc`(255) - if (preg_match('/((?:PRIMARY )|(?:UNIQUE )|(?:FULLTEXT ))?KEY `?(\w+)?`?\s(\('.$keyFieldList.'\))/i', $sql, $m)) - { //key and index operations - $type = strtolower(trim($m[1])); - $name = trim($m[2]); - $fields = trim($m[3]); - switch($action) - { - case 'drop': - if ($type=='primary') - { - $result.= 'DROP PRIMARY KEY'; - } - else - { - $result.= 'DROP INDEX `'.$name.'`'; - } - break; - case 'add': - if ($type=='primary') - { - $result.= 'ADD PRIMARY KEY '.$fields; - } - elseif ($type=='') - { - $result.= 'ADD INDEX `'.$name.'` '.$fields; - } - else - { - $result .='ADD '.strtoupper($type).' `'.$name.'` '.$fields;//fulltext or unique - } - break; - case 'modify': - if ($type=='primary') - { - $result.='DROP PRIMARY KEY, ADD PRIMARY KEY '.$fields; - } - elseif ($type=='') - { - $result.='DROP INDEX `'.$name.'`, ADD INDEX `'.$name.'` '.$fields; - } - else - { - $result.='DROP INDEX `'.$name.'`, ADD '.strtoupper($type).' `'.$name.'` '.$fields;//fulltext or unique - } - break; + /** + * Compiles update sql + * @access private + * @param string $action - 'drop', 'add' or 'modify' + * @param string $tab table name + * @param string $sql definition of the element to change + * @return string update sql + */ + function getActionSql($action, $tab, $sql) + { + $result = 'ALTER TABLE `'.$tab.'` '; + $action = strtolower($action); + $keyField = '`?\w`?(?:\(\d+\))?';//matches `name`(10) + $keyFieldList = '(?:'.$keyField.'(?:,\s?)?)+';//matches `name`(10),`desc`(255) + if (preg_match('/((?:PRIMARY )|(?:UNIQUE )|(?:FULLTEXT ))?KEY `?(\w+)?`?\s(\('.$keyFieldList.'\))/i', $sql, $m)) + { //key and index operations + $type = strtolower(trim($m[1])); + $name = trim($m[2]); + $fields = trim($m[3]); + switch($action) + { + case 'drop': + if ($type=='primary') + { + $result.= 'DROP PRIMARY KEY'; + } + else + { + $result.= 'DROP INDEX `'.$name.'`'; + } + break; + case 'add': + if ($type=='primary') + { + $result.= 'ADD PRIMARY KEY '.$fields; + } + elseif ($type=='') + { + $result.= 'ADD INDEX `'.$name.'` '.$fields; + } + else + { + $result .='ADD '.strtoupper($type).' `'.$name.'` '.$fields;//fulltext or unique + } + break; + case 'modify': + if ($type=='primary') + { + $result.='DROP PRIMARY KEY, ADD PRIMARY KEY '.$fields; + } + elseif ($type=='') + { + $result.='DROP INDEX `'.$name.'`, ADD INDEX `'.$name.'` '.$fields; + } + else + { + $result.='DROP INDEX `'.$name.'`, ADD '.strtoupper($type).' `'.$name.'` '.$fields;//fulltext or unique + } + break; - } - } - else //fields operations - { - $sql = rtrim(trim($sql), ','); - $result.= strtoupper($action); - if ($action=='drop') - { - $spacePos = strpos($sql, ' '); - $result.= ' '.substr($sql, 0, $spacePos); - } - else - { - $result.= ' '.$sql; - } - } - return $result; - } + } + } + else //fields operations + { + $sql = rtrim(trim($sql), ','); + $result.= strtoupper($action); + if ($action=='drop') + { + $spacePos = strpos($sql, ' '); + $result.= ' '.substr($sql, 0, $spacePos); + } + else + { + $result.= ' '.$sql; + } + } + return $result; + } - /** - * Searches for the position of the next delimiter which is not inside string literal like 'this ; ' or - * like "this ; ". - * - * Handles escaped \" and \'. Also handles sql comments. - * Actualy it is regex-based Finit State Machine (FSN) - */ - function getDelimPos($string, $offset=0, $delim=';', $skipInBrackets=false) - { - $stack = array(); - $rbs = '\\\\'; //reg - escaped backslash - $regPrefix = "(?getDelimPos($string, $offset, $delim, $skipInBrackets); - if ($pos===false) - { - return false; - } - do - { - $newPos=$this->getDelimPos($string, $pos+1, $delim, $skipInBrackets); - if ($newPos !== false) - { - $pos = $newPos; - } - } - while($newPos!==false); - return $pos; - } + /** + * works the same as getDelimPos except returns position of the first occurence of the delimiter starting from + * the end of the string + */ + function getDelimRpos($string, $offset=0, $delim=';', $skipInBrackets=false) + { + $pos = $this->getDelimPos($string, $offset, $delim, $skipInBrackets); + if ($pos===false) + { + return false; + } + do + { + $newPos=$this->getDelimPos($string, $pos+1, $delim, $skipInBrackets); + if ($newPos !== false) + { + $pos = $newPos; + } + } + while($newPos!==false); + return $pos; + } - /** - * Converts string to lowercase and replaces repeated spaces with the single one - - * to be used for the comparison purposes only - * @param string $str string to normaize - */ - function normalizeString($str) - { - $str = strtolower($str); - $str = preg_replace('/\s+/', ' ', $str); - return $str; - } + /** + * Converts string to lowercase and replaces repeated spaces with the single one - + * to be used for the comparison purposes only + * @param string $str string to normaize + */ + function normalizeString($str) + { + $str = strtolower($str); + $str = preg_replace('/\s+/', ' ', $str); + return $str; + } } \ No newline at end of file diff --git a/htdocs/okapi/views/devel/cronreport.php b/htdocs/okapi/views/devel/cronreport.php index 13979c26..27f2c6cf 100644 --- a/htdocs/okapi/views/devel/cronreport.php +++ b/htdocs/okapi/views/devel/cronreport.php @@ -17,63 +17,63 @@ use okapi\cronjobs\CronJobController; class View { - public static function call() - { - # This is a hidden page for OKAPI developers. It will output a cronjobs - # report. This is useful for debugging. + public static function call() + { + # This is a hidden page for OKAPI developers. It will output a cronjobs + # report. This is useful for debugging. - $response = new OkapiHttpResponse(); - $response->content_type = "text/plain; charset=utf-8"; - ob_start(); + $response = new OkapiHttpResponse(); + $response->content_type = "text/plain; charset=utf-8"; + ob_start(); - require_once($GLOBALS['rootpath']."okapi/cronjobs.php"); - $schedule = Cache::get("cron_schedule"); - if ($schedule == null) - $schedule = array(); - print "Nearest event: "; - if (Okapi::get_var('cron_nearest_event')) - print "in ".(Okapi::get_var('cron_nearest_event') - time())." seconds.\n\n"; - else - print "NOT SET\n\n"; - $cronjobs = CronJobController::get_enabled_cronjobs(); - usort($cronjobs, function($a, $b) { - $cmp = function($a, $b) { return ($a < $b) ? -1 : (($a > $b) ? 1 : 0); }; - $by_type = $cmp($a->get_type(), $b->get_type()); - if ($by_type != 0) - return $by_type; - return $cmp($a->get_name(), $b->get_name()); - }); - print str_pad("TYPE", 11)." ".str_pad("NAME", 35)." SCHEDULE\n"; - print str_pad("----", 11)." ".str_pad("----", 35)." --------\n"; - foreach ($cronjobs as $cronjob) - { - $type = $cronjob->get_type(); - $name = $cronjob->get_name(); - print str_pad($type, 11)." ".str_pad($name, 35)." "; - if (!isset($schedule[$name])) - print "NOT YET SCHEDULED\n"; - elseif ($schedule[$name] <= time()) - print "PAST: should be run ".(time() - $schedule[$name])." seconds ago\n"; - else - print "scheduled to run in ".str_pad($schedule[$name] - time(), 6, " ", STR_PAD_LEFT)." seconds\n"; - } - print "\n"; - print "Crontab last ping: "; - if (Cache::get('crontab_last_ping')) - print (time() - Cache::get('crontab_last_ping'))." seconds ago"; - else - print "NEVER"; - print " (crontab_check_counter: ".Cache::get('crontab_check_counter').").\n"; - print "clog_revisions_daily: "; - if (Cache::get('clog_revisions_daily')) - { - foreach (Cache::get('clog_revisions_daily') as $time => $rev) - print "$rev "; - print "\n"; - } else { - print "NULL\n"; - } - $response->body = ob_get_clean(); - return $response; - } + require_once($GLOBALS['rootpath']."okapi/cronjobs.php"); + $schedule = Cache::get("cron_schedule"); + if ($schedule == null) + $schedule = array(); + print "Nearest event: "; + if (Okapi::get_var('cron_nearest_event')) + print "in ".(Okapi::get_var('cron_nearest_event') - time())." seconds.\n\n"; + else + print "NOT SET\n\n"; + $cronjobs = CronJobController::get_enabled_cronjobs(); + usort($cronjobs, function($a, $b) { + $cmp = function($a, $b) { return ($a < $b) ? -1 : (($a > $b) ? 1 : 0); }; + $by_type = $cmp($a->get_type(), $b->get_type()); + if ($by_type != 0) + return $by_type; + return $cmp($a->get_name(), $b->get_name()); + }); + print str_pad("TYPE", 11)." ".str_pad("NAME", 35)." SCHEDULE\n"; + print str_pad("----", 11)." ".str_pad("----", 35)." --------\n"; + foreach ($cronjobs as $cronjob) + { + $type = $cronjob->get_type(); + $name = $cronjob->get_name(); + print str_pad($type, 11)." ".str_pad($name, 35)." "; + if (!isset($schedule[$name])) + print "NOT YET SCHEDULED\n"; + elseif ($schedule[$name] <= time()) + print "PAST: should be run ".(time() - $schedule[$name])." seconds ago\n"; + else + print "scheduled to run in ".str_pad($schedule[$name] - time(), 6, " ", STR_PAD_LEFT)." seconds\n"; + } + print "\n"; + print "Crontab last ping: "; + if (Cache::get('crontab_last_ping')) + print (time() - Cache::get('crontab_last_ping'))." seconds ago"; + else + print "NEVER"; + print " (crontab_check_counter: ".Cache::get('crontab_check_counter').").\n"; + print "clog_revisions_daily: "; + if (Cache::get('clog_revisions_daily')) + { + foreach (Cache::get('clog_revisions_daily') as $time => $rev) + print "$rev "; + print "\n"; + } else { + print "NULL\n"; + } + $response->body = ob_get_clean(); + return $response; + } } diff --git a/htdocs/okapi/views/devel/dbstruct.php b/htdocs/okapi/views/devel/dbstruct.php index 99ec2cf8..5e0b2a53 100644 --- a/htdocs/okapi/views/devel/dbstruct.php +++ b/htdocs/okapi/views/devel/dbstruct.php @@ -19,136 +19,136 @@ use okapi\BadRequest; class View { - public static function call() - { - # This is a hidden page for OKAPI developers. It will output a complete - # structure of the database. This is useful for making OKAPI compatible - # across different OC installations. + public static function call() + { + # This is a hidden page for OKAPI developers. It will output a complete + # structure of the database. This is useful for making OKAPI compatible + # across different OC installations. - $user = Settings::get('DB_USERNAME'); - $password = Settings::get('DB_PASSWORD'); - $dbname = Settings::get('DB_NAME'); - $dbserver = Settings::get('DB_SERVER'); + $user = Settings::get('DB_USERNAME'); + $password = Settings::get('DB_PASSWORD'); + $dbname = Settings::get('DB_NAME'); + $dbserver = Settings::get('DB_SERVER'); - # Some security measures are taken to hinder us from accidentally dumping - # database contents: - # - try to set memory limit so that no big data chunk can be stored - # - reassure that we use the --no-data option - # - plausibility test for data amount - # - verify that the output does not contain table contents + # Some security measures are taken to hinder us from accidentally dumping + # database contents: + # - try to set memory limit so that no big data chunk can be stored + # - reassure that we use the --no-data option + # - plausibility test for data amount + # - verify that the output does not contain table contents - ini_set('memory_limit', '16M'); - $shell_arguments = "mysqldump --no-data -h$dbserver -u$user -p$password $dbname"; - if (!strpos($shell_arguments,"--no-data")) - throw new Exception("wrong database dump arguments"); - $struct = shell_exec($shell_arguments); - if (strlen($struct) > 1000000) - throw new Exception("something went terribly wrong while dumping table structures"); - if (stripos($struct,"dumping data") !== FALSE) - throw new Exception("something went terribly wrong while dumping table structures"); + ini_set('memory_limit', '16M'); + $shell_arguments = "mysqldump --no-data -h$dbserver -u$user -p$password $dbname"; + if (!strpos($shell_arguments,"--no-data")) + throw new Exception("wrong database dump arguments"); + $struct = shell_exec($shell_arguments); + if (strlen($struct) > 1000000) + throw new Exception("something went terribly wrong while dumping table structures"); + if (stripos($struct,"dumping data") !== FALSE) + throw new Exception("something went terribly wrong while dumping table structures"); - # Remove the "AUTO_INCREMENT=..." values. They break the diffs. + # Remove the "AUTO_INCREMENT=..." values. They break the diffs. - $struct = preg_replace("/ AUTO_INCREMENT=([0-9]+)/i", "", $struct); + $struct = preg_replace("/ AUTO_INCREMENT=([0-9]+)/i", "", $struct); - # This method can be invoked with "compare_to" parameter, which points to - # an alternate database structure (generated by the same script in other - # *public* OKAPI instance). When invoked this way, we will attempt to - # generate SQL script which alters LOCAL database is such a way that it - # will become the other (public) database. + # This method can be invoked with "compare_to" parameter, which points to + # an alternate database structure (generated by the same script in other + # *public* OKAPI instance). When invoked this way, we will attempt to + # generate SQL script which alters LOCAL database is such a way that it + # will become the other (public) database. - $response = new OkapiHttpResponse(); - $response->content_type = "text/plain; charset=utf-8"; - if (isset($_GET['compare_to'])) - { - self::requireSafe($_GET['compare_to']); - $scheme = parse_url($_GET['compare_to'], PHP_URL_SCHEME); - if (in_array($scheme, array('http', 'https'))) - { - try { - $alternate_struct = @file_get_contents($_GET['compare_to']); - } catch (Exception $e) { - throw new BadRequest("Failed to load ".$_GET['compare_to']); - } - $response->body = - "-- Automatically generated database diff. Use with caution!\n". - "-- Differences obtained with help of cool library by Kirill Gerasimenko.\n\n". - "-- Note: The following script has some limitations. It will render database\n". - "-- structure compatible, but not necessarilly EXACTLY the same. It might be\n". - "-- better to use manual diff instead.\n\n"; - require_once("comparator.inc.php"); - $updater = new \dbStructUpdater(); - if (isset($_GET['reverse']) && ($_GET['reverse'] == 'true')) - { - $response->body .= - "-- REVERSE MODE. The following will alter [2], so that it has the structure of [1].\n". - "-- 1. ".Settings::get('SITE_URL')."okapi/devel/dbstruct (".md5($struct).")\n". - "-- 2. ".$_GET['compare_to']." (".md5($alternate_struct).")\n\n"; - $alters = $updater->getUpdates($alternate_struct, $struct); - } - else - { - $response->body .= - "-- The following will alter [1], so that it has the structure of [2].\n". - "-- 1. ".Settings::get('SITE_URL')."okapi/devel/dbstruct (".md5($struct).")\n". - "-- 2. ".$_GET['compare_to']." (".md5($alternate_struct).")\n\n"; - $alters = $updater->getUpdates($struct, $alternate_struct); - } - # Add semicolons - foreach ($alters as &$alter_ref) - $alter_ref .= ";"; - # Comment out all differences containing "okapi_". These should be executed - # by OKAPI update scripts. - foreach ($alters as &$alter_ref) - { - if (strpos($alter_ref, "okapi_") !== false) - { - $lines = explode("\n", $alter_ref); - $alter_ref = "-- Probably you should NOT execute this one. Use okapi/update instead.\n-- {{{\n-- ". - implode("\n-- ", $lines)."\n-- }}}"; - } - } - if (count($alters) > 0) - $response->body .= implode("\n", $alters)."\n"; - else - $response->body .= "-- No differences found\n"; - } - else - { - $response->body = "HTTP(S) only!"; - } - } - else - { - $response->body = $struct; - } - return $response; - } + $response = new OkapiHttpResponse(); + $response->content_type = "text/plain; charset=utf-8"; + if (isset($_GET['compare_to'])) + { + self::requireSafe($_GET['compare_to']); + $scheme = parse_url($_GET['compare_to'], PHP_URL_SCHEME); + if (in_array($scheme, array('http', 'https'))) + { + try { + $alternate_struct = @file_get_contents($_GET['compare_to']); + } catch (Exception $e) { + throw new BadRequest("Failed to load ".$_GET['compare_to']); + } + $response->body = + "-- Automatically generated database diff. Use with caution!\n". + "-- Differences obtained with help of cool library by Kirill Gerasimenko.\n\n". + "-- Note: The following script has some limitations. It will render database\n". + "-- structure compatible, but not necessarilly EXACTLY the same. It might be\n". + "-- better to use manual diff instead.\n\n"; + require_once("comparator.inc.php"); + $updater = new \dbStructUpdater(); + if (isset($_GET['reverse']) && ($_GET['reverse'] == 'true')) + { + $response->body .= + "-- REVERSE MODE. The following will alter [2], so that it has the structure of [1].\n". + "-- 1. ".Settings::get('SITE_URL')."okapi/devel/dbstruct (".md5($struct).")\n". + "-- 2. ".$_GET['compare_to']." (".md5($alternate_struct).")\n\n"; + $alters = $updater->getUpdates($alternate_struct, $struct); + } + else + { + $response->body .= + "-- The following will alter [1], so that it has the structure of [2].\n". + "-- 1. ".Settings::get('SITE_URL')."okapi/devel/dbstruct (".md5($struct).")\n". + "-- 2. ".$_GET['compare_to']." (".md5($alternate_struct).")\n\n"; + $alters = $updater->getUpdates($struct, $alternate_struct); + } + # Add semicolons + foreach ($alters as &$alter_ref) + $alter_ref .= ";"; + # Comment out all differences containing "okapi_". These should be executed + # by OKAPI update scripts. + foreach ($alters as &$alter_ref) + { + if (strpos($alter_ref, "okapi_") !== false) + { + $lines = explode("\n", $alter_ref); + $alter_ref = "-- Probably you should NOT execute this one. Use okapi/update instead.\n-- {{{\n-- ". + implode("\n-- ", $lines)."\n-- }}}"; + } + } + if (count($alters) > 0) + $response->body .= implode("\n", $alters)."\n"; + else + $response->body .= "-- No differences found\n"; + } + else + { + $response->body = "HTTP(S) only!"; + } + } + else + { + $response->body = $struct; + } + return $response; + } - /** - * Check if the URL can be safely retrieved. See issue #252. - */ - private static function requireSafe($url) - { - require_once($GLOBALS['rootpath'].'okapi/service_runner.php'); - $installations = OkapiServiceRunner::call( - "services/apisrv/installations", - new OkapiInternalRequest( - new OkapiInternalConsumer(), null, array() - ) - ); - $allowed = array(); - foreach ($installations as $i) { - $allowed_url = $i['okapi_base_url']."devel/dbstruct"; - $allowed[] = $allowed_url; - if ($url == $allowed_url) { - return; - } - } - throw new BadRequest( - "The following URL is not on our whitelist: \"".$url."\".\n\n". - "Please use one of the following:\n". - "\"".implode("\",\n\"", $allowed)."\"." - ); - } + /** + * Check if the URL can be safely retrieved. See issue #252. + */ + private static function requireSafe($url) + { + require_once($GLOBALS['rootpath'].'okapi/service_runner.php'); + $installations = OkapiServiceRunner::call( + "services/apisrv/installations", + new OkapiInternalRequest( + new OkapiInternalConsumer(), null, array() + ) + ); + $allowed = array(); + foreach ($installations as $i) { + $allowed_url = $i['okapi_base_url']."devel/dbstruct"; + $allowed[] = $allowed_url; + if ($url == $allowed_url) { + return; + } + } + throw new BadRequest( + "The following URL is not on our whitelist: \"".$url."\".\n\n". + "Please use one of the following:\n". + "\"".implode("\",\n\"", $allowed)."\"." + ); + } } diff --git a/htdocs/okapi/views/devel/tilereport.php b/htdocs/okapi/views/devel/tilereport.php index 41eb1631..a3e6dfc1 100644 --- a/htdocs/okapi/views/devel/tilereport.php +++ b/htdocs/okapi/views/devel/tilereport.php @@ -17,146 +17,146 @@ use okapi\cronjobs\CronJobController; class View { - public static function call() - { - # Flush the stats, so the page is fresh upon every request. + public static function call() + { + # Flush the stats, so the page is fresh upon every request. - require_once($GLOBALS['rootpath']."okapi/cronjobs.php"); - CronJobController::force_run("StatsWriterCronJob"); + require_once($GLOBALS['rootpath']."okapi/cronjobs.php"); + CronJobController::force_run("StatsWriterCronJob"); - # When services/caches/map/tile method is called, it writes some extra - # stats in the okapi_stats_hourly table. This page retrieves and - # formats these stats in a readable manner (for debugging). + # When services/caches/map/tile method is called, it writes some extra + # stats in the okapi_stats_hourly table. This page retrieves and + # formats these stats in a readable manner (for debugging). - $response = new OkapiHttpResponse(); - $response->content_type = "text/plain; charset=utf-8"; - ob_start(); + $response = new OkapiHttpResponse(); + $response->content_type = "text/plain; charset=utf-8"; + ob_start(); - $start = isset($_GET['start']) ? $_GET['start'] : date( - "Y-m-d 00:00:00", time() - 7*86400); - $end = isset($_GET['end']) ? $_GET['end'] : date("Y-m-d 23:59:59"); + $start = isset($_GET['start']) ? $_GET['start'] : date( + "Y-m-d 00:00:00", time() - 7*86400); + $end = isset($_GET['end']) ? $_GET['end'] : date("Y-m-d 23:59:59"); - print "From: $start\n"; - print " To: $end\n\n"; + print "From: $start\n"; + print " To: $end\n\n"; - $rs = Db::query(" - select - service_name, - sum(total_calls), - sum(total_runtime) - from okapi_stats_hourly - where - period_start >= '".mysql_real_escape_string($start)."' - and period_start < '".mysql_real_escape_string($end)."' - and service_name like '%caches/map/tile%' - group by service_name - "); + $rs = Db::query(" + select + service_name, + sum(total_calls), + sum(total_runtime) + from okapi_stats_hourly + where + period_start >= '".mysql_real_escape_string($start)."' + and period_start < '".mysql_real_escape_string($end)."' + and service_name like '%caches/map/tile%' + group by service_name + "); - $total_calls = 0; - $total_runtime = 0.0; - $calls = array('A' => 0, 'B' => 0, 'C' => 0, 'D' => 0); - $runtime = array('A' => 0.0, 'B' => 0.0, 'C' => 0.0, 'D' => 0.0); + $total_calls = 0; + $total_runtime = 0.0; + $calls = array('A' => 0, 'B' => 0, 'C' => 0, 'D' => 0); + $runtime = array('A' => 0.0, 'B' => 0.0, 'C' => 0.0, 'D' => 0.0); - while (list($name, $c, $r) = mysql_fetch_array($rs)) - { - if ($name == 'services/caches/map/tile') - { - $total_calls = $c; - $total_runtime = $r; - } - elseif (strpos($name, 'extra/caches/map/tile/checkpoint') === 0) - { - $calls[$name[32]] = $c; - $runtime[$name[32]] = $r; - } - } - if ($total_calls != $calls['A']) - { - print "Partial results. Only ".$calls['A']." out of $total_calls are covered.\n"; - print "All other will count as \"unaccounted for\".\n\n"; - $total_calls = $calls['A']; - } + while (list($name, $c, $r) = mysql_fetch_array($rs)) + { + if ($name == 'services/caches/map/tile') + { + $total_calls = $c; + $total_runtime = $r; + } + elseif (strpos($name, 'extra/caches/map/tile/checkpoint') === 0) + { + $calls[$name[32]] = $c; + $runtime[$name[32]] = $r; + } + } + if ($total_calls != $calls['A']) + { + print "Partial results. Only ".$calls['A']." out of $total_calls are covered.\n"; + print "All other will count as \"unaccounted for\".\n\n"; + $total_calls = $calls['A']; + } - $calls_left = $total_calls; - $runtime_left = $total_runtime; + $calls_left = $total_calls; + $runtime_left = $total_runtime; - $perc = function($a, $b) { return ($b > 0) ? sprintf("%.1f", 100 * $a / $b)."%" : "(?)"; }; - $avg = function($a, $b) { return ($b > 0) ? sprintf("%.4f", $a / $b)."s" : "(?)"; }; - $get_stats = function() use (&$calls_left, &$runtime_left, &$total_calls, &$total_runtime, &$perc) - { - return ( - str_pad($perc($calls_left, $total_calls), 6, " ", STR_PAD_LEFT). - str_pad($perc($runtime_left, $total_runtime), 7, " ", STR_PAD_LEFT) - ); - }; + $perc = function($a, $b) { return ($b > 0) ? sprintf("%.1f", 100 * $a / $b)."%" : "(?)"; }; + $avg = function($a, $b) { return ($b > 0) ? sprintf("%.4f", $a / $b)."s" : "(?)"; }; + $get_stats = function() use (&$calls_left, &$runtime_left, &$total_calls, &$total_runtime, &$perc) + { + return ( + str_pad($perc($calls_left, $total_calls), 6, " ", STR_PAD_LEFT). + str_pad($perc($runtime_left, $total_runtime), 7, " ", STR_PAD_LEFT) + ); + }; - print "%CALLS %TIME Description\n"; - print "====== ====== ======================================================================\n"; - print $get_stats()." $total_calls responses served. Total runtime: ".sprintf("%.2f", $total_runtime)."s\n"; - print "\n"; - print " All of these requests needed a TileTree build/lookup. The average runtime of\n"; - print " these lookups was ".$avg($runtime['A'], $total_calls).". ".$perc($runtime['A'], $total_runtime)." of total runtime was spent here.\n"; - print "\n"; + print "%CALLS %TIME Description\n"; + print "====== ====== ======================================================================\n"; + print $get_stats()." $total_calls responses served. Total runtime: ".sprintf("%.2f", $total_runtime)."s\n"; + print "\n"; + print " All of these requests needed a TileTree build/lookup. The average runtime of\n"; + print " these lookups was ".$avg($runtime['A'], $total_calls).". ".$perc($runtime['A'], $total_runtime)." of total runtime was spent here.\n"; + print "\n"; - $runtime_left -= $runtime['A']; + $runtime_left -= $runtime['A']; - print $get_stats()." All calls passed here after ~".$avg($runtime['A'], $total_calls)."\n"; + print $get_stats()." All calls passed here after ~".$avg($runtime['A'], $total_calls)."\n"; - print "\n"; - print " Lookup result was then processed and \"image description\" was created. It was\n"; - print " passed on to the TileRenderer to compute the ETag hash string. The average runtime\n"; - print " of this part was ".$avg($runtime['B'], $total_calls).". ".$perc($runtime['B'], $total_runtime)." of total runtime was spent here.\n"; - print "\n"; + print "\n"; + print " Lookup result was then processed and \"image description\" was created. It was\n"; + print " passed on to the TileRenderer to compute the ETag hash string. The average runtime\n"; + print " of this part was ".$avg($runtime['B'], $total_calls).". ".$perc($runtime['B'], $total_runtime)." of total runtime was spent here.\n"; + print "\n"; - $runtime_left -= $runtime['B']; + $runtime_left -= $runtime['B']; - print $get_stats()." All calls passed here after ~".$avg($runtime['A'] + $runtime['B'], $total_calls)."\n"; + print $get_stats()." All calls passed here after ~".$avg($runtime['A'] + $runtime['B'], $total_calls)."\n"; - $etag_hits = $calls['B'] - $calls['C']; + $etag_hits = $calls['B'] - $calls['C']; - print "\n"; - print " $etag_hits of the requests matched the ETag and were served an HTTP 304 response.\n"; - print "\n"; + print "\n"; + print " $etag_hits of the requests matched the ETag and were served an HTTP 304 response.\n"; + print "\n"; - $calls_left = $calls['C']; + $calls_left = $calls['C']; - print $get_stats()." $calls_left calls passed here after ~".$avg($runtime['A'] + $runtime['B'], $total_calls)."\n"; + print $get_stats()." $calls_left calls passed here after ~".$avg($runtime['A'] + $runtime['B'], $total_calls)."\n"; - $imagecache_hits = $calls['C'] - $calls['D']; + $imagecache_hits = $calls['C'] - $calls['D']; - print "\n"; - print " $imagecache_hits of these calls hit the server image cache.\n"; - print " ".$perc($runtime['C'], $total_runtime)." of total runtime was spent to find these.\n"; - print "\n"; + print "\n"; + print " $imagecache_hits of these calls hit the server image cache.\n"; + print " ".$perc($runtime['C'], $total_runtime)." of total runtime was spent to find these.\n"; + print "\n"; - $calls_left = $calls['D']; - $runtime_left -= $runtime['C']; + $calls_left = $calls['D']; + $runtime_left -= $runtime['C']; - print $get_stats()." $calls_left calls passed here after ~".$avg($runtime['A'] + $runtime['B'] + $runtime['C'], $total_calls)."\n"; - print "\n"; - print " These calls required the tile to be rendered. On average, it took\n"; - print " ".$avg($runtime['D'], $calls['D'])." to *render* a tile.\n"; - print " ".$perc($runtime['D'], $total_runtime)." of total runtime was spent here.\n"; - print "\n"; + print $get_stats()." $calls_left calls passed here after ~".$avg($runtime['A'] + $runtime['B'] + $runtime['C'], $total_calls)."\n"; + print "\n"; + print " These calls required the tile to be rendered. On average, it took\n"; + print " ".$avg($runtime['D'], $calls['D'])." to *render* a tile.\n"; + print " ".$perc($runtime['D'], $total_runtime)." of total runtime was spent here.\n"; + print "\n"; - $runtime_left -= $runtime['D']; + $runtime_left -= $runtime['D']; - print $perc($runtime_left, $total_runtime)." of runtime was unaccounted for (other processing).\n"; - print "Average response time was ".$avg($total_runtime, $total_calls).".\n\n"; + print $perc($runtime_left, $total_runtime)." of runtime was unaccounted for (other processing).\n"; + print "Average response time was ".$avg($total_runtime, $total_calls).".\n\n"; - print "Current okapi_cache score distribution:\n"; - $rs = Db::query(" - select floor(log2(score)), count(*), sum(length(value)) - from okapi_cache - where score is not null - group by floor(log2(score)) - "); - while (list($log2, $count, $size) = mysql_fetch_array($rs)) - { - print $count." elements ($size bytes) with score between ".pow(2, $log2)." and ".pow(2, $log2 + 1).".\n"; - } + print "Current okapi_cache score distribution:\n"; + $rs = Db::query(" + select floor(log2(score)), count(*), sum(length(value)) + from okapi_cache + where score is not null + group by floor(log2(score)) + "); + while (list($log2, $count, $size) = mysql_fetch_array($rs)) + { + print $count." elements ($size bytes) with score between ".pow(2, $log2)." and ".pow(2, $log2 + 1).".\n"; + } - $response->body = ob_get_clean(); - return $response; - } + $response->body = ob_get_clean(); + return $response; + } } diff --git a/htdocs/okapi/views/examples.php b/htdocs/okapi/views/examples.php index c6d92bfb..0717d310 100644 --- a/htdocs/okapi/views/examples.php +++ b/htdocs/okapi/views/examples.php @@ -15,25 +15,25 @@ use okapi\views\menu\OkapiMenu; class View { - public static function call() - { - require_once($GLOBALS['rootpath'].'okapi/service_runner.php'); - require_once($GLOBALS['rootpath'].'okapi/views/menu.inc.php'); + public static function call() + { + require_once($GLOBALS['rootpath'].'okapi/service_runner.php'); + require_once($GLOBALS['rootpath'].'okapi/views/menu.inc.php'); - $vars = array( - 'menu' => OkapiMenu::get_menu_html("examples.html"), - 'okapi_base_url' => Settings::get('SITE_URL')."okapi/", - 'site_url' => Settings::get('SITE_URL'), - 'installations' => OkapiMenu::get_installations(), - 'okapi_rev' => Okapi::$revision, - 'site_name' => Okapi::get_normalized_site_name(), - ); + $vars = array( + 'menu' => OkapiMenu::get_menu_html("examples.html"), + 'okapi_base_url' => Settings::get('SITE_URL')."okapi/", + 'site_url' => Settings::get('SITE_URL'), + 'installations' => OkapiMenu::get_installations(), + 'okapi_rev' => Okapi::$revision, + 'site_name' => Okapi::get_normalized_site_name(), + ); - $response = new OkapiHttpResponse(); - $response->content_type = "text/html; charset=utf-8"; - ob_start(); - include 'examples.tpl.php'; - $response->body = ob_get_clean(); - return $response; - } + $response = new OkapiHttpResponse(); + $response->content_type = "text/html; charset=utf-8"; + ob_start(); + include 'examples.tpl.php'; + $response->body = ob_get_clean(); + return $response; + } } diff --git a/htdocs/okapi/views/examples.tpl.php b/htdocs/okapi/views/examples.tpl.php index 1b18b45c..c0ad02ec 100644 --- a/htdocs/okapi/views/examples.tpl.php +++ b/htdocs/okapi/views/examples.tpl.php @@ -1,31 +1,31 @@ - - - OKAPI Examples - - - - - - - -
      -
      - - - - +
      - - + + + OKAPI Examples + + + + + + + +
      +
      + + + + -
      + +

      Examples, libraries and tools

      @@ -42,21 +42,11 @@ a basic OAuth Console for OKAPI methods. It is an open-source project. You can

      Are there any client libraries?

      -

      OKAPI does not require you to use any special libraries, usually you will want to -use OKAPI "as is", via basic HTTP requests and responses.

      -

      However, some third-party libraries exist and you can use them if you want. With proper -libraries, OKAPI might be easier to use. We give you the list of all libraries we know of. -It's your choice to decide which are "proper".

      -
        -
      • If you're developing with .NET, you may want to check out - .NET - client library by Oliver Dietz.
      • -
      • (if you've developed your own library and would like to include it here, - post the details in a comment thread below)
      • -
      -

      You should check with the author of the library before you use it, to make sure it is -up-to-date. If you believe it is not, then keep in mind that learning to use our REST -protocol might be the safest choice.

      +

      OKAPI does not require you to use any special libraries. You should use +OKAPI "as is", via basic HTTP requests and responses.

      + +

      However, if you know of any useful third-party libraries, then let us know. +We may choose to post a link somewhere here.

      @@ -85,11 +75,11 @@ There are some limitations of both these techniques though.

      This example does the following:

        -
      • Pulls the services/apisrv/installations.html'>list of all OKAPI installations - from one of the OKAPI servers and displays it in a select-box. Note, that this method does not - require Consumer Key (Level 0 Authentication).
      • -
      • Asks you to share your location (modern browser can do that).
      • -
      • Retrieves a list of nearest geocaches. (This time, it uses the Consumer Key you have to supply.)
      • +
      • Pulls the services/apisrv/installations.html'>list of all OKAPI installations + from one of the OKAPI servers and displays it in a select-box. Note, that this method does not + require Consumer Key (Level 0 Authentication).
      • +
      • Asks you to share your location (modern browser can do that).
      • +
      • Retrieves a list of nearest geocaches. (This time, it uses the Consumer Key you have to supply.)

      static/examples/javascript_nearest.html' style='font-size: 130%; font-weight: bold'>Run this example

      @@ -108,11 +98,11 @@ modules to dynamically retrieve the current list of OKAPI installations and meth
      -
      -
      -
      -
      -
      - +
      +
      +
      +
      +
      + diff --git a/htdocs/okapi/views/http404.php b/htdocs/okapi/views/http404.php index d2baea0c..0fc0e572 100644 --- a/htdocs/okapi/views/http404.php +++ b/htdocs/okapi/views/http404.php @@ -10,23 +10,23 @@ use okapi\views\menu\OkapiMenu; class View { - public static function call() - { - require_once('menu.inc.php'); + public static function call() + { + require_once('menu.inc.php'); - $vars = array( - 'okapi_base_url' => Settings::get('SITE_URL')."okapi/", - 'menu' => OkapiMenu::get_menu_html(), - 'installations' => OkapiMenu::get_installations(), - 'okapi_rev' => Okapi::$revision, - ); + $vars = array( + 'okapi_base_url' => Settings::get('SITE_URL')."okapi/", + 'menu' => OkapiMenu::get_menu_html(), + 'installations' => OkapiMenu::get_installations(), + 'okapi_rev' => Okapi::$revision, + ); - $response = new OkapiHttpResponse(); - $response->status = "404 Not Found"; - $response->content_type = "text/html; charset=utf-8"; - ob_start(); - include 'http404.tpl.php'; - $response->body = ob_get_clean(); - return $response; - } + $response = new OkapiHttpResponse(); + $response->status = "404 Not Found"; + $response->content_type = "text/html; charset=utf-8"; + ob_start(); + include 'http404.tpl.php'; + $response->body = ob_get_clean(); + return $response; + } } diff --git a/htdocs/okapi/views/http404.tpl.php b/htdocs/okapi/views/http404.tpl.php index 345db1d0..18a004fc 100644 --- a/htdocs/okapi/views/http404.tpl.php +++ b/htdocs/okapi/views/http404.tpl.php @@ -1,30 +1,30 @@ - - - 404 Page Not Found - - - - -
      -
      - - - - -
      - - -

      - Page Not Found -
      :: 404 Error
      -

      -

      The page you requested does not exist. Try one of the pages in the side menu!

      -
      -
      -
      -
      -
      - + + + 404 Page Not Found + + + + +
      +
      + + + + +
      + + +

      + Page Not Found +
      :: 404 Error
      +

      +

      The page you requested does not exist. Try one of the pages in the side menu!

      +
      +
      +
      +
      +
      + diff --git a/htdocs/okapi/views/index.php b/htdocs/okapi/views/index.php index 8de18c8d..288deaea 100644 --- a/htdocs/okapi/views/index.php +++ b/htdocs/okapi/views/index.php @@ -14,12 +14,12 @@ use okapi\OkapiInternalRequest; class View { - public static function call() - { - # This is called when someone displays "http://../okapi/" (with no - # html path at the end). We will redirect to the introduction page. + public static function call() + { + # This is called when someone displays "http://../okapi/" (with no + # html path at the end). We will redirect to the introduction page. - return new OkapiRedirectResponse(Settings::get('SITE_URL'). - "okapi/introduction.html"); - } + return new OkapiRedirectResponse(Settings::get('SITE_URL'). + "okapi/introduction.html"); + } } diff --git a/htdocs/okapi/views/installations_box.tpl.php b/htdocs/okapi/views/installations_box.tpl.php index 7222d5e0..7d68b38a 100644 --- a/htdocs/okapi/views/installations_box.tpl.php +++ b/htdocs/okapi/views/installations_box.tpl.php @@ -1,9 +1,9 @@ -
      -
      - -
      +
      +
      + +
      \ No newline at end of file diff --git a/htdocs/okapi/views/introduction.php b/htdocs/okapi/views/introduction.php index f72584b0..0c1153e5 100644 --- a/htdocs/okapi/views/introduction.php +++ b/htdocs/okapi/views/introduction.php @@ -16,26 +16,26 @@ use okapi\OkapiInternalConsumer; class View { - public static function call() - { - require_once($GLOBALS['rootpath'].'okapi/service_runner.php'); - require_once($GLOBALS['rootpath'].'okapi/views/menu.inc.php'); + public static function call() + { + require_once($GLOBALS['rootpath'].'okapi/service_runner.php'); + require_once($GLOBALS['rootpath'].'okapi/views/menu.inc.php'); - $vars = array( - 'menu' => OkapiMenu::get_menu_html("introduction.html"), - 'okapi_base_url' => Settings::get('SITE_URL')."okapi/", - 'site_url' => Settings::get('SITE_URL'), - 'method_index' => OkapiServiceRunner::call('services/apiref/method_index', - new OkapiInternalRequest(new OkapiInternalConsumer(), null, array())), - 'installations' => OkapiMenu::get_installations(), - 'okapi_rev' => Okapi::$revision, - ); + $vars = array( + 'menu' => OkapiMenu::get_menu_html("introduction.html"), + 'okapi_base_url' => Settings::get('SITE_URL')."okapi/", + 'site_url' => Settings::get('SITE_URL'), + 'method_index' => OkapiServiceRunner::call('services/apiref/method_index', + new OkapiInternalRequest(new OkapiInternalConsumer(), null, array())), + 'installations' => OkapiMenu::get_installations(), + 'okapi_rev' => Okapi::$revision, + ); - $response = new OkapiHttpResponse(); - $response->content_type = "text/html; charset=utf-8"; - ob_start(); - include 'introduction.tpl.php'; - $response->body = ob_get_clean(); - return $response; - } + $response = new OkapiHttpResponse(); + $response->content_type = "text/html; charset=utf-8"; + ob_start(); + include 'introduction.tpl.php'; + $response->body = ob_get_clean(); + return $response; + } } diff --git a/htdocs/okapi/views/introduction.tpl.php b/htdocs/okapi/views/introduction.tpl.php index b074b05a..e31cffe9 100644 --- a/htdocs/okapi/views/introduction.tpl.php +++ b/htdocs/okapi/views/introduction.tpl.php @@ -1,37 +1,37 @@ - - - OKAPI - Opencaching API - - - - - - - -
      -
      - - - - +
      - - + + + OKAPI - Opencaching API + + + + + + + +
      +
      + + + + -
      + +

      - The OKAPI Project -
      :: Opencaching API Reference
      + The OKAPI Project +
      :: Opencaching API Reference

      OKAPI is a public API project for National Opencaching sites (also known as Opencaching Nodes).

        -
      • It provides OC site with a set of useful well-documented API methods,
      • -
      • Allows external developers to easily read public Opencaching data,
      • -
      • Allows read and write private (user-related) data with OAuth 3-legged authentication.
      • +
      • It provides OC site with a set of useful well-documented API methods,
      • +
      • Allows external developers to easily read public Opencaching data,
      • +
      • Allows read and write private (user-related) data with OAuth 3-legged authentication.

      The project is aiming to become a standard API for all National Opencaching.xx sites. This OKAPI installation provides API for the @@ -39,9 +39,9 @@ This OKAPI installation provides API for the Check out other OKAPI installations:

        - -
      • - '>
      • - + +
      • - '>
      • +

      Opencaching.DE includes the sites Opencaching.IT and OpencachingSpain.ES, @@ -51,8 +51,8 @@ OKAPI installation and select Italian or Spanish language.

      And also:

      @@ -65,12 +65,12 @@ and you receive a JSON-formatted data, that you may parse and use within your ow

      Example. Click the following link to run a method that prints out the list of all available methods:

      You've made your first OKAPI request! This method was a simple one. It didn't require any arguments and it didn't require you to use a Consumer Key. @@ -86,43 +86,43 @@ authentication, you have to use "Level 1" authentication or higher

      Important: Most developers will only need to use "Level 1" authentication and don't have to care about OAuth.

        -
      • -

        Level 0. Anonymous. You may call this method with no extra - arguments.

        -

        some_method?arg=44

        -
      • -
      • -

        Level 1. Simple Consumer Authentication. You must call this - method with consumer_key argument and provide the key which has - been generated for your application on the okapi/signup.html'>Sign up page.

        -

        some_method?arg=44&consumer_key=a7Lkeqf8CjNQTL522dH8

        -
      • -
      • -

        Level 2. OAuth Consumer Signature. You must call this method - with proper OAuth Consumer signature (based on your Consumer Secret).

        -

        some_method
        - ?arg=44
        - &oauth_consumer_key=a7Lkeqf8CjNQTL522dH8
        - &oauth_nonce=1987981
        - &oauth_signature_method=HMAC-SHA1
        - &oauth_timestamp=1313882320
        - &oauth_version=1.0
        - &oauth_signature=mWEpK2e%2fm8QYZk1BMm%2fRR74B3Co%3d

        -
      • -
      • -

        Level 3. OAuth Consumer+Token Signature. You must call this method - with proper OAuth Consumer+Token signature (based on both Consumer Secret and - Token Secret).

        -

        some_method
        - ?arg=44
        - &oauth_consumer_key=a7Lkeqf8CjNQTL522dH8
        - &oauth_nonce=2993717
        - &oauth_signature_method=HMAC-SHA1
        - &oauth_timestamp=1313882596
        - &oauth_token=AKQbwa28Afp1YvQAqSyK
        - &oauth_version=1.0
        - &oauth_signature=qbNiWkUS93fz6ADoNcjuJ7psB%2bQ%3d

        -
      • +
      • +

        Level 0. Anonymous. You may call this method with no extra + arguments.

        +

        some_method?arg=44

        +
      • +
      • +

        Level 1. Simple Consumer Authentication. You must call this + method with consumer_key argument and provide the key which has + been generated for your application on the okapi/signup.html'>Sign up page.

        +

        some_method?arg=44&consumer_key=a7Lkeqf8CjNQTL522dH8

        +
      • +
      • +

        Level 2. OAuth Consumer Signature. You must call this method + with proper OAuth Consumer signature (based on your Consumer Secret).

        +

        some_method
        + ?arg=44
        + &oauth_consumer_key=a7Lkeqf8CjNQTL522dH8
        + &oauth_nonce=1987981
        + &oauth_signature_method=HMAC-SHA1
        + &oauth_timestamp=1313882320
        + &oauth_version=1.0
        + &oauth_signature=mWEpK2e%2fm8QYZk1BMm%2fRR74B3Co%3d

        +
      • +
      • +

        Level 3. OAuth Consumer+Token Signature. You must call this method + with proper OAuth Consumer+Token signature (based on both Consumer Secret and + Token Secret).

        +

        some_method
        + ?arg=44
        + &oauth_consumer_key=a7Lkeqf8CjNQTL522dH8
        + &oauth_nonce=2993717
        + &oauth_signature_method=HMAC-SHA1
        + &oauth_timestamp=1313882596
        + &oauth_token=AKQbwa28Afp1YvQAqSyK
        + &oauth_version=1.0
        + &oauth_signature=qbNiWkUS93fz6ADoNcjuJ7psB%2bQ%3d

        +
      @@ -153,28 +153,28 @@ of strings and integers. Such objects can be formatted in several ways using parameters required for the method to run):

        -
      • -

        format - name of the format in which you'd like your result - to be returned in. Currently supported output formats include:

        -
          -
        • -

          json - JSON format (default),

          -

          Use Chrome - or Firefox extensions - to view JSON results directly in your browser. This simplifies debugging a lot!

          -
        • -
        • jsonp - JSONP format, if - you choose this one, you have to specify the callback parameter,
        • -
        • xmlmap - deprecated (why?),
        • -
        • xmlmap2 - XML format. This is produced by mapping JSON data types to XML elements. - Keep in mind, that XML format is larger than JSON and it takes more time to generate - and parse. Try to use JSON when it's possible.
        • -
        -
      • -
      • - callback - (when using JSONP output format) name of the JavaScript function - to be executed with the result as its parameter. -
      • +
      • +

        format - name of the format in which you'd like your result + to be returned in. Currently supported output formats include:

        +
          +
        • +

          json - JSON format (default),

          +

          Use Chrome + or Firefox extensions + to view JSON results directly in your browser. This simplifies debugging a lot!

          +
        • +
        • jsonp - JSONP format, if + you choose this one, you have to specify the callback parameter,
        • +
        • xmlmap - deprecated (why?),
        • +
        • xmlmap2 - XML format. This is produced by mapping JSON data types to XML elements. + Keep in mind, that XML format is larger than JSON and it takes more time to generate + and parse. Try to use JSON when it's possible.
        • +
        +
      • +
      • + callback - (when using JSONP output format) name of the JavaScript function + to be executed with the result as its parameter. +

      Important: Almost all of the returned data types are extendible. This means, @@ -196,42 +196,42 @@ a JPEG or a GPX file. Such methods do not accept common formatting parameters method calls and redirects which provide you with an Access Token).

      The three OAuth request URLs defined in the OAuth specification are:

      Things you should pay attention to:

        -
      • -

        The oauth_callback argument of the request_token method is required.

        -

        As the OAuth 1.0a specification states, it should be set to "oob" or a callback URL - (this usually starts with http:// or https://, but you can use any other myapp:// scheme).

        -

        For most OAuth client libraries, you just should provide - "okapi/services/oauth/request_token?oauth_callback=oob" - as the request_token URL, to get it started. Later, probably you'd want to switch "oob" - to something more useful.

        -
      • -
      • -

        The oauth_verifier argument of the access_token method is also required.

        -

        When user authorizes your application, he will receive a PIN code (OAuth verifier). You - have to use this code to receive your Access Token.

        -
      • -
      • -

        Access Tokens do not expire (but can be revoked). This means, that once the user - authorizes your application, you receive a "lifetime access" to his/her account. - User may still revoke access to his account from your - application - when this happens, you will have to redo the authorization dance.

        -
      • +
      • +

        The oauth_callback argument of the request_token method is required.

        +

        As the OAuth 1.0a specification states, it should be set to "oob" or a callback URL + (this usually starts with http:// or https://, but you can use any other myapp:// scheme).

        +

        For most OAuth client libraries, you just should provide + "okapi/services/oauth/request_token?oauth_callback=oob" + as the request_token URL, to get it started. Later, probably you'd want to switch "oob" + to something more useful.

        +
      • +
      • +

        The oauth_verifier argument of the access_token method is also required.

        +

        When user authorizes your application, he will receive a PIN code (OAuth verifier). You + have to use this code to receive your Access Token.

        +
      • +
      • +

        Access Tokens do not expire (but can be revoked). This means, that once the user + authorizes your application, you receive a "lifetime access" to his/her account. + User may still revoke access to his account from your + application - when this happens, you will have to redo the authorization dance.

        +
      @@ -240,21 +240,21 @@ method calls and redirects which provide you with an Access Token).

      Basic rules apply:

        -
      • If all goes well, OKAPI will respond with a HTTP 200 status.
      • +
      • If all goes well, OKAPI will respond with a HTTP 200 status.
      • -
      • If there is something wrong with your request, you will get a HTTP 4xx - response (with a JSON object described below). These kind of responses should - trigger some kind of an exception inside your application.
      • +
      • If there is something wrong with your request, you will get a HTTP 4xx + response (with a JSON object described below). These kind of responses should + trigger some kind of an exception inside your application.
      • -
      • If something goes wrong on our part, you will get a HTTP 5xx response. - We will try to fix such errors as soon as possible.
      • +
      • If something goes wrong on our part, you will get a HTTP 5xx response. + We will try to fix such errors as soon as possible.
      • -
      • Sometimes, due to invalid server configuration, you may receive HTTP 200 - instead of HTTP 500. We know that's "unprofessional", but we cannot guarantee - that all OC servers are configured properly - (example). - If you get HTTP 200 and you cannot parse the server response, you should - treat it as HTTP 500.
      • +
      • Sometimes, due to invalid server configuration, you may receive HTTP 200 + instead of HTTP 500. We know that's "unprofessional", but we cannot guarantee + that all OC servers are configured properly + (example). + If you get HTTP 200 and you cannot parse the server response, you should + treat it as HTTP 500.

      Each HTTP 4xx error will be properly described in the response, using a JSON error @@ -265,101 +265,105 @@ need to do this. All other applications are fine with threating all HTTP 4xx err

      The error response is a dictionary with a single error key. Its value contains at least the following keys:

        -
      • developer_message - description of the error,
      • -
      • reason_stack - list of keywords (see below for valid values) which may be - use to subclass exceptions,
      • -
      • status - HTTP status code (the same which you'll get in response headers), -
      • more_info - url pointing to a more detailed description of the error - (or, more probably, to the page you're now reading).
      • +
      • developer_message - description of the error,
      • +
      • reason_stack - list of keywords (see below for valid values) which may be + use to subclass exceptions,
      • +
      • status - HTTP status code (the same which you'll get in response headers), +
      • more_info - url pointing to a more detailed description of the error + (or, more probably, to the page you're now reading).

      Depending on the values on the reason_stack, the error dictionary may contain additional keys. Possible values of the reason_stack include:

        -
      • -

        ["bad_request"] - you've made a bad request. -

        Subclasses:

        -
          -
        • -

          [ ... , "missing_parameter"] - you didn't supply a required - parameter. Extra keys:

          -
            -
          • parameter - the name of the missing parameter.
          • -
          -
        • -
        • -

          [ ... , "invalid_parameter"] - one of your parameters - has invalid value. Extra keys:

          -
            -
          • parameter - the name of the parameter,
          • -
          • whats_wrong_about_it - description of what was wrong about it.
          • -
          -
        • -
        -
      • -
      • -

        ["invalid_oauth_request"] - you've tried to use OAuth, but your request - was invalid.

        -

        Subclasses:

        -
          -
        • -

          [ ... , "unsupported_oauth_version"] - you tried - to use unsupported OAuth version (OKAPI requires OAuth 1.0a).

          -
        • -
        • -

          [ ... , "missing_parameter"] - you didn't supply - a required parameter. Extra keys:

          -
            -
          • parameter - the name of the missing parameter.
          • -
          -
        • -
        • -

          [ ... , "unsupported_signature_method"] - you - tried to use an unsupported OAuth signature method (OKAPI requires - HMAC-SHA1).

          -
        • -
        • -

          [ ... , "invalid_consumer"] - your consumer - does not exist.

          -
        • -
        • -

          [ ... , "invalid_token"] - your token - does not exist. This is pretty common, it may have expired (in case - of request tokens) or may have been revoked (in case of access tokens). - You should ask your user to redo the authorization dance.

          -
        • -
        • -

          [ ... , "invalid_signature"] - your request - signature was invalid.

          -
        • -
        • -

          [ ... , "invalid_timestamp"] - you used a timestamp - which was too far off, compared to the current server time. This is - pretty common, especially when your app is for mobile phones. You should - ask your user to fix the clock or use the provided extra keys to adjust - it yourself. Extra keys:

          -
            -
          • yours - timestamp you have supplied,
          • -
          • ours - timestamp on our server,
          • -
          • difference - the difference (to be added to your clock),
          • -
          • threshold - the maximum allowed difference.
          • -
          -
        • -
        • -

          [ ... , "nonce_already_used"] - most probably, - you have supplied the same request twice (user double-clicked something?). - Or, you have some error in the nonce generator in your OAuth client.

          -
        • -
        -
      • +
      • +

        ["bad_request"] - you've made a bad request. +

        Subclasses:

        +
          +
        • +

          [ ... , "missing_parameter"] - you didn't supply a required + parameter. Extra keys:

          +
            +
          • parameter - the name of the missing parameter.
          • +
          +
        • +
        • +

          [ ... , "invalid_parameter"] - one of your parameters + has invalid value. Extra keys:

          +
            +
          • parameter - the name of the parameter,
          • +
          • whats_wrong_about_it - description of what was wrong about it.
          • +
          +
        • +
        +
      • +
      • +

        ["invalid_oauth_request"] - you've tried to use OAuth, but your request + was invalid.

        +

        Subclasses:

        +
          +
        • +

          [ ... , "unsupported_oauth_version"] - you tried + to use unsupported OAuth version (OKAPI requires OAuth 1.0a).

          +
        • +
        • +

          [ ... , "missing_parameter"] - you didn't supply + a required parameter. Extra keys:

          +
            +
          • parameter - the name of the missing parameter.
          • +
          +
        • +
        • +

          [ ... , "unsupported_signature_method"] - you + tried to use an unsupported OAuth signature method (OKAPI requires + HMAC-SHA1).

          +
        • +
        • +

          [ ... , "invalid_consumer"] - your consumer + does not exist.

          +
        • +
        • +

          [ ... , "invalid_token"] - your token + does not exist. This is pretty common, it may have expired (in case + of request tokens) or may have been revoked (in case of access tokens). + You should ask your user to redo the authorization dance.

          +
        • +
        • +

          [ ... , "invalid_signature"] - your request + signature was invalid.

          +
        • +
        • +

          [ ... , "invalid_timestamp"] - you used a timestamp + which was too far off, compared to the current server time. This is + pretty common, especially when your app is for mobile phones. You should + ask your user to fix the clock or use the provided extra keys to adjust + it yourself. Extra keys:

          +
            +
          • + yours - timestamp you have supplied (this used to be a + string, but now it is being casted to an integer, see + here), +
          • +
          • ours - timestamp on our server,
          • +
          • difference - the difference (to be added to your clock),
          • +
          • threshold - the maximum allowed difference.
          • +
          +
        • +
        • +

          [ ... , "nonce_already_used"] - most probably, + you have supplied the same request twice (user double-clicked something?). + Or, you have some error in the nonce generator in your OAuth client.

          +
        • +
        +

      Almost always, you should be fine with catching just three of those:

        -
      • ["invalid_oauth_request", "invalid_token"] (reauthorize),
      • -
      • ["invalid_oauth_request", "invalid_timestamp"] (adjust the timestamp),
      • -
      • and "any other 4xx status exception" (send yourself a bug report).
      • +
      • ["invalid_oauth_request", "invalid_token"] (reauthorize),
      • +
      • ["invalid_oauth_request", "invalid_timestamp"] (adjust the timestamp),
      • +
      • and "any other 4xx status exception" (send yourself a bug report).
      @@ -384,17 +388,17 @@ if you want.

      OKAPI web services (methods) currently available on this server:

        - -
      • '> -
      • - + +
      • '> -
      • +
      -
      -
      -
      -
      -
      - +
      +
      +
      +
      +
      + diff --git a/htdocs/okapi/views/menu.inc.php b/htdocs/okapi/views/menu.inc.php index 8b3c78d7..7cb42663 100644 --- a/htdocs/okapi/views/menu.inc.php +++ b/htdocs/okapi/views/menu.inc.php @@ -13,63 +13,63 @@ require_once($GLOBALS['rootpath'].'okapi/service_runner.php'); class OkapiMenu { - private static function link($current_path, $link_path, $link_name) - { - return "$link_name
      "; - } + private static function link($current_path, $link_path, $link_name) + { + return "$link_name
      "; + } - /** Get HTML-formatted side menu representation. */ - public static function get_menu_html($current_path = null) - { - $chunks = array(); - if (Okapi::$revision) - $chunks[] = "
      rev. ".Okapi::$revision."
      "; - $chunks[] = "
      "; - $chunks[] = self::link($current_path, "introduction.html", "Introduction"); - $chunks[] = self::link($current_path, "signup.html", "Sign up"); - $chunks[] = self::link($current_path, "examples.html", "Examples"); - $chunks[] = "
      "; + /** Get HTML-formatted side menu representation. */ + public static function get_menu_html($current_path = null) + { + $chunks = array(); + if (Okapi::$revision) + $chunks[] = "
      rev. ".Okapi::$revision."
      "; + $chunks[] = "
      "; + $chunks[] = self::link($current_path, "introduction.html", "Introduction"); + $chunks[] = self::link($current_path, "signup.html", "Sign up"); + $chunks[] = self::link($current_path, "examples.html", "Examples"); + $chunks[] = "
      "; - # We need a list of all methods. We do not need their descriptions, so - # we won't use the apiref/method_index method to get it, the static list - # within OkapiServiceRunner will do. + # We need a list of all methods. We do not need their descriptions, so + # we won't use the apiref/method_index method to get it, the static list + # within OkapiServiceRunner will do. - $methodnames = OkapiServiceRunner::$all_names; - sort($methodnames); + $methodnames = OkapiServiceRunner::$all_names; + sort($methodnames); - # We'll break them up into modules, for readability. + # We'll break them up into modules, for readability. - $module_methods = array(); - foreach ($methodnames as $methodname) - { - $pos = strrpos($methodname, "/"); - $modulename = substr($methodname, 0, $pos); - $method_short_name = substr($methodname, $pos + 1); - if (!isset($module_methods[$modulename])) - $module_methods[$modulename] = array(); - $module_methods[$modulename][] = $method_short_name; - } - $modulenames = array_keys($module_methods); - sort($modulenames); + $module_methods = array(); + foreach ($methodnames as $methodname) + { + $pos = strrpos($methodname, "/"); + $modulename = substr($methodname, 0, $pos); + $method_short_name = substr($methodname, $pos + 1); + if (!isset($module_methods[$modulename])) + $module_methods[$modulename] = array(); + $module_methods[$modulename][] = $method_short_name; + } + $modulenames = array_keys($module_methods); + sort($modulenames); - foreach ($modulenames as $modulename) - { - $chunks[] = "
      $modulename
      "; - $chunks[] = "
      "; - foreach ($module_methods[$modulename] as $method_short_name) - $chunks[] = self::link($current_path, "$modulename/$method_short_name.html", "$method_short_name"); - $chunks[] = "
      "; - } - return implode("", $chunks); - } + foreach ($modulenames as $modulename) + { + $chunks[] = "
      $modulename
      "; + $chunks[] = "
      "; + foreach ($module_methods[$modulename] as $method_short_name) + $chunks[] = self::link($current_path, "$modulename/$method_short_name.html", "$method_short_name"); + $chunks[] = "
      "; + } + return implode("", $chunks); + } - public static function get_installations() - { - $installations = OkapiServiceRunner::call("services/apisrv/installations", - new OkapiInternalRequest(new OkapiInternalConsumer(), null, array())); - foreach ($installations as &$inst_ref) - $inst_ref['selected'] = ($inst_ref['site_url'] == Settings::get('SITE_URL')); - return $installations; - } + public static function get_installations() + { + $installations = OkapiServiceRunner::call("services/apisrv/installations", + new OkapiInternalRequest(new OkapiInternalConsumer(), null, array())); + foreach ($installations as &$inst_ref) + $inst_ref['selected'] = ($inst_ref['site_url'] == Settings::get('SITE_URL')); + return $installations; + } } diff --git a/htdocs/okapi/views/method_call.php b/htdocs/okapi/views/method_call.php index d8bfc20b..34dacfd3 100644 --- a/htdocs/okapi/views/method_call.php +++ b/htdocs/okapi/views/method_call.php @@ -16,15 +16,15 @@ use okapi\views\menu\OkapiMenu; class View { - public static function call($methodname) - { - require_once($GLOBALS['rootpath'].'okapi/service_runner.php'); + public static function call($methodname) + { + require_once($GLOBALS['rootpath'].'okapi/service_runner.php'); - if (!OkapiServiceRunner::exists($methodname)) - throw new BadRequest("Method '$methodname' does not exist. ". - "See OKAPI docs at ".Settings::get('SITE_URL')."okapi/"); - $options = OkapiServiceRunner::options($methodname); - $request = new OkapiHttpRequest($options); - return OkapiServiceRunner::call($methodname, $request); - } + if (!OkapiServiceRunner::exists($methodname)) + throw new BadRequest("Method '$methodname' does not exist. ". + "See OKAPI docs at ".Settings::get('SITE_URL')."okapi/"); + $options = OkapiServiceRunner::options($methodname); + $request = new OkapiHttpRequest($options); + return OkapiServiceRunner::call($methodname, $request); + } } diff --git a/htdocs/okapi/views/method_doc.php b/htdocs/okapi/views/method_doc.php index 902c41c4..0329d3c8 100644 --- a/htdocs/okapi/views/method_doc.php +++ b/htdocs/okapi/views/method_doc.php @@ -17,33 +17,33 @@ use okapi\views\menu\OkapiMenu; class View { - public static function call($methodname) - { - require_once($GLOBALS['rootpath'].'okapi/service_runner.php'); - require_once($GLOBALS['rootpath'].'okapi/views/menu.inc.php'); + public static function call($methodname) + { + require_once($GLOBALS['rootpath'].'okapi/service_runner.php'); + require_once($GLOBALS['rootpath'].'okapi/views/menu.inc.php'); - try - { - $method = OkapiServiceRunner::call('services/apiref/method', new OkapiInternalRequest( - null, null, array('name' => $methodname))); - } - catch (BadRequest $e) - { - throw new Http404(); - } - $vars = array( - 'method' => $method, - 'menu' => OkapiMenu::get_menu_html($methodname.".html"), - 'okapi_base_url' => Settings::get('SITE_URL')."okapi/", - 'installations' => OkapiMenu::get_installations(), - 'okapi_rev' => Okapi::$revision, - ); + try + { + $method = OkapiServiceRunner::call('services/apiref/method', new OkapiInternalRequest( + null, null, array('name' => $methodname))); + } + catch (BadRequest $e) + { + throw new Http404(); + } + $vars = array( + 'method' => $method, + 'menu' => OkapiMenu::get_menu_html($methodname.".html"), + 'okapi_base_url' => Settings::get('SITE_URL')."okapi/", + 'installations' => OkapiMenu::get_installations(), + 'okapi_rev' => Okapi::$revision, + ); - $response = new OkapiHttpResponse(); - $response->content_type = "text/html; charset=utf-8"; - ob_start(); - include 'method_doc.tpl.php'; - $response->body = ob_get_clean(); - return $response; - } + $response = new OkapiHttpResponse(); + $response->content_type = "text/html; charset=utf-8"; + ob_start(); + include 'method_doc.tpl.php'; + $response->body = ob_get_clean(); + return $response; + } } diff --git a/htdocs/okapi/views/method_doc.tpl.php b/htdocs/okapi/views/method_doc.tpl.php index 5ef55114..41d8d03e 100644 --- a/htdocs/okapi/views/method_doc.tpl.php +++ b/htdocs/okapi/views/method_doc.tpl.php @@ -6,88 +6,89 @@ $m = $vars['method']; ?> - - - <?= $m['brief_description'] ?> - OKAPI Reference - - - - - - - -
      -
      - - - - +
      - - + + + <?= $m['brief_description'] ?> - OKAPI Reference + + + + + + + +
      +
      + + + + -
      + + -

      - -
      :: method
      -

      - - - - - - - - - - - - ' id=''> - - - - - - - - - -
      - - - -
      Minimum Authentication: '>Level (see introduction.html#auth_levels'>Authentication Levels)
      -
      - -
      - -
      '> - -
      - - No additional authentication parameters are required. - - Plus required consumer_key argument, assigned for your application. - - Plus required - standard OAuth Consumer signing arguments: - oauth_consumer_key, oauth_nonce, oauth_timestamp, oauth_signature, - oauth_signature_method, oauth_version. - Plus oauth_token - for Token authorization. - -
      -

      Returned value:

      - -
      - -
      '>
      - -
      -
      -
      -
      -
      - +

      + +
      :: method
      +

      + + + + + + + + + + + + ' id=''> + + + + + + + + + +
      + + + +
      Minimum Authentication: '>Level (see introduction.html#auth_levels'>Authentication Levels)
      +
      + +
      + +
      '> + +
      + + No additional authentication parameters are required. + + Plus required consumer_key argument, assigned for your application. + + Plus required + standard OAuth Consumer signing arguments: + oauth_consumer_key, oauth_nonce, oauth_timestamp, oauth_signature, + oauth_signature_method, oauth_version. + + Plus required oauth_token for Token authorization. + + +
      +

      Returned value:

      + +
      + +
      '>
      + +
      +
      +
      +
      +
      + diff --git a/htdocs/okapi/views/signup.php b/htdocs/okapi/views/signup.php index 5c40768e..1342ab7a 100644 --- a/htdocs/okapi/views/signup.php +++ b/htdocs/okapi/views/signup.php @@ -16,65 +16,65 @@ use okapi\OkapiHttpRequest; class View { - public static function call() - { - if (isset($_REQUEST['posted'])) - { - $appname = isset($_REQUEST['appname']) ? $_REQUEST['appname'] : ""; - $appname = trim($appname); - $appurl = isset($_REQUEST['appurl']) ? $_REQUEST['appurl'] : ""; - $email = isset($_REQUEST['email']) ? $_REQUEST['email'] : ""; - $accepted_terms = isset($_REQUEST['terms']) ? $_REQUEST['terms'] : ""; - $ok = false; - if (!$appname) - $notice = "Please provide a valid application name."; - elseif (mb_strlen($appname) > 100) - $notice = "Application name should be less than 100 characters long."; - elseif (mb_strlen($appurl) > 250) - $notice = "Application URL should be less than 250 characters long."; - elseif ($appurl && (substr($appurl, 0, 7) != "http://") && (substr($appurl, 0, 8) != "https://")) - $notice = "Application homepage URL should start with http(s)://. (Note: this URL is OPTIONAL and it is NOT for OAuth callback.)"; - elseif (!$email) - $notice = "Please provide a valid email address."; - elseif (mb_strlen($email) > 70) - $notice = "Email address should be less than 70 characters long."; - elseif (!$accepted_terms) - $notice = "You have to read and accept OKAPI Terms of Use."; - else - { - $ok = true; - Okapi::register_new_consumer($appname, $appurl, $email); - $notice = "Consumer Key generated successfully.\nCheck your email!"; - } - $response = new OkapiHttpResponse(); - $response->content_type = "application/json; charset=utf-8"; - $response->body = json_encode(array( - 'ok' => $ok, - 'notice' => $notice - )); - return $response; - } + public static function call() + { + if (isset($_REQUEST['posted'])) + { + $appname = isset($_REQUEST['appname']) ? $_REQUEST['appname'] : ""; + $appname = trim($appname); + $appurl = isset($_REQUEST['appurl']) ? $_REQUEST['appurl'] : ""; + $email = isset($_REQUEST['email']) ? $_REQUEST['email'] : ""; + $accepted_terms = isset($_REQUEST['terms']) ? $_REQUEST['terms'] : ""; + $ok = false; + if (!$appname) + $notice = "Please provide a valid application name."; + elseif (mb_strlen($appname) > 100) + $notice = "Application name should be less than 100 characters long."; + elseif (mb_strlen($appurl) > 250) + $notice = "Application URL should be less than 250 characters long."; + elseif ($appurl && (substr($appurl, 0, 7) != "http://") && (substr($appurl, 0, 8) != "https://")) + $notice = "Application homepage URL should start with http(s)://. (Note: this URL is OPTIONAL and it is NOT for OAuth callback.)"; + elseif (!$email) + $notice = "Please provide a valid email address."; + elseif (mb_strlen($email) > 70) + $notice = "Email address should be less than 70 characters long."; + elseif (!$accepted_terms) + $notice = "You have to read and accept OKAPI Terms of Use."; + else + { + $ok = true; + Okapi::register_new_consumer($appname, $appurl, $email); + $notice = "Consumer Key generated successfully.\nCheck your email!"; + } + $response = new OkapiHttpResponse(); + $response->content_type = "application/json; charset=utf-8"; + $response->body = json_encode(array( + 'ok' => $ok, + 'notice' => $notice + )); + return $response; + } - require_once($GLOBALS['rootpath'].'okapi/service_runner.php'); - require_once($GLOBALS['rootpath'].'okapi/views/menu.inc.php'); + require_once($GLOBALS['rootpath'].'okapi/service_runner.php'); + require_once($GLOBALS['rootpath'].'okapi/views/menu.inc.php'); - $vars = array( - 'menu' => OkapiMenu::get_menu_html("signup.html"), - 'okapi_base_url' => Settings::get('SITE_URL')."okapi/", - 'site_url' => Settings::get('SITE_URL'), - 'site_name' => Okapi::get_normalized_site_name(), - 'installations' => OkapiMenu::get_installations(), - 'okapi_rev' => Okapi::$revision, - 'data_license_html' => Settings::get('DATA_LICENSE_URL') - ? "Data License" - : "Data License", - ); + $vars = array( + 'menu' => OkapiMenu::get_menu_html("signup.html"), + 'okapi_base_url' => Settings::get('SITE_URL')."okapi/", + 'site_url' => Settings::get('SITE_URL'), + 'site_name' => Okapi::get_normalized_site_name(), + 'installations' => OkapiMenu::get_installations(), + 'okapi_rev' => Okapi::$revision, + 'data_license_html' => Settings::get('DATA_LICENSE_URL') + ? "Data License" + : "Data License", + ); - $response = new OkapiHttpResponse(); - $response->content_type = "text/html; charset=utf-8"; - ob_start(); - include 'signup.tpl.php'; - $response->body = ob_get_clean(); - return $response; - } + $response = new OkapiHttpResponse(); + $response->content_type = "text/html; charset=utf-8"; + ob_start(); + include 'signup.tpl.php'; + $response->body = ob_get_clean(); + return $response; + } } diff --git a/htdocs/okapi/views/signup.tpl.php b/htdocs/okapi/views/signup.tpl.php index d69b87c7..d14111e4 100644 --- a/htdocs/okapi/views/signup.tpl.php +++ b/htdocs/okapi/views/signup.tpl.php @@ -1,126 +1,126 @@ - - - Sign up for an API Key - - - - - - - - -
      -
      - - - - +
      - - + + + Sign up for an API Key + + + + + + + + +
      +
      + + + + -
      + + -

      - Sign up for an API Key -
      :: Opencaching API Reference
      -

      +

      + Sign up for an API Key +
      :: Opencaching API Reference
      +

      -
      - - - - - - - - - - - - - - - - - - -

      * - required fields

      -

      If you want to test OKAPI only, use your email address as application name. - You must provide a valid email address at which we may contact you later.

      -

      Application homepage URL is optional and will be used for hyperlinks, - to allow users to find your application more easily. If provided, it must - be a "http://" web address. (In case you were wondering, it has nothing - to do with OAuth callback.)

      -
      +
      + + + + + + + + + + + + + + + + + + +

      * - required fields

      +

      If you want to test OKAPI only, use your email address as application name. + You must provide a valid email address at which we may contact you later.

      +

      Application homepage URL is optional and will be used for hyperlinks, + to allow users to find your application more easily. If provided, it must + be a "http://" web address. (In case you were wondering, it has nothing + to do with OAuth callback.)

      +
      -

      Terms of Use

      +

      Terms of Use

      -

      When using data from you must - attribute it to the author and to in the form - specifyed in the . - OKAPI supports you in doing so by either appending the proper attribution to - geocache descriptions or providing it in a separate data field (if you are not - showing cache descriptions you must attribute the data using this separate field). - Refer to the geocache method - for further advice.

      +

      When using data from you must + attribute it to the author and to in the form + specifyed in the . + OKAPI supports you in doing so by either appending the proper attribution to + geocache descriptions or providing it in a separate data field (if you are not + showing cache descriptions you must attribute the data using this separate field). + Refer to the geocache method + for further advice.

      -

      You must comply with the requirements of the - when reproducing, displaying, copying, - formatting, compiling, or otherwise using Geocaching Data acquired through OKAPI.

      -

      When possible must be a clickable link - to site and if the information being displayed is - about a particular geocache, it should link to that geocache's page on - .

      -

      You may not:

      -
        -
      • Copy, publish, compile, print, display, format, assemble, or otherwise use Geocaching Data - acquired through OKAPI in a manner contrary to - .
      • -
      • Delete, obscure, modify, or in any manner alter any warning, notice, attribution, or link - that appears in OKAPI or the served content.
      • -
      • Hide, modify, or otherwise obscure your identity, your system's identity, or your user's - account information, when using OKAPI.
      • -
      -
      -
      -
      -
      -
      -
      - +

      You must comply with the requirements of the + when reproducing, displaying, copying, + formatting, compiling, or otherwise using Geocaching Data acquired through OKAPI.

      +

      When possible must be a clickable link + to site and if the information being displayed is + about a particular geocache, it should link to that geocache's page on + .

      +

      You may not:

      +
        +
      • Copy, publish, compile, print, display, format, assemble, or otherwise use Geocaching Data + acquired through OKAPI in a manner contrary to + .
      • +
      • Delete, obscure, modify, or in any manner alter any warning, notice, attribution, or link + that appears in OKAPI or the served content.
      • +
      • Hide, modify, or otherwise obscure your identity, your system's identity, or your user's + account information, when using OKAPI.
      • +
      +
      +
      +
      +
      +
      +
      + diff --git a/htdocs/okapi/views/tilestress.php b/htdocs/okapi/views/tilestress.php index 10981c3d..75207a41 100644 --- a/htdocs/okapi/views/tilestress.php +++ b/htdocs/okapi/views/tilestress.php @@ -24,74 +24,74 @@ require_once($GLOBALS['rootpath']."okapi/service_runner.php"); class View { - public static function out($str) - { - print $str; - flush(); - } + public static function out($str) + { + print $str; + flush(); + } - public static function call() - { - # By default, this view is turned off in the urls.php file. - # This view is for debugging TileMap performace only! + public static function call() + { + # By default, this view is turned off in the urls.php file. + # This view is for debugging TileMap performace only! - set_time_limit(0); + set_time_limit(0); - header("Content-Type: text/plain; charset=utf-8"); + header("Content-Type: text/plain; charset=utf-8"); - $user_id = $_GET['u']; - self::out("Yo. I'm $user_id.\n\n"); + $user_id = $_GET['u']; + self::out("Yo. I'm $user_id.\n\n"); - while (true) - { - srand(floor(time() / 10)); - $mode2 = rand(0, 9) <= 7; - if ($mode2) { - $row = Db::select_row(" - select z, x, y - from okapi_tile_status - where status = 2 and z < 20 - order by rand() - limit 1; - "); - $z = $row['z'] + 1; - $x = $row['x'] << 1; - $y = $row['y'] << 1; - $x += rand(0, 1); - $y += rand(0, 1); - } else { - $z = rand(5, 21); - $x = rand(0, (1 << $z) - 1); - $y = rand(0, (1 << $z) - 1); - } + while (true) + { + srand(floor(time() / 10)); + $mode2 = rand(0, 9) <= 7; + if ($mode2) { + $row = Db::select_row(" + select z, x, y + from okapi_tile_status + where status = 2 and z < 20 + order by rand() + limit 1; + "); + $z = $row['z'] + 1; + $x = $row['x'] << 1; + $y = $row['y'] << 1; + $x += rand(0, 1); + $y += rand(0, 1); + } else { + $z = rand(5, 21); + $x = rand(0, (1 << $z) - 1); + $y = rand(0, (1 << $z) - 1); + } - $tiles = array(); - for ($xx=$x; $xx<$x+4; $xx++) - for ($yy=$y; $yy<$y+4; $yy++) - $tiles[] = array($xx, $yy); - srand(); - shuffle($tiles); + $tiles = array(); + for ($xx=$x; $xx<$x+4; $xx++) + for ($yy=$y; $yy<$y+4; $yy++) + $tiles[] = array($xx, $yy); + srand(); + shuffle($tiles); - foreach ($tiles as $tile) - { - list($x, $y) = $tile; - self::out("Loading ".str_pad("($z, $x, $y)... ", 30)); - $time_started = microtime(true); - try { - $response = OkapiServiceRunner::call('services/caches/map/tile', new OkapiInternalRequest( - new OkapiInternalConsumer(), new OkapiInternalAccessToken($user_id), - array('z' => "$z", 'x' => "$x", 'y' => "$y"))); - $runtime = microtime(true) - $time_started; - $ds = floor($runtime * 100); - self::out(str_repeat("#", $ds)." "); - $b = floor(strlen($response->get_body()) / 256); - self::out(str_repeat("@", $b)."\n"); - } catch (Exception $e) { - self::out("\n\n".OkapiExceptionHandler::get_exception_info($e)); - die(); - } - } - } - } + foreach ($tiles as $tile) + { + list($x, $y) = $tile; + self::out("Loading ".str_pad("($z, $x, $y)... ", 30)); + $time_started = microtime(true); + try { + $response = OkapiServiceRunner::call('services/caches/map/tile', new OkapiInternalRequest( + new OkapiInternalConsumer(), new OkapiInternalAccessToken($user_id), + array('z' => "$z", 'x' => "$x", 'y' => "$y"))); + $runtime = microtime(true) - $time_started; + $ds = floor($runtime * 100); + self::out(str_repeat("#", $ds)." "); + $b = floor(strlen($response->get_body()) / 256); + self::out(str_repeat("@", $b)."\n"); + } catch (Exception $e) { + self::out("\n\n".OkapiExceptionHandler::get_exception_info($e)); + die(); + } + } + } + } } diff --git a/htdocs/okapi/views/update.php b/htdocs/okapi/views/update.php index 92355d60..0eac05dd 100644 --- a/htdocs/okapi/views/update.php +++ b/htdocs/okapi/views/update.php @@ -17,664 +17,692 @@ use okapi\OkapiInternalRequest; use okapi\Settings; use okapi\OkapiLock; use okapi\cronjobs\CronJobController; +use okapi\services\replicate\ReplicateCommon; require_once($GLOBALS['rootpath']."okapi/cronjobs.php"); class View { - public static function get_current_version() - { - try - { - return Okapi::get_var('db_version', 0) + 0; - } - catch (Exception $e) - { - if (strpos($e->getMessage(), "okapi_vars' doesn't exist") !== false) - return 0; - throw $e; - } - } + public static function get_current_version() + { + try + { + return Okapi::get_var('db_version', 0) + 0; + } + catch (Exception $e) + { + if (strpos($e->getMessage(), "okapi_vars' doesn't exist") !== false) + return 0; + throw $e; + } + } - public static function get_max_version() - { - $max_db_version = 0; - foreach (get_class_methods(__CLASS__) as $name) - { - if (strpos($name, "ver") === 0) - { - $ver = substr($name, 3) + 0; - if ($ver > $max_db_version) - $max_db_version = $ver; - } - } - return $max_db_version; - } + public static function get_max_version() + { + $max_db_version = 0; + foreach (get_class_methods(__CLASS__) as $name) + { + if (strpos($name, "ver") === 0) + { + $ver = substr($name, 3) + 0; + if ($ver > $max_db_version) + $max_db_version = $ver; + } + } + return $max_db_version; + } - public static function out($str) - { - print $str; - # No ob_flush(). Output buffering should not be started (see controller.php). - # Therefore, calling ob_flush would give an error. - flush(); - } + public static function out($str) + { + print $str; + # No ob_flush(). Output buffering should not be started (see controller.php). + # Therefore, calling ob_flush would give an error. + flush(); + } - public static function call() - { - # First, let's acquire a lock to make sure the update isn't already running. + public static function call() + { + # First, let's acquire a lock to make sure the update isn't already running. - $lock = OkapiLock::get('db-update'); - $lock->acquire(); + $lock = OkapiLock::get('db-update'); + $lock->acquire(); - try - { - self::_call(); - $lock->release(); - } - catch (Exception $e) - { - # Error occured. Make sure the lock is released and rethrow. + try + { + self::_call(); + $lock->release(); + } + catch (Exception $e) + { + # Error occured. Make sure the lock is released and rethrow. - $lock->release(); - throw $e; - } - } + $lock->release(); + throw $e; + } + } - private static function _call() - { - ignore_user_abort(true); - set_time_limit(0); + private static function _call() + { + ignore_user_abort(true); + set_time_limit(0); - header("Content-Type: text/plain; charset=utf-8"); + header("Content-Type: text/plain; charset=utf-8"); - $current_ver = self::get_current_version(); - $max_ver = self::get_max_version(); - self::out("Current OKAPI database version: $current_ver\n"); - if ($current_ver == 0 && ((!isset($_GET['install'])) || ($_GET['install'] != 'true'))) - { - self::out("Current OKAPI settings are:\n\n".Settings::describe_settings()."\n\n". - "Make sure they are correct, then append '?install=true' to your query."); - return; - } - elseif ($max_ver == $current_ver) - { - self::out("It is up-to-date.\n\n"); - } - elseif ($max_ver < $current_ver) - throw new Exception(); - else - { - self::out("Updating to version $max_ver... PLEASE WAIT\n\n"); + $current_ver = self::get_current_version(); + $max_ver = self::get_max_version(); + self::out("Current OKAPI database version: $current_ver\n"); + if ($current_ver == 0 && ((!isset($_GET['install'])) || ($_GET['install'] != 'true'))) + { + self::out("Current OKAPI settings are:\n\n".Settings::describe_settings()."\n\n". + "Make sure they are correct, then append '?install=true' to your query."); + try + { + Db::select_value("select 1 from caches limit 1"); + } + catch (Exception $e) + { + self::out( + "\n\n". + "IMPORTANT: If you're trying to install OKAPI on an empty database then\n". + "you will fail. OKAPI is not a standalone application, it is a plugin\n". + "for Opencaching sites. Please read this:\n\n". + "https://code.google.com/p/opencaching-api/issues/detail?id=299" + ); + } + return; + } + elseif ($max_ver == $current_ver) + { + self::out("It is up-to-date.\n\n"); + } + elseif ($max_ver < $current_ver) + throw new Exception(); + else + { + self::out("Updating to version $max_ver... PLEASE WAIT\n\n"); - while ($current_ver < $max_ver) - { - $version_to_apply = $current_ver + 1; - self::out("Applying mutation #$version_to_apply..."); - try { - call_user_func(array(__CLASS__, "ver".$version_to_apply)); - self::out(" OK!\n"); - Okapi::set_var('db_version', $version_to_apply); - $current_ver += 1; - } catch (Exception $e) { - self::out(" ERROR\n\n"); - throw $e; - } - } - self::out("\nDatabase updated.\n\n"); - } + while ($current_ver < $max_ver) + { + $version_to_apply = $current_ver + 1; + self::out("Applying mutation #$version_to_apply..."); + try { + call_user_func(array(__CLASS__, "ver".$version_to_apply)); + self::out(" OK!\n"); + Okapi::set_var('db_version', $version_to_apply); + $current_ver += 1; + } catch (Exception $e) { + self::out(" ERROR\n\n"); + throw $e; + } + } + self::out("\nDatabase updated.\n\n"); + } - self::out("Registering new cronjobs...\n"); - # Validate all cronjobs (some might have been added). - Okapi::set_var("cron_nearest_event", 0); - Okapi::execute_prerequest_cronjobs(); + self::out("Registering new cronjobs...\n"); + # Validate all cronjobs (some might have been added). + Okapi::set_var("cron_nearest_event", 0); + Okapi::execute_prerequest_cronjobs(); - self::out("\nUpdate complete.\n"); - } + self::out("\nUpdate complete.\n"); + } - /** - * Return the list of email addresses of developers who used any of the given - * method names at least once. If $days is not null, then only consumers which - * used the method in last X $days will be returned. - */ - public static function get_consumers_of($service_names, $days = null) - { - return Db::select_column(" - select distinct c.email - from - okapi_consumers c, - okapi_stats_hourly sh - where - sh.consumer_key = c.`key` - and sh.service_name in ('".implode("','", array_map('mysql_real_escape_string', $service_names))."') - ".(($days != null) ? "and sh.period_start > date_add(now(), interval '".mysql_real_escape_string(-$days)."' day)" : "")." - "); - } + /** + * Return the list of email addresses of developers who used any of the given + * method names at least once. If $days is not null, then only consumers which + * used the method in last X $days will be returned. + */ + public static function get_consumers_of($service_names, $days = null) + { + return Db::select_column(" + select distinct c.email + from + okapi_consumers c, + okapi_stats_hourly sh + where + sh.consumer_key = c.`key` + and sh.service_name in ('".implode("','", array_map('mysql_real_escape_string', $service_names))."') + ".(($days != null) ? "and sh.period_start > date_add(now(), interval '".mysql_real_escape_string(-$days)."' day)" : "")." + "); + } - private static function ver1() - { - ob_start(); - print "Hi!\n\n"; - print "Since this is your first time you run okapi/update, you should be\n"; - print "prepared to receive a bunch of update-history emails. These are all\n"; - print "the emails that all the other OC admins received \"over the years\",\n"; - print "whenever they have updated OKAPI.\n\n"; - print "Each email describes an action which OKAPI performed on your\n"; - print "database, OR which we need YOU to perform.\n\n"; - print "If you receive any error messages during the update process,\n"; - print "please contact me - rygielski@mimuw.edu.pl.\n\n"; - print "-- \n"; - print "Wojciech Rygielski, OKAPI developer"; - Okapi::mail_admins("Starting OKAPI installation", ob_get_clean()); + private static function ver1() + { + ob_start(); + print "Hi!\n\n"; + print "Since this is your first time you run okapi/update, you should be\n"; + print "prepared to receive a bunch of update-history emails. These are all\n"; + print "the emails that all the other OC admins received \"over the years\",\n"; + print "whenever they have updated OKAPI.\n\n"; + print "Each email describes an action which OKAPI performed on your\n"; + print "database, OR which we need YOU to perform.\n\n"; + print "If you receive any error messages during the update process,\n"; + print "please contact me - rygielski@mimuw.edu.pl.\n\n"; + print "-- \n"; + print "Wojciech Rygielski, OKAPI developer"; + Okapi::mail_admins("Starting OKAPI installation", ob_get_clean()); - Db::execute(" - CREATE TABLE okapi_vars ( - var varchar(32) charset ascii collate ascii_bin NOT NULL, - value text, - PRIMARY KEY (var) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - "); - } + Db::execute(" + CREATE TABLE okapi_vars ( + var varchar(32) charset ascii collate ascii_bin NOT NULL, + value text, + PRIMARY KEY (var) + ) ENGINE=MyISAM DEFAULT CHARSET=utf8; + "); + } - private static function ver2() - { - Db::execute(" - CREATE TABLE okapi_authorizations ( - consumer_key varchar(20) charset ascii collate ascii_bin NOT NULL, - user_id int(11) NOT NULL, - last_access_token datetime default NULL, - PRIMARY KEY (consumer_key,user_id) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - "); - } + private static function ver2() + { + Db::execute(" + CREATE TABLE okapi_authorizations ( + consumer_key varchar(20) charset ascii collate ascii_bin NOT NULL, + user_id int(11) NOT NULL, + last_access_token datetime default NULL, + PRIMARY KEY (consumer_key,user_id) + ) ENGINE=MyISAM DEFAULT CHARSET=utf8; + "); + } - private static function ver3() - { - Db::execute(" - CREATE TABLE okapi_consumers ( - `key` varchar(20) charset ascii collate ascii_bin NOT NULL, - name varchar(100) collate utf8_general_ci NOT NULL, - secret varchar(40) charset ascii collate ascii_bin NOT NULL, - url varchar(250) collate utf8_general_ci default NULL, - email varchar(70) collate utf8_general_ci default NULL, - date_created datetime NOT NULL, - PRIMARY KEY (`key`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - "); - } + private static function ver3() + { + Db::execute(" + CREATE TABLE okapi_consumers ( + `key` varchar(20) charset ascii collate ascii_bin NOT NULL, + name varchar(100) collate utf8_general_ci NOT NULL, + secret varchar(40) charset ascii collate ascii_bin NOT NULL, + url varchar(250) collate utf8_general_ci default NULL, + email varchar(70) collate utf8_general_ci default NULL, + date_created datetime NOT NULL, + PRIMARY KEY (`key`) + ) ENGINE=MyISAM DEFAULT CHARSET=utf8; + "); + } - private static function ver4() - { - Db::execute(" - CREATE TABLE okapi_nonces ( - consumer_key varchar(20) charset ascii collate ascii_bin NOT NULL, - `key` varchar(255) charset ascii collate ascii_bin NOT NULL, - timestamp int(10) NOT NULL, - PRIMARY KEY (consumer_key, `key`, `timestamp`) - ) ENGINE=MEMORY DEFAULT CHARSET=utf8; - "); - } + private static function ver4() + { + Db::execute(" + CREATE TABLE okapi_nonces ( + consumer_key varchar(20) charset ascii collate ascii_bin NOT NULL, + `key` varchar(255) charset ascii collate ascii_bin NOT NULL, + timestamp int(10) NOT NULL, + PRIMARY KEY (consumer_key, `key`, `timestamp`) + ) ENGINE=MEMORY DEFAULT CHARSET=utf8; + "); + } - private static function ver5() - { - Db::execute(" - CREATE TABLE okapi_tokens ( - `key` varchar(20) charset ascii collate ascii_bin NOT NULL, - secret varchar(40) charset ascii collate ascii_bin NOT NULL, - token_type enum('request','access') NOT NULL, - timestamp int(10) NOT NULL, - user_id int(10) default NULL, - consumer_key varchar(20) charset ascii collate ascii_bin NOT NULL, - verifier varchar(10) charset ascii collate ascii_bin default NULL, - callback varchar(2083) character set utf8 collate utf8_general_ci default NULL, - PRIMARY KEY (`key`), - KEY by_consumer (consumer_key) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - "); - } + private static function ver5() + { + Db::execute(" + CREATE TABLE okapi_tokens ( + `key` varchar(20) charset ascii collate ascii_bin NOT NULL, + secret varchar(40) charset ascii collate ascii_bin NOT NULL, + token_type enum('request','access') NOT NULL, + timestamp int(10) NOT NULL, + user_id int(10) default NULL, + consumer_key varchar(20) charset ascii collate ascii_bin NOT NULL, + verifier varchar(10) charset ascii collate ascii_bin default NULL, + callback varchar(2083) character set utf8 collate utf8_general_ci default NULL, + PRIMARY KEY (`key`), + KEY by_consumer (consumer_key) + ) ENGINE=MyISAM DEFAULT CHARSET=utf8; + "); + } - private static function ver6() - { - # Removed this update. It seemed dangerous to run such updates on unknown OC installations. - } + private static function ver6() + { + # Removed this update. It seemed dangerous to run such updates on unknown OC installations. + } - private static function ver7() - { - # In fact, this should be "alter cache_logs add column okapi_consumer_key...", but - # I don't want for OKAPI to mess with the rest of DB. Keeping it separete for now. - # One day, this table could come in handy. See: - # http://code.google.com/p/opencaching-api/issues/detail?id=64 - Db::execute(" - CREATE TABLE okapi_cache_logs ( - log_id int(11) NOT NULL, - consumer_key varchar(20) charset ascii collate ascii_bin NOT NULL, - PRIMARY KEY (log_id), - KEY by_consumer (consumer_key) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - "); - } + private static function ver7() + { + # In fact, this should be "alter cache_logs add column okapi_consumer_key...", but + # I don't want for OKAPI to mess with the rest of DB. Keeping it separete for now. + # One day, this table could come in handy. See: + # http://code.google.com/p/opencaching-api/issues/detail?id=64 + Db::execute(" + CREATE TABLE okapi_cache_logs ( + log_id int(11) NOT NULL, + consumer_key varchar(20) charset ascii collate ascii_bin NOT NULL, + PRIMARY KEY (log_id), + KEY by_consumer (consumer_key) + ) ENGINE=MyISAM DEFAULT CHARSET=utf8; + "); + } - private static function ver8() - { - Db::execute(" - CREATE TABLE okapi_cache ( - `key` varchar(32) NOT NULL, - value blob, - expires datetime, - PRIMARY KEY (`key`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - "); - } + private static function ver8() + { + Db::execute(" + CREATE TABLE okapi_cache ( + `key` varchar(32) NOT NULL, + value blob, + expires datetime, + PRIMARY KEY (`key`) + ) ENGINE=MyISAM DEFAULT CHARSET=utf8; + "); + } - private static function ver9() { Db::execute("alter table okapi_consumers modify column `key` varchar(20) not null"); } - private static function ver10() { Db::execute("alter table okapi_consumers modify column secret varchar(40) not null"); } - private static function ver11() { Db::execute("alter table okapi_tokens modify column `key` varchar(20) not null"); } - private static function ver12() { Db::execute("alter table okapi_tokens modify column secret varchar(40) not null"); } - private static function ver13() { Db::execute("alter table okapi_tokens modify column consumer_key varchar(20) not null"); } - private static function ver14() { Db::execute("alter table okapi_tokens modify column verifier varchar(10) default null"); } - private static function ver15() { Db::execute("alter table okapi_authorizations modify column consumer_key varchar(20) not null"); } - private static function ver16() { Db::execute("alter table okapi_nonces modify column consumer_key varchar(20) not null"); } - private static function ver17() { Db::execute("alter table okapi_nonces modify column `key` varchar(255) not null"); } - private static function ver18() { Db::execute("alter table okapi_cache_logs modify column consumer_key varchar(20) not null"); } - private static function ver19() { Db::execute("alter table okapi_vars modify column `var` varchar(32) not null"); } + private static function ver9() { Db::execute("alter table okapi_consumers modify column `key` varchar(20) not null"); } + private static function ver10() { Db::execute("alter table okapi_consumers modify column secret varchar(40) not null"); } + private static function ver11() { Db::execute("alter table okapi_tokens modify column `key` varchar(20) not null"); } + private static function ver12() { Db::execute("alter table okapi_tokens modify column secret varchar(40) not null"); } + private static function ver13() { Db::execute("alter table okapi_tokens modify column consumer_key varchar(20) not null"); } + private static function ver14() { Db::execute("alter table okapi_tokens modify column verifier varchar(10) default null"); } + private static function ver15() { Db::execute("alter table okapi_authorizations modify column consumer_key varchar(20) not null"); } + private static function ver16() { Db::execute("alter table okapi_nonces modify column consumer_key varchar(20) not null"); } + private static function ver17() { Db::execute("alter table okapi_nonces modify column `key` varchar(255) not null"); } + private static function ver18() { Db::execute("alter table okapi_cache_logs modify column consumer_key varchar(20) not null"); } + private static function ver19() { Db::execute("alter table okapi_vars modify column `var` varchar(32) not null"); } - private static function ver20() { Db::execute("alter table okapi_consumers modify column `key` varchar(20) collate utf8_bin not null"); } - private static function ver21() { Db::execute("alter table okapi_consumers modify column secret varchar(40) collate utf8_bin not null"); } - private static function ver22() { Db::execute("alter table okapi_tokens modify column `key` varchar(20) collate utf8_bin not null"); } - private static function ver23() { Db::execute("alter table okapi_tokens modify column secret varchar(40) collate utf8_bin not null"); } - private static function ver24() { Db::execute("alter table okapi_tokens modify column consumer_key varchar(20) collate utf8_bin not null"); } - private static function ver25() { Db::execute("alter table okapi_tokens modify column verifier varchar(10) collate utf8_bin default null"); } - private static function ver26() { Db::execute("alter table okapi_authorizations modify column consumer_key varchar(20) collate utf8_bin not null"); } - private static function ver27() { Db::execute("alter table okapi_nonces modify column consumer_key varchar(20) collate utf8_bin not null"); } - private static function ver28() { Db::execute("alter table okapi_nonces modify column `key` varchar(255) collate utf8_bin not null"); } - private static function ver29() { Db::execute("alter table okapi_cache_logs modify column consumer_key varchar(20) collate utf8_bin not null"); } - private static function ver30() { Db::execute("alter table okapi_vars modify column `var` varchar(32) collate utf8_bin not null"); } + private static function ver20() { Db::execute("alter table okapi_consumers modify column `key` varchar(20) collate utf8_bin not null"); } + private static function ver21() { Db::execute("alter table okapi_consumers modify column secret varchar(40) collate utf8_bin not null"); } + private static function ver22() { Db::execute("alter table okapi_tokens modify column `key` varchar(20) collate utf8_bin not null"); } + private static function ver23() { Db::execute("alter table okapi_tokens modify column secret varchar(40) collate utf8_bin not null"); } + private static function ver24() { Db::execute("alter table okapi_tokens modify column consumer_key varchar(20) collate utf8_bin not null"); } + private static function ver25() { Db::execute("alter table okapi_tokens modify column verifier varchar(10) collate utf8_bin default null"); } + private static function ver26() { Db::execute("alter table okapi_authorizations modify column consumer_key varchar(20) collate utf8_bin not null"); } + private static function ver27() { Db::execute("alter table okapi_nonces modify column consumer_key varchar(20) collate utf8_bin not null"); } + private static function ver28() { Db::execute("alter table okapi_nonces modify column `key` varchar(255) collate utf8_bin not null"); } + private static function ver29() { Db::execute("alter table okapi_cache_logs modify column consumer_key varchar(20) collate utf8_bin not null"); } + private static function ver30() { Db::execute("alter table okapi_vars modify column `var` varchar(32) collate utf8_bin not null"); } - private static function ver31() - { - Db::execute(" - CREATE TABLE `okapi_stats_temp` ( - `datetime` datetime NOT NULL, - `consumer_key` varchar(32) NOT NULL DEFAULT 'internal', - `user_id` int(10) NOT NULL DEFAULT '-1', - `service_name` varchar(80) NOT NULL, - `calltype` enum('internal','http') NOT NULL, - `runtime` float NOT NULL DEFAULT '0' - ) ENGINE=MEMORY DEFAULT CHARSET=utf8 - "); - } + private static function ver31() + { + Db::execute(" + CREATE TABLE `okapi_stats_temp` ( + `datetime` datetime NOT NULL, + `consumer_key` varchar(32) NOT NULL DEFAULT 'internal', + `user_id` int(10) NOT NULL DEFAULT '-1', + `service_name` varchar(80) NOT NULL, + `calltype` enum('internal','http') NOT NULL, + `runtime` float NOT NULL DEFAULT '0' + ) ENGINE=MEMORY DEFAULT CHARSET=utf8 + "); + } - private static function ver32() - { - Db::execute(" - CREATE TABLE `okapi_stats_hourly` ( - `consumer_key` varchar(32) NOT NULL, - `user_id` int(10) NOT NULL, - `period_start` datetime NOT NULL, - `service_name` varchar(80) NOT NULL, - `total_calls` int(10) NOT NULL, - `http_calls` int(10) NOT NULL, - `total_runtime` float NOT NULL DEFAULT '0', - `http_runtime` float NOT NULL DEFAULT '0', - PRIMARY KEY (`consumer_key`,`user_id`,`period_start`,`service_name`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8 - "); - } + private static function ver32() + { + Db::execute(" + CREATE TABLE `okapi_stats_hourly` ( + `consumer_key` varchar(32) NOT NULL, + `user_id` int(10) NOT NULL, + `period_start` datetime NOT NULL, + `service_name` varchar(80) NOT NULL, + `total_calls` int(10) NOT NULL, + `http_calls` int(10) NOT NULL, + `total_runtime` float NOT NULL DEFAULT '0', + `http_runtime` float NOT NULL DEFAULT '0', + PRIMARY KEY (`consumer_key`,`user_id`,`period_start`,`service_name`) + ) ENGINE=MyISAM DEFAULT CHARSET=utf8 + "); + } - private static function ver33() - { - try - { - Db::execute("alter table cache_logs add key `uuid` (`uuid`)"); - } - catch (Exception $e) - { - // key exists - } - } + private static function ver33() + { + try + { + Db::execute("alter table cache_logs add key `uuid` (`uuid`)"); + } + catch (Exception $e) + { + // key exists + } + } - private static function ver34() - { - Db::execute(" - CREATE TABLE `okapi_clog` ( - id int(10) not null auto_increment, - data blob default null, - PRIMARY KEY (id) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8 - "); - } + private static function ver34() + { + Db::execute(" + CREATE TABLE `okapi_clog` ( + id int(10) not null auto_increment, + data blob default null, + PRIMARY KEY (id) + ) ENGINE=MyISAM DEFAULT CHARSET=utf8 + "); + } - private static function ver35() - { - # Inform the admin about the new cronjobs. - Okapi::mail_admins( - "Additional setup needed: cronjobs support", - "Hello there, you've just updated OKAPI on your server. Thanks!\n\n". - "We need you to do one more thing. This version of OKAPI requires\n". - "additional crontab entry. Please add the following line to your crontab:\n\n". - "*/5 * * * * wget -O - -q -t 1 ".Settings::get('SITE_URL')."okapi/cron5\n\n". - "This is required for OKAPI to function properly from now on.\n\n". - "-- \n". - "Thanks, OKAPI developers." - ); - } + private static function ver35() + { + # Inform the admin about the new cronjobs. + Okapi::mail_admins( + "Additional setup needed: cronjobs support", + "Hello there, you've just updated OKAPI on your server. Thanks!\n\n". + "We need you to do one more thing. This version of OKAPI requires\n". + "additional crontab entry. Please add the following line to your crontab:\n\n". + "*/5 * * * * wget -O - -q -t 1 ".Settings::get('SITE_URL')."okapi/cron5\n\n". + "This is required for OKAPI to function properly from now on.\n\n". + "-- \n". + "Thanks, OKAPI developers." + ); + } - private static function ver36() { Db::execute("alter table okapi_cache modify column `key` varchar(64) not null"); } - private static function ver37() { Db::execute("delete from okapi_vars where var='last_clog_update'"); } - private static function ver38() { Db::execute("alter table okapi_clog modify column data mediumblob"); } - private static function ver39() { Db::execute("delete from okapi_clog"); } - private static function ver40() { Db::execute("alter table okapi_cache modify column value mediumblob"); } + private static function ver36() { Db::execute("alter table okapi_cache modify column `key` varchar(64) not null"); } + private static function ver37() { Db::execute("delete from okapi_vars where var='last_clog_update'"); } + private static function ver38() { Db::execute("alter table okapi_clog modify column data mediumblob"); } + private static function ver39() { Db::execute("delete from okapi_clog"); } + private static function ver40() { Db::execute("alter table okapi_cache modify column value mediumblob"); } - private static function ver41() - { - # Force changelog reset (will be produced one day back) - Db::execute("delete from okapi_vars where var='last_clog_update'"); + private static function ver41() + { + # Force changelog reset (will be produced one day back) + Db::execute("delete from okapi_vars where var='last_clog_update'"); - # Force all cronjobs rerun - Okapi::set_var("cron_nearest_event", 0); - Cache::delete('cron_schedule'); - } + # Force all cronjobs rerun + Okapi::set_var("cron_nearest_event", 0); + Cache::delete('cron_schedule'); + } - private static function ver42() { Db::execute("delete from okapi_cache where length(value) = 65535"); } + private static function ver42() { Db::execute("delete from okapi_cache where length(value) = 65535"); } - private static function ver43() - { - $emails = self::get_consumers_of(array('services/replicate/changelog', 'services/replicate/fulldump'), 14); - ob_start(); - print "Hi!\n\n"; - print "We send this email to all developers who used 'replicate' module\n"; - print "in last 14 days. Thank you for testing our BETA-status module.\n\n"; - print "As you probably know, BETA status implies that we may decide to\n"; - print "modify something in a backward-incompatible way. One of such\n"; - print "modifications just happened and it may concern you.\n\n"; - print "We removed 'attrnames' from the list of synchronized fields of\n"; - print "'geocache'-type objects. Watch our blog for updates!\n\n"; - print "-- \n"; - print "OKAPI Team"; - Okapi::mail_from_okapi($emails, "A change in the 'replicate' module.", ob_get_clean()); - } + private static function ver43() + { + $emails = self::get_consumers_of(array('services/replicate/changelog', 'services/replicate/fulldump'), 14); + ob_start(); + print "Hi!\n\n"; + print "We send this email to all developers who used 'replicate' module\n"; + print "in last 14 days. Thank you for testing our BETA-status module.\n\n"; + print "As you probably know, BETA status implies that we may decide to\n"; + print "modify something in a backward-incompatible way. One of such\n"; + print "modifications just happened and it may concern you.\n\n"; + print "We removed 'attrnames' from the list of synchronized fields of\n"; + print "'geocache'-type objects. Watch our blog for updates!\n\n"; + print "-- \n"; + print "OKAPI Team"; + Okapi::mail_from_okapi($emails, "A change in the 'replicate' module.", ob_get_clean()); + } - private static function ver44() { Db::execute("alter table caches add column okapi_syncbase timestamp not null after last_modified;"); } - private static function ver45() { Db::execute("update caches set okapi_syncbase=last_modified;"); } - private static function ver46() { /* no longer necessary */ } + private static function ver44() { Db::execute("alter table caches add column okapi_syncbase timestamp not null after last_modified;"); } + private static function ver45() { Db::execute("update caches set okapi_syncbase=last_modified;"); } + private static function ver46() { /* no longer necessary */ } - private static function ver47() - { - Db::execute(" - update caches - set okapi_syncbase=now() - where cache_id in ( - select cache_id - from cache_logs - where date_created > '2012-03-11' -- the day when 'replicate' module was introduced - ); - "); - } + private static function ver47() + { + Db::execute(" + update caches + set okapi_syncbase=now() + where cache_id in ( + select cache_id + from cache_logs + where date_created > '2012-03-11' -- the day when 'replicate' module was introduced + ); + "); + } - private static function ver48() - { - ob_start(); - print "Hi!\n\n"; - print "OKAPI just added additional field (along with an index) 'okapi_syncbase'\n"; - print "on your 'caches' table. It is required by OKAPI's 'replicate' module to\n"; - print "function properly.\n\n"; - self::print_common_db_alteration_info(); - print "-- \n"; - print "OKAPI Team"; - Okapi::mail_admins("Database modification notice: caches.okapi_syncbase", ob_get_clean()); - } + private static function ver48() + { + ob_start(); + print "Hi!\n\n"; + print "OKAPI just added additional field (along with an index) 'okapi_syncbase'\n"; + print "on your 'caches' table. It is required by OKAPI's 'replicate' module to\n"; + print "function properly.\n\n"; + self::print_common_db_alteration_info(); + print "-- \n"; + print "OKAPI Team"; + Okapi::mail_admins("Database modification notice: caches.okapi_syncbase", ob_get_clean()); + } - private static function print_common_db_alteration_info() - { - print "-- About OKAPI's database modifications --\n\n"; - print "OKAPI takes care of its own tables (the ones with the \"okapi_\"\n"; - print "prefix), but it won't usually alter other tables in your\n"; - print "database. Still, sometimes we may change something\n"; - print "slightly (either to make OKAPI work properly OR as a part of\n"; - print "bigger \"international Opencaching unification\" ideas).\n\n"; - print "We will let you know every time OKAPI alters database structure\n"; - print "(outside of the \"okapi_\" table-scope). If you have any comments\n"; - print "on this procedure, please submit them to our issue tracker.\n\n"; - } + private static function print_common_db_alteration_info() + { + print "-- About OKAPI's database modifications --\n\n"; + print "OKAPI takes care of its own tables (the ones with the \"okapi_\"\n"; + print "prefix), but it won't usually alter other tables in your\n"; + print "database. Still, sometimes we may change something\n"; + print "slightly (either to make OKAPI work properly OR as a part of\n"; + print "bigger \"international Opencaching unification\" ideas).\n\n"; + print "We will let you know every time OKAPI alters database structure\n"; + print "(outside of the \"okapi_\" table-scope). If you have any comments\n"; + print "on this procedure, please submit them to our issue tracker.\n\n"; + } - private static function ver49() { Db::execute("alter table caches add key okapi_syncbase (okapi_syncbase);"); } - private static function ver50() { /* no longer necessary */ } + private static function ver49() { Db::execute("alter table caches add key okapi_syncbase (okapi_syncbase);"); } + private static function ver50() { /* no longer necessary */ } - private static function ver51() - { - # Before revision 417, OKAPI used to make the following change: - # - Db::execute("alter table cache_logs modify column last_modified timestamp not null;"); - # It doesn't do that anymore. Instead, it adds a separate column for itself (okapi_syncbase). - } + private static function ver51() + { + # Before revision 417, OKAPI used to make the following change: + # - Db::execute("alter table cache_logs modify column last_modified timestamp not null;"); + # It doesn't do that anymore. Instead, it adds a separate column for itself (okapi_syncbase). + } - private static function ver52() - { - # Before revision 417, OKAPI used to make the following change (on OCDE branch): - # - Db::execute("alter table cache_logs_archived modify column last_modified timestamp not null;"); - # It doesn't do that anymore. Instead, it adds a separate column for itself (okapi_syncbase). - } + private static function ver52() + { + # Before revision 417, OKAPI used to make the following change (on OCDE branch): + # - Db::execute("alter table cache_logs_archived modify column last_modified timestamp not null;"); + # It doesn't do that anymore. Instead, it adds a separate column for itself (okapi_syncbase). + } - private static function ver53() { Db::execute("alter table cache_logs add column okapi_syncbase timestamp not null after last_modified;"); } - private static function ver54() { Db::execute("update cache_logs set okapi_syncbase=last_modified;"); } + private static function ver53() { Db::execute("alter table cache_logs add column okapi_syncbase timestamp not null after last_modified;"); } + private static function ver54() { Db::execute("update cache_logs set okapi_syncbase=last_modified;"); } - private static function ver55() - { - if (Settings::get('OC_BRANCH') == 'oc.pl') - { - # OCPL does not have cache_logs_archived table. - return; - } - Db::execute("alter table cache_logs_archived add column okapi_syncbase timestamp not null after last_modified;"); - } + private static function ver55() + { + if (Settings::get('OC_BRANCH') == 'oc.pl') + { + # OCPL does not have cache_logs_archived table. + return; + } + Db::execute("alter table cache_logs_archived add column okapi_syncbase timestamp not null after last_modified;"); + } - private static function ver56() - { - if (Settings::get('OC_BRANCH') == 'oc.pl') - { - # OCPL does not have cache_logs_archived table. - return; - } - Db::execute("update cache_logs_archived set okapi_syncbase=last_modified;"); - } + private static function ver56() + { + if (Settings::get('OC_BRANCH') == 'oc.pl') + { + # OCPL does not have cache_logs_archived table. + return; + } + Db::execute("update cache_logs_archived set okapi_syncbase=last_modified;"); + } - private static function ver57() - { - ob_start(); - print "Hi!\n\n"; - print "OKAPI just added additional field (along with an index) 'okapi_syncbase'\n"; - print "on your 'cache_logs' AND 'cache_logs_archived' tables. It is required by\n"; - print "OKAPI's 'replicate' module to function properly.\n\n"; - self::print_common_db_alteration_info(); - print "-- \n"; - print "OKAPI Team"; - Okapi::mail_admins("Database modification notice: caches.okapi_syncbase", ob_get_clean()); - } + private static function ver57() + { + ob_start(); + print "Hi!\n\n"; + print "OKAPI just added additional field (along with an index) 'okapi_syncbase'\n"; + print "on your 'cache_logs' AND 'cache_logs_archived' tables. It is required by\n"; + print "OKAPI's 'replicate' module to function properly.\n\n"; + self::print_common_db_alteration_info(); + print "-- \n"; + print "OKAPI Team"; + Okapi::mail_admins("Database modification notice: caches.okapi_syncbase", ob_get_clean()); + } - private static function ver58() - { - # - # Starting with revision 417, OKAPI hides all caches with statuses > 3. - # Hence, we need such caches to be removed from external databases replicated - # via the "replicate" module. By reseting the "okapi_syncbase" timestamp, - # we force changelog generator cronjob to issue proper "delete" statements - # to the changelog. - # - Db::execute(" - update caches - set okapi_syncbase = now() - where status > 3 - "); - } + private static function ver58() + { + # + # Starting with revision 417, OKAPI hides all caches with statuses > 3. + # Hence, we need such caches to be removed from external databases replicated + # via the "replicate" module. By reseting the "okapi_syncbase" timestamp, + # we force changelog generator cronjob to issue proper "delete" statements + # to the changelog. + # + Db::execute(" + update caches + set okapi_syncbase = now() + where status > 3 + "); + } - private static function ver59() - { - # As above. - Db::execute(" - update - cache_logs cl, - caches c - set cl.okapi_syncbase = now() - where - cl.cache_id = c.cache_id - and c.status > 3 - "); - } + private static function ver59() + { + # As above. + Db::execute(" + update + cache_logs cl, + caches c + set cl.okapi_syncbase = now() + where + cl.cache_id = c.cache_id + and c.status > 3 + "); + } - private static function ver60() - { - # Turns out there can be only one valid TIMESTAMP field in one table! - # Fields added ver53-ver59 don't work properly *if* ver51-ver52 had been run. - # - # We'll check if ver51-ver52 had been run and try to withdraw it AND - # *rerun* missing ver53-ver59 updates. - # - $row = Db::select_row("show create table cache_logs"); - $stmt = $row["Create Table"]; - if (strpos($stmt, "timestamp NOT NULL DEFAULT '0000-00-00 00:00:00'") > 0) - { - Db::execute("alter table cache_logs modify column last_modified datetime not null;"); - Db::execute("alter table cache_logs modify column okapi_syncbase timestamp not null;"); - Db::execute("update cache_logs set okapi_syncbase=now() where okapi_syncbase='0000-00-00 00:00:00';"); - if (Settings::get('OC_BRANCH') == 'oc.de') - { - Db::execute("alter table cache_logs_archived modify column last_modified datetime not null;"); - Db::execute("alter table cache_logs_archived modify column okapi_syncbase timestamp not null;"); - Db::execute("update cache_logs_archived set okapi_syncbase=now() where okapi_syncbase='0000-00-00 00:00:00';"); - } - } - } + private static function ver60() + { + # Turns out there can be only one valid TIMESTAMP field in one table! + # Fields added ver53-ver59 don't work properly *if* ver51-ver52 had been run. + # + # We'll check if ver51-ver52 had been run and try to withdraw it AND + # *rerun* missing ver53-ver59 updates. + # + $row = Db::select_row("show create table cache_logs"); + $stmt = $row["Create Table"]; + if (strpos($stmt, "timestamp NOT NULL DEFAULT '0000-00-00 00:00:00'") > 0) + { + Db::execute("alter table cache_logs modify column last_modified datetime not null;"); + Db::execute("alter table cache_logs modify column okapi_syncbase timestamp not null;"); + Db::execute("update cache_logs set okapi_syncbase=now() where okapi_syncbase='0000-00-00 00:00:00';"); + if (Settings::get('OC_BRANCH') == 'oc.de') + { + Db::execute("alter table cache_logs_archived modify column last_modified datetime not null;"); + Db::execute("alter table cache_logs_archived modify column okapi_syncbase timestamp not null;"); + Db::execute("update cache_logs_archived set okapi_syncbase=now() where okapi_syncbase='0000-00-00 00:00:00';"); + } + } + } - private static function ver61() { Db::execute("alter table cache_logs add key okapi_syncbase (okapi_syncbase);"); } + private static function ver61() { Db::execute("alter table cache_logs add key okapi_syncbase (okapi_syncbase);"); } - private static function ver62() - { - if (Settings::get('OC_BRANCH') == 'oc.pl') - { - # OCPL does not have cache_logs_archived table. - return; - } - Db::execute("alter table cache_logs_archived add key okapi_syncbase (okapi_syncbase);"); - } + private static function ver62() + { + if (Settings::get('OC_BRANCH') == 'oc.pl') + { + # OCPL does not have cache_logs_archived table. + return; + } + Db::execute("alter table cache_logs_archived add key okapi_syncbase (okapi_syncbase);"); + } - private static function ver63() - { - Db::execute(" - CREATE TABLE `okapi_tile_status` ( - `z` tinyint(2) NOT NULL, - `x` mediumint(6) unsigned NOT NULL, - `y` mediumint(6) unsigned NOT NULL, - `status` tinyint(1) unsigned NOT NULL, - PRIMARY KEY (`z`,`x`,`y`) - ) ENGINE=MEMORY DEFAULT CHARSET=utf8; - "); - } + private static function ver63() + { + Db::execute(" + CREATE TABLE `okapi_tile_status` ( + `z` tinyint(2) NOT NULL, + `x` mediumint(6) unsigned NOT NULL, + `y` mediumint(6) unsigned NOT NULL, + `status` tinyint(1) unsigned NOT NULL, + PRIMARY KEY (`z`,`x`,`y`) + ) ENGINE=MEMORY DEFAULT CHARSET=utf8; + "); + } - private static function ver64() - { - Db::execute(" - CREATE TABLE `okapi_tile_caches` ( - `z` tinyint(2) NOT NULL, - `x` mediumint(6) unsigned NOT NULL, - `y` mediumint(6) unsigned NOT NULL, - `cache_id` mediumint(6) unsigned NOT NULL, - `z21x` int(10) unsigned NOT NULL, - `z21y` int(10) unsigned NOT NULL, - `status` tinyint(1) unsigned NOT NULL, - `type` tinyint(1) unsigned NOT NULL, - `rating` tinyint(1) unsigned DEFAULT NULL, - `flags` tinyint(1) unsigned NOT NULL, - PRIMARY KEY (`z`,`x`,`y`,`cache_id`) - ) ENGINE=MEMORY DEFAULT CHARSET=utf8; - "); - } + private static function ver64() + { + Db::execute(" + CREATE TABLE `okapi_tile_caches` ( + `z` tinyint(2) NOT NULL, + `x` mediumint(6) unsigned NOT NULL, + `y` mediumint(6) unsigned NOT NULL, + `cache_id` mediumint(6) unsigned NOT NULL, + `z21x` int(10) unsigned NOT NULL, + `z21y` int(10) unsigned NOT NULL, + `status` tinyint(1) unsigned NOT NULL, + `type` tinyint(1) unsigned NOT NULL, + `rating` tinyint(1) unsigned DEFAULT NULL, + `flags` tinyint(1) unsigned NOT NULL, + PRIMARY KEY (`z`,`x`,`y`,`cache_id`) + ) ENGINE=MEMORY DEFAULT CHARSET=utf8; + "); + } - private static function ver65() { Db::execute("alter table okapi_tile_status engine=innodb;"); } - private static function ver66() { Db::execute("alter table okapi_tile_caches engine=innodb;"); } + private static function ver65() { Db::execute("alter table okapi_tile_status engine=innodb;"); } + private static function ver66() { Db::execute("alter table okapi_tile_caches engine=innodb;"); } - private static function ver67() - { - # Remove unused locks (these might have been created in previous versions of OKAPI). + private static function ver67() + { + # Remove unused locks (these might have been created in previous versions of OKAPI). - for ($z=0; $z<=2; $z++) - for ($x=0; $x<(1<<$z); $x++) - for ($y=0; $y<(1<<$z); $y++) - { - $lockname = "tile-computation-$z-$x-$y"; - if (OkapiLock::exists($lockname)) - OkapiLock::get($lockname)->remove(); - } - } + for ($z=0; $z<=2; $z++) + for ($x=0; $x<(1<<$z); $x++) + for ($y=0; $y<(1<<$z); $y++) + { + $lockname = "tile-computation-$z-$x-$y"; + if (OkapiLock::exists($lockname)) + OkapiLock::get($lockname)->remove(); + } + } - private static function ver68() - { - # Once again, remove unused locks. + private static function ver68() + { + # Once again, remove unused locks. - for ($z=0; $z<=21; $z++) - { - foreach (array("", "-0", "-1") as $suffix) - { - $lockname = "tile-$z$suffix"; - if (OkapiLock::exists($lockname)) - OkapiLock::get($lockname)->remove(); - } - } - } + for ($z=0; $z<=21; $z++) + { + foreach (array("", "-0", "-1") as $suffix) + { + $lockname = "tile-$z$suffix"; + if (OkapiLock::exists($lockname)) + OkapiLock::get($lockname)->remove(); + } + } + } - private static function ver69() - { - # TileTree border margins changed. We need to recalculate all nodes - # but the root. + private static function ver69() + { + # TileTree border margins changed. We need to recalculate all nodes + # but the root. - Db::execute("delete from okapi_tile_caches where z > 0"); - Db::execute("delete from okapi_tile_status where z > 0"); - } + Db::execute("delete from okapi_tile_caches where z > 0"); + Db::execute("delete from okapi_tile_status where z > 0"); + } - private static function ver70() - { - Db::execute(" - CREATE TABLE `okapi_cache_reads` ( - `cache_key` varchar(64) NOT NULL - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - "); - } + private static function ver70() + { + Db::execute(" + CREATE TABLE `okapi_cache_reads` ( + `cache_key` varchar(64) NOT NULL + ) ENGINE=MyISAM DEFAULT CHARSET=utf8; + "); + } - private static function ver71() { Db::execute("alter table okapi_cache add column score float(4,2) default null after `key`"); } - private static function ver72() { Db::execute("alter table okapi_cache change column expires expires datetime after score"); } - private static function ver73() { Db::execute("update okapi_cache set score=1, expires=date_add(now(), interval 360 day) where `key` like 'tile/%'"); } - private static function ver74() { Db::execute("update okapi_cache set score=1, expires=date_add(now(), interval 360 day) where `key` like 'tilecaption/%'"); } - private static function ver75() { Db::execute("alter table okapi_cache modify column score float default null"); } - private static function ver76() { Db::execute("update okapi_cache set expires=date_add(now(), interval 100 year) where `key` like 'clog#geocache#%'"); } + private static function ver71() { Db::execute("alter table okapi_cache add column score float(4,2) default null after `key`"); } + private static function ver72() { Db::execute("alter table okapi_cache change column expires expires datetime after score"); } + private static function ver73() { Db::execute("update okapi_cache set score=1, expires=date_add(now(), interval 360 day) where `key` like 'tile/%'"); } + private static function ver74() { Db::execute("update okapi_cache set score=1, expires=date_add(now(), interval 360 day) where `key` like 'tilecaption/%'"); } + private static function ver75() { Db::execute("alter table okapi_cache modify column score float default null"); } + private static function ver76() { Db::execute("update okapi_cache set expires=date_add(now(), interval 100 year) where `key` like 'clog#geocache#%'"); } - private static function ver77() - { - Db::execute(" - CREATE TABLE okapi_search_sets ( - id mediumint(6) unsigned not null auto_increment, - params_hash varchar(64) not null, - primary key (id), - key by_hash (params_hash, id) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - "); - } + private static function ver77() + { + Db::execute(" + CREATE TABLE okapi_search_sets ( + id mediumint(6) unsigned not null auto_increment, + params_hash varchar(64) not null, + primary key (id), + key by_hash (params_hash, id) + ) ENGINE=MyISAM DEFAULT CHARSET=utf8; + "); + } - private static function ver78() - { - Db::execute(" - CREATE TABLE okapi_search_results ( - set_id mediumint(6) unsigned not null, - cache_id mediumint(6) unsigned not null, - primary key (set_id, cache_id) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8; - "); - } + private static function ver78() + { + Db::execute(" + CREATE TABLE okapi_search_results ( + set_id mediumint(6) unsigned not null, + cache_id mediumint(6) unsigned not null, + primary key (set_id, cache_id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8; + "); + } - private static function ver79() { Db::execute("alter table okapi_search_results engine=MyISAM"); } - private static function ver80() { Db::execute("alter table okapi_search_sets add column date_created datetime not null"); } - private static function ver81() { Db::execute("alter table okapi_search_sets add column expires datetime not null"); } - private static function ver82() { CronJobController::reset_job_schedule("FulldumpGeneratorJob"); } - private static function ver83() { Db::execute("alter table okapi_stats_temp engine=InnoDB"); } - private static function ver84() { Db::execute("truncate okapi_nonces;"); } - private static function ver85() { Db::execute("alter table okapi_nonces drop primary key;"); } - private static function ver86() { Db::execute("alter table okapi_nonces change column `key` nonce_hash varchar(32) character set utf8 collate utf8_bin not null;"); } - private static function ver87() { Db::execute("alter table okapi_nonces add primary key (consumer_key, nonce_hash);"); } - private static function ver88() { Db::execute("alter table okapi_consumers add column admin tinyint not null default 0;"); } + private static function ver79() { Db::execute("alter table okapi_search_results engine=MyISAM"); } + private static function ver80() { Db::execute("alter table okapi_search_sets add column date_created datetime not null"); } + private static function ver81() { Db::execute("alter table okapi_search_sets add column expires datetime not null"); } + private static function ver82() { CronJobController::reset_job_schedule("FulldumpGeneratorJob"); } + private static function ver83() { Db::execute("alter table okapi_stats_temp engine=InnoDB"); } + private static function ver84() { Db::execute("truncate okapi_nonces;"); } + private static function ver85() { Db::execute("alter table okapi_nonces drop primary key;"); } + private static function ver86() { Db::execute("alter table okapi_nonces change column `key` nonce_hash varchar(32) character set utf8 collate utf8_bin not null;"); } + private static function ver87() { Db::execute("alter table okapi_nonces add primary key (consumer_key, nonce_hash);"); } + private static function ver88() { Db::execute("alter table okapi_consumers add column admin tinyint not null default 0;"); } + + private static function ver89() + { + # Ignore newly added replicate fields. This way we will avoid generating + # changelog entries for these fields. + + require_once($GLOBALS['rootpath']."okapi/services/replicate/replicate_common.inc.php"); + $new_geocache_fields = array( + 'attr_acodes', 'willattends', 'country', 'state', 'preview_image', + 'trip_time', 'trip_distance', 'gc_code', 'hints2', 'protection_areas' + ); + ReplicateCommon::verify_clog_consistency(true, $new_geocache_fields); + } }