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); }
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; }
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); }
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; }
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); }
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; }
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); }
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/"); }
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); }
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; }
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; }
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 "); } }
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; }
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"); }
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); }
/** * 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 "); }
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); } }
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']; } }
/** * 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; }
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 "); }
/** * 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 "); }
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; }
/** * 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; }
/** * 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 "); }
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); }
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 "); }
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); }
/** * 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); }
/** * 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); }
/** * 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; }