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 = '" . mysql_real_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 = mysql_fetch_assoc($rs)) { $vars['apps'][] = $row; } mysql_free_result($rs); $response = new OkapiHttpResponse(); $response->content_type = "text/html; charset=utf-8"; ob_start(); Okapi::gettext_domain_init($langprefs); include 'index.tpl.php'; $response->body = ob_get_clean(); Okapi::gettext_domain_restore(); return $response; }
function validate_image_uuid($request) { $image_uuid = $request->get_parameter('image_uuid'); if (!$image_uuid) { throw new ParamMissing('image_uuid'); } # When uploading images, OCPL stores the user_id of the uploader # in the 'pictures' table. This is redundant to cache_logs.user_id, # because only the log entry author may append images. We will stick # to log_entries.user_id here, which is the original value and works # for all OC branches, and ignore pictures.user_id. $rs = Db::query("\n select\n cache_logs.id log_internal_id,\n cache_logs.user_id,\n pictures.node\n from cache_logs\n join pictures on pictures.object_id = cache_logs.id\n where pictures.object_type = 1 and pictures.uuid = '" . Db::escape_string($image_uuid) . "'\n "); $row = Db::fetch_assoc($rs); Db::free_result($rs); if (!$row) { throw new InvalidParam('image_uuid', "There is no log entry image with uuid '" . $image_uuid . "'."); } if ($row['node'] != Settings::get('OC_NODE_ID')) { throw new Exception("This site's database contains the image '{$image_uuid}' which has been" . " imported from another OC node. OKAPI is not prepared for that."); } if ($row['user_id'] != $request->token->user_id) { throw new InvalidParam('image_uuid', "The user of your access token is not the author of the associated log entry."); } return array($image_uuid, $row['log_internal_id']); }
public static function call(OkapiRequest $request) { $usernames = $request->get_parameter('usernames'); if (!$usernames) { throw new ParamMissing('usernames'); } $usernames = explode("|", $usernames); if (count($usernames) > 500) { throw new InvalidParam('usernames', "Maximum allowed number of referenced users " . "is 500. You provided " . count($usernames) . " usernames."); } $fields = $request->get_parameter('fields'); if (!$fields) { throw new ParamMissing('fields'); } # There's no need to validate the fields parameter as the 'users' # method does this (it will raise a proper exception on invalid values). $rs = Db::query("\n select username, uuid\n from user\n where username collate " . Settings::get('DB_CHARSET') . "_general_ci in ('" . implode("','", array_map('\\okapi\\Db::escape_string', $usernames)) . "')\n "); $lower_username2useruuid = array(); while ($row = Db::fetch_assoc($rs)) { $lower_username2useruuid[mb_strtolower($row['username'], 'utf-8')] = $row['uuid']; } Db::free_result($rs); # Retrieve data for the found user_uuids. if (count($lower_username2useruuid) > 0) { $id_results = OkapiServiceRunner::call('services/users/users', new OkapiInternalRequest($request->consumer, $request->token, array('user_uuids' => implode("|", array_values($lower_username2useruuid)), 'fields' => $fields))); } else { $id_results = array(); } # Map user_uuids back to usernames. Also check which usernames were not found # and mark them with null. $results = array(); foreach ($usernames as $username) { if (!isset($lower_username2useruuid[mb_strtolower($username, 'utf-8')])) { $results[$username] = null; } else { $results[$username] = $id_results[$lower_username2useruuid[mb_strtolower($username, 'utf-8')]]; } } return Okapi::formatted_response($request, $results); }
/** * Return MySQL's result set iterator over all caches which are present * in the given result set AND in the given tile. * * Each row is an array of the following format: * list(cache_id, $pixel_x, $pixel_y, status, type, rating, flags, name_crc, [count]). * * Note that $pixels can also be negative or >=256 (up to a margin of 32px). * Count is the number of other caches "eclipsed" by this geocache (such * eclipsed geocaches are not included in the result). */ public static function query_fast($zoom, $x, $y, $set_id) { # First, we check if the cache-set for this tile was already computed # (and if it was, was it empty). $status = self::get_tile_status($zoom, $x, $y); if ($status === null) { # Note, that computing the tile does not involve taking any # search parameters. $status = self::compute_tile($zoom, $x, $y); } if ($status === 1) { # This tile was already computed and it is empty. return null; } # If we got here, then the tile is computed and not empty (status 2). $tile_upper_x = $x << 8; $tile_leftmost_y = $y << 8; $zoom_escaped = "'" . Db::escape_string($zoom) . "'"; $tile_upper_x_escaped = "'" . Db::escape_string($tile_upper_x) . "'"; $tile_leftmost_y_escaped = "'" . Db::escape_string($tile_leftmost_y) . "'"; return Db::query("\n select\n otc.cache_id,\n cast(otc.z21x >> (21 - {$zoom_escaped}) as signed) - {$tile_upper_x_escaped} as px,\n cast(otc.z21y >> (21 - {$zoom_escaped}) as signed) - {$tile_leftmost_y_escaped} as py,\n otc.status, otc.type, otc.rating, otc.flags, otc.name_crc, count(*)\n from\n okapi_tile_caches otc,\n okapi_search_results osr\n where\n z = {$zoom_escaped}\n and x = '" . Db::escape_string($x) . "'\n and y = '" . Db::escape_string($y) . "'\n and otc.cache_id = osr.cache_id\n and osr.set_id = '" . Db::escape_string($set_id) . "'\n group by\n z21x >> (3 + (21 - {$zoom_escaped})),\n z21y >> (3 + (21 - {$zoom_escaped}))\n order by\n z21y >> (3 + (21 - {$zoom_escaped})),\n z21x >> (3 + (21 - {$zoom_escaped}))\n "); }
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); }
private static function handle_geocache_replace($c) { # Check if any relevant geocache attributes have changed. # We will pick up "our" copy of the cache from zero-zoom level. try { $cache = OkapiServiceRunner::call("services/caches/geocache", new OkapiInternalRequest(new OkapiInternalConsumer(), null, array('cache_code' => $c['object_key']['code'], 'fields' => 'internal_id|code|name|location|type|status|rating|recommendations|founds|trackables_count'))); } catch (InvalidParam $e) { # Unprobable, but possible. Ignore changelog entry. return; } # Fetch our copy of the cache. $ours = mysql_fetch_row(Db::query("\n select cache_id, z21x, z21y, status, type, rating, flags, name_crc\n from okapi_tile_caches\n where\n z=0\n and cache_id = '" . mysql_real_escape_string($cache['internal_id']) . "'\n ")); # Caches near the poles caused our computations to break here. We will # ignore such caches! list($lat, $lon) = explode("|", $cache['location']); if (floatval($lat) >= 89.98999999999999 || floatval($lat) <= -89.98999999999999) { if ($ours) { self::remove_geocache_from_cached_tiles($ours[0]); } return; } # Compute the new row for okapi_tile_caches. Compare with the old one. $theirs = TileTree::generate_short_row($cache); if (!$ours) { # Aaah, a new geocache! How nice... ;) self::add_geocache_to_cached_tiles($theirs); } elseif ($ours[1] != $theirs[1] || $ours[2] != $theirs[2]) { # Location changed. self::remove_geocache_from_cached_tiles($ours[0]); self::add_geocache_to_cached_tiles($theirs); } elseif ($ours != $theirs) { self::update_geocache_attributes_in_cached_tiles($theirs); } else { # No need to update anything. This is very common (i.e. when the # cache was simply found, not actually changed). Replicate module generates # many updates which do not influence our cache. } }
public static function call(OkapiRequest $request) { $internal_ids = $request->get_parameter('internal_ids'); if (!$internal_ids) { throw new ParamMissing('internal_ids'); } $internal_ids = explode("|", $internal_ids); if (count($internal_ids) > 500) { throw new InvalidParam('internal_ids', "Maximum allowed number of referenced users " . "is 500. You provided " . count($internal_ids) . " references."); } $fields = $request->get_parameter('fields'); if (!$fields) { throw new ParamMissing('fields'); } # There's no need to validate the fields parameter as the 'users' # method does this (it will raise a proper exception on invalid values). $rs = Db::query("\n select user_id, uuid\n from user\n where user_id in ('" . implode("','", array_map('mysql_real_escape_string', $internal_ids)) . "')\n "); $internalid2useruuid = array(); while ($row = mysql_fetch_assoc($rs)) { $internalid2useruuid[$row['user_id']] = $row['uuid']; } mysql_free_result($rs); # Retrieve data on given user_uuids. $id_results = OkapiServiceRunner::call('services/users/users', new OkapiInternalRequest($request->consumer, $request->token, array('user_uuids' => implode("|", array_values($internalid2useruuid)), 'fields' => $fields))); # Map user_uuids to internal_ids. Also check which internal_ids were not found # and mark them with null. $results = array(); foreach ($internal_ids as $internal_id) { if (!isset($internalid2useruuid[$internal_id])) { $results[$internal_id] = null; } else { $results[$internal_id] = $id_results[$internalid2useruuid[$internal_id]]; } } return Okapi::formatted_response($request, $results); }
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() { Db::query("optimize table okapi_tile_caches"); Db::query("optimize table okapi_tile_status"); }
public static function call() { # Flush the stats, so the page is fresh upon every request. require_once $GLOBALS['rootpath'] . "okapi/cronjobs.php"; CronJobController::force_run("StatsWriterCronJob"); # When services/caches/map/tile method is called, it writes some extra # stats in the okapi_stats_hourly table. This page retrieves and # formats these stats in a readable manner (for debugging). $response = new OkapiHttpResponse(); $response->content_type = "text/plain; charset=utf-8"; ob_start(); $start = isset($_GET['start']) ? $_GET['start'] : date("Y-m-d 00:00:00", time() - 7 * 86400); $end = isset($_GET['end']) ? $_GET['end'] : date("Y-m-d 23:59:59"); print "From: {$start}\n"; print " To: {$end}\n\n"; $rs = Db::query("\n select\n service_name,\n sum(total_calls),\n sum(total_runtime)\n from okapi_stats_hourly\n where\n period_start >= '" . mysql_real_escape_string($start) . "'\n and period_start < '" . mysql_real_escape_string($end) . "'\n and service_name like '%caches/map/tile%'\n group by service_name\n "); $total_calls = 0; $total_runtime = 0.0; $calls = array('A' => 0, 'B' => 0, 'C' => 0, 'D' => 0); $runtime = array('A' => 0.0, 'B' => 0.0, 'C' => 0.0, 'D' => 0.0); while (list($name, $c, $r) = mysql_fetch_array($rs)) { if ($name == 'services/caches/map/tile') { $total_calls = $c; $total_runtime = $r; } elseif (strpos($name, 'extra/caches/map/tile/checkpoint') === 0) { $calls[$name[32]] = $c; $runtime[$name[32]] = $r; } } if ($total_calls != $calls['A']) { print "Partial results. Only " . $calls['A'] . " out of {$total_calls} are covered.\n"; print "All other will count as \"unaccounted for\".\n\n"; $total_calls = $calls['A']; } $calls_left = $total_calls; $runtime_left = $total_runtime; $perc = function ($a, $b) { return $b > 0 ? sprintf("%.1f", 100 * $a / $b) . "%" : "(?)"; }; $avg = function ($a, $b) { return $b > 0 ? sprintf("%.4f", $a / $b) . "s" : "(?)"; }; $get_stats = function () use(&$calls_left, &$runtime_left, &$total_calls, &$total_runtime, &$perc) { return str_pad($perc($calls_left, $total_calls), 6, " ", STR_PAD_LEFT) . str_pad($perc($runtime_left, $total_runtime), 7, " ", STR_PAD_LEFT); }; print "%CALLS %TIME Description\n"; print "====== ====== ======================================================================\n"; print $get_stats() . " {$total_calls} responses served. Total runtime: " . sprintf("%.2f", $total_runtime) . "s\n"; print "\n"; print " All of these requests needed a TileTree build/lookup. The average runtime of\n"; print " these lookups was " . $avg($runtime['A'], $total_calls) . ". " . $perc($runtime['A'], $total_runtime) . " of total runtime was spent here.\n"; print "\n"; $runtime_left -= $runtime['A']; print $get_stats() . " All calls passed here after ~" . $avg($runtime['A'], $total_calls) . "\n"; print "\n"; print " Lookup result was then processed and \"image description\" was created. It was\n"; print " passed on to the TileRenderer to compute the ETag hash string. The average runtime\n"; print " of this part was " . $avg($runtime['B'], $total_calls) . ". " . $perc($runtime['B'], $total_runtime) . " of total runtime was spent here.\n"; print "\n"; $runtime_left -= $runtime['B']; print $get_stats() . " All calls passed here after ~" . $avg($runtime['A'] + $runtime['B'], $total_calls) . "\n"; $etag_hits = $calls['B'] - $calls['C']; print "\n"; print " {$etag_hits} of the requests matched the ETag and were served an HTTP 304 response.\n"; print "\n"; $calls_left = $calls['C']; print $get_stats() . " {$calls_left} calls passed here after ~" . $avg($runtime['A'] + $runtime['B'], $total_calls) . "\n"; $imagecache_hits = $calls['C'] - $calls['D']; print "\n"; print " {$imagecache_hits} of these calls hit the server image cache.\n"; print " " . $perc($runtime['C'], $total_runtime) . " of total runtime was spent to find these.\n"; print "\n"; $calls_left = $calls['D']; $runtime_left -= $runtime['C']; print $get_stats() . " {$calls_left} calls passed here after ~" . $avg($runtime['A'] + $runtime['B'] + $runtime['C'], $total_calls) . "\n"; print "\n"; print " These calls required the tile to be rendered. On average, it took\n"; print " " . $avg($runtime['D'], $calls['D']) . " to *render* a tile.\n"; print " " . $perc($runtime['D'], $total_runtime) . " of total runtime was spent here.\n"; print "\n"; $runtime_left -= $runtime['D']; print $perc($runtime_left, $total_runtime) . " of runtime was unaccounted for (other processing).\n"; print "Average response time was " . $avg($total_runtime, $total_calls) . ".\n\n"; print "Current okapi_cache score distribution:\n"; $rs = Db::query("\n select floor(log2(score)), count(*), sum(length(value))\n from okapi_cache\n where score is not null\n group by floor(log2(score))\n "); while (list($log2, $count, $size) = mysql_fetch_array($rs)) { print $count . " elements ({$size} bytes) with score between " . pow(2, $log2) . " and " . pow(2, $log2 + 1) . ".\n"; } $response->body = ob_get_clean(); return $response; }
private static function update_notes($cache_id, $user_id, $new_notes) { if (Settings::get('OC_BRANCH') == 'oc.de') { /* See: * * - https://github.com/OpencachingDeutschland/oc-server3/tree/master/htdocs/libse/CacheNote * - http://www.opencaching.de/okapi/devel/dbstruct */ $rs = Db::query("\n select max(id) as id\n 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 "); $id = null; if ($row = Db::fetch_assoc($rs)) { $id = $row['id']; } if ($id == null) { Db::query("\n insert into coordinates (\n type, latitude, longitude, cache_id, user_id,\n description\n ) values (\n 2, 0, 0,\n '" . Db::escape_string($cache_id) . "',\n '" . Db::escape_string($user_id) . "',\n '" . Db::escape_string($new_notes) . "'\n )\n "); } else { Db::query("\n update coordinates\n set description = '" . Db::escape_string($new_notes) . "'\n where\n id = '" . Db::escape_string($id) . "'\n and type = 2\n "); } } else { $rs = Db::query("\n select max(note_id) as id\n from cache_notes\n where\n cache_id = '" . Db::escape_string($cache_id) . "'\n and user_id = '" . Db::escape_string($user_id) . "'\n "); $id = null; if ($row = Db::fetch_assoc($rs)) { $id = $row['id']; } if ($id == null) { Db::query("\n insert into cache_notes (\n cache_id, user_id, date, desc_html, `desc`\n ) values (\n '" . Db::escape_string($cache_id) . "',\n '" . Db::escape_string($user_id) . "',\n NOW(), 0,\n '" . Db::escape_string($new_notes) . "'\n )\n "); } else { Db::query("\n update cache_notes\n set\n `desc` = '" . Db::escape_string($new_notes) . "',\n desc_html = 0,\n date = NOW()\n where note_id = '" . Db::escape_string($id) . "'\n "); } } }
public static function call(OkapiRequest $request) { $user_uuids = $request->get_parameter('user_uuids'); if (!$user_uuids) { throw new ParamMissing('user_uuids'); } $user_uuids = explode("|", $user_uuids); if (count($user_uuids) > 500) { throw new InvalidParam('user_uuids', "Maximum allowed number of referenced users " . "is 500. You provided " . count($user_uuids) . " user IDs."); } $fields = $request->get_parameter('fields'); if (!$fields) { throw new ParamMissing('fields'); } $fields = explode("|", $fields); foreach ($fields as $field) { if (!in_array($field, self::$valid_field_names)) { throw new InvalidParam('fields', "'{$field}' is not a valid field code."); } } $rs = Db::query("\n select user_id, uuid, username, admin, latitude, longitude, date_created\n from user\n where uuid in ('" . implode("','", array_map('\\okapi\\Db::escape_string', $user_uuids)) . "')\n "); $results = array(); $id2uuid = array(); $uuid2id = array(); while ($row = Db::fetch_assoc($rs)) { $id2uuid[$row['user_id']] = $row['uuid']; $uuid2id[$row['uuid']] = $row['user_id']; $entry = array(); foreach ($fields as $field) { switch ($field) { case 'uuid': $entry['uuid'] = $row['uuid']; break; case 'username': $entry['username'] = $row['username']; break; case 'profile_url': $entry['profile_url'] = Settings::get('SITE_URL') . "viewprofile.php?userid=" . $row['user_id']; break; case 'is_admin': if (!$request->token) { $entry['is_admin'] = null; } elseif ($request->token->user_id != $row['user_id']) { $entry['is_admin'] = null; } else { $entry['is_admin'] = $row['admin'] ? true : false; } break; case 'internal_id': $entry['internal_id'] = $row['user_id']; break; case 'date_registered': $entry['date_registered'] = date("Y-m-d", strtotime($row['date_created'])); case 'caches_found': /* handled separately */ break; case 'caches_notfound': /* handled separately */ break; case 'caches_hidden': /* handled separately */ break; case 'rcmds_given': /* handled separately */ break; case 'home_location': if (!$request->token) { $entry['home_location'] = null; } elseif ($request->token->user_id != $row['user_id']) { $entry['home_location'] = null; } elseif (!$row['latitude'] && !$row['longitude']) { # OCPL sets NULL/NULL for unknown location, OCDE sets 0/0. # It is safe to return null also for OCPL 0/0, as this value # does not make sense. $entry['home_location'] = null; } else { $entry['home_location'] = round($row['latitude'], 6) . "|" . round($row['longitude'], 6); } break; default: throw new Exception("Missing field case: " . $field); } } $results[$row['uuid']] = $entry; } Db::free_result($rs); # caches_found, caches_notfound, caches_hidden if (in_array('caches_found', $fields) || in_array('caches_notfound', $fields) || in_array('caches_hidden', $fields) || in_array('rcmds_given', $fields)) { # We will load all these stats together. Then we may remove these which # the user doesn't need. $extras = array(); if (Settings::get('OC_BRANCH') == 'oc.pl') { # OCPL stores user stats in 'user' table. $rs = Db::query("\n select user_id, founds_count, notfounds_count, hidden_count\n from user\n where user_id in ('" . implode("','", array_map('\\okapi\\Db::escape_string', array_keys($id2uuid))) . "')\n "); } else { # OCDE stores user stats in 'stat_user' table. $rs = Db::query("\n select\n u.user_id,\n ifnull(su.found, 0) as founds_count,\n ifnull(su.notfound, 0) as notfounds_count,\n ifnull(su.hidden, 0) as hidden_count\n from\n user u\n left join stat_user su\n on su.user_id = u.user_id\n where u.user_id in ('" . implode("','", array_map('\\okapi\\Db::escape_string', array_keys($id2uuid))) . "')\n "); } while ($row = Db::fetch_assoc($rs)) { $extras[$row['user_id']] = array(); $extra_ref =& $extras[$row['user_id']]; $extra_ref['caches_found'] = 0 + $row['founds_count']; $extra_ref['caches_notfound'] = 0 + $row['notfounds_count']; $extra_ref['caches_hidden'] = 0 + $row['hidden_count']; } Db::free_result($rs); if (in_array('rcmds_given', $fields)) { $rs = Db::query("\n select user_id, count(*) as rcmds_given\n from cache_rating\n where user_id in ('" . implode("','", array_map('\\okapi\\Db::escape_string', array_keys($id2uuid))) . "')\n group by user_id\n "); $rcmds_counts = array(); while ($row = Db::fetch_assoc($rs)) { $rcmds_counts[$row['user_id']] = $row['rcmds_given']; } foreach ($extras as $user_id => &$extra_ref) { $extra_ref['rcmds_given'] = isset($rcmds_counts[$user_id]) ? 0 + $rcmds_counts[$user_id] : 0; } } # "Apply" only those fields which the consumer wanted. foreach (array('caches_found', 'caches_notfound', 'caches_hidden', 'rcmds_given') as $field) { if (!in_array($field, $fields)) { continue; } foreach ($results as $uuid => &$result_ref) { $result_ref[$field] = $extras[$uuid2id[$uuid]][$field]; } } } # Check which user IDs were not found and mark them with null. foreach ($user_uuids as $user_uuid) { if (!isset($results[$user_uuid])) { $results[$user_uuid] = null; } } return Okapi::formatted_response($request, $results); }
public static function call(OkapiRequest $request) { # Retrieve the list of URLs to check. $tmp = $request->get_parameter('urls'); if (!$tmp) { throw new ParamMissing('urls'); } $urls = explode('|', $tmp); $as_dict = $request->get_parameter('as_dict'); if (!$as_dict) { $as_dict = 'false'; } if (!in_array($as_dict, array('true', 'false'))) { throw new InvalidParam('as_dict'); } $as_dict = $as_dict == 'true'; # Generate the lists of keys. $results = array(); $urls_with = array('cache_code' => array(), 'internal_id' => array(), 'uuid' => array()); foreach ($urls as &$url_ref) { $key = self::get_cache_key($url_ref); if ($key != null) { $urls_with[$key[0]][$url_ref] = $key[1]; } else { $results[$url_ref] = null; } } # Include 'cache_code' references. foreach ($urls_with['cache_code'] as $url => $cache_code) { $results[$url] = $cache_code; } # Include 'internal_id' references. $internal_ids = array_values($urls_with['internal_id']); if (count($internal_ids) > 0) { $rs = Db::query("\n select cache_id, wp_oc\n from caches\n where\n cache_id in ('" . implode("','", array_map('mysql_real_escape_string', $internal_ids)) . "')\n and status in (1,2,3)\n "); $dict = array(); while ($row = mysql_fetch_assoc($rs)) { $dict[$row['cache_id']] = $row['wp_oc']; } foreach ($urls_with['internal_id'] as $url => $internal_id) { if (isset($dict[$internal_id])) { $results[$url] = $dict[$internal_id]; } else { $results[$url] = null; } } } # Include 'uuid' references. $uuids = array_values($urls_with['uuid']); if (count($uuids) > 0) { $rs = Db::query("\n select uuid, wp_oc\n from caches\n where\n uuid in ('" . implode("','", array_map('mysql_real_escape_string', $uuids)) . "')\n and status in (1,2,3)\n "); $dict = array(); while ($row = mysql_fetch_assoc($rs)) { $dict[$row['uuid']] = $row['wp_oc']; } foreach ($urls_with['uuid'] as $url => $uuid) { if (isset($dict[$uuid])) { $results[$url] = $dict[$uuid]; } else { $results[$url] = null; } } } # Format the results according to the 'as_dict' parameter. if ($as_dict) { return Okapi::formatted_response($request, $results); } else { $cache_codes = array(); foreach ($results as $url => $cache_code) { if ($cache_code != null) { $cache_codes[$cache_code] = true; } } $flattened = array('results' => array_keys($cache_codes)); return Okapi::formatted_response($request, $flattened); } }
/** * Get an array of all site-specific log-types (id => name in English). */ private static function get_all_logtypes() { if (Settings::get('OC_BRANCH') == 'oc.pl') { # OCPL branch does not store cache types in many languages (just two). $rs = Db::query("select id, en from log_types order by id"); } else { # OCDE branch uses translation tables. $rs = Db::query("\n select\n lt.id,\n stt.text as en\n from\n log_types lt\n left join sys_trans_text stt\n on lt.trans_id = stt.trans_id\n and stt.lang = 'EN'\n order by lt.id\n "); } $dict = array(); while ($row = mysql_fetch_assoc($rs)) { $dict[$row['id']] = $row['en']; } return $dict; }
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='" . mysql_real_escape_string($user_uuid) . "'"); if ($user_id == null) { throw new InvalidParam('user_uuid', "User not found."); } if ($request->token != null && $request->token->user_id != $user_id) { throw new InvalidParam('user_uuid', "User does not match the Access Token used."); } } elseif ($user_uuid == null && $request->token != null) { $user_id = $request->token->user_id; } else { $user_id = null; } $lpc = $request->get_parameter('lpc'); if ($lpc === null) { $lpc = 10; } if ($lpc == 'all') { $lpc = null; } else { if (!is_numeric($lpc)) { throw new InvalidParam('lpc', "Invalid number: '{$lpc}'"); } $lpc = intval($lpc); if ($lpc < 0) { throw new InvalidParam('lpc', "Must be a positive value."); } } if (in_array('distance', $fields) || in_array('bearing', $fields) || in_array('bearing2', $fields) || in_array('bearing3', $fields)) { $tmp = $request->get_parameter('my_location'); if (!$tmp) { throw new BadRequest("When using 'distance' or 'bearing' fields, you have to supply 'my_location' parameter."); } $parts = explode('|', $tmp); if (count($parts) != 2) { throw new InvalidParam('my_location', "Expecting 2 pipe-separated parts, got " . count($parts) . "."); } foreach ($parts as &$part_ref) { if (!preg_match("/^-?[0-9]+(\\.?[0-9]*)\$/", $part_ref)) { throw new InvalidParam('my_location', "'{$part_ref}' is not a valid float number."); } $part_ref = floatval($part_ref); } list($center_lat, $center_lon) = $parts; if ($center_lat > 90 || $center_lat < -90) { throw new InvalidParam('current_position', "Latitudes have to be within -90..90 range."); } if ($center_lon > 180 || $center_lon < -180) { throw new InvalidParam('current_position', "Longitudes have to be within -180..180 range."); } } if (Settings::get('OC_BRANCH') == 'oc.de') { # DE branch: # - Caches do not have ratings. # - Total numbers of founds and notfounds are kept in the "stat_caches" table. # - search_time and way_length are both round trip values and cannot be null; # 0 = not specified # - will-attend-count is stored in separate field $rs = Db::query("\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.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\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('mysql_real_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, 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\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('mysql_real_escape_string', $cache_codes)) . "')\n and c.status in (1,2,3)\n "); } $results = new ArrayObject(); $cacheid2wptcode = array(); $owner_ids = array(); while ($row = mysql_fetch_assoc($rs)) { $entry = array(); $cacheid2wptcode[$row['cache_id']] = $row['wp_oc']; foreach ($fields as $field) { switch ($field) { case 'code': $entry['code'] = $row['wp_oc']; break; case 'gc_code': // OC software allows entering anything here, and that's what users do. // We do a formal verification so that only a valid GC code is returned: if (preg_match('/^\\s*[Gg][Cc][A-Za-z0-9]+\\s*$/', $row['wp_gc'])) { $entry['gc_code'] = strtoupper(trim($row['wp_gc'])); } else { $entry['gc_code'] = null; } break; case 'name': $entry['name'] = $row['name']; break; case 'names': $entry['names'] = array(Settings::get('SITELANG') => $row['name']); break; // for the future // for the future case 'location': $entry['location'] = round($row['latitude'], 6) . "|" . round($row['longitude'], 6); break; case 'type': $entry['type'] = Okapi::cache_type_id2name($row['type']); break; case 'status': $entry['status'] = Okapi::cache_status_id2name($row['status']); break; case 'url': $entry['url'] = Settings::get('SITE_URL') . "viewcache.php?wp=" . $row['wp_oc']; break; case 'owner': $owner_ids[$row['wp_oc']] = $row['user_id']; /* continued later */ break; case 'distance': $entry['distance'] = (int) Okapi::get_distance($center_lat, $center_lon, $row['latitude'], $row['longitude']); break; case 'bearing': $tmp = Okapi::get_bearing($center_lat, $center_lon, $row['latitude'], $row['longitude']); $entry['bearing'] = $tmp !== null ? (int) (10 * $tmp) / 10.0 : null; break; case 'bearing2': $tmp = Okapi::get_bearing($center_lat, $center_lon, $row['latitude'], $row['longitude']); $entry['bearing2'] = Okapi::bearing_as_two_letters($tmp); break; case 'bearing3': $tmp = Okapi::get_bearing($center_lat, $center_lon, $row['latitude'], $row['longitude']); $entry['bearing3'] = Okapi::bearing_as_three_letters($tmp); break; case 'is_found': /* handled separately */ break; case 'is_not_found': /* handled separately */ break; case 'is_watched': /* handled separately */ break; case 'is_ignored': /* handled separately */ break; case 'founds': $entry['founds'] = $row['founds'] + 0; break; case 'notfounds': if ($row['type'] != 6) { # non-event $entry['notfounds'] = $row['notfounds'] + 0; } else { # event $entry['notfounds'] = 0; } break; case 'willattends': if ($row['type'] == 6) { # event $entry['willattends'] = $row['willattends'] + 0; } else { # non-event $entry['willattends'] = 0; } break; case 'size': # Deprecated. Leave it for backward-compatibility. See issue 155. switch (Okapi::cache_sizeid_to_size2($row['size'])) { case 'none': $entry['size'] = null; break; case 'nano': $entry['size'] = 1.0; break; # same as micro # 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; } mysql_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('mysql_real_escape_string', array_values($owner_ids))) . "')\n "); $tmp = array(); while ($row = mysql_fetch_assoc($rs)) { $tmp[$row['user_id']] = $row; } foreach ($results as $cache_code => &$result_ref) { $row = $tmp[$owner_ids[$cache_code]]; $result_ref['owner'] = array('uuid' => $row['uuid'], 'username' => $row['username'], 'profile_url' => Settings::get('SITE_URL') . "viewprofile.php?userid=" . $row['user_id']); } } # is_found if (in_array('is_found', $fields)) { if ($user_id == null) { throw new BadRequest("Either 'user_uuid' parameter OR Level 3 Authentication is required to access 'is_found' field."); } $tmp = Db::select_column("\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 '" . mysql_real_escape_string(Okapi::logtypename2id("Found it")) . "',\n '" . mysql_real_escape_string(Okapi::logtypename2id("Attended")) . "'\n )\n and cl.user_id = '" . mysql_real_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 = '" . mysql_real_escape_string(Okapi::logtypename2id("Didn't find it")) . "'\n and cl.user_id = '" . mysql_real_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 = '" . mysql_real_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 = '" . mysql_real_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 = '" . mysql_real_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('mysql_real_escape_string', array_keys($cacheid2wptcode))) . "')\n "); while ($row = mysql_fetch_assoc($rs)) { $cache_code = $cacheid2wptcode[$row['cache_id']]; // strtolower - ISO 639-1 codes are lowercase if ($row['desc']) { /* Note, that the "owner" and "internal_id" fields are automatically included, * whenever the cache description is included. */ $tmp = Okapi::fix_oc_html($row['desc']); if ($attribution_append != 'none') { $tmp .= "\n<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"; } $sql = "\n select object_id, uuid, url, title, spoiler, " . $preview_field . " as preview\n from pictures\n where\n object_id in ('" . implode("','", array_map('mysql_real_escape_string', array_keys($cacheid2wptcode))) . "')\n and display = 1\n and object_type = 2\n and unknown_format = 0\n "; if (Settings::get('OC_BRANCH') == 'oc.pl') { // oc.pl installation allows arbitrary order of the geocache's images $sql .= "order by object_id, seq, date_created"; } else { $sql .= "order by object_id, date_created"; } $rs = Db::query($sql); unset($sql); $prev_cache_code = null; while ($row = mysql_fetch_assoc($rs)) { $cache_code = $cacheid2wptcode[$row['object_id']]; if ($cache_code != $prev_cache_code) { # Group images together. Images must have unique captions within one cache. self::reset_unique_captions(); $prev_cache_code = $cache_code; } 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('mysql_real_escape_string', array_keys($cacheid2wptcode))) . "')\n "); while ($row = mysql_fetch_assoc($rs)) { $cache_code = $cacheid2wptcode[$row['cache_id']]; $attr_internal_id = $row['attrib_id']; if (!isset($internal2acode[$attr_internal_id])) { # Unknown attribute. Ignore. continue; } $results[$cache_code]['attr_acodes'][] = $internal2acode[$attr_internal_id]; } # Now, each cache object has a list of its acodes. We can get # the attrnames now. if (in_array('attrnames', $fields)) { $acode2bestname = AttrHelper::get_acode_to_name_mapping($langpref); foreach ($results as &$result_ref) { $result_ref['attrnames'] = array(); foreach ($result_ref['attr_acodes'] as $acode) { $result_ref['attrnames'][] = $acode2bestname[$acode]; } } } } # Latest log entries. if (in_array('latest_logs', $fields)) { foreach ($results as &$result_ref) { $result_ref['latest_logs'] = array(); } # Get all log IDs with dates. Sort in groups. Filter out latest ones. This is the fastest # technique I could think of... $rs = Db::query("\n select cache_id, uuid, date\n from cache_logs\n where\n cache_id in ('" . implode("','", array_map('mysql_real_escape_string', array_keys($cacheid2wptcode))) . "')\n and " . (Settings::get('OC_BRANCH') == 'oc.pl' ? "deleted = 0" : "true") . "\n order by cache_id, date desc, date_created desc\n "); $loguuids = array(); $log2cache_map = array(); if ($lpc !== null) { # User wants some of the latest logs. $tmp = array(); while ($row = mysql_fetch_assoc($rs)) { $tmp[$row['cache_id']][] = $row; } foreach ($tmp as $cache_key => &$rowslist_ref) { usort($rowslist_ref, function ($rowa, $rowb) { # (reverse order by date) return $rowa['date'] < $rowb['date'] ? 1 : ($rowa['date'] == $rowb['date'] ? 0 : -1); }); for ($i = 0; $i < min(count($rowslist_ref), $lpc); $i++) { $loguuids[] = $rowslist_ref[$i]['uuid']; $log2cache_map[$rowslist_ref[$i]['uuid']] = $cacheid2wptcode[$rowslist_ref[$i]['cache_id']]; } } } else { # User wants ALL logs. while ($row = mysql_fetch_assoc($rs)) { $loguuids[] = $row['uuid']; $log2cache_map[$row['uuid']] = $cacheid2wptcode[$row['cache_id']]; } } # We need to retrieve logs/entry for each of the $logids. We do this in groups # (there is a limit for log uuids passed to logs/entries method). try { foreach (Okapi::make_groups($loguuids, 500) as $subset) { $entries = OkapiServiceRunner::call("services/logs/entries", new OkapiInternalRequest($request->consumer, $request->token, array('log_uuids' => implode("|", $subset), 'fields' => $log_fields))); foreach ($subset as $log_uuid) { if ($entries[$log_uuid]) { $results[$log2cache_map[$log_uuid]]['latest_logs'][] = $entries[$log_uuid]; } } } } catch (Exception $e) { if ($e instanceof InvalidParam && $e->paramName == 'fields') { throw new InvalidParam('log_fields', $e->whats_wrong_about_it); } else { /* Something is wrong with OUR code. */ throw new Exception($e); } } } # My notes if (in_array('my_notes', $fields)) { if ($request->token == null) { throw new BadRequest("Level 3 Authentication is required to access 'my_notes' field."); } foreach ($results as &$result_ref) { $result_ref['my_notes'] = null; } if (Settings::get('OC_BRANCH') == 'oc.pl') { # OCPL uses cache_notes table to store notes. $rs = Db::query("\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('mysql_real_escape_string', array_keys($cacheid2wptcode))) . "')\n and user_id = '" . mysql_real_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('mysql_real_escape_string', array_keys($cacheid2wptcode))) . "')\n and user_id = '" . mysql_real_escape_string($request->token->user_id) . "'\n group by cache_id\n "); } while ($row = mysql_fetch_assoc($rs)) { # This one is plain-text. We may add my_notes_html for those who want it in HTML. $results[$cacheid2wptcode[$row['cache_id']]]['my_notes'] = strip_tags($row['desc']); } } if (in_array('trackables', $fields)) { # Currently we support Geokrety only. But this interface should remain # compatible. In future, other trackables might be returned the same way. $rs = Db::query("\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('mysql_real_escape_string', $cache_codes)) . "')\n "); $trs = array(); while ($row = mysql_fetch_assoc($rs)) { $trs[$row['cache_code']][] = $row; } foreach ($results as $cache_code => &$result_ref) { $result_ref['trackables'] = array(); if (!isset($trs[$cache_code])) { continue; } foreach ($trs[$cache_code] as $t) { $result_ref['trackables'][] = array('code' => 'GK' . str_pad(strtoupper(dechex($t['gk_id'])), 4, "0", STR_PAD_LEFT), 'name' => $t['name'], 'url' => 'http://geokrety.org/konkret.php?id=' . $t['gk_id']); } } unset($trs); } if (in_array('trackables_count', $fields)) { if (in_array('trackables', $fields)) { # We already got all trackables data, no need to query database again. foreach ($results as $cache_code => &$result_ref) { $result_ref['trackables_count'] = count($result_ref['trackables']); } } else { $rs = Db::query("\n select wp as cache_code, count(*) as count\n from gk_item_waypoint\n where wp in ('" . implode("','", array_map('mysql_real_escape_string', $cache_codes)) . "')\n group by wp\n "); $tr_counts = new ArrayObject(); while ($row = mysql_fetch_assoc($rs)) { $tr_counts[$row['cache_code']] = $row['count']; } foreach ($results as $cache_code => &$result_ref) { if (isset($tr_counts[$cache_code])) { $result_ref['trackables_count'] = $tr_counts[$cache_code] + 0; } else { $result_ref['trackables_count'] = 0; } } unset($tr_counts); } } # Alternate/Additional waypoints. if (in_array('alt_wpts', $fields)) { $internal_wpt_type_id2names = array(); if (Settings::get('OC_BRANCH') == 'oc.de') { $rs = Db::query("\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 = mysql_fetch_assoc($rs)) { $internal_wpt_type_id2names[$row['id']][$row['language']] = $row['text']; } mysql_free_result($rs); } else { $rs = Db::query("\n select id, pl, en\n from waypoint_type\n where id > 0\n "); while ($row = mysql_fetch_assoc($rs)) { $internal_wpt_type_id2names[$row['id']]['pl'] = $row['pl']; $internal_wpt_type_id2names[$row['id']]['en'] = $row['en']; } } foreach ($results as &$result_ref) { $result_ref['alt_wpts'] = array(); } $cache_codes_escaped_and_imploded = "'" . implode("','", array_map('mysql_real_escape_string', array_keys($cacheid2wptcode))) . "'"; if (Settings::get('OC_BRANCH') == 'oc.pl') { # OCPL uses 'waypoints' table to store additional waypoints and defines # waypoint types in 'waypoint_type' table. # OCPL also have a special 'status' field to denote a hidden waypoint # (i.e. final location of a multicache). Such hidden waypoints are not # exposed by OKAPI. $cacheid2waypoints = Db::select_group_by("cache_id", "\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 = '" . mysql_real_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 = '" . mysql_real_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('mysql_real_escape_string', $cache_codes)) . "')\n "); $country_codes2names = array(); while ($row = mysql_fetch_assoc($rs)) { $country_codes2names[$row['country']][$row['language']] = $row['text']; } mysql_free_result($rs); # get geocache countries and states $rs = Db::query("\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('mysql_real_escape_string', $cache_codes)) . "')\n "); while ($row = mysql_fetch_assoc($rs)) { if (!isset($country_codes2names[$row['country_code']])) { $countries[$row['cache_code']] = ''; } else { $countries[$row['cache_code']] = Okapi::pick_best_language($country_codes2names[$row['country_code']], $langpref); } $states[$row['cache_code']] = $row['state']; } mysql_free_result($rs); } else { # OCPL: # - cache_location data is entered by the user. # - The state is in adm3 field. # get geocache countries and states $rs = Db::query("\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('mysql_real_escape_string', $cache_codes)) . "')\n and c.cache_id = cl.cache_id\n "); while ($row = mysql_fetch_assoc($rs)) { $countries[$row['cache_code']] = $row['country']; $states[$row['cache_code']] = $row['state']; } mysql_free_result($rs); } if (in_array('country', $fields)) { foreach ($results as $cache_code => &$row_ref) { $row_ref['country'] = isset($countries[$cache_code]) ? $countries[$cache_code] : null; } } if (in_array('state', $fields)) { foreach ($results as $cache_code => &$row_ref) { $row_ref['state'] = isset($states[$cache_code]) ? $states[$cache_code] : null; } } unset($countries); unset($states); } # Attribution note if (in_array('attribution_note', $fields)) { /* Note, that the "owner" and "internal_id" fields are automatically included, * whenever the attribution_note is included. */ foreach ($results as $cache_code => &$result_ref) { $result_ref['attribution_note'] = self::get_cache_attribution_note($result_ref['internal_id'], $langpref[0], $langpref, $results[$cache_code]['owner'], 'full'); } } # Protection areas if (in_array('protection_areas', $fields)) { $cache_ids_escaped_and_imploded = "'" . implode("','", array_map('mysql_real_escape_string', array_keys($cacheid2wptcode))) . "'"; if (Settings::get('OC_BRANCH') == 'oc.de') { $rs = Db::query("\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 (Settings::get('ORIGIN_URL') == 'http://opencaching.pl/' || Settings::get('ORIGIN_URL') == 'http://www.opencaching.nl/') { # Current OCPL table definitions use collation 'latin1' for parkipl # and 'utf8' for np_areas. Union needs identical collations. # To be sure, we convert both to utf8. # # 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 = mysql_fetch_assoc($rs)) { $results[$row['cache_code']]['protection_areas'][] = array('type' => $row['type'], 'name' => $row['name']); } mysql_free_result($rs); } } # Check which cache codes were not found and mark them with null. foreach ($cache_codes as $cache_code) { if (!isset($results[$cache_code])) { $results[$cache_code] = null; } } if (count($fields_to_remove_later) > 0) { # Some of the fields in $results were added only temporarily # (the Consumer did not ask for them). We will remove these fields now. foreach ($results as &$result_ref) { foreach ($fields_to_remove_later as $field) { unset($result_ref[$field]); } } } # Order the results in the same order as the input codes were given. # This might come in handy for languages which support ordered dictionaries # (especially with conjunction with the search_and_retrieve method). # See issue#97. PHP dictionaries (assoc arrays) are ordered structures, # so we just have to rewrite it (sequentially). $ordered_results = new ArrayObject(); foreach ($cache_codes as $cache_code) { $ordered_results[$cache_code] = $results[$cache_code]; } /* Handle OCPL's "access logs" feature. */ if (Settings::get('OC_BRANCH') == 'oc.pl' && Settings::get('OCPL_ENABLE_GEOCACHE_ACCESS_LOGS')) { $cache_ids = array_keys($cacheid2wptcode); /* Log this event only if some specific fields were accessed. */ if (in_array('location', $fields) && count(array_intersect(array('hint', 'hints', 'hint2', 'hints2', 'description', 'descriptions'), $fields)) > 0) { require_once $GLOBALS['rootpath'] . 'okapi/lib/ocpl_access_logs.php'; \okapi\OCPLAccessLogs::log_geocache_access($request, $cache_ids); } } return Okapi::formatted_response($request, $ordered_results); }
/** * 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; }
public static function call(OkapiRequest $request) { $log_uuids = $request->get_parameter('log_uuids'); if ($log_uuids === null) { throw new ParamMissing('log_uuids'); } if ($log_uuids === "") { $log_uuids = array(); } else { $log_uuids = explode("|", $log_uuids); } if (count($log_uuids) > 500 && !$request->skip_limits) { throw new InvalidParam('log_uuids', "Maximum allowed number of referenced " . "log entries is 500. You provided " . count($log_uuids) . " UUIDs."); } if (count($log_uuids) != count(array_unique($log_uuids))) { throw new InvalidParam('log_uuids', "Duplicate UUIDs detected (make sure each UUID is referenced only once)."); } $fields = $request->get_parameter('fields'); if (!$fields) { $fields = "date|user|type|comment"; } $fields = explode("|", $fields); foreach ($fields as $field) { if (!in_array($field, self::$valid_field_names)) { throw new InvalidParam('fields', "'{$field}' is not a valid field code."); } } if (Settings::get('OC_BRANCH') == 'oc.de') { $teamentry_field = 'cl.oc_team_comment'; $ratingdate_condition = 'and cr.rating_date=cl.date'; $needs_maintenance_SQL = 'cl.needs_maintenance'; $listing_is_outdated_SQL = 'cl.listing_outdated'; } else { $teamentry_field = '(cl.type=12)'; $ratingdate_condition = ''; $needs_maintenance_SQL = 'IF(cl.type=5, 2, IF(cl.type=6, 1, 0))'; $listing_is_outdated_SQL = '0'; } $rs = Db::query("\n select\n cl.id, c.wp_oc as cache_code, cl.uuid, cl.type,\n " . $teamentry_field . " as oc_team_entry,\n " . $needs_maintenance_SQL . " as needs_maintenance2,\n " . $listing_is_outdated_SQL . " as listing_is_outdated,\n unix_timestamp(cl.date) as date, cl.text,\n u.uuid as user_uuid, u.username, u.user_id,\n if(cr.user_id is null, 0, 1) as was_recommended\n from\n (cache_logs cl,\n user u,\n caches c)\n left join cache_rating cr\n on cr.user_id = u.user_id\n and cr.cache_id = c.cache_id\n " . $ratingdate_condition . "\n and cl.type in (\n " . Okapi::logtypename2id("Found it") . ",\n " . Okapi::logtypename2id("Attended") . "\n )\n where\n cl.uuid in ('" . implode("','", array_map('\\okapi\\Db::escape_string', $log_uuids)) . "')\n and " . (Settings::get('OC_BRANCH') == 'oc.pl' ? "cl.deleted = 0" : "true") . "\n and cl.user_id = u.user_id\n and c.cache_id = cl.cache_id\n and c.status in (1,2,3)\n "); $results = array(); $log_id2uuid = array(); /* Maps logs' internal_ids to uuids */ $flag_options = array('null', 'false', 'true'); while ($row = Db::fetch_assoc($rs)) { $results[$row['uuid']] = array('uuid' => $row['uuid'], 'cache_code' => $row['cache_code'], 'date' => date('c', $row['date']), 'user' => array('uuid' => $row['user_uuid'], 'username' => $row['username'], 'profile_url' => Settings::get('SITE_URL') . "viewprofile.php?userid=" . $row['user_id']), 'type' => Okapi::logtypeid2name($row['type']), 'was_recommended' => $row['was_recommended'] ? true : false, 'needs_maintenance2' => $flag_options[$row['needs_maintenance2']], 'listing_is_outdated' => $flag_options[$row['listing_is_outdated']], 'oc_team_entry' => $row['oc_team_entry'] ? true : false, 'comment' => Okapi::fix_oc_html($row['text'], Okapi::OBJECT_TYPE_CACHE_LOG), 'images' => array(), 'internal_id' => $row['id']); $log_id2uuid[$row['id']] = $row['uuid']; } Db::free_result($rs); # fetch images if (in_array('images', $fields)) { # For OCPL log entry images, pictures.seq currently is always = 1, # while OCDE uses it for ordering the images. $rs = Db::query("\n select object_id, uuid, url, title, spoiler\n from pictures\n where\n object_type = 1\n and object_id in ('" . implode("','", array_map('\\okapi\\Db::escape_string', array_keys($log_id2uuid))) . "')\n and display = 1 /* currently is always 1 for logpix */\n and unknown_format = 0\n order by seq, date_created\n "); if (Settings::get('OC_BRANCH') == 'oc.de') { $object_type_param = 'type=1&'; } else { $object_type_param = ''; } while ($row = Db::fetch_assoc($rs)) { $results[$log_id2uuid[$row['object_id']]]['images'][] = array('uuid' => $row['uuid'], 'url' => $row['url'], 'thumb_url' => Settings::get('SITE_URL') . 'thumbs.php?' . $object_type_param . 'uuid=' . $row['uuid'], 'caption' => $row['title'], 'is_spoiler' => $row['spoiler'] ? true : false); } Db::free_result($rs); } # Check which UUIDs were not found and mark them with null. foreach ($log_uuids as $log_uuid) { if (!isset($results[$log_uuid])) { $results[$log_uuid] = null; } } # Remove unwanted fields. foreach (self::$valid_field_names as $field) { if (!in_array($field, $fields)) { foreach ($results as &$result_ref) { unset($result_ref[$field]); } } } # Order the results in the same order as the input codes were given. $ordered_results = array(); foreach ($log_uuids as $log_uuid) { $ordered_results[$log_uuid] = $results[$log_uuid]; } return Okapi::formatted_response($request, $ordered_results); }
/** Do 'get' on many keys at once. */ public static function get_many($keys) { $dict = array(); $rs = Db::query("\n select `key`, value\n from okapi_cache\n where\n `key` in ('" . implode("','", array_map('mysql_real_escape_string', $keys)) . "')\n and expires > now()\n "); while ($row = mysql_fetch_assoc($rs)) { try { $dict[$row['key']] = unserialize(gzinflate($row['value'])); } catch (ErrorException $e) { unset($dict[$row['key']]); Okapi::mail_admins("Debug: Unserialize error", "Could not unserialize key '" . $row['key'] . "' from Cache.\n" . "Probably something REALLY big was put there and data has been truncated.\n" . "Consider upgrading cache table to LONGBLOB.\n\n" . "Length of data, compressed: " . strlen($row['value'])); } } if (count($dict) < count($keys)) { foreach ($keys as $key) { if (!isset($dict[$key])) { $dict[$key] = null; } } } return $dict; }
/** * Generate a GPX file. * * @param OkapiRequest $request * @param integer $flags * @throws BadRequest * @return An array with GPX file content under 'gpx' key */ public static function create_gpx(OkapiRequest $request, $flags = null) { $vars = array(); # Validating arguments. We will also assign some of them to the # $vars variable which we will use later in the GPS template. $cache_codes = $request->get_parameter('cache_codes'); if ($cache_codes === null) { throw new ParamMissing('cache_codes'); } # Issue 106 requires us to allow empty list of cache codes to be passed into this method. # All of the queries below have to be ready for $cache_codes to be empty! $langpref = $request->get_parameter('langpref'); if (!$langpref) { $langpref = "en"; } $langpref .= "|" . Settings::get('SITELANG'); foreach (array('ns_ground', 'ns_gsak', 'ns_ox', 'latest_logs', 'alt_wpts', 'mark_found') as $param) { $val = $request->get_parameter($param); if (!$val) { $val = "false"; } elseif (!in_array($val, array("true", "false"))) { throw new InvalidParam($param); } $vars[$param] = $val == "true"; } if ($vars['latest_logs'] && !$vars['ns_ground']) { throw new BadRequest("In order for 'latest_logs' to work you have to also include 'ns_ground' extensions."); } $tmp = $request->get_parameter('my_notes'); $vars['my_notes'] = array(); if ($tmp && $tmp != 'none') { $tmp = explode('|', $tmp); foreach ($tmp as $elem) { if ($elem == 'none') { /* pass */ } elseif (in_array($elem, array('desc:text', 'gc:personal_note'))) { if (in_array('none', $tmp)) { throw new InvalidParam('my_notes', "You cannot mix 'none' and '{$elem}'"); } if ($request->token == null) { throw new BadRequest("Level 3 Authentication is required to access my_notes data."); } $vars['my_notes'][] = $elem; } else { throw new InvalidParam('my_notes', "Invalid list entry: '{$elem}'"); } } } $images = $request->get_parameter('images'); if (!$images) { $images = 'descrefs:nonspoilers'; } if (!in_array($images, array('none', 'descrefs:thumblinks', 'descrefs:nonspoilers', 'descrefs:all', 'ox:all'))) { throw new InvalidParam('images', "'{$images}'"); } $vars['images'] = $images; $tmp = $request->get_parameter('attrs'); if (!$tmp) { $tmp = 'desc:text'; } $tmp = explode("|", $tmp); $vars['attrs'] = array(); foreach ($tmp as $elem) { if ($elem == 'none') { /* pass */ } elseif (in_array($elem, array('desc:text', 'ox:tags', 'gc:attrs', 'gc_ocde:attrs'))) { if ($elem == 'gc_ocde:attrs' && Settings::get('OC_BRANCH') != 'oc.de') { $vars['attrs'][] = 'gc:attrs'; } else { $vars['attrs'][] = $elem; } } else { throw new InvalidParam('attrs', "Invalid list entry: '{$elem}'"); } } $protection_areas = $request->get_parameter('protection_areas'); if (!$protection_areas || $protection_areas == 'desc:auto') { if (Settings::get('OC_BRANCH') == 'oc.de') { $protection_areas = 'desc:text'; } else { $protection_areas = 'none'; } } if (!in_array($protection_areas, array('none', 'desc:text'))) { throw new InvalidParam('protection_areas', "'{$protection_areas}'"); } $vars['protection_areas'] = $protection_areas; $tmp = $request->get_parameter('trackables'); if (!$tmp) { $tmp = 'none'; } if (!in_array($tmp, array('none', 'desc:list', 'desc:count'))) { throw new InvalidParam('trackables', "'{$tmp}'"); } $vars['trackables'] = $tmp; $tmp = $request->get_parameter('recommendations'); if (!$tmp) { $tmp = 'none'; } if (!in_array($tmp, array('none', 'desc:count'))) { throw new InvalidParam('recommendations', "'{$tmp}'"); } $vars['recommendations'] = $tmp; $lpc = $request->get_parameter('lpc'); if ($lpc === null) { $lpc = 10; } # will be checked in services/caches/geocaches call $user_uuid = $request->get_parameter('user_uuid'); # location_source (part 1 of 2) $location_source = $request->get_parameter('location_source'); if (!$location_source) { $location_source = 'default-coords'; } # Make sure location_source has prefix alt_wpt: if ($location_source != 'default-coords' && strncmp($location_source, 'alt_wpt:', 8) != 0) { throw new InvalidParam('location_source', '\'' . $location_source . '\''); } # Make sure we have sufficient authorization if ($location_source == 'alt_wpt:user-coords' && $request->token == null) { throw new BadRequest("Level 3 Authentication is required to access 'alt_wpt:user-coords'."); } # Which fields of the services/caches/geocaches method do we need? $fields = 'code|name|location|date_created|url|type|status|size|size2|oxsize' . '|difficulty|terrain|description|hint2|rating|owner|url|internal_id' . '|protection_areas|short_description'; if ($vars['images'] != 'none') { $fields .= "|images"; } if (count($vars['attrs']) > 0) { $fields .= "|attrnames|attr_acodes"; } if ($vars['trackables'] == 'desc:list') { $fields .= "|trackables"; } elseif ($vars['trackables'] == 'desc:count') { $fields .= "|trackables_count"; } if ($vars['alt_wpts'] == 'true' || $location_source != 'default-coords') { $fields .= "|alt_wpts"; } if ($vars['recommendations'] != 'none') { $fields .= "|recommendations|founds"; } if (count($vars['my_notes']) > 0) { $fields .= "|my_notes"; } if ($vars['latest_logs']) { $fields .= "|latest_logs"; } if ($vars['mark_found']) { $fields .= "|is_found"; } $vars['caches'] = OkapiServiceRunner::call('services/caches/geocaches', new OkapiInternalRequest($request->consumer, $request->token, array('cache_codes' => $cache_codes, 'langpref' => $langpref, 'fields' => $fields, 'lpc' => $lpc, 'user_uuid' => $user_uuid, 'log_fields' => 'uuid|date|user|type|comment|internal_id|was_recommended'))); # Get rid of invalid cache references. $valid = array(); foreach ($vars['caches'] as $key => &$ref) { if ($ref !== null) { $valid[$key] =& $ref; } } $vars['caches'] =& $valid; unset($valid); # Get all the other data need. $vars['installation'] = OkapiServiceRunner::call('services/apisrv/installation', new OkapiInternalRequest(new OkapiInternalConsumer(), null, array())); $vars['cache_GPX_types'] = self::$cache_GPX_types; $vars['cache_GPX_sizes'] = self::$cache_GPX_sizes; if (count($vars['attrs']) > 0) { /* The user asked for some kind of attribute output. We'll fetch all * the data we MAY need. This is often far too much, but thanks to * caching, it will work fast. */ $vars['attr_index'] = OkapiServiceRunner::call('services/attrs/attribute_index', new OkapiInternalRequest($request->consumer, $request->token, array('only_locally_used' => 'true', 'langpref' => $langpref, 'fields' => 'name|gc_equivs'))); # prepare GS attribute data $vars['gc_attrs'] = in_array('gc:attrs', $vars['attrs']); $vars['gc_ocde_attrs'] = in_array('gc_ocde:attrs', $vars['attrs']); if ($vars['gc_attrs'] || $vars['gc_ocde_attrs']) { if ($vars['gc_ocde_attrs']) { # As this is an OCDE compatibility feature, we use the same Pseudo-GS # attribute names here as OCDE. Note that this code is specific to OCDE # database; OCPL stores attribute names in a different way and may use # different names for equivalent attributes. $ocde_attrnames = Db::select_group_by('id', "\n select id, name\n from cache_attrib\n "); $attr_dict = AttrHelper::get_attrdict(); } foreach ($vars['caches'] as &$cache_ref) { $cache_ref['gc_attrs'] = array(); foreach ($cache_ref['attr_acodes'] as $acode) { $has_gc_equivs = false; foreach ($vars['attr_index'][$acode]['gc_equivs'] as $gc) { # The assignment via GC-ID as array key will prohibit duplicate # GC attributes, which can result from # - assigning the same GC ID to multiple A-Codes, # - contradicting attributes in one OC listing, e.g. 24/4 + not 24/7. $cache_ref['gc_attrs'][$gc['id']] = $gc; $has_gc_equivs = true; } if (!$has_gc_equivs && $vars['gc_ocde_attrs']) { # Generate an OCDE pseudo-GS attribute; # see https://github.com/opencaching/okapi/issues/190 and # https://github.com/opencaching/okapi/issues/271. # # Groundspeak uses ID 1..65 (as of June, 2013), and OCDE makeshift # IDs start at 106, so there is space for 40 new GS attributes. $internal_id = $attr_dict[$acode]['internal_id']; $cache_ref['gc_attrs'][100 + $internal_id] = array('inc' => 1, 'name' => $ocde_attrnames[$internal_id][0]['name']); } } } } } /* OC sites always used internal user_ids in their generated GPX files. * This might be considered an error in itself (Groundspeak's XML namespace * doesn't allow that), but it very common (Garmin's OpenCaching.COM * also does that). Therefore, for backward-compatibility reasons, OKAPI * will do it the same way. See issue 174. * * Currently, the caches method does not expose "owner.internal_id" and * "latest_logs.user.internal_id" fields, we will read them manually * from the database here. */ $dict = array(); foreach ($vars['caches'] as &$cache_ref) { $dict[$cache_ref['owner']['uuid']] = true; if (isset($cache_ref['latest_logs'])) { foreach ($cache_ref['latest_logs'] as &$log_ref) { $dict[$log_ref['user']['uuid']] = true; } } } $rs = Db::query("\n select uuid, user_id\n from user\n where uuid in ('" . implode("','", array_map('mysql_real_escape_string', array_keys($dict))) . "')\n "); while ($row = mysql_fetch_assoc($rs)) { $dict[$row['uuid']] = $row['user_id']; } $vars['user_uuid_to_internal_id'] =& $dict; unset($dict); # location_source (part 2 of 2) if ($location_source != 'default-coords') { $location_change_prefix = $request->get_parameter('location_change_prefix'); if (!$location_change_prefix) { $location_change_prefix = '# '; } # lets find requested coords foreach ($vars['caches'] as &$cache_ref) { foreach ($cache_ref['alt_wpts'] as $alt_wpt_key => $alt_wpt) { if ('alt_wpt:' . $alt_wpt['type'] == $location_source) { # Switch locations between primary wpt and alternate wpt. # Also alter the cache name and make sure to append a proper # notice. $original_location = $cache_ref['location']; $cache_ref['location'] = $alt_wpt['location']; $cache_ref['name_2'] = $location_change_prefix . $cache_ref['name']; if ($location_source == "alt_wpt:user-coords") { # In case of "user-coords", replace the default warning with a custom-tailored one. $cache_ref['warning_prefix'] = _("<b>Geocache coordinates have been changed.</b> They have been replaced with " . "your own custom coordinates which you have provided for this geocache."); } else { # Default warning $cache_ref['warning_prefix'] = _("<b>Geocache coordinates have been changed.</b> Currently they " . "point to one of the alternate waypoints originally described as:") . " " . $alt_wpt['description']; } # remove current alt waypoint unset($cache_ref['alt_wpts'][$alt_wpt_key]); # add original location as alternate if ($vars['alt_wpts']) { $cache_ref['alt_wpts'][] = array('name' => $cache_ref['code'] . '-DEFAULT-COORDS', 'location' => $original_location, 'type' => 'default-coords', 'type_name' => _("Original geocache location"), 'sym' => 'Block, Blue', 'description' => sprintf(_("Original (owner-supplied) location of the %s geocache"), $cache_ref['code'])); } break; } } } } # Do we need a GGZ index? if ($flags & self::FLAG_CREATE_GGZ_IDX) { # GGZ index consist of entries - one per each waypoint in the GPX file. # We will keep a list of all such entries here. $ggz_entries = array(); foreach ($vars['caches'] as &$cache_ref) { # Every $cache_ref will also be holding a reference to its entry. # Note, that more attributes are added while processing gpsfile.tpl.php! if (!isset($cache_ref['ggz_entry'])) { $cache_ref['ggz_entry'] = array(); } $ggz_entry =& $cache_ref['ggz_entry']; $ggz_entries[] =& $ggz_entry; $ggz_entry['code'] = $cache_ref['code']; $ggz_entry['name'] = isset($cache_ref['name_2']) ? $cache_ref['name_2'] : $cache_ref['name']; $ggz_entry['type'] = $vars['cache_GPX_types'][$cache_ref['type']]; list($lat, $lon) = explode("|", $cache_ref['location']); $ggz_entry['lat'] = $lat; $ggz_entry['lon'] = $lon; $ggz_entry['ratings'] = array(); $ratings_ref =& $ggz_entry['ratings']; if (isset($cache_ref['rating'])) { $ratings_ref['awesomeness'] = $cache_ref['rating']; } $ratings_ref['difficulty'] = $cache_ref['difficulty']; if (!isset($cache_ref['size'])) { $ratings_ref['size'] = 0; // Virtual, Event } else { if ($cache_ref['oxsize'] !== null) { // is this ox size one-to-one? $ratings_ref['size'] = $cache_ref['oxsize']; } } $ratings_ref['terrain'] = $cache_ref['terrain']; if ($vars['mark_found'] && $cache_ref['is_found']) { $ggz_entry['found'] = true; } # Additional waypoints. Currently, we're not 100% sure if their entries should # be included in the GGZ file (the format is undocumented). if (isset($cache_ref['alt_wpts'])) { $idx = 1; foreach ($cache_ref['alt_wpts'] as &$alt_wpt_ref) { if (!isset($alt_wpt_ref['ggz_entry'])) { $alt_wpt_ref['ggz_entry'] = array(); } $ggz_entry =& $alt_wpt_ref['ggz_entry']; $ggz_entries[] =& $ggz_entry; $ggz_entry['code'] = $cache_ref['code'] . '-' . $idx; $ggz_entry['name'] = $alt_wpt_ref['type_name']; $ggz_entry['type'] = $alt_wpt_ref['sym']; list($lat, $lon) = explode("|", $alt_wpt_ref['location']); $ggz_entry['lat'] = $lat; $ggz_entry['lon'] = $lon; $idx++; } } } } ob_start(); Okapi::gettext_domain_init(explode("|", $langpref)); # Consumer gets properly localized GPX file. include 'gpxfile.tpl.php'; Okapi::gettext_domain_restore(); $result = array('gpx' => ob_get_clean()); if ($flags & self::FLAG_CREATE_GGZ_IDX) { $result['ggz_entries'] = $ggz_entries; } return $result; }
/** * 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 '" . mysql_real_escape_string($from) . "' and '" . mysql_real_escape_string($to) . "'\n order by id\n "); $chunk = array(); while ($row = mysql_fetch_assoc($rs)) { $chunk[] = unserialize(gzinflate($row['data'])); } # Cache timeout depends on the chunk starting and ending point. Chunks # which start and end on the boundries of chunk_size should be cached # longer (they can be accessed even after 10 days). Other chunks won't # be ever accessed after the next revision appears, so there is not point # in storing them that long. if ($from % self::$chunk_size === 0 && $to % self::$chunk_size === 0) { $timeout = 10 * 86400; } else { $timeout = 86400; } Cache::set($cache_key, $chunk, $timeout); } return $chunk; }
/** * Append a new image to a log entry and return the image uuid and position. * Throws CannotPublishException or BadRequest on errors. */ private static function _call(OkapiRequest $request) { require_once 'log_images_common.inc.php'; # Developers! Please notice the fundamental difference between throwing # CannotPublishException and the "standard" BadRequest/InvalidParam # exceptions. You're reading the "_call" method now (see below for # "call"). # validate the 'log_uuid' parameter $log_uuid = $request->get_parameter('log_uuid'); if (!$log_uuid) { throw new ParamMissing('log_uuid'); } $rs = Db::query("\n select id, node, user_id\n from cache_logs\n where uuid = '" . Db::escape_string($log_uuid) . "'"); $row = Db::fetch_assoc($rs); Db::free_result($rs); if (!$row) { throw new InvalidParam('log_uuid', "There is no log entry with uuid '" . $log_uuid . "'."); } if ($row['node'] != Settings::get('OC_NODE_ID')) { throw new Exception("This site's database contains the log entry '{$log_uuid}' which has been" . " imported from another OC node. OKAPI is not prepared for that."); } if ($row['user_id'] != $request->token->user_id) { throw new InvalidParam('log_uuid', "The user of your access token is not the log entry's author."); } $log_internal_id = $row['id']; unset($row); # validate the 'caption', 'is_spoiler' and 'position' parameters $caption = $request->get_parameter('caption'); if (!$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) { $is_spoiler = 'false'; } if (!in_array($is_spoiler, array('true', 'false'))) { throw new InvalidParam('is_spoiler'); } $position = LogImagesCommon::validate_position($request); # validate the 'image' parameter $base64_image = $request->get_parameter('image'); if (!$base64_image) { throw new ParamMissing('image'); } $estimated_decoded_size = strlen($base64_image) / 4 * 3 - 2; if ($estimated_decoded_size > Settings::get('IMAGE_MAX_UPLOAD_SIZE')) { $estimated_decoded_size_MB = round($estimated_decoded_size / 1024 / 1024, 1); $max_upload_size_MB = round(Settings::get('IMAGE_MAX_UPLOAD_SIZE') / 1024 / 1024, 1); throw new CannotPublishException(sprintf(_("Your image file is too large (%s.%s MB); %s accepts a maximum image size of %s.%s MB."), floor($estimated_decoded_size_MB), $estimated_decoded_size_MB * 10 % 10, Okapi::get_normalized_site_name(), floor($max_upload_size_MB), $max_upload_size_MB * 10 % 10)); } $image = base64_decode($base64_image); if (!$image) { throw new InvalidParam('image', "bad base64 encoding"); } try { $image_properties = getimagesizefromstring($image); # can throw if (!$image_properties) { throw new Exception(); } list($width, $height, $image_type) = $image_properties; if (!in_array($image_type, array(IMAGETYPE_JPEG, IMAGETYPE_PNG, IMAGETYPE_GIF))) { # This will happen e.g. for BMP and XBM images, which are supported by GD. throw new Exception(); } } catch (Exception $e) { # Note: There may be *subtypes* which are not accepted by the GD library. # About 1 of 2000 JPEGs at OC.de is not readable by the PHP functions, # though they can be displayed by web browsers. throw new CannotPublishException(sprintf(_("The uploaded image file is broken, or the image type is not accepted by %s. Allowed types are JPEG, PNG and GIF."), Okapi::get_normalized_site_name())); } unset($image_properties); if ($width * $height > self::max_pixels($base64_image)) { # This large image may crash the image processing functions. throw new CannotPublishException(sprintf(_("The uploaded image is too large (%s megapixels), please downscale it."), round($width * $height / 1024 / 1024))); } try { $image = imagecreatefromstring($image); # can throw if (!$image) { throw new Exception(); } } catch (Exception $e) { throw new CannotPublishException(_("The uploaded image file is broken.")); } # Now all supplied paramters are validated. # Do any postprocessing like scaling and rotating $image = self::postprocess_image($base64_image, $image, $image_type, $width, $height); unset($base64_image); # Save the image file. By saving it always from the $image object instead of # the original image data (even if not downscaled or rotated), we # - strip JPEG EXIF information, which is intentional for privacy reasons, # - eliminate any data flaws which have may been in the source files. $image_uuid = Okapi::create_uuid(); $imagepath = Settings::get('IMAGES_DIR') . '/' . $image_uuid; switch ($image_type) { case IMAGETYPE_JPEG: $file_ext = '.jpg'; $quality = Settings::get('JPEG_QUALITY'); $result = imagejpeg($image, $imagepath . $file_ext, $quality); break; case IMAGETYPE_PNG: $file_ext = '.png'; $result = imagepng($image, $imagepath . $file_ext); break; case IMAGETYPE_GIF: $file_ext = '.gif'; $result = imagegif($image, $imagepath . $file_ext); break; default: $file_ext = '.???'; $result = false; } if (!$result) { throw new Exception("could not save image file '" . $imagepath . $file_ext . "'"); } # insert image into database try { $position = self::db_insert_image($request->consumer->key, $request->token->user_id, $log_internal_id, $image_uuid, $position, $caption, $is_spoiler, $file_ext); } catch (Exception $e) { # TODO: Proper handling of nested exception if the unlink() fails # (which is very unlikely, and will just add a bit more of garbage # to that which is already present in the images directory). try { unlink($imagepath . $file_ext); } catch (Exception $e2) { } throw $e; } return array($image_uuid, $position); }