/** * Note, there is no $timeout (time to live) parameter. Currently, * OKAPI will delete every old file after certain amount of time. * See CacheCleanupCronJob for details. */ public static function set($key, $value) { $filename = Okapi::get_var_dir() . "/okapi_filecache_" . md5($key); file_put_contents($filename, $value); return $filename; }
public function execute() { # Delete all expired elements. Db::execute("\n delete from okapi_cache\n where expires < now()\n "); # Update the "score" stats. $multiplier = 0.9; # Every hour, all scores are multiplied by this. $limit = 0.01; # When a score reaches this limit, the entry is deleted. # Every time the entry is read, its score is incread by 1. If an entry # is saved, but never read, it will be deleted after log(L,M) hours # (log(0.01, 0.9) = 43h). If an entry is read 1000000 times and then # never read anymore, it will be deleted after log(1000000/L, 1/M) # hours (log(1000000/0.01, 1/0.9) = 174h = 7 days). Db::execute("\n update okapi_cache\n set score = score * '" . mysql_real_escape_string($multiplier) . "'\n where score is not null\n "); Db::execute("\n update\n okapi_cache c,\n (\n select cache_key, count(*) as count\n from okapi_cache_reads\n group by cache_key\n ) cr\n set c.score = c.score + cr.count\n where\n c.`key` = cr.cache_key\n and c.score is not null\n "); Db::execute("truncate okapi_cache_reads"); # Delete elements with the lowest score. Entries which have been set # but never read will be removed after 36 hours (0.9^36 < 0.02 < 0.9^35). Db::execute("\n delete from okapi_cache\n where\n score is not null\n and score < '" . mysql_real_escape_string($limit) . "'\n "); Db::query("optimize table okapi_cache"); # FileCache does not have an expiry date. We will delete all files older # than 24 hours. $dir = Okapi::get_var_dir(); if ($dh = opendir($dir)) { while (($file = readdir($dh)) !== false) { if (strpos($file, "okapi_filecache_") === 0) { if (filemtime("{$dir}/{$file}") < time() - 86400) { unlink("{$dir}/{$file}"); } } } closedir($dh); } }
/** * Publish a new log entry and return log entry uuid. Throws * CannotPublishException or BadRequest on errors. */ private static function _call(OkapiRequest $request) { # Developers! Please notice the fundamental difference between throwing # CannotPublishException and standard BadRequest/InvalidParam exceptions! # Notice, that this is "_call" method, not the usual "call" (see below # for "call"). $cache_code = $request->get_parameter('cache_code'); if (!$cache_code) { throw new ParamMissing('cache_code'); } $logtype = $request->get_parameter('logtype'); if (!$logtype) { throw new ParamMissing('logtype'); } if (!in_array($logtype, array('Found it', "Didn't find it", 'Comment', 'Will attend', 'Attended'))) { throw new InvalidParam('logtype', "'{$logtype}' in not a valid logtype code."); } $comment = $request->get_parameter('comment'); if (!$comment) { $comment = ""; } $comment_format = $request->get_parameter('comment_format'); if (!$comment_format) { $comment_format = "auto"; } if (!in_array($comment_format, array('auto', 'html', 'plaintext'))) { throw new InvalidParam('comment_format', $comment_format); } $tmp = $request->get_parameter('when'); if ($tmp) { $when = strtotime($tmp); if (!$when) { throw new InvalidParam('when', "'{$tmp}' is not in a valid format or is not a valid date."); } if ($when > time() + 5 * 60) { throw new CannotPublishException(_("You are trying to publish a log entry with a date in future. " . "Cache log entries are allowed to be published in the past, but NOT in the future.")); } } else { $when = time(); } $on_duplicate = $request->get_parameter('on_duplicate'); if (!$on_duplicate) { $on_duplicate = "silent_success"; } if (!in_array($on_duplicate, array('silent_success', 'user_error', 'continue'))) { throw new InvalidParam('on_duplicate', "Unknown option: '{$on_duplicate}'."); } $rating = $request->get_parameter('rating'); if ($rating !== null && !in_array($rating, array(1, 2, 3, 4, 5))) { throw new InvalidParam('rating', "If present, it must be an integer in the 1..5 scale."); } if ($rating && $logtype != 'Found it' && $logtype != 'Attended') { throw new BadRequest("Rating is allowed only for 'Found it' and 'Attended' logtypes."); } if ($rating !== null && Settings::get('OC_BRANCH') == 'oc.de') { # We will remove the rating request and change the success message # (which will be returned IF the rest of the query will meet all the # requirements). self::$success_message .= " " . sprintf(_("However, your cache rating was ignored, because %s does not have a rating system."), Okapi::get_normalized_site_name()); $rating = null; } $recommend = $request->get_parameter('recommend'); if (!$recommend) { $recommend = 'false'; } if (!in_array($recommend, array('true', 'false'))) { throw new InvalidParam('recommend', "Unknown option: '{$recommend}'."); } $recommend = $recommend == 'true'; if ($recommend && $logtype != 'Found it') { if ($logtype != 'Attended') { throw new BadRequest("Recommending is allowed only for 'Found it' and 'Attended' logs."); } else { if (Settings::get('OC_BRANCH') == 'oc.pl') { # We will remove the recommendation request and change the success message # (which will be returned IF the rest of the query will meet all the # requirements). self::$success_message .= " " . sprintf(_("However, your cache recommendation was ignored, because %s does not allow recommending event caches."), Okapi::get_normalized_site_name()); $recommend = null; } } } $needs_maintenance = $request->get_parameter('needs_maintenance'); if (!$needs_maintenance) { $needs_maintenance = 'false'; } if (!in_array($needs_maintenance, array('true', 'false'))) { throw new InvalidParam('needs_maintenance', "Unknown option: '{$needs_maintenance}'."); } $needs_maintenance = $needs_maintenance == 'true'; if ($needs_maintenance && !Settings::get('SUPPORTS_LOGTYPE_NEEDS_MAINTENANCE')) { # If not supported, just ignore it. self::$success_message .= " " . sprintf(_("However, your \"needs maintenance\" flag was ignored, because %s does not support this feature."), Okapi::get_normalized_site_name()); $needs_maintenance = false; } # Check if cache exists and retrieve cache internal ID (this will throw # a proper exception on invalid cache_code). Also, get the user object. $cache = OkapiServiceRunner::call('services/caches/geocache', new OkapiInternalRequest($request->consumer, null, array('cache_code' => $cache_code, 'fields' => 'internal_id|status|owner|type|req_passwd'))); $user = OkapiServiceRunner::call('services/users/by_internal_id', new OkapiInternalRequest($request->consumer, $request->token, array('internal_id' => $request->token->user_id, 'fields' => 'is_admin|uuid|internal_id|caches_found|rcmds_given'))); # Various integrity checks. if ($cache['type'] == 'Event') { if (!in_array($logtype, array('Will attend', 'Attended', 'Comment'))) { throw new CannotPublishException(_('This cache is an Event cache. You cannot "Find" it (but you can attend it, or comment on it)!')); } } else { if (in_array($logtype, array('Will attend', 'Attended'))) { throw new CannotPublishException(_('This cache is NOT an Event cache. You cannot "Attend" it (but you can find it, or comment on it)!')); } else { if (!in_array($logtype, array('Found it', "Didn't find it", 'Comment'))) { throw new Exception("Unknown log entry - should be documented here."); } } } if ($logtype == 'Comment' && strlen(trim($comment)) == 0) { throw new CannotPublishException(_("Your have to supply some text for your comment.")); } # Password check. if (($logtype == 'Found it' || $logtype == 'Attended') && $cache['req_passwd']) { $valid_password = Db::select_value("\n select logpw\n from caches\n where cache_id = '" . mysql_real_escape_string($cache['internal_id']) . "'\n "); $supplied_password = $request->get_parameter('password'); if (!$supplied_password) { throw new CannotPublishException(_("This cache requires a password. You didn't provide one!")); } if (strtolower($supplied_password) != strtolower($valid_password)) { throw new CannotPublishException(_("Invalid password!")); } } # Prepare our comment to be inserted into the database. This may require # some reformatting which depends on the current OC installation. if (Settings::get('OC_BRANCH') == 'oc.de') { # OCDE stores all comments in HTML format, while the 'text_html' field # indicates their *original* format as delivered by the user. This # allows processing the 'text' field contents without caring about the # original format, while still being able to re-create the comment in # its original form. It requires us to HTML-encode plaintext comments # and to indicate this by setting 'html_text' to FALSE. # # For user-supplied HTML comments, OCDE requires us to do additional # HTML purification prior to the insertion into the database. if ($comment_format == 'plaintext') { $formatted_comment = htmlspecialchars($comment, ENT_QUOTES); $formatted_comment = nl2br($formatted_comment); $value_for_text_html_field = 0; } else { if ($comment_format == 'auto') { # 'Auto' is for backward compatibility. Before the "comment_format" # was introduced, OKAPI used a weird format in between (it allowed # HTML, but applied nl2br too). $formatted_comment = nl2br($comment); } else { $formatted_comment = $comment; } # NOTICE: We are including EXTERNAL OCDE library here! This # code does not belong to OKAPI! $opt['rootpath'] = $GLOBALS['rootpath']; $opt['html_purifier'] = Settings::get('OCDE_HTML_PURIFIER_SETTINGS'); require_once $GLOBALS['rootpath'] . 'lib2/OcHTMLPurifier.class.php'; $purifier = new \OcHTMLPurifier($opt); $formatted_comment = $purifier->purify($formatted_comment); $value_for_text_html_field = 1; } } else { # OCPL is even weirder. It also stores HTML-lized comments in the database # (it doesn't really matter if 'text_html' field is set to FALSE). OKAPI must # save it in HTML either way. However, escaping plain-text doesn't work! # If we put "<b>" in, it still gets converted to "<b>" before display! # NONE of this process is documented within OCPL code. OKAPI uses a dirty # "hack" to save PLAINTEXT comments (let us hope the hack will remain valid). # # OCPL doesn't require HTML purification prior to the database insertion. # HTML seems to be purified dynamically, before it is displayed. if ($comment_format == 'plaintext') { $formatted_comment = htmlspecialchars($comment, ENT_QUOTES); $formatted_comment = nl2br($formatted_comment); $formatted_comment = str_replace("&", "&#38;", $formatted_comment); $formatted_comment = str_replace("<", "&#60;", $formatted_comment); $formatted_comment = str_replace(">", "&#62;", $formatted_comment); $value_for_text_html_field = 0; // WRTODO: get rid of } elseif ($comment_format == 'auto') { $formatted_comment = nl2br($comment); $value_for_text_html_field = 1; } else { $formatted_comment = $comment; $value_for_text_html_field = 1; } } unset($comment); # Duplicate detection. if ($on_duplicate != 'continue') { # Attempt to find a log entry made by the same user, for the same cache, with # the same date, type, comment, etc. Note, that these are not ALL the fields # we could check, but should work ok in most cases. Also note, that we # DO NOT guarantee that duplicate detection will succeed. If it doesn't, # nothing bad happens (user will just post two similar log entries). # Keep this simple! $duplicate_uuid = Db::select_value("\n select uuid\n from cache_logs\n where\n user_id = '" . mysql_real_escape_string($request->token->user_id) . "'\n and cache_id = '" . mysql_real_escape_string($cache['internal_id']) . "'\n and type = '" . mysql_real_escape_string(Okapi::logtypename2id($logtype)) . "'\n and date = from_unixtime('" . mysql_real_escape_string($when) . "')\n and text = '" . mysql_real_escape_string($formatted_comment) . "'\n " . (Settings::get('OC_BRANCH') == 'oc.pl' ? "and deleted = 0" : "") . "\n limit 1\n "); if ($duplicate_uuid != null) { if ($on_duplicate == 'silent_success') { # Act as if the log has been submitted successfully. return $duplicate_uuid; } elseif ($on_duplicate == 'user_error') { throw new CannotPublishException(_("You have already submitted a log entry with exactly the same contents.")); } } } # Check if already found it (and make sure the user is not the owner). # # OCPL forbids logging 'Found it' or "Didn't find" for an already found cache, # while OCDE allows all kinds of duplicate logs. if (Settings::get('OC_BRANCH') == 'oc.pl' && ($logtype == 'Found it' || $logtype == "Didn't find it")) { $has_already_found_it = Db::select_value("\n select 1\n from cache_logs\n where\n user_id = '" . mysql_real_escape_string($user['internal_id']) . "'\n and cache_id = '" . mysql_real_escape_string($cache['internal_id']) . "'\n and type = '" . mysql_real_escape_string(Okapi::logtypename2id("Found it")) . "'\n and " . (Settings::get('OC_BRANCH') == 'oc.pl' ? "deleted = 0" : "true") . "\n "); if ($has_already_found_it) { throw new CannotPublishException(_("You have already submitted a \"Found it\" log entry once. Now you may submit \"Comments\" only!")); } if ($user['uuid'] == $cache['owner']['uuid']) { throw new CannotPublishException(_("You are the owner of this cache. You may submit \"Comments\" only!")); } } # Check if the user has already rated the cache. BTW: I don't get this one. # If we already know, that the cache was NOT found yet, then HOW could the # user submit a rating for it? Anyway, I will stick to the procedure # found in log.php. On the bright side, it's fail-safe. if ($rating) { $has_already_rated = Db::select_value("\n select 1\n from scores\n where\n user_id = '" . mysql_real_escape_string($user['internal_id']) . "'\n and cache_id = '" . mysql_real_escape_string($cache['internal_id']) . "'\n "); if ($has_already_rated) { throw new CannotPublishException(_("You have already rated this cache once. Your rating cannot be changed.")); } } # If user wants to recommend... if ($recommend) { # Do the same "fail-safety" check as we did for the rating. $already_recommended = Db::select_value("\n select 1\n from cache_rating\n where\n user_id = '" . mysql_real_escape_string($user['internal_id']) . "'\n and cache_id = '" . mysql_real_escape_string($cache['internal_id']) . "'\n "); if ($already_recommended) { throw new CannotPublishException(_("You have already recommended this cache once.")); } # Check the number of recommendations. $founds = $user['caches_found'] + 1; // +1, because he'll find THIS ONE in a moment, right? # Note: caches_found includes event attendance on both, OCDE and OCPL. # Though OCPL does not allow recommending events, for each 10 event # attendances the user may recommend a non-event cache. $rcmds_left = floor($founds / 10.0) - $user['rcmds_given']; if ($rcmds_left <= 0) { throw new CannotPublishException(_("You don't have any recommendations to give. Find more caches first!")); } } # If user checked the "needs_maintenance" flag, we will shuffle things a little... if ($needs_maintenance) { # If we're here, then we also know that the "Needs maintenance" log type is supported # by this OC site. However, it's a separate log type, so we might have to submit # two log types together: if ($logtype == 'Comment') { # If user submits a "Comment", we'll just change its type to "Needs maintenance". # Only one log entry will be issued. $logtype = 'Needs maintenance'; $second_logtype = null; $second_formatted_comment = null; } elseif ($logtype == 'Found it') { # If "Found it", then we'll issue two log entries: one "Found it" with the # original comment, and second one "Needs maintenance" with empty comment. $second_logtype = 'Needs maintenance'; $second_formatted_comment = ""; } elseif ($logtype == "Didn't find it") { # If "Didn't find it", then we'll issue two log entries, but this time # we'll do this the other way around. The first "Didn't find it" entry # will have an empty comment. We will move the comment to the second # "Needs maintenance" log entry. (It's okay for this behavior to change # in the future, but it seems natural to me.) $second_logtype = 'Needs maintenance'; $second_formatted_comment = $formatted_comment; $formatted_comment = ""; } else { if ($logtype == 'Will attend' || $logtype == 'Attended') { # OC branches which know maintenance logs do not allow them on event caches. throw new CannotPublishException(_("Event caches cannot \"need maintenance\".")); } else { throw new Exception(); } } } else { # User didn't check the "Needs maintenance" flag OR "Needs maintenance" log type # isn't supported by this server. $second_logtype = null; $second_formatted_comment = null; } # Finally! Insert the rows into the log entries table. Update # cache stats and user stats. $log_uuid = self::insert_log_row($request->consumer->key, $cache['internal_id'], $user['internal_id'], $logtype, $when, $formatted_comment, $value_for_text_html_field); self::increment_cache_stats($cache['internal_id'], $when, $logtype); self::increment_user_stats($user['internal_id'], $logtype); if ($second_logtype != null) { # Reminder: This will never be called while SUPPORTS_LOGTYPE_NEEDS_MAINTENANCE is off. self::insert_log_row($request->consumer->key, $cache['internal_id'], $user['internal_id'], $second_logtype, $when + 1, $second_formatted_comment, $value_for_text_html_field); self::increment_cache_stats($cache['internal_id'], $when + 1, $second_logtype); self::increment_user_stats($user['internal_id'], $second_logtype); } # Save the rating. if ($rating) { # This code will be called for OCPL branch only. Earlier, we made sure, # to set $rating to null, if we're running on OCDE. # OCPL has a little strange way of storing cumulative rating. Instead # of storing the sum of all ratings, OCPL stores the computed average # and update it using multiple floating-point operations. Moreover, # the "score" field in the database is on the -3..3 scale (NOT 1..5), # and the translation made at retrieval time is DIFFERENT than the # one made here (both of them are non-linear). Also, once submitted, # the rating can never be changed. It surely feels quite inconsistent, # but presumably has some deep logic into it. See also here (Polish): # http://wiki.opencaching.pl/index.php/Oceny_skrzynek switch ($rating) { case 1: $db_score = -2.0; break; case 2: $db_score = -0.5; break; case 3: $db_score = 0.7; break; case 4: $db_score = 1.7; break; case 5: $db_score = 3.0; break; default: throw new Exception(); } Db::execute("\n update caches\n set\n score = (score*votes + '" . mysql_real_escape_string($db_score) . "')/(votes + 1),\n votes = votes + 1\n where cache_id = '" . mysql_real_escape_string($cache['internal_id']) . "'\n "); Db::execute("\n insert into scores (user_id, cache_id, score)\n values (\n '" . mysql_real_escape_string($user['internal_id']) . "',\n '" . mysql_real_escape_string($cache['internal_id']) . "',\n '" . mysql_real_escape_string($db_score) . "'\n );\n "); } # Save recommendation. if ($recommend) { if (Db::field_exists('cache_rating', 'rating_date')) { Db::execute("\n insert into cache_rating (user_id, cache_id, rating_date)\n values (\n '" . mysql_real_escape_string($user['internal_id']) . "',\n '" . mysql_real_escape_string($cache['internal_id']) . "',\n from_unixtime('" . mysql_real_escape_string($when) . "')\n );\n "); } else { Db::execute("\n insert into cache_rating (user_id, cache_id)\n values (\n '" . mysql_real_escape_string($user['internal_id']) . "',\n '" . mysql_real_escape_string($cache['internal_id']) . "'\n );\n "); } } # We need to delete the copy of stats-picture for this user. Otherwise, # the legacy OC code won't detect that the picture needs to be refreshed. $filepath = Okapi::get_var_dir() . '/images/statpics/statpic' . $user['internal_id'] . '.jpg'; if (file_exists($filepath)) { unlink($filepath); } # Success. Return the uuid. return $log_uuid; }
/** * Generate a new fulldump file and put it into the OKAPI cache table. * Return the cache key. */ public static function generate_fulldump() { # First we will create temporary files, then compress them in the end. $revision = self::get_revision(); $generated_at = date('c', time()); $dir = Okapi::get_var_dir() . "/okapi-db-dump"; $i = 1; $json_files = array(); # Cleanup (from a previous, possibly unsuccessful, execution) shell_exec("rm -f {$dir}/*"); shell_exec("rmdir {$dir}"); shell_exec("mkdir {$dir}"); shell_exec("chmod 777 {$dir}"); # Geocaches $cache_codes = Db::select_column("select wp_oc from caches"); $cache_code_groups = Okapi::make_groups($cache_codes, self::$chunk_size); unset($cache_codes); foreach ($cache_code_groups as $cache_codes) { $basename = "part" . str_pad($i, 5, "0", STR_PAD_LEFT); $json_files[] = $basename . ".json"; $entries = self::generate_changelog_entries('services/caches/geocaches', 'geocache', 'cache_codes', 'code', $cache_codes, self::$logged_cache_fields, true, false); $filtered = array(); foreach ($entries as $entry) { if ($entry['change_type'] == 'replace') { $filtered[] = $entry; } } unset($entries); file_put_contents("{$dir}/{$basename}.json", json_encode($filtered)); unset($filtered); $i++; } unset($cache_code_groups); # Log entries. We cannot load all the uuids at one time, this would take # too much memory. Hence the offset/limit loop. $offset = 0; while (true) { $log_uuids = Db::select_column("\n select uuid\n from cache_logs\n where " . (Settings::get('OC_BRANCH') == 'oc.pl' ? "deleted = 0" : "true") . "\n order by uuid\n limit {$offset}, 10000\n "); if (count($log_uuids) == 0) { break; } $offset += 10000; $log_uuid_groups = Okapi::make_groups($log_uuids, 500); unset($log_uuids); foreach ($log_uuid_groups as $log_uuids) { $basename = "part" . str_pad($i, 5, "0", STR_PAD_LEFT); $json_files[] = $basename . ".json"; $entries = self::generate_changelog_entries('services/logs/entries', 'log', 'log_uuids', 'uuid', $log_uuids, self::$logged_log_entry_fields, true, false); $filtered = array(); foreach ($entries as $entry) { if ($entry['change_type'] == 'replace') { $filtered[] = $entry; } } unset($entries); file_put_contents("{$dir}/{$basename}.json", json_encode($filtered)); unset($filtered); $i++; } } # Package data. $metadata = array('revision' => $revision, 'data_files' => $json_files, 'meta' => array('site_name' => Okapi::get_normalized_site_name(), 'okapi_version_number' => Okapi::$version_number, 'okapi_revision' => Okapi::$version_number, 'okapi_git_revision' => Okapi::$git_revision, 'generated_at' => $generated_at)); file_put_contents("{$dir}/index.json", json_encode($metadata)); # Compute uncompressed size. $size = filesize("{$dir}/index.json"); foreach ($json_files as $filename) { $size += filesize("{$dir}/{$filename}"); } # Create JSON archive. We use tar options: -j for bzip2, -z for gzip # (bzip2 is MUCH slower). $use_bzip2 = true; $dumpfilename = "okapi-dump.tar." . ($use_bzip2 ? "bz2" : "gz"); shell_exec("tar --directory {$dir} -c" . ($use_bzip2 ? "j" : "z") . "f {$dir}/{$dumpfilename} index.json " . implode(" ", $json_files) . " 2>&1"); # Delete temporary files. shell_exec("rm -f {$dir}/*.json"); # Move the archive one directory upwards, replacing the previous one. # Remove the temporary directory. shell_exec("mv -f {$dir}/{$dumpfilename} " . Okapi::get_var_dir()); shell_exec("rmdir {$dir}"); # Update the database info. $metadata['meta']['filepath'] = Okapi::get_var_dir() . '/' . $dumpfilename; $metadata['meta']['content_type'] = $use_bzip2 ? "application/octet-stream" : "application/x-gzip"; $metadata['meta']['public_filename'] = 'okapi-dump-r' . $metadata['revision'] . '.tar.' . ($use_bzip2 ? "bz2" : "gz"); $metadata['meta']['uncompressed_size'] = $size; $metadata['meta']['compressed_size'] = filesize($metadata['meta']['filepath']); Cache::set("last_fulldump", $metadata, 10 * 86400); }
/** * Publish a new log entry and return log entry uuid. Throws * CannotPublishException or BadRequest on errors. */ private static function _call(OkapiRequest $request) { # Developers! Please notice the fundamental difference between throwing # CannotPublishException and the "standard" BadRequest/InvalidParam # exceptions. You're reading the "_call" method now (see below for # "call"). $cache_code = $request->get_parameter('cache_code'); if (!$cache_code) { throw new ParamMissing('cache_code'); } $logtype = $request->get_parameter('logtype'); if (!$logtype) { throw new ParamMissing('logtype'); } if (!in_array($logtype, array('Found it', "Didn't find it", 'Comment', 'Will attend', 'Attended'))) { throw new InvalidParam('logtype', "'{$logtype}' in not a valid logtype code."); } $comment = $request->get_parameter('comment'); if (!$comment) { $comment = ""; } $comment_format = $request->get_parameter('comment_format'); if (!$comment_format) { $comment_format = "auto"; } if (!in_array($comment_format, array('auto', 'html', 'plaintext'))) { throw new InvalidParam('comment_format', $comment_format); } $tmp = $request->get_parameter('when'); if ($tmp) { $when = strtotime($tmp); if ($when < 1) { throw new InvalidParam('when', "'{$tmp}' is not in a valid format or is not a valid date."); } if ($when > time() + 5 * 60) { throw new CannotPublishException(_("You are trying to publish a log entry with a date in " . "future. Cache log entries are allowed to be published in " . "the past, but NOT in the future.")); } } else { $when = time(); } $on_duplicate = $request->get_parameter('on_duplicate'); if (!$on_duplicate) { $on_duplicate = "silent_success"; } if (!in_array($on_duplicate, array('silent_success', 'user_error', 'continue'))) { throw new InvalidParam('on_duplicate', "Unknown option: '{$on_duplicate}'."); } $rating = $request->get_parameter('rating'); if ($rating !== null && !in_array($rating, array(1, 2, 3, 4, 5))) { throw new InvalidParam('rating', "If present, it must be an integer in the 1..5 scale."); } if ($rating && $logtype != 'Found it' && $logtype != 'Attended') { throw new BadRequest("Rating is allowed only for 'Found it' and 'Attended' logtypes."); } if ($rating !== null && Settings::get('OC_BRANCH') == 'oc.de') { # We will remove the rating request and change the success message # (which will be returned IF the rest of the query will meet all the # requirements). self::$success_message .= " " . sprintf(_("However, your cache rating was ignored, because %s does not " . "have a rating system."), Okapi::get_normalized_site_name()); $rating = null; } $recommend = $request->get_parameter('recommend'); if (!$recommend) { $recommend = 'false'; } if (!in_array($recommend, array('true', 'false'))) { throw new InvalidParam('recommend', "Unknown option: '{$recommend}'."); } $recommend = $recommend == 'true'; if ($recommend && $logtype != 'Found it') { if ($logtype != 'Attended') { throw new BadRequest("Recommending is allowed only for 'Found it' and 'Attended' logs."); } else { if (Settings::get('OC_BRANCH') == 'oc.pl') { # We will remove the recommendation request and change the success message # (which will be returned IF the rest of the query will meet all the # requirements). self::$success_message .= " " . sprintf(_("However, your cache recommendation was ignored, because " . "%s does not allow recommending event caches."), Okapi::get_normalized_site_name()); $recommend = null; } } } # We'll parse both 'needs_maintenance' and 'needs_maintenance2' here, but # we'll use only the $needs_maintenance2 variable afterwards. $needs_maintenance = $request->get_parameter('needs_maintenance'); $needs_maintenance2 = $request->get_parameter('needs_maintenance2'); if ($needs_maintenance && $needs_maintenance2) { throw new BadRequest("You cannot use both of these parameters at the same time: " . "needs_maintenance and needs_maintenance2."); } if (!$needs_maintenance2) { $needs_maintenance2 = 'null'; } # Parse $needs_maintenance and get rid of it. if ($needs_maintenance) { if ($needs_maintenance == 'true') { $needs_maintenance2 = 'true'; } else { if ($needs_maintenance == 'false') { $needs_maintenance2 = 'null'; } else { throw new InvalidParam('needs_maintenance', "Unknown option: '{$needs_maintenance}'."); } } } unset($needs_maintenance); # At this point, $needs_maintenance2 is set exactly as the user intended # it to be set. if (!in_array($needs_maintenance2, array('null', 'true', 'false'))) { throw new InvalidParam('needs_maintenance2', "Unknown option: '{$needs_maintenance2}'."); } if ($needs_maintenance2 == 'false' && Settings::get('OC_BRANCH') == 'oc.pl') { # If not supported, just ignore it. self::$success_message .= " " . sprintf(_("However, your \"does not need maintenance\" flag was ignored, because " . "%s does not yet support this feature."), Okapi::get_normalized_site_name()); $needs_maintenance2 = 'null'; } # Check if cache exists and retrieve cache internal ID (this will throw # a proper exception on invalid cache_code). Also, get the user object. $cache = OkapiServiceRunner::call('services/caches/geocache', new OkapiInternalRequest($request->consumer, null, array('cache_code' => $cache_code, 'fields' => 'internal_id|status|owner|type|req_passwd'))); $user = OkapiServiceRunner::call('services/users/by_internal_id', new OkapiInternalRequest($request->consumer, $request->token, array('internal_id' => $request->token->user_id, 'fields' => 'is_admin|uuid|internal_id|caches_found|rcmds_given'))); # Various integrity checks. if ($cache['type'] == 'Event') { if (!in_array($logtype, array('Will attend', 'Attended', 'Comment'))) { throw new CannotPublishException(_('This cache is an Event cache. You cannot "Find" it (but ' . 'you can attend it, or comment on it)!')); } } else { if (in_array($logtype, array('Will attend', 'Attended'))) { throw new CannotPublishException(_('This cache is NOT an Event cache. You cannot "Attend" it ' . '(but you can find it, or comment on it)!')); } else { if (!in_array($logtype, array('Found it', "Didn't find it", 'Comment'))) { throw new Exception("Unknown log entry - should be documented here."); } } } if ($logtype == 'Comment' && strlen(trim($comment)) == 0) { throw new CannotPublishException(_("Your have to supply some text for your comment.")); } # Password check. if (($logtype == 'Found it' || $logtype == 'Attended') && $cache['req_passwd']) { $valid_password = Db::select_value("\n select logpw\n from caches\n where cache_id = '" . Db::escape_string($cache['internal_id']) . "'\n "); $supplied_password = $request->get_parameter('password'); if (!$supplied_password) { throw new CannotPublishException(_("This cache requires a password. You didn't provide one!")); } if (strtolower($supplied_password) != strtolower($valid_password)) { throw new CannotPublishException(_("Invalid password!")); } } # Prepare our comment to be inserted into the database. This may require # some reformatting which depends on the current OC installation. # OC sites store all comments in HTML format, while the 'text_html' field # indicates their *original* format as delivered by the user. This # allows processing the 'text' field contents without caring about the # original format, while still being able to re-create the comment in # its original form. if ($comment_format == 'plaintext') { # This code is identical to the plaintext processing in OC code, # including a space handling bug: Multiple consecutive spaces will # get semantically lost in the generated HTML. $formatted_comment = htmlspecialchars($comment, ENT_COMPAT); $formatted_comment = nl2br($formatted_comment); if (Settings::get('OC_BRANCH') == 'oc.de') { $value_for_text_html_field = 0; } else { # 'text_html' = 0 (false) is broken in OCPL code and has been # deprecated; OCPL code was changed to always set it to 1 (true). # For OKAPI, the value has been changed from 0 to 1 with commit # cb7d222, after an email discussion with Harrie Klomp. This is # an ID of the appropriate email thread: # # Message-ID: <*****@*****.**> $value_for_text_html_field = 1; } } elseif ($comment_format == 'auto') { # 'Auto' is for backward compatibility. Before the "comment_format" # was introduced, OKAPI used a weird format in between (it allowed # HTML, but applied nl2br too). $formatted_comment = nl2br($comment); $value_for_text_html_field = 1; } else { $formatted_comment = $comment; # For user-supplied HTML comments, OC sites require us to do # additional HTML purification prior to the insertion into the # database. if (Settings::get('OC_BRANCH') == 'oc.de') { # NOTICE: We are including EXTERNAL OCDE library here! This # code does not belong to OKAPI! $opt['rootpath'] = $GLOBALS['rootpath']; $opt['html_purifier'] = Settings::get('OCDE_HTML_PURIFIER_SETTINGS'); require_once $GLOBALS['rootpath'] . 'lib2/OcHTMLPurifier.class.php'; $purifier = new \OcHTMLPurifier($opt); $formatted_comment = $purifier->purify($formatted_comment); } else { # TODO: Add OCPL HTML filtering. # See https://github.com/opencaching/okapi/issues/412. } $value_for_text_html_field = 1; } if (Settings::get('OC_BRANCH') == 'oc.pl') { # The HTML processing in OCPL code is broken. Effectively, it # will decode < > and & (and maybe other things?) # before display so that text contents may be interpreted as HTML. # We work around this by applying a double-encoding for & < >: $formatted_comment = str_replace("&", "&#38;", $formatted_comment); $formatted_comment = str_replace("<", "&#60;", $formatted_comment); $formatted_comment = str_replace(">", "&#62;", $formatted_comment); # Note: This problem also exists when submitting logs on OCPL websites. # If you e.g. enter "<text>" in the editor, it will get lost. # See https://github.com/opencaching/opencaching-pl/issues/469. } unset($comment); # Prevent bug #367. Start the transaction and lock all the rows of this # (user, cache) pair. In theory, we want to lock even smaller number of # rows here (user, cache, type=1), but this wouldn't work, because there's # no index for this. # # http://stackoverflow.com/questions/17068686/ Db::execute("start transaction"); Db::select_column("\n select 1\n from cache_logs\n where\n user_id = '" . Db::escape_string($request->token->user_id) . "'\n and cache_id = '" . Db::escape_string($cache['internal_id']) . "'\n for update\n "); # Duplicate detection. if ($on_duplicate != 'continue') { # Attempt to find a log entry made by the same user, for the same cache, with # the same date, type, comment, etc. Note, that these are not ALL the fields # we could check, but should work ok in most cases. Also note, that we # DO NOT guarantee that duplicate detection will succeed. If it doesn't, # nothing bad happens (user will just post two similar log entries). # Keep this simple! $duplicate_uuid = Db::select_value("\n select uuid\n from cache_logs\n where\n user_id = '" . Db::escape_string($request->token->user_id) . "'\n and cache_id = '" . Db::escape_string($cache['internal_id']) . "'\n and type = '" . Db::escape_string(Okapi::logtypename2id($logtype)) . "'\n and date = from_unixtime('" . Db::escape_string($when) . "')\n and text = '" . Db::escape_string($formatted_comment) . "'\n " . (Settings::get('OC_BRANCH') == 'oc.pl' ? "and deleted = 0" : "") . "\n limit 1\n "); if ($duplicate_uuid != null) { if ($on_duplicate == 'silent_success') { # Act as if the log has been submitted successfully. return $duplicate_uuid; } elseif ($on_duplicate == 'user_error') { throw new CannotPublishException(_("You have already submitted a log entry with exactly " . "the same contents.")); } } } # Check if already found it (and make sure the user is not the owner). # # OCPL forbids logging 'Found it' or "Didn't find" for an already found cache, # while OCDE allows all kinds of duplicate logs. if (Settings::get('OC_BRANCH') == 'oc.pl' && ($logtype == 'Found it' || $logtype == "Didn't find it")) { $has_already_found_it = Db::select_value("\n select 1\n from cache_logs\n where\n user_id = '" . Db::escape_string($user['internal_id']) . "'\n and cache_id = '" . Db::escape_string($cache['internal_id']) . "'\n and type = '" . Db::escape_string(Okapi::logtypename2id("Found it")) . "'\n and " . (Settings::get('OC_BRANCH') == 'oc.pl' ? "deleted = 0" : "true") . "\n "); if ($has_already_found_it) { throw new CannotPublishException(_("You have already submitted a \"Found it\" log entry once. " . "Now you may submit \"Comments\" only!")); } if ($user['uuid'] == $cache['owner']['uuid']) { throw new CannotPublishException(_("You are the owner of this cache. You may submit " . "\"Comments\" only!")); } } # Check if the user has already rated the cache. BTW: I don't get this one. # If we already know, that the cache was NOT found yet, then HOW could the # user submit a rating for it? Anyway, I will stick to the procedure # found in log.php. On the bright side, it's fail-safe. if ($rating) { $has_already_rated = Db::select_value("\n select 1\n from scores\n where\n user_id = '" . Db::escape_string($user['internal_id']) . "'\n and cache_id = '" . Db::escape_string($cache['internal_id']) . "'\n "); if ($has_already_rated) { throw new CannotPublishException(_("You have already rated this cache once. Your rating " . "cannot be changed.")); } } # If user wants to recommend... if ($recommend) { # Do the same "fail-safety" check as we did for the rating. $already_recommended = Db::select_value("\n select 1\n from cache_rating\n where\n user_id = '" . Db::escape_string($user['internal_id']) . "'\n and cache_id = '" . Db::escape_string($cache['internal_id']) . "'\n "); if ($already_recommended) { throw new CannotPublishException(_("You have already recommended this cache once.")); } # Check the number of recommendations. $founds = $user['caches_found'] + 1; // +1, because he'll find THIS ONE in a moment # Note: caches_found includes the number of attended events (both on # OCDE and OCPL). OCPL does not allow recommending events, but the # number of attended events influences $rcmds_left the same way a # normal "Fount it" log does. $rcmds_left = floor($founds / 10.0) - $user['rcmds_given']; if ($rcmds_left <= 0) { throw new CannotPublishException(_("You don't have any recommendations to give. Find more " . "caches first!")); } } # If user checked the "needs_maintenance(2)" flag for OCPL, we will shuffle things # a little... if (Settings::get('OC_BRANCH') == 'oc.pl' && $needs_maintenance2 == 'true') { # If we're here, then we also know that the "Needs maintenance" log # type is supported by this OC site. However, it's a separate log # type, so we might have to submit two log types together: if ($logtype == 'Comment') { # If user submits a "Comment", we'll just change its type to # "Needs maintenance". Only one log entry will be issued. $logtype = 'Needs maintenance'; $second_logtype = null; $second_formatted_comment = null; } elseif ($logtype == 'Found it') { # If "Found it", then we'll issue two log entries: one "Found # it" with the original comment, and second one "Needs # maintenance" with empty comment. $second_logtype = 'Needs maintenance'; $second_formatted_comment = ""; } elseif ($logtype == "Didn't find it") { # If "Didn't find it", then we'll issue two log entries, but this time # we'll do this the other way around. The first "Didn't find it" entry # will have an empty comment. We will move the comment to the second # "Needs maintenance" log entry. (It's okay for this behavior to change # in the future, but it seems natural to me.) $second_logtype = 'Needs maintenance'; $second_formatted_comment = $formatted_comment; $formatted_comment = ""; } else { if ($logtype == 'Will attend' || $logtype == 'Attended') { # OC branches which allow maintenance logs, still don't allow them on # event caches. throw new CannotPublishException(_("Event caches cannot \"need maintenance\".")); } else { throw new Exception(); } } } else { # User didn't check the "Needs maintenance" flag OR "Needs maintenance" # log type isn't supported by this server. $second_logtype = null; $second_formatted_comment = null; } # Finally! Insert the rows into the log entries table. Update # cache stats and user stats. $log_uuids = array(self::insert_log_row($request->consumer->key, $cache['internal_id'], $user['internal_id'], $logtype, $when, $formatted_comment, $value_for_text_html_field, $needs_maintenance2)); self::increment_cache_stats($cache['internal_id'], $when, $logtype); self::increment_user_stats($user['internal_id'], $logtype); if ($second_logtype != null) { # Reminder: This will only be called for OCPL branch. $log_uuids[] = self::insert_log_row($request->consumer->key, $cache['internal_id'], $user['internal_id'], $second_logtype, $when + 1, $second_formatted_comment, $value_for_text_html_field, 'null'); self::increment_cache_stats($cache['internal_id'], $when + 1, $second_logtype); self::increment_user_stats($user['internal_id'], $second_logtype); } # Save the rating. if ($rating) { # This code will be called for OCPL branch only. Earlier, we made sure, # to set $rating to null, if we're running on OCDE. # OCPL has a little strange way of storing cumulative rating. Instead # of storing the sum of all ratings, OCPL stores the computed average # and update it using multiple floating-point operations. Moreover, # the "score" field in the database is on the -3..3 scale (NOT 1..5), # and the translation made at retrieval time is DIFFERENT than the # one made here (both of them are non-linear). Also, once submitted, # the rating can never be changed. It surely feels quite inconsistent, # but presumably has some deep logic into it. See also here (Polish): # http://wiki.opencaching.pl/index.php/Oceny_skrzynek switch ($rating) { case 1: $db_score = -2.0; break; case 2: $db_score = -0.5; break; case 3: $db_score = 0.7; break; case 4: $db_score = 1.7; break; case 5: $db_score = 3.0; break; default: throw new Exception(); } Db::execute("\n update caches\n set\n score = (\n score*votes + '" . Db::escape_string($db_score) . "'\n ) / (votes + 1),\n votes = votes + 1\n where cache_id = '" . Db::escape_string($cache['internal_id']) . "'\n "); Db::execute("\n insert into scores (user_id, cache_id, score)\n values (\n '" . Db::escape_string($user['internal_id']) . "',\n '" . Db::escape_string($cache['internal_id']) . "',\n '" . Db::escape_string($db_score) . "'\n );\n "); } # Save recommendation. if ($recommend) { if (Db::field_exists('cache_rating', 'rating_date')) { Db::execute("\n insert into cache_rating (user_id, cache_id, rating_date)\n values (\n '" . Db::escape_string($user['internal_id']) . "',\n '" . Db::escape_string($cache['internal_id']) . "',\n from_unixtime('" . Db::escape_string($when) . "')\n );\n "); } else { Db::execute("\n insert into cache_rating (user_id, cache_id)\n values (\n '" . Db::escape_string($user['internal_id']) . "',\n '" . Db::escape_string($cache['internal_id']) . "'\n );\n "); } } # Finalize the transaction. Db::execute("commit"); # We need to delete the copy of stats-picture for this user. Otherwise, # the legacy OC code won't detect that the picture needs to be refreshed. $filepath = Okapi::get_var_dir() . '/images/statpics/statpic' . $user['internal_id'] . '.jpg'; if (file_exists($filepath)) { unlink($filepath); } # Success. Return the uuids. return $log_uuids; }