Пример #1
0
 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')));
     # 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("\n                    insert ignore into cache_watches (cache_id, user_id)\n                    values (\n                        '" . Db::escape_string($geocache['internal_id']) . "',\n                        '" . Db::escape_string($request->token->user_id) . "'\n                    );\n                ");
         } elseif ($tmp == 'false') {
             Db::execute("\n                    delete from cache_watches\n                    where\n                        cache_id = '" . Db::escape_string($geocache['internal_id']) . "'\n                        and user_id = '" . Db::escape_string($request->token->user_id) . "';\n                ");
         }
     }
     # 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("\n                    insert ignore into cache_ignore (cache_id, user_id)\n                    values (\n                        '" . Db::escape_string($geocache['internal_id']) . "',\n                        '" . Db::escape_string($request->token->user_id) . "'\n                    );\n                ");
         } elseif ($tmp == 'false') {
             Db::execute("\n                    delete from cache_ignore\n                    where\n                        cache_id = '" . Db::escape_string($geocache['internal_id']) . "'\n                        and user_id = '" . Db::escape_string($request->token->user_id) . "'\n                ");
         }
     }
     $result = array('success' => true);
     return Okapi::formatted_response($request, $result);
 }
Пример #2
0
 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.
     Db::execute("\n            delete from okapi_tokens\n            where `key` = '" . Db::escape_string($token->key) . "'\n        ");
     # 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("\n            select `key`, secret\n            from okapi_tokens\n            where\n                token_type = 'access'\n                and user_id = '" . Db::escape_string($token->authorized_by_user_id) . "'\n                and consumer_key = '" . Db::escape_string($consumer->key) . "'\n        ");
     if ($row) {
         # Use existing Access Token
         $access_token = new OkapiAccessToken($row['key'], $row['secret'], $consumer->key, $token->authorized_by_user_id);
     } else {
         # Generate a new Access Token.
         $access_token = new OkapiAccessToken(Okapi::generate_key(20), Okapi::generate_key(40), $consumer->key, $token->authorized_by_user_id);
         Db::execute("\n                insert into okapi_tokens\n                    (`key`, secret, token_type, timestamp, user_id, consumer_key)\n                values (\n                    '" . Db::escape_string($access_token->key) . "',\n                    '" . Db::escape_string($access_token->secret) . "',\n                    'access',\n                    unix_timestamp(),\n                    '" . Db::escape_string($access_token->user_id) . "',\n                    '" . Db::escape_string($consumer->key) . "'\n                );\n            ");
     }
     return $access_token;
 }
Пример #3
0
 static function call(OkapiRequest $request)
 {
     require_once 'log_images_common.inc.php';
     list($image_uuid, $log_internal_id) = LogImagesCommon::validate_image_uuid($request);
     $image_uuid_escaped = Db::escape_string($image_uuid);
     Db::execute('start transaction');
     $image_row = Db::select_row("\n            select id, node, url, local\n            from pictures\n            where uuid = '" . $image_uuid_escaped . "'\n        ");
     Db::execute("\n            delete from pictures where uuid = '" . $image_uuid_escaped . "'\n        ");
     # Remember that OCPL picture sequence numbers are always 1, and
     # OCDE sequence numbers may have gaps. So we do not need to adjust
     # any numbers after deleting from table 'pictures'.
     if (Settings::get('OC_BRANCH') == 'oc.de') {
         # OCDE does all the housekeeping by triggers
     } else {
         Db::execute("\n                INSERT INTO removed_objects (\n                    localID, uuid, type, removed_date, node\n                )\n                VALUES (\n                    " . $image_row['id'] . "\n                    '" . $image_uuid_escaped . "',\n                    6,\n                    NOW(),\n                    " . $image_row['node'] . "\n                )\n            ");
         # This will also update cache_logs.okapi_syncbase, so that replication
         # can output the updated log entry with one image less. For OCDE
         # that's done by DB trigges.
         Db::execute("\n                update cache_logs\n                set\n                    picturescount = greatest(0, picturescount - 1),\n                    last_modified = NOW()\n                where id = '" . Db::escape_string($log_internal_id) . "'\n            ");
     }
     Db::execute('commit');
     if ($image_row['local']) {
         $filename = basename($image_row['url']);
         unlink(Settings::get('IMAGES_DIR') . '/' . $filename);
     }
     $result = array('success' => true);
     return Okapi::formatted_response($request, $result);
 }
Пример #4
0
 public static function call()
 {
     $langpref = isset($_GET['langpref']) ? $_GET['langpref'] : Settings::get('SITELANG');
     $langprefs = explode("|", $langpref);
     # Determine which user is logged in to OC.
     require_once $GLOBALS['rootpath'] . "okapi/lib/oc_session.php";
     $OC_user_id = OCSession::get_user_id();
     if ($OC_user_id == null) {
         $after_login = "******" . ($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.
     $rs = Db::query("\n            select c.`key`, c.name, c.url\n            from\n                okapi_consumers c,\n                okapi_authorizations a\n            where\n                a.user_id = '" . Db::escape_string($OC_user_id) . "'\n                and c.`key` = a.consumer_key\n            order by c.name\n        ");
     $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 = Db::fetch_assoc($rs)) {
         $vars['apps'][] = $row;
     }
     Db::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;
 }
Пример #5
0
 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";
     }
     $langpref .= "|" . Settings::get('SITELANG');
     $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).
     $results = OkapiServiceRunner::call('services/caches/geocaches', new OkapiInternalRequest($request->consumer, $request->token, $params));
     $result = $results[$cache_code];
     if ($result === null) {
         # Two errors messages (for OCDE). Makeshift solution for issue #350.
         $exists = Db::select_value("\n                select 1\n                from caches\n                where wp_oc='" . Db::escape_string($cache_code) . "'\n            ");
         if ($exists) {
             throw new InvalidParam('cache_code', "This cache is not accessible via OKAPI.");
         } else {
             throw new InvalidParam('cache_code', "This cache does not exist.");
         }
     }
     return Okapi::formatted_response($request, $result);
 }
Пример #6
0
 public static function call()
 {
     if (!isset($_GET['id'])) {
         throw new ParamMissing("id");
     }
     $tmp = Db::select_value("\n            select data\n            from okapi_clog\n            where id='" . Db::escape_string($_GET['id']) . "'\n        ");
     $data = unserialize(gzinflate($tmp));
     $response = new OkapiHttpResponse();
     $response->content_type = "application/json; charset=utf-8";
     $response->body = json_encode($data);
     return $response;
 }
Пример #7
0
 public static function call(OkapiRequest $request)
 {
     $cache_code = $request->get_parameter('cache_code');
     if (!$cache_code) {
         throw new ParamMissing('cache_code');
     }
     $fields = $request->get_parameter('fields');
     if (!$fields) {
         $fields = "uuid|date|user|type|comment";
     }
     $offset = $request->get_parameter('offset');
     if (!$offset) {
         $offset = "0";
     }
     if ((int) $offset != $offset || (int) $offset < 0) {
         throw new InvalidParam('offset', "Expecting non-negative integer.");
     }
     $limit = $request->get_parameter('limit');
     if (!$limit) {
         $limit = "none";
     }
     if ($limit == "none") {
         $limit = "999999999";
     }
     if ((int) $limit != $limit || (int) $limit < 0) {
         throw new InvalidParam('limit', "Expecting non-negative integer or 'none'.");
     }
     # Check if code exists and retrieve cache ID (this will throw
     # a proper exception on invalid code).
     $cache = OkapiServiceRunner::call('services/caches/geocache', new OkapiInternalRequest($request->consumer, null, array('cache_code' => $cache_code, 'fields' => 'internal_id')));
     # Cache exists. Getting the uuids of its logs.
     $log_uuids = Db::select_column("\n            select uuid\n            from cache_logs\n            where\n                cache_id = '" . Db::escape_string($cache['internal_id']) . "'\n                and " . (Settings::get('OC_BRANCH') == 'oc.pl' ? "deleted = 0" : "true") . "\n            order by date desc\n            limit {$offset}, {$limit}\n        ");
     # 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];
     }
     /* 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);
 }
Пример #8
0
 public static function call()
 {
     # Determine which user is logged in to OC.
     require_once $GLOBALS['rootpath'] . "okapi/lib/oc_session.php";
     $OC_user_id = OCSession::get_user_id();
     # Ensure a user is logged in.
     if ($OC_user_id == null) {
         $after_login = "******";
         # 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'] : '';
     # Just remove app (if it doesn't exist - nothing wrong will happen anyway).
     Db::execute("\n            delete from okapi_tokens\n            where\n                user_id = '" . Db::escape_string($OC_user_id) . "'\n                and consumer_key = '" . Db::escape_string($consumer_key) . "'\n        ");
     Db::execute("\n            delete from okapi_authorizations\n            where\n                user_id = '" . Db::escape_string($OC_user_id) . "'\n                and consumer_key = '" . Db::escape_string($consumer_key) . "'\n        ");
     # Redirect back to the apps page.
     return new OkapiRedirectResponse(Settings::get('SITE_URL') . "okapi/apps/");
 }
Пример #9
0
 public static function call(OkapiRequest $request)
 {
     $user_uuid = $request->get_parameter('user_uuid');
     if (!$user_uuid) {
         throw new ParamMissing('user_uuid');
     }
     $limit = $request->get_parameter('limit');
     if (!$limit) {
         $limit = "20";
     }
     if (!is_numeric($limit)) {
         throw new InvalidParam('limit', "'{$limit}'");
     }
     $limit = intval($limit);
     if ($limit < 1 || $limit > 1000) {
         throw new InvalidParam('limit', "Has to be in range 1..1000.");
     }
     $offset = $request->get_parameter('offset');
     if (!$offset) {
         $offset = "0";
     }
     if (!is_numeric($offset)) {
         throw new InvalidParam('offset', "'{$offset}'");
     }
     $offset = intval($offset);
     if ($offset < 0) {
         throw new InvalidParam('offset', "'{$offset}'");
     }
     # Check if user exists and retrieve user's ID (this will throw
     # a proper exception on invalid UUID).
     $user = OkapiServiceRunner::call('services/users/user', new OkapiInternalRequest($request->consumer, null, array('user_uuid' => $user_uuid, 'fields' => 'internal_id')));
     # User exists. Retrieving logs.
     $rs = Db::query("\n            select cl.id, cl.uuid, cl.type, unix_timestamp(cl.date) as date, cl.text,\n                c.wp_oc as cache_code\n            from cache_logs cl, caches c\n            where\n                cl.user_id = '" . Db::escape_string($user['internal_id']) . "'\n                and " . (Settings::get('OC_BRANCH') == 'oc.pl' ? "cl.deleted = 0" : "true") . "\n                and c.status in (1,2,3)\n                and cl.cache_id = c.cache_id\n            order by cl.date desc\n            limit {$offset}, {$limit}\n        ");
     $results = array();
     while ($row = Db::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);
 }
Пример #10
0
 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("\n            select\n                c.`key` as consumer_key,\n                c.name as consumer_name,\n                c.url as consumer_url,\n                t.verifier\n            from\n                okapi_consumers c,\n                okapi_tokens t\n            where\n                t.`key` = '" . Db::escape_string($token_key) . "'\n                and t.consumer_key = c.`key`\n        ");
     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;
 }
Пример #11
0
 private static function insert_log_row($consumer_key, $cache_internal_id, $user_internal_id, $logtype, $when, $formatted_comment, $text_html, $needs_maintenance2)
 {
     if (Settings::get('OC_BRANCH') == 'oc.de') {
         $needs_maintenance_field_SQL = ', needs_maintenance';
         if ($needs_maintenance2 == 'true') {
             $needs_maintenance_SQL = ',2';
         } else {
             if ($needs_maintenance2 == 'false') {
                 $needs_maintenance_SQL = ',1';
             } else {
                 // 'null'
                 $needs_maintenance_SQL = ',0';
             }
         }
     } else {
         $needs_maintenance_field_SQL = '';
         $needs_maintenance_SQL = '';
     }
     $log_uuid = Okapi::create_uuid();
     Db::execute("\n            insert into cache_logs (\n                uuid, cache_id, user_id, type, date, text, text_html,\n                last_modified, date_created, node" . $needs_maintenance_field_SQL . "\n            ) values (\n                '" . Db::escape_string($log_uuid) . "',\n                '" . Db::escape_string($cache_internal_id) . "',\n                '" . Db::escape_string($user_internal_id) . "',\n                '" . Db::escape_string(Okapi::logtypename2id($logtype)) . "',\n                from_unixtime('" . Db::escape_string($when) . "'),\n                '" . Db::escape_string($formatted_comment) . "',\n                '" . Db::escape_string($text_html) . "',\n                now(),\n                now(),\n                '" . Db::escape_string(Settings::get('OC_NODE_ID')) . "'\n                " . $needs_maintenance_SQL . "\n            );\n        ");
     $log_internal_id = Db::last_insert_id();
     # Store additional information on consumer_key which has created this log entry.
     # (Maybe we'll want to display this somewhen later.)
     Db::execute("\n            insert into okapi_submitted_objects (object_type, object_id, consumer_key)\n            values (\n                " . Okapi::OBJECT_TYPE_CACHE_LOG . ",\n                '" . Db::escape_string($log_internal_id) . "',\n                '" . Db::escape_string($consumer_key) . "'\n            );\n        ");
     return $log_uuid;
 }
Пример #12
0
 private static function remove_notes($cache_id, $user_id)
 {
     if (Settings::get('OC_BRANCH') == 'oc.de') {
         # we can delete row if and only if there are no coords in it
         $affected_row_count = Db::execute("\n                delete from coordinates\n                where\n                    type = 2  -- personal note\n                    and cache_id = '" . Db::escape_string($cache_id) . "'\n                    and user_id = '" . Db::escape_string($user_id) . "'\n                    and longitude = 0\n                    and latitude = 0\n            ");
         if ($affected_row_count <= 0) {
             # no rows deleted - record either doesn't exist, or has coords
             # remove only description
             Db::execute("\n                    update coordinates\n                    set description = null\n                    where\n                        type = 2\n                        and cache_id = '" . Db::escape_string($cache_id) . "'\n                        and user_id = '" . Db::escape_string($user_id) . "'\n                ");
         }
     } else {
         # oc.pl branch
         Db::execute("\n                delete from cache_notes\n                where\n                    cache_id = '" . Db::escape_string($cache_id) . "'\n                    and user_id = '" . Db::escape_string($user_id) . "'\n            ");
     }
 }
Пример #13
0
 public static function call(OkapiRequest $request)
 {
     $checkpointA_started = microtime(true);
     # Make sure the request is internal.
     if (in_array($request->consumer->key, array('internal', 'facade'))) {
         /* Okay, these two consumers can always access it. */
     } elseif ($request->consumer->hasFlag(OkapiConsumer::FLAG_MAPTILE_ACCESS)) {
         /* If the Consumer is aware that it is not backward-compatible, then
          * he may be granted permission to access it. */
     } else {
         throw new BadRequest("Your Consumer Key has not been allowed to access this method.");
     }
     # zoom, x, y - required tile-specific parameters.
     $zoom = self::require_uint($request, 'z');
     if ($zoom > 21) {
         throw new InvalidParam('z', "Maximum value for this parameter is 21.");
     }
     $x = self::require_uint($request, 'x');
     $y = self::require_uint($request, 'y');
     if ($x >= 1 << $zoom) {
         throw new InvalidParam('x', "Should be in 0.." . ((1 << $zoom) - 1) . ".");
     }
     if ($y >= 1 << $zoom) {
         throw new InvalidParam('y', "Should be in 0.." . ((1 << $zoom) - 1) . ".");
     }
     # 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', new OkapiInternalRequest($request->consumer, $request->token, $request->get_all_parameters_including_unknown()));
     $set_id = $search_set['set_id'];
     # 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 = Db::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.
     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();
             # Ignored caches.
             $rs = Db::query("\n                    select cache_id\n                    from cache_ignore\n                    where user_id = '" . Db::escape_string($request->token->user_id) . "'\n                ");
             $user['ignored'] = array();
             while (list($cache_id) = Db::fetch_row($rs)) {
                 $user['ignored'][$cache_id] = true;
             }
             # Found caches.
             $rs = Db::query("\n                    select distinct cache_id\n                    from cache_logs\n                    where\n                        user_id = '" . Db::escape_string($request->token->user_id) . "'\n                        and type = 1\n                        and " . (Settings::get('OC_BRANCH') == 'oc.pl' ? "deleted = 0" : "true") . "\n                ");
             $user['found'] = array();
             while (list($cache_id) = Db::fetch_row($rs)) {
                 $user['found'][$cache_id] = true;
             }
             # Own caches.
             $rs = Db::query("\n                    select distinct cache_id\n                    from caches\n                    where user_id = '" . Db::escape_string($request->token->user_id) . "'\n                ");
             $user['own'] = array();
             while (list($cache_id) = Db::fetch_row($rs)) {
                 $user['own'][$cache_id] = true;
             }
             Cache::set($cache_key, $user, 30);
         }
         # 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).
             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).
     $tile = new TileRenderer($zoom, $rows);
     $image_fingerprint = $tile->get_unique_hash();
     # Start creating response.
     $response = new OkapiHttpResponse();
     $response->content_type = $tile->get_content_type();
     $response->cache_control = "Cache-Control: private, max-age=600";
     $response->etag = 'W/"' . $image_fingerprint . '"';
     $response->allow_gzip = false;
     // images are usually compressed, prevent compression at Apache level
     # Check if the request didn't include the same ETag.
     OkapiServiceRunner::save_stats_extra("caches/map/tile/checkpointB", null, microtime(true) - $checkpointB_started);
     $checkpointC_started = microtime(true);
     if (self::$USE_ETAGS_CACHE && $request->etag == $response->etag) {
         # Hit. Report the content was unmodified.
         $response->etag = null;
         $response->status = "304 Not Modified";
         return $response;
     }
     # Check if the image was recently rendered and is kept in image cache.
     $cache_key = "tile/" . $image_fingerprint;
     $response->body = self::$USE_IMAGE_CACHE ? Cache::get($cache_key) : null;
     OkapiServiceRunner::save_stats_extra("caches/map/tile/checkpointC", null, microtime(true) - $checkpointC_started);
     $checkpointD_started = microtime(true);
     if ($response->body !== null) {
         # Hit. We will use the cached version of the image.
         return $response;
     }
     # Miss. Render the image. Cache the result.
     $response->body = $tile->render();
     Cache::set_scored($cache_key, $response->body);
     OkapiServiceRunner::save_stats_extra("caches/map/tile/checkpointD", null, microtime(true) - $checkpointD_started);
     return $response;
 }
Пример #14
0
 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("\n            delete from okapi_clog\n            where id < '" . Db::escape_string($new_min_revision) . "'\n        ");
     Cache::set($cache_key, $new_data, 10 * 86400);
     Db::query("optimize table okapi_clog");
 }
Пример #15
0
 public static function call(OkapiRequest $request)
 {
     # You may wonder, why there are no parameters like "bbox" or "center" in the
     # "search/all" method. This is *intentional* and should be kept this way.
     # Such parameters would fall in conflict with each other and - in result -
     # make the documentation very fuzzy. That's why they were intentionally
     # left out of the "search/all" method, and put in separate (individual) ones.
     # It's much easier to grasp their meaning this way.
     $tmp = $request->get_parameter('center');
     if (!$tmp) {
         throw new ParamMissing('center');
     }
     $parts = explode('|', $tmp);
     if (count($parts) != 2) {
         throw new InvalidParam('center', "Expecting 2 pipe-separated parts, got " . count($parts) . ".");
     }
     foreach ($parts as &$part_ref) {
         if (!preg_match("/^-?[0-9]+(\\.?[0-9]*)\$/", $part_ref)) {
             throw new InvalidParam('center', "'{$part_ref}' is not a valid float number.");
         }
         $part_ref = floatval($part_ref);
     }
     list($center_lat, $center_lon) = $parts;
     if ($center_lat > 90 || $center_lat < -90) {
         throw new InvalidParam('center', "Latitudes have to be within -90..90 range.");
     }
     if ($center_lon > 180 || $center_lon < -180) {
         throw new InvalidParam('center', "Longitudes have to be within -180..180 range.");
     }
     #
     # In the method description, we promised to return caches ordered by the *rough*
     # distance from the center point. We'll use ORDER BY with a simplified distance
     # formula and combine it with the LIMIT clause to get the best results.
     #
     $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.
     $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);
         # is given in kilometers
         if ($radius <= 0) {
             throw new InvalidParam('radius', "Has to be a positive number.");
         }
         # Apply a latitude-range prefilter if it looks promising.
         # See https://github.com/opencaching/okapi/issues/363 for more info.
         $optimization_radius = 100;
         # in kilometers, optimized for Opencaching.de
         $km2degrees_upper_estimate_factor = 0.01;
         if ($radius <= $optimization_radius) {
             $radius_degrees = $radius * $km2degrees_upper_estimate_factor;
             $where_conds[] = "\n                    caches.latitude >= '" . Db::escape_string($center_lat - $radius_degrees) . "'\n                    and caches.latitude <= '" . Db::escape_string($center_lat + $radius_degrees) . "'\n                    ";
         }
         $radius *= 1000;
         # convert from kilometers to meters
         $where_conds[] = "{$distance_formula} <= '" . Db::escape_string($radius) . "'";
     }
     $search_params = $search_assistant->get_search_params();
     $search_params['where_conds'] = array_merge($where_conds, $search_params['where_conds']);
     $search_params['caches_indexhint'] = "use index (latitude)";
     $search_params['order_by'][] = $distance_formula;
     # not replaced; added to the end!
     $search_assistant->set_search_params($search_params);
     $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);
 }
Пример #16
0
 /**
  * Remove all OAuth Access Tokens bound to a certain user. This method
  * should be called (once) e.g. after a user is banned. It will disable his
  * ability to submit cache logs, etc.
  *
  * Note, that OKAPI will *automatically* remove Access Tokens of banned
  * users, but it will perform this action with a slight delay. This method
  * can be used to do this immediatelly. See #432 for details.
  */
 public static function remove_user_tokens($user_id)
 {
     Db::execute("\n            delete from okapi_tokens\n            where user_id = '" . Db::escape_string($user_id) . "'\n        ");
 }
Пример #17
0
 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.
     $force_relogin = isset($_GET['interactivity']) && $_GET['interactivity'] == 'confirm_user';
     $token = Db::select_row("\n            select\n                t.`key` as `key`,\n                c.`key` as consumer_key,\n                c.name as consumer_name,\n                c.url as consumer_url,\n                t.callback,\n                t.verifier\n            from\n                okapi_consumers c,\n                okapi_tokens t\n            where\n                t.`key` = '" . Db::escape_string($token_key) . "'\n                and t.consumer_key = c.`key`\n                and t.user_id is null\n        ");
     $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!
         $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.
     require_once $GLOBALS['rootpath'] . "okapi/lib/oc_session.php";
     $OC_user_id = OCSession::get_user_id();
     # Ensure a user is logged in (or force re-login).
     if ($force_relogin || $OC_user_id == null) {
         # TODO: confirm_user should first ask the user if he's "the proper one",
         # and then offer to sign in as a different user.
         $login_page = 'login.php?';
         if ($OC_user_id !== null) {
             if (Settings::get('OC_BRANCH') == 'oc.de') {
                 # OCDE login.php?action=logout&target=... will NOT logout and
                 # then redirect to the target, but it will log out, prompt for
                 # login and then redirect to the target after logging in -
                 # that's exactly the relogin that we want.
                 $login_page .= 'action=logout&';
             } else {
                 # OCPL 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...
                 # Try to destroy EVERYTHING. (This still won't necessarilly work,
                 # because OC may store cookies in separate paths, but hopefully
                 # they won't).
                 if (isset($_SERVER['HTTP_COOKIE'])) {
                     $cookies = explode(';', $_SERVER['HTTP_COOKIE']);
                     foreach ($cookies as $cookie) {
                         $parts = explode('=', $cookie);
                         $name = trim($parts[0]);
                         setcookie($name, '', time() - 1000);
                         setcookie($name, '', time() - 1000, '/');
                         foreach (self::getPossibleCookieDomains() as $domain) {
                             setcookie($name, '', time() - 1000, '/', $domain);
                         }
                     }
                 }
                 # We should be logged out now. Let's login again.
             }
         }
         $after_login = "******" . ($langpref != Settings::get('SITELANG') ? "&langpref=" . $langpref : "");
         $login_url = Settings::get('SITE_URL') . $login_page . "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.
     $authorized = Db::select_value("\n            select 1\n            from okapi_authorizations\n            where\n                user_id = '" . Db::escape_string($OC_user_id) . "'\n                and consumer_key = '" . Db::escape_string($token['consumer_key']) . "'\n        ", 0);
     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("\n                        insert ignore into okapi_authorizations (consumer_key, user_id)\n                        values (\n                            '" . Db::escape_string($token['consumer_key']) . "',\n                            '" . Db::escape_string($OC_user_id) . "'\n                        );\n                    ");
                 $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" . "&oauth_token=" . $token['key']);
                 } 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.
     Db::execute("\n            update okapi_tokens\n            set user_id = '" . Db::escape_string($OC_user_id) . "'\n            where `key` = '" . Db::escape_string($token_key) . "';\n        ");
     # 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);
     }
 }
Пример #18
0
 public function __construct($options)
 {
     Okapi::init_internals();
     $this->init_request();
     #
     # Parsing options.
     #
     $DEBUG_AS_USERNAME = null;
     foreach ($options as $key => $value) {
         switch ($key) {
             case 'min_auth_level':
                 if (!in_array($value, array(0, 1, 2, 3))) {
                     throw new Exception("'min_auth_level' option has invalid value: {$value}");
                 }
                 $this->opt_min_auth_level = $value;
                 break;
             case 'token_type':
                 if (!in_array($value, array("request", "access"))) {
                     throw new Exception("'token_type' option has invalid value: {$value}");
                 }
                 $this->opt_token_type = $value;
                 break;
             case 'DEBUG_AS_USERNAME':
                 $DEBUG_AS_USERNAME = $value;
                 break;
             default:
                 throw new Exception("Unknown option: {$key}");
                 break;
         }
     }
     if ($this->opt_min_auth_level === null) {
         throw new Exception("Required 'min_auth_level' option is missing.");
     }
     if ($DEBUG_AS_USERNAME != null) {
         # Enables debugging Level 2 and Level 3 methods. Should not be committed
         # at any time! If run on production server, make it an error.
         if (!Settings::get('DEBUG')) {
             throw new Exception("Attempted to use DEBUG_AS_USERNAME in " . "non-debug environment. Accidental commit?");
         }
         # Lower required authentication to Level 0, to pass the checks.
         $this->opt_min_auth_level = 0;
     }
     #
     # Let's see if the request is signed. If it is, verify the signature.
     # It it's not, check if it isn't against the rules defined in the $options.
     #
     if ($this->get_parameter('oauth_signature')) {
         # User is using OAuth.
         # Check for duplicate keys in the parameters. (Datastore doesn't
         # do that on its own, it caused vague server errors - issue #307.)
         $this->get_parameter('oauth_consumer');
         $this->get_parameter('oauth_version');
         $this->get_parameter('oauth_token');
         $this->get_parameter('oauth_nonce');
         # Verify OAuth request.
         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.");
             }
         }
     }
     if (is_object($this->consumer) && $this->consumer->hasFlag(OkapiConsumer::FLAG_KEY_REVOKED)) {
         throw new InvalidParam('consumer_key', "Your application was denied access to the " . Okapi::get_normalized_site_name() . " site " . "(this consumer key has been revoked).");
     }
     if (is_object($this->consumer) && $this->consumer->hasFlag(OkapiConsumer::FLAG_SKIP_LIMITS)) {
         $this->skip_limits = true;
     }
     #
     # Prevent developers from accessing request parameters with PHP globals.
     # Remember, that OKAPI requests can be nested within other OKAPI requests!
     # Search the code for "new OkapiInternalRequest" to see examples.
     #
     $_GET = $_POST = $_REQUEST = null;
     # When debugging, simulate as if been run using a proper Level 3 Authentication.
     if ($DEBUG_AS_USERNAME != null) {
         # Note, that this will override any other valid authentication the
         # developer might have issued.
         $debug_user_id = Db::select_value("select user_id from user where username='******'DEBUG_AS_USERNAME']) . "'");
         if ($debug_user_id == null) {
             throw new Exception("Invalid user name in DEBUG_AS_USERNAME: '******'DEBUG_AS_USERNAME'] . "'");
         }
         $this->consumer = new OkapiDebugConsumer();
         $this->token = new OkapiDebugAccessToken($debug_user_id);
     }
     # Read the ETag.
     if (isset($_SERVER['HTTP_IF_NONE_MATCH'])) {
         $this->etag = $_SERVER['HTTP_IF_NONE_MATCH'];
     }
 }
Пример #19
0
 /**
  * Edit an log entry image and return its (new) position.
  * Throws CannotPublishException or BadRequest on errors.
  */
 private static function _call(OkapiRequest $request)
 {
     # Developers! Please notice the fundamental difference between throwing
     # CannotPublishException and the "standard" BadRequest/InvalidParam
     # exceptions. CannotPublishException will be caught by the service's
     # call() function and returns a message to be displayed to the user.
     require_once 'log_images_common.inc.php';
     # validate the 'image_uuid' parameter
     list($image_uuid, $log_internal_id) = LogImagesCommon::validate_image_uuid($request);
     # validate the 'caption', 'is_spoiler' and 'position' parameters
     $caption = $request->get_parameter('caption');
     if ($caption !== null && $caption == '') {
         throw new CannotPublishException(sprintf(_("Please enter an image caption."), Okapi::get_normalized_site_name()));
     }
     $is_spoiler = $request->get_parameter('is_spoiler');
     if ($is_spoiler !== null) {
         if (!in_array($is_spoiler, array('true', 'false'))) {
             throw new InvalidParam('is_spoiler');
         }
     }
     $position = LogImagesCommon::validate_position($request);
     if ($caption === null && $is_spoiler === null && $position === null) {
         # If no-params were allowed, what would be the success message?
         # It's more reasonable to assume that this was a developer's error.
         throw new BadRequest("At least one of the parameters 'caption', 'is_spoiler' and 'position' must be supplied");
     }
     $image_uuid_escaped = Db::escape_string($image_uuid);
     $log_entry_modified = false;
     # update caption
     if ($caption !== null) {
         Db::execute("\n                update pictures\n                set title = '" . Db::escape_string($caption) . "'\n                where uuid = '" . $image_uuid_escaped . "'\n            ");
         $log_entry_modified = true;
     }
     # update spoiler flag
     if ($is_spoiler !== null) {
         Db::execute("\n                update pictures\n                set spoiler = " . ($is_spoiler == 'true' ? 1 : 0) . "\n                where uuid = '" . $image_uuid_escaped . "'\n            ");
         $log_entry_modified = true;
     }
     # update position
     if ($position !== null) {
         if (Settings::get('OC_BRANCH') == 'oc.pl') {
             # OCPL as no arbitrary log picture ordering => ignore position parameter
             # and return the picture's current position.
             $image_uuids = Db::select_column("\n                    select uuid from pictures\n                    where object_type = 1 and object_id = '" . Db::escape_string($log_internal_id) . "'\n                    order by date_created\n                ");
             $position = array_search($image_uuid, $image_uuids);
         } else {
             list($position, $seq) = LogImagesCommon::prepare_position($log_internal_id, $position, 0);
             # For OCDE the pictures table is write locked now.
             $old_seq = DB::select_value("\n                    select seq from pictures where uuid = '" . $image_uuid_escaped . "'\n                ");
             if ($seq != $old_seq) {
                 # First move the edited picture to the end, to make space for rotating.
                 # Remember that we have no transactions at OC.de. If something goes wrong,
                 # the image will stay at the end of the list.
                 $max_seq = Db::select_value("\n                        select max(seq)\n                        from pictures\n                        where object_type = 1 and object_id = '" . Db::escape_string($log_internal_id) . "'\n                    ");
                 Db::query("\n                        update pictures\n                        set seq = '" . Db::escape_string($max_seq + 1) . "'\n                        where uuid = '" . $image_uuid_escaped . "'\n                    ");
                 # now move the pictures inbetween
                 if ($seq < $old_seq) {
                     Db::execute("\n                            update pictures\n                            set seq = seq + 1\n                            where\n                                object_type = 1\n                                and object_id = '" . Db::escape_string($log_internal_id) . "'\n                                and seq >= '" . Db::escape_string($seq) . "'\n                                and seq < '" . Db::escape_string($old_seq) . "'\n                            order by seq desc\n                        ");
                 } else {
                     Db::execute("\n                            update pictures\n                            set seq = seq - 1\n                            where\n                                object_type = 1\n                                and object_id = '" . Db::escape_string($log_internal_id) . "'\n                                and seq <= '" . Db::escape_string($seq) . "'\n                                and seq > '" . Db::escape_string($old_seq) . "'\n                            order by seq asc\n                        ");
                 }
                 # and finally move the edited picture into place
                 Db::query("\n                        update pictures\n                        set seq = '" . Db::escape_string($seq) . "'\n                        where uuid = '" . $image_uuid_escaped . "'\n                    ");
             }
             Db::execute('unlock tables');
             $log_entry_modified = true;
         }
     }
     if (Settings::get('OC_BRANCH') == 'oc.pl' && $log_entry_modified) {
         # OCDE touches the log entry via trigger, OCPL needs an explicit update.
         # This will also update okapi_syncbase.
         Db::query("\n                update cache_logs\n                set last_modified = NOW()\n                where id = '" . Db::escape_string($log_internal_id) . "'\n            ");
         # OCPL code currently does not update pictures.last_modified when
         # editing, but that is a bug, see
         # https://github.com/opencaching/opencaching-pl/issues/341.
     }
     return $position;
 }
Пример #20
0
 private static function count_calls($consumer_key, $days)
 {
     return Db::select_value("\n                select count(*)\n                from okapi_stats_temp\n                where\n                    consumer_key = '" . Db::escape_string($consumer_key) . "'\n                    and service_name='services/replicate/fulldump'\n            ") + Db::select_value("\n                select sum(total_calls)\n                from okapi_stats_hourly\n                where\n                    consumer_key = '" . Db::escape_string($consumer_key) . "'\n                    and service_name='services/replicate/fulldump'\n                    and period_start > date_add(now(), interval -{$days} day)\n                limit 1\n            ");
 }
Пример #21
0
 /**
  * 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("\n            select distinct c.email\n            from\n                okapi_consumers c,\n                okapi_stats_hourly sh\n            where\n                sh.consumer_key = c.`key`\n                and sh.service_name in ('" . implode("','", array_map('\\okapi\\Db::escape_string', $service_names)) . "')\n                " . ($days != null ? "and sh.period_start > date_add(now(), interval '" . Db::escape_string(-$days) . "' day)" : "") . "\n        ");
 }
Пример #22
0
 private static function db_insert_image($consumer_key, $user_id, $log_internal_id, $image_uuid, $position, $caption, $is_spoiler, $file_ext)
 {
     require_once 'log_images_common.inc.php';
     list($position, $seq, $log_images_count) = LogImagesCommon::prepare_position($log_internal_id, $position, +1);
     # For OCDE the pictures table is write locked now.
     # Transactions do not work on OCDE MyISAM tables. However, the worst
     # thing that can happen on OCDE is creating a sequence number gap,
     # which is allowed.
     #
     # For OCPL InnoDB tables, the transactions DO and MUST work, because
     # we write to two dependent tables.
     Db::execute('start transaction');
     # shift positions of existing images to make space for the new one
     if ($position < $log_images_count && Settings::get('OC_BRANCH') == 'oc.de') {
         Db::execute("\n                update pictures\n                set seq = seq + 1\n                where\n                    object_type = 1\n                    and object_id = '" . Db::escape_string($log_internal_id) . "'\n                    and seq >= '" . Db::escape_string($seq) . "'\n                order by seq desc\n            ");
     }
     if (Settings::get('OC_BRANCH') == 'oc.de') {
         $local_fields_SQL = "seq";
         $local_values_escaped_SQL = "'" . Db::escape_string($seq) . "'";
         # All other fields are set by trigger or defaults for OCDE.
     } else {
         # These are the additional fields that OCPL newpic.php supplies
         # (seq is set from default):
         $local_fields_SQL = "date_created, last_modified, description, desc_html, last_url_check, user_id";
         $local_values_escaped_SQL = "NOW(), NOW(), '', 0, NOW(), '" . Db::escape_string($user_id) . "'";
     }
     Db::execute("\n            insert into pictures (\n                uuid, node, local, title, spoiler, url, object_type, object_id,\n                unknown_format, display,\n                " . $local_fields_SQL . "\n            )\n            values (\n                '" . Db::escape_string($image_uuid) . "',\n                '" . Db::escape_string(Settings::get('OC_NODE_ID')) . "',\n                1,\n                '" . Db::escape_string($caption) . "',\n                '" . ($is_spoiler == 'true' ? 1 : 0) . "',\n                '" . Db::escape_string(Settings::get('IMAGES_URL') . $image_uuid . $file_ext) . "',\n                1,\n                '" . Db::escape_string($log_internal_id) . "',\n                0,\n                1,\n                " . $local_values_escaped_SQL . "\n            )\n        ");
     $image_internal_id = Db::last_insert_id();
     # update OCPL log entry properties; OCDE does everything necessary by triggers
     if (Settings::get('OC_BRANCH') == 'oc.pl') {
         # This will also update cache_logs.okapi_syncbase, so that replication
         # can output the updated log entry with one image less. For OCDE
         # that's done by DB triggers.
         Db::execute("\n                update cache_logs\n                set\n                    picturescount = picturescount + 1,\n                    last_modified = NOW()\n                where id = '" . Db::escape_string($log_internal_id) . "'\n            ");
     }
     # Store information on the consumer_key which uploaded this image.
     # (Maybe we'll want to display this somewhen later.)
     Db::execute("\n            insert into okapi_submitted_objects (object_type, object_id, consumer_key)\n            values (\n                '" . Okapi::OBJECT_TYPE_CACHE_LOG_IMAGE . "',\n                '" . Db::escape_string($image_internal_id) . "',\n                '" . Db::escape_string($consumer_key) . "'\n            );\n        ");
     Db::execute('commit');
     Db::execute('unlock tables');
     return $position;
 }
Пример #23
0
 /**
  * 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.
     $status = self::get_tile_status($zoom, $x, $y);
     if ($status !== null) {
         return $status;
     }
     if ($zoom === 0) {
         # When computing zoom zero, we don't have a parent to speed up
         # the computation. We need to use the caches table. Note, that
         # zoom level 0 contains *entire world*, so we don't have to use
         # any WHERE condition in the following query.
         # This can be done a little faster (without the use of internal requests),
         # but there is *no need* to - this query is run seldom and is cached.
         $params = array();
         $params['status'] = "Available|Temporarily unavailable|Archived";
         # we want them all
         $params['limit'] = "10000000";
         # no limit
         $internal_request = new OkapiInternalRequest(new OkapiInternalConsumer(), null, $params);
         $internal_request->skip_limits = true;
         $response = OkapiServiceRunner::call("services/caches/search/all", $internal_request);
         $cache_codes = $response['results'];
         $internal_request = new OkapiInternalRequest(new OkapiInternalConsumer(), null, array('cache_codes' => implode('|', $cache_codes), 'fields' => 'internal_id|code|name|location|type|status|rating|recommendations|founds|trackables_count'));
         $internal_request->skip_limits = true;
         $caches = OkapiServiceRunner::call("services/caches/geocaches", $internal_request);
         foreach ($caches as $cache) {
             $row = self::generate_short_row($cache);
             if (!$row) {
                 /* Some caches cannot be included, e.g. the ones near the poles. */
                 continue;
             }
             Db::execute("\n                    replace into okapi_tile_caches (\n                        z, x, y, cache_id, z21x, z21y, status, type, rating, flags, name_crc\n                    ) values (\n                        0, 0, 0,\n                        '" . Db::escape_string($row[0]) . "',\n                        '" . Db::escape_string($row[1]) . "',\n                        '" . Db::escape_string($row[2]) . "',\n                        '" . Db::escape_string($row[3]) . "',\n                        '" . Db::escape_string($row[4]) . "',\n                        " . ($row[5] === null ? "null" : "'" . Db::escape_string($row[5]) . "'") . ",\n                        '" . Db::escape_string($row[6]) . "',\n                        '" . Db::escape_string($row[7]) . "'\n                    );\n                ");
         }
         $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;
         $status = self::get_tile_status($parent_zoom, $parent_x, $parent_y);
         if ($status === null) {
             $time_started = microtime(true);
             $status = self::compute_tile($parent_zoom, $parent_x, $parent_y);
         }
         if ($status === 1) {
             # No need to check.
         } else {
             $scale = 8 + 21 - $zoom;
             $parentcenter_z21x = ($parent_x << 1 | 1) << $scale;
             $parentcenter_z21y = ($parent_y << 1 | 1) << $scale;
             $margin = 1 << $scale - 2;
             $left_z21x = ($parent_x << 1 << $scale) - $margin;
             $right_z21x = ($parent_x + 1 << 1 << $scale) + $margin;
             $top_z21y = ($parent_y << 1 << $scale) - $margin;
             $bottom_z21y = ($parent_y + 1 << 1 << $scale) + $margin;
             # Choose the right quarter.
             # |1 2|
             # |3 4|
             if ($x & 1) {
                 # 2 or 4
                 $left_z21x = $parentcenter_z21x - $margin;
             } else {
                 # 1 or 3
                 $right_z21x = $parentcenter_z21x + $margin;
             }
             if ($y & 1) {
                 # 3 or 4
                 $top_z21y = $parentcenter_z21y - $margin;
             } else {
                 # 1 or 2
                 $bottom_z21y = $parentcenter_z21y + $margin;
             }
             # Cache the result.
             # Avoid deadlocks, see https://github.com/opencaching/okapi/issues/388
             Db::execute("lock tables okapi_tile_caches write, okapi_tile_caches tc2 read");
             Db::execute("\n                    replace into okapi_tile_caches (\n                        z, x, y, cache_id, z21x, z21y, status, type, rating,\n                        flags, name_crc\n                    )\n                    select\n                        '" . Db::escape_string($zoom) . "',\n                        '" . Db::escape_string($x) . "',\n                        '" . Db::escape_string($y) . "',\n                        cache_id, z21x, z21y, status, type, rating,\n                        flags, name_crc\n                    from okapi_tile_caches tc2\n                    where\n                        z = '" . Db::escape_string($parent_zoom) . "'\n                        and x = '" . Db::escape_string($parent_x) . "'\n                        and y = '" . Db::escape_string($parent_y) . "'\n                        and z21x between {$left_z21x} and {$right_z21x}\n                        and z21y between {$top_z21y} and {$bottom_z21y}\n                ");
             Db::execute("unlock tables");
             $test = Db::select_value("\n                    select 1\n                    from okapi_tile_caches\n                    where\n                        z = '" . Db::escape_string($zoom) . "'\n                        and x = '" . Db::escape_string($x) . "'\n                        and y = '" . Db::escape_string($y) . "'\n                    limit 1;\n                ");
             if ($test) {
                 $status = 2;
             } else {
                 $status = 1;
             }
         }
     }
     # Mark tile as computed.
     Db::execute("\n            replace into okapi_tile_status (z, x, y, status)\n            values (\n                '" . Db::escape_string($zoom) . "',\n                '" . Db::escape_string($x) . "',\n                '" . Db::escape_string($y) . "',\n                '" . Db::escape_string($status) . "'\n            );\n        ");
     return $status;
 }
Пример #24
0
 /**
  * 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_ids)
 {
     return Db::select_column("\n            select cache_id\n            from cache_logs\n            where\n                user_id in ('" . implode("','", array_map('\\okapi\\Db::escape_string', $internal_user_ids)) . "')\n                and type in (\n                    '" . Db::escape_string(Okapi::logtypename2id("Found it")) . "',\n                    '" . Db::escape_string(Okapi::logtypename2id("Attended")) . "'\n                )\n                and " . (Settings::get('OC_BRANCH') == 'oc.pl' ? "deleted = 0" : "true") . "\n        ");
 }
Пример #25
0
 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 .= "|" . Settings::get('SITELANG');
     $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='" . Db::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("\n                select\n                    c.cache_id, c.name, c.longitude, c.latitude, c.listing_last_modified as last_modified,\n                    c.date_created, c.type, c.status, c.date_hidden, c.size, c.difficulty,\n                    c.terrain, c.wp_oc, c.wp_gc, c.wp_gc_maintained, c.logpw, c.user_id,\n                    if(c.search_time=0, null, c.search_time) as trip_time,\n                    if(c.way_length=0, null, c.way_length) as trip_distance,\n                    c.listing_outdated, c.needs_maintenance,\n                    ifnull(sc.toprating, 0) as topratings,\n                    ifnull(sc.found, 0) as founds,\n                    ifnull(sc.notfound, 0) as notfounds,\n                    ifnull(sc.will_attend, 0) as willattends,\n                    sc.last_found,\n                    0 as votes, 0 as score\n                    -- SEE ALSO OC.PL BRANCH BELOW\n                from\n                    caches c\n                    left join stat_caches as sc on c.cache_id = sc.cache_id\n                where\n                    wp_oc in ('" . implode("','", array_map('\\okapi\\Db::escape_string', $cache_codes)) . "')\n                    and status in (1,2,3)\n            ");
     } 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("\n                select\n                    c.cache_id, c.name, c.longitude, c.latitude, c.last_modified,\n                    c.date_created, c.type, c.status, c.date_hidden, c.size, c.difficulty,\n                    c.terrain, c.wp_oc, c.wp_gc, '' as wp_gc_maintained, c.logpw, c.user_id,\n                    if(c.search_time=0, null, c.search_time) as trip_time,\n                    if(c.way_length=0, null, c.way_length) as trip_distance,\n                    0 as listing_outdated, 0 as needs_maintenance,\n                    c.topratings,\n                    c.founds,\n                    c.notfounds,\n                    c.notfounds as willattends,\n                    c.last_found,\n                    c.votes, c.score\n                    -- SEE ALSO OC.DE BRANCH ABOVE\n                from\n                    caches c\n                where\n                    wp_oc in ('" . implode("','", array_map('\\okapi\\Db::escape_string', $cache_codes)) . "')\n                    and c.status in (1,2,3)\n            ");
     }
     $results = new ArrayObject();
     $cacheid2wptcode = array();
     $owner_ids = array();
     $outdated_listings = array();
     while ($row = Db::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':
                     $wp_gc = $row['wp_gc_maintained'] ? $row['wp_gc_maintained'] : $row['wp_gc'];
                     // 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*$/', $wp_gc)) {
                         $entry['gc_code'] = strtoupper(trim($wp_gc));
                     } else {
                         $entry['gc_code'] = null;
                     }
                     unset($wp_gc);
                     break;
                 case 'name':
                     $entry['name'] = $row['name'];
                     break;
                 case 'names':
                     $entry['names'] = array(Settings::get('SITELANG') => $row['name']);
                     break;
                     // for the future
                 // 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 'needs_maintenance':
                     $entry['needs_maintenance'] = $row['needs_maintenance'] > 0;
                     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
                         # 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
                         # 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;
         if ($row['listing_outdated'] > 0) {
             $outdated_listings[] = $row['wp_oc'];
         }
     }
     Db::free_result($rs);
     # owner
     if (in_array('owner', $fields) && count($results) > 0) {
         $rs = Db::query("\n                select user_id, uuid, username\n                from user\n                where user_id in ('" . implode("','", array_map('\\okapi\\Db::escape_string', array_values($owner_ids))) . "')\n            ");
         $tmp = array();
         while ($row = Db::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("\n                select c.wp_oc\n                from\n                    caches c,\n                    cache_logs cl\n                where\n                    c.cache_id = cl.cache_id\n                    and cl.type in (\n                        '" . Db::escape_string(Okapi::logtypename2id("Found it")) . "',\n                        '" . Db::escape_string(Okapi::logtypename2id("Attended")) . "'\n                    )\n                    and cl.user_id = '" . Db::escape_string($user_id) . "'\n                    " . (Settings::get('OC_BRANCH') == 'oc.pl' ? "and cl.deleted = 0" : "") . "\n            ");
         $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("\n                select c.wp_oc\n                from\n                    caches c,\n                    cache_logs cl\n                where\n                    c.cache_id = cl.cache_id\n                    and cl.type = '" . Db::escape_string(Okapi::logtypename2id("Didn't find it")) . "'\n                    and cl.user_id = '" . Db::escape_string($user_id) . "'\n                    " . (Settings::get('OC_BRANCH') == 'oc.pl' ? "and cl.deleted = 0" : "") . "\n            ");
         $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("\n                select c.wp_oc\n                from\n                    caches c,\n                    cache_watches cw\n                where\n                    c.cache_id = cw.cache_id\n                    and cw.user_id = '" . Db::escape_string($request->token->user_id) . "'\n            ");
         $tmp2 = array();
         foreach ($tmp as $cache_code) {
             $tmp2[$cache_code] = true;
         }
         # OCDE caches can also be indirectly watched by watching cache lists:
         if (Settings::get('OC_BRANCH') == 'oc.de') {
             $tmp = Db::select_column("\n                  select c.wp_oc\n                  from\n                      caches c,\n                      cache_list_items cli,\n                      cache_list_watches clw\n                  where\n                      cli.cache_id = c.cache_id\n                      and clw.cache_list_id = cli.cache_list_id\n                      and clw.user_id = '" . Db::escape_string($request->token->user_id) . "'\n              ");
             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("\n                select c.wp_oc\n                from\n                    caches c,\n                    cache_ignore ci\n                where\n                    c.cache_id = ci.cache_id\n                    and ci.user_id = '" . Db::escape_string($request->token->user_id) . "'\n            ");
         $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("\n                select cache_id, language, `desc`, short_desc, hint\n                from cache_desc\n                where cache_id in ('" . implode("','", array_map('\\okapi\\Db::escape_string', array_keys($cacheid2wptcode))) . "')\n            ");
         while ($row = Db::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'], Okapi::OBJECT_TYPE_CACHE);
                 if (in_array($cache_code, $outdated_listings)) {
                     Okapi::gettext_domain_init(array_merge(array($row['language']), $langpref));
                     $tmp = "<p style='color:#c00000'><strong>" . _('Parts of this geocache listing may be outdated.') . "</strong> " . _('See the log entries for more information.') . "</p>\n" . $tmp;
                     Okapi::gettext_domain_restore();
                 }
                 if ($attribution_append != 'none') {
                     $tmp .= "\n<p><em>" . self::get_cache_attribution_note($row['cache_id'], strtolower($row['language']), $langpref, $results[$cache_code]['owner'], $attribution_append) . "</em></p>";
                 }
                 $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("<br />", "", $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("\n                select object_id, uuid, url, title, spoiler, " . $preview_field . " as preview\n                from pictures\n                where\n                    object_id in ('" . implode("','", array_map('\\okapi\\Db::escape_string', array_keys($cacheid2wptcode))) . "')\n                    and display = 1\n                    and object_type = 2\n                    and unknown_format = 0\n                order by object_id, seq, date_created\n            ");
         unset($sql);
         $prev_cache_code = null;
         while ($row = Db::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;
             }
             if (Settings::get('OC_BRANCH') == 'oc.de') {
                 $object_type_param = 'type=2&';
             } else {
                 $object_type_param = '';
             }
             $image = array('uuid' => $row['uuid'], 'url' => $row['url'], 'thumb_url' => Settings::get('SITE_URL') . 'thumbs.php?' . $object_type_param . '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("\n                select cache_id, attrib_id\n                from caches_attributes\n                where cache_id in ('" . implode("','", array_map('\\okapi\\Db::escape_string', array_keys($cacheid2wptcode))) . "')\n            ");
         while ($row = Db::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 in proper order, then filter out the latest
         # ones. This should be the fastest technique ...
         # OCDE allows to submit logs without time. To minimize problems
         # when ordering logs with and without time on the same day, there
         # is a separate order_date field which is caluclated from 'date'
         # and 'date_created'.
         #
         # OCPL log entries are all submitted with time, so the 'date'
         # field is sufficient for ordering.
         if (Settings::get('OC_BRANCH') == 'oc.de') {
             $logs_order_field_SQL = 'order_date';
         } else {
             $logs_order_field_SQL = 'date';
         }
         $rs = Db::query("\n                select cache_id, uuid\n                from cache_logs\n                where\n                    cache_id in ('" . implode("','", array_map('\\okapi\\Db::escape_string', array_keys($cacheid2wptcode))) . "')\n                    and " . (Settings::get('OC_BRANCH') == 'oc.pl' ? "deleted = 0" : "true") . "\n                order by cache_id, " . $logs_order_field_SQL . " desc, date_created desc, id desc\n            ");
         $loguuids = array();
         $log2cache_map = array();
         if ($lpc !== null) {
             # User wants some of the latest logs.
             $tmp = array();
             while ($row = Db::fetch_assoc($rs)) {
                 $tmp[$row['cache_id']][] = $row;
             }
             foreach ($tmp as $cache_key => &$rowslist_ref) {
                 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 = Db::fetch_assoc($rs)) {
                 $loguuids[] = $row['uuid'];
                 $log2cache_map[$row['uuid']] = $cacheid2wptcode[$row['cache_id']];
             }
         }
         Db::free_result($rs);
         # 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("\n                    select cache_id, max(date) as date, group_concat(`desc`) as `desc`\n                    from cache_notes\n                    where\n                        cache_id in ('" . implode("','", array_map('\\okapi\\Db::escape_string', array_keys($cacheid2wptcode))) . "')\n                        and user_id = '" . Db::escape_string($request->token->user_id) . "'\n                    group by cache_id\n                ");
         } else {
             # OCDE uses coordinates table (with type == 2) to store notes (this is somewhat weird).
             $rs = Db::query("\n                    select cache_id, null as date, group_concat(description) as `desc`\n                    from coordinates\n                    where\n                        type = 2  -- personal note\n                        and cache_id in ('" . implode("','", array_map('\\okapi\\Db::escape_string', array_keys($cacheid2wptcode))) . "')\n                        and user_id = '" . Db::escape_string($request->token->user_id) . "'\n                    group by cache_id\n                ");
         }
         while ($row = Db::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("\n                select\n                    gkiw.wp as cache_code,\n                    gki.id as gk_id,\n                    gki.name\n                from\n                    gk_item_waypoint gkiw,\n                    gk_item gki\n                where\n                    gkiw.id = gki.id\n                    and gkiw.wp in ('" . implode("','", array_map('\\okapi\\Db::escape_string', $cache_codes)) . "')\n            ");
         $trs = array();
         while ($row = Db::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("\n                    select wp as cache_code, count(*) as count\n                    from gk_item_waypoint\n                    where wp in ('" . implode("','", array_map('\\okapi\\Db::escape_string', $cache_codes)) . "')\n                    group by wp\n                ");
             $tr_counts = new ArrayObject();
             while ($row = Db::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("\n                    select\n                        ct.id,\n                        LOWER(stt.lang) as language,\n                        stt.`text`\n                    from\n                        coordinates_type ct\n                        left join sys_trans_text stt on stt.trans_id = ct.trans_id\n                ");
             while ($row = Db::fetch_assoc($rs)) {
                 $internal_wpt_type_id2names[$row['id']][$row['language']] = $row['text'];
             }
             Db::free_result($rs);
         } else {
             $rs = Db::query("\n                    select id, pl, en\n                    from waypoint_type\n                    where id > 0\n                ");
             while ($row = Db::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('\\okapi\\Db::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", "\n                    select\n                        cache_id, stage, latitude, longitude, `desc`,\n                        type as internal_type_id,\n                        case type\n                            when 3 then 'Flag, Red'\n                            when 4 then 'Circle with X'\n                            when 5 then 'Parking Area'\n                            else 'Flag, Green'\n                        end as sym,\n                        case type\n                            when 1 then 'physical-stage'\n                            when 2 then 'virtual-stage'\n                            when 3 then 'final'\n                            when 4 then 'poi'\n                            when 5 then 'parking'\n                            else 'other'\n                        end as okapi_type\n                    from waypoints\n                    where\n                        cache_id in (" . $cache_codes_escaped_and_imploded . ")\n                        and status = 1\n                    order by cache_id, stage, `desc`\n                ");
         } 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", "\n                    select\n                        cache_id,\n                        false as stage,\n                        latitude, longitude,\n                        description as `desc`,\n                        subtype as internal_type_id,\n                        case subtype\n                            when 1 then 'Parking Area'\n                            when 3 then 'Flag, Blue'\n                            when 4 then 'Circle with X'\n                            when 5 then 'Diamond, Green'\n                            else 'Flag, Green'\n                        end as sym,\n                        case subtype\n                            when 1 then 'parking'\n                            when 2 then 'stage'\n                            when 3 then 'path'\n                            when 4 then 'final'\n                            when 5 then 'poi'\n                            else 'other'\n                        end as okapi_type\n                    from coordinates\n                    where\n                        type = 1\n                        and cache_id in (" . $cache_codes_escaped_and_imploded . ")\n                    order by cache_id, id\n                ");
         }
         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', "\n                        select\n                            cache_id, longitude, latitude\n                        from cache_mod_cords\n                        where\n                            cache_id in ({$cache_codes_escaped_and_imploded})\n                            and user_id = '" . Db::escape_string($request->token->user_id) . "'\n                    ");
             } else {
                 # oc.de
                 $cacheid2user_coords = Db::select_group_by('cache_id', "\n                        select\n                            cache_id, longitude, latitude\n                        from coordinates\n                        where\n                            cache_id in ({$cache_codes_escaped_and_imploded})\n                            and user_id = '" . Db::escape_string($request->token->user_id) . "'\n                            and type = 2\n                            and longitude != 0\n                            and latitude != 0\n                    ");
             }
             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("\n                    select distinct\n                        c.country,\n                        lower(stt.lang) as language,\n                        stt.`text`\n                    from\n                        caches c\n                        inner join countries on countries.short=c.country\n                        inner join sys_trans_text stt on stt.trans_id = countries.trans_id\n                    where\n                        c.wp_oc in ('" . implode("','", array_map('\\okapi\\Db::escape_string', $cache_codes)) . "')\n                ");
             $country_codes2names = array();
             while ($row = Db::fetch_assoc($rs)) {
                 $country_codes2names[$row['country']][$row['language']] = $row['text'];
             }
             Db::free_result($rs);
             # get geocache countries and states
             $rs = Db::query("\n                    select\n                        c.wp_oc as cache_code,\n                        c.country as country_code,\n                        ifnull(if(c.country<>cl.code1,'',cl.adm2),'') as state\n                    from\n                        caches c\n                        left join cache_location cl on c.cache_id = cl.cache_id\n                    where\n                        c.wp_oc in ('" . implode("','", array_map('\\okapi\\Db::escape_string', $cache_codes)) . "')\n                ");
             while ($row = Db::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'];
             }
             Db::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("\n                    select\n                        c.wp_oc as cache_code,\n                        cl.adm1 as country,\n                        cl.adm3 as state\n                    from\n                        caches c,\n                        cache_location cl\n                    where\n                        c.wp_oc in ('" . implode("','", array_map('\\okapi\\Db::escape_string', $cache_codes)) . "')\n                        and c.cache_id = cl.cache_id\n                ");
             while ($row = Db::fetch_assoc($rs)) {
                 $countries[$row['cache_code']] = $row['country'];
                 $states[$row['cache_code']] = $row['state'];
             }
             Db::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('\\okapi\\Db::escape_string', array_keys($cacheid2wptcode))) . "'";
         if (Settings::get('OC_BRANCH') == 'oc.de') {
             $rs = Db::query("\n                    select\n                        c.wp_oc as cache_code,\n                        npa_types.name as type,\n                        npa_areas.name as name\n                    from\n                        caches c\n                        inner join cache_npa_areas on cache_npa_areas.cache_id=c.cache_id\n                        inner join npa_areas on cache_npa_areas.npa_id = npa_areas.id\n                        inner join npa_types on npa_areas.type_id = npa_types.id\n                    where\n                        c.cache_id in (" . $cache_ids_escaped_and_imploded . ")\n                    group by npa_areas.type_id, npa_areas.name\n                    order by npa_types.ordinal\n                ");
         } else {
             if (in_array(Okapi::get_oc_schema_code(), array("OCPL", "OCNL"))) {
                 # 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.
                 #
                 # TODO: use DB_CHARSET setting instead of literal 'utf8'
                 $rs = Db::query("\n                    select\n                        c.wp_oc as cache_code,\n                        '" . _('National Park / Landscape') . "' as type,\n                        CONVERT(parkipl.name USING utf8) as name\n                    from\n                        caches c\n                        inner join cache_npa_areas on cache_npa_areas.cache_id=c.cache_id\n                        inner join parkipl on cache_npa_areas.parki_id=parkipl.id\n                    where\n                        c.cache_id in (" . $cache_ids_escaped_and_imploded . ")\n                        and cache_npa_areas.parki_id != 0\n                    union\n                    select\n                        c.wp_oc as cache_code,\n                        'Natura 2000' as type,\n                        CONVERT(npa_areas.sitename USING utf8) as name\n                    from\n                        caches c\n                        inner join cache_npa_areas on cache_npa_areas.cache_id=c.cache_id\n                        inner join npa_areas on cache_npa_areas.npa_id=npa_areas.id\n                    where\n                        c.cache_id in (" . $cache_ids_escaped_and_imploded . ")\n                        and cache_npa_areas.npa_id != 0\n                ");
             } 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 = Db::fetch_assoc($rs)) {
                 $results[$row['cache_code']]['protection_areas'][] = array('type' => $row['type'], 'name' => $row['name']);
             }
             Db::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);
 }
Пример #26
0
 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 (Settings::get('OC_BRANCH') == 'oc.de' && $user_id != -1) {
         $user_id = 0;
     }
     Db::execute("\n            insert into okapi_stats_temp (`datetime`, consumer_key, user_id, service_name, calltype, runtime)\n            values (\n                now(),\n                '" . Db::escape_string($consumer_key) . "',\n                '" . Db::escape_string($user_id) . "',\n                '" . Db::escape_string($service_name) . "',\n                '" . Db::escape_string($calltype) . "',\n                '" . Db::escape_string($runtime) . "'\n            );\n        ");
 }
Пример #27
0
 public static function call(OkapiRequest $request)
 {
     # You may wonder, why there are no parameters like "bbox" or "center" in the
     # "search/all" method. This is *intentional* and should be kept this way.
     # Such parameters would fall in conflict with each other and - in result -
     # make the documentation very fuzzy. That's why they were intentionally
     # left out of the "search/all" method, and put in separate (individual) ones.
     # It's much easier to grasp their meaning this way.
     $tmp = $request->get_parameter('bbox');
     if (!$tmp) {
         throw new ParamMissing('bbox');
     }
     $parts = explode('|', $tmp);
     if (count($parts) != 4) {
         throw new InvalidParam('bbox', "Expecting 4 pipe-separated parts, got " . count($parts) . ".");
     }
     foreach ($parts as &$part_ref) {
         if (!preg_match("/^-?[0-9]+(\\.?[0-9]*)\$/", $part_ref)) {
             throw new InvalidParam('bbox', "'{$part_ref}' is not a valid float number.");
         }
         $part_ref = floatval($part_ref);
     }
     list($bbsouth, $bbwest, $bbnorth, $bbeast) = $parts;
     if ($bbnorth <= $bbsouth) {
         throw new InvalidParam('bbox', "Northern edge must be situated to the north of the southern edge.");
     }
     if ($bbeast == $bbwest) {
         throw new InvalidParam('bbox', "Eastern edge longitude is the same as the western one.");
     }
     if ($bbnorth > 90 || $bbnorth < -90 || $bbsouth > 90 || $bbsouth < -90) {
         throw new InvalidParam('bbox', "Latitudes have to be within -90..90 range.");
     }
     if ($bbeast > 180 || $bbeast < -180 || $bbwest > 180 || $bbwest < -180) {
         throw new InvalidParam('bbox', "Longitudes have to be within -180..180 range.");
     }
     # Construct SQL conditions for the specified bounding box.
     $search_assistant = new SearchAssistant($request);
     $search_assistant->prepare_common_search_params();
     $search_assistant->prepare_location_search_params();
     $where_conds = array();
     $lat = $search_assistant->get_latitude_expr();
     $lon = $search_assistant->get_longitude_expr();
     $where_conds[] = "(\n            {$lat} >= '" . Db::escape_string($bbsouth) . "'\n            and {$lat} < '" . Db::escape_string($bbnorth) . "'\n        )";
     if ($bbeast > $bbwest) {
         # Easy one.
         $where_conds[] = "(\n                {$lon} >= '" . Db::escape_string($bbwest) . "'\n                and {$lon} < '" . Db::escape_string($bbeast) . "'\n            )";
     } else {
         # We'll have to assume that this bbox goes through the 180-degree meridian.
         # For example, $bbwest = 179 and $bbeast = -179.
         $where_conds[] = "(\n                {$lon} >= '" . Db::escape_string($bbwest) . "'\n                or {$lon} < '" . Db::escape_string($bbeast) . "'\n            )";
     }
     #
     # In the method description, we promised to return caches ordered by the *rough*
     # distance from the center of the bounding box. We'll use ORDER BY with a simplified
     # distance formula and combine it with the LIMIT clause to get the best results.
     #
     $center_lat = ($bbsouth + $bbnorth) / 2.0;
     $center_lon = ($bbwest + $bbeast) / 2.0;
     $search_params = $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);
     $result = $search_assistant->get_common_search_result();
     return Okapi::formatted_response($request, $result);
 }
Пример #28
0
 /**
  * Return 64x26 bitmap with the caption (name) for the given geocache.
  */
 private function get_caption($cache_id, $name_crc)
 {
     # Check cache.
     $cache_key = "tilecaption/" . self::$VERSION . "/" . $cache_id . "/" . $name_crc;
     $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("\n                select name\n                from caches\n                where cache_id = '" . Db::escape_string($cache_id) . "'\n            ");
         # 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);
 }
Пример #29
0
 /**
  * OCDE supports arbitrary ordering of log images. The pictures table
  * contains sequence numbers, which are always > 0 and need not to be
  * consecutive (may have gaps). There is a unique index which prevents
  * inserting duplicate seq numbers for the same log.
  *
  * OCPL sequence numbers currently are always = 1.
  *
  * The purpose of this function is to bring the supplied 'position'
  * parameter into bounds, and to calculate an appropriate sequence number
  * from it.
  *
  * This function is always called when adding images. When editing images,
  * it is called only for OCDE and if the position parameter was supplied.
  */
 static function prepare_position($log_internal_id, $position, $end_offset)
 {
     if (Settings::get('OC_BRANCH') == 'oc.de' && $position !== null) {
         # Prevent race conditions when creating sequence numbers if a
         # user tries to upload multiple images simultaneously. With a
         # few picture uploads per hour - most of them probably witout
         # a 'position' parameter - the lock is neglectable.
         Db::execute('lock tables pictures write');
     }
     $log_images_count = Db::select_value("\n            select count(*)\n            from pictures\n            where object_type = 1 and object_id = '" . Db::escape_string($log_internal_id) . "'\n        ");
     if (Settings::get('OC_BRANCH') == 'oc.pl') {
         # Ignore the position parameter, always insert at end.
         # Remember that this function is NOT called when editing OCPL images.
         $position = $log_images_count;
         $seq = 1;
     } else {
         if ($position === null || $position >= $log_images_count) {
             $position = $log_images_count - 1 + $end_offset;
             $seq = Db::select_value("\n                    select max(seq)\n                    from pictures\n                    where object_type = 1 and object_id = '" . Db::escape_string($log_internal_id) . "'\n                ") + $end_offset;
         } else {
             if ($position <= 0) {
                 $position = 0;
                 $seq = 1;
             } else {
                 $seq = Db::select_value("\n                    select seq\n                    from pictures\n                    where object_type = 1 and object_id = '" . Db::escape_string($log_internal_id) . "'\n                    order by seq\n                    limit " . ($position + 0) . ", 1\n                ");
             }
         }
     }
     # $position may have become a string, as returned by database queries.
     return array($position + 0, $seq, $log_images_count);
 }
Пример #30
0
 /**
  * Return changelog chunk, starting at $from, ending as $to.
  */
 public static function get_chunk($from, $to)
 {
     if ($to < $from) {
         return array();
     }
     if ($to - $from > self::$chunk_size) {
         throw new Exception("You should not get chunksize bigger than " . self::$chunk_size . " entries at one time.");
     }
     # Check if we already have this chunk in cache.
     $cache_key = 'clog_chunk#' . $from . '-' . $to;
     $chunk = Cache::get($cache_key);
     if ($chunk === null) {
         $rs = Db::query("\n                select id, data\n                from okapi_clog\n                where id between '" . Db::escape_string($from) . "' and '" . Db::escape_string($to) . "'\n                order by id\n            ");
         $chunk = array();
         while ($row = Db::fetch_assoc($rs)) {
             $chunk[] = unserialize(gzinflate($row['data']));
         }
         # Cache timeout depends on the chunk starting and ending point. Chunks
         # which start and end on the boundries of chunk_size should be cached
         # longer (they can be accessed even after 10 days). Other chunks won't
         # be ever accessed after the next revision appears, so there is not point
         # in storing them that long.
         if ($from % self::$chunk_size === 0 && $to % self::$chunk_size === 0) {
             $timeout = 10 * 86400;
         } else {
             $timeout = 86400;
         }
         Cache::set($cache_key, $chunk, $timeout);
     }
     return $chunk;
 }