Beispiel #1
0
 public static function call()
 {
     $langpref = isset($_GET['langpref']) ? $_GET['langpref'] : Settings::get('SITELANG');
     $langprefs = explode("|", $langpref);
     # Determine which user is logged in to OC.
     require_once $GLOBALS['rootpath'] . "okapi/lib/oc_session.php";
     $OC_user_id = OCSession::get_user_id();
     if ($OC_user_id == null) {
         $after_login = "******" . ($langpref != Settings::get('SITELANG') ? "?langpref=" . $langpref : "");
         $login_url = Settings::get('SITE_URL') . "login.php?target=" . urlencode($after_login);
         return new OkapiRedirectResponse($login_url);
     }
     # Get the list of authorized apps.
     $rs = Db::query("\n            select c.`key`, c.name, c.url\n            from\n                okapi_consumers c,\n                okapi_authorizations a\n            where\n                a.user_id = '" . 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;
 }
Beispiel #2
0
 public static function call(OkapiRequest $request)
 {
     $result = array();
     $result['site_url'] = Settings::get('SITE_URL');
     $result['okapi_base_url'] = $result['site_url'] . "okapi/";
     $result['site_name'] = Okapi::get_normalized_site_name();
     $result['okapi_revision'] = Okapi::$revision;
     return Okapi::formatted_response($request, $result);
 }
 public static function call()
 {
     require_once $GLOBALS['rootpath'] . 'okapi/service_runner.php';
     require_once $GLOBALS['rootpath'] . 'okapi/views/menu.inc.php';
     $vars = array('menu' => OkapiMenu::get_menu_html("examples.html"), 'okapi_base_url' => Settings::get('SITE_URL') . "okapi/", 'site_url' => Settings::get('SITE_URL'), 'installations' => OkapiMenu::get_installations(), 'okapi_rev' => Okapi::$version_number, 'site_name' => Okapi::get_normalized_site_name());
     $response = new OkapiHttpResponse();
     $response->content_type = "text/html; charset=utf-8";
     ob_start();
     include 'examples.tpl.php';
     $response->body = ob_get_clean();
     return $response;
 }
 public static function call(OkapiRequest $request)
 {
     $result = array();
     $result['site_url'] = Settings::get('SITE_URL');
     $result['okapi_base_url'] = $result['site_url'] . "okapi/";
     $result['site_name'] = Okapi::get_normalized_site_name();
     $result['okapi_version_number'] = Okapi::$version_number;
     $result['okapi_revision'] = Okapi::$version_number;
     /* Important for backward-compatibility! */
     $result['okapi_git_revision'] = Okapi::$git_revision;
     return Okapi::formatted_response($request, $result);
 }
Beispiel #5
0
 public static function call()
 {
     require_once $GLOBALS['rootpath'] . 'okapi/views/menu.inc.php';
     require_once $GLOBALS['rootpath'] . 'okapi/views/changelog_helper.inc.php';
     $changelog = new Changelog();
     $vars = array('menu' => OkapiMenu::get_menu_html("changelog.html"), 'okapi_base_url' => Settings::get('SITE_URL') . "okapi/", 'site_url' => Settings::get('SITE_URL'), 'installations' => OkapiMenu::get_installations(), 'okapi_rev' => Okapi::$version_number, 'site_name' => Okapi::get_normalized_site_name(), 'changes' => array('unavailable' => $changelog->unavailable_changes, 'available' => $changelog->available_changes));
     $response = new OkapiHttpResponse();
     $response->content_type = "text/html; charset=utf-8";
     ob_start();
     include 'changelog.tpl.php';
     $response->body = ob_get_clean();
     return $response;
 }
Beispiel #6
0
 public static function call(OkapiRequest $request)
 {
     $result = array();
     $result['site_url'] = Settings::get('SITE_URL');
     $result['okapi_base_url'] = Okapi::get_recommended_base_url();
     $result['okapi_base_urls'] = Okapi::get_allowed_base_urls();
     $result['site_name'] = Okapi::get_normalized_site_name();
     $result['okapi_version_number'] = Okapi::$version_number;
     $result['okapi_revision'] = Okapi::$version_number;
     /* Important for backward-compatibility! */
     $result['okapi_git_revision'] = Okapi::$git_revision;
     $result['registration_url'] = Settings::get('REGISTRATION_URL');
     $result['mobile_registration_url'] = Settings::get('MOBILE_REGISTRATION_URL');
     $result['image_max_upload_size'] = Settings::get('IMAGE_MAX_UPLOAD_SIZE');
     $result['image_rcmd_max_pixels'] = Settings::get('IMAGE_MAX_PIXEL_COUNT');
     return Okapi::formatted_response($request, $result);
 }
Beispiel #7
0
 public static function call()
 {
     if (isset($_REQUEST['posted'])) {
         $appname = isset($_REQUEST['appname']) ? $_REQUEST['appname'] : "";
         $appname = trim($appname);
         $appurl = isset($_REQUEST['appurl']) ? $_REQUEST['appurl'] : "";
         $email = isset($_REQUEST['email']) ? $_REQUEST['email'] : "";
         $accepted_terms = isset($_REQUEST['terms']) ? $_REQUEST['terms'] : "";
         $ok = false;
         if (!$appname) {
             $notice = "Please provide a valid application name.";
         } elseif (mb_strlen($appname) > 100) {
             $notice = "Application name should be less than 100 characters long.";
         } elseif (mb_strlen($appurl) > 250) {
             $notice = "Application URL should be less than 250 characters long.";
         } elseif ($appurl && substr($appurl, 0, 7) != "http://" && substr($appurl, 0, 8) != "https://") {
             $notice = "Application homepage URL should start with http(s)://. (Note: this URL is OPTIONAL and it is NOT for OAuth callback.)";
         } elseif (!$email) {
             $notice = "Please provide a valid email address.";
         } elseif (mb_strlen($email) > 70) {
             $notice = "Email address should be less than 70 characters long.";
         } elseif (!$accepted_terms) {
             $notice = "You have to read and accept OKAPI Terms of Use.";
         } else {
             $ok = true;
             Okapi::register_new_consumer($appname, $appurl, $email);
             $notice = "Consumer Key generated successfully.\nCheck your email!";
         }
         $response = new OkapiHttpResponse();
         $response->content_type = "application/json; charset=utf-8";
         $response->body = json_encode(array('ok' => $ok, 'notice' => $notice));
         return $response;
     }
     require_once $GLOBALS['rootpath'] . 'okapi/service_runner.php';
     require_once $GLOBALS['rootpath'] . 'okapi/views/menu.inc.php';
     $vars = array('menu' => OkapiMenu::get_menu_html("signup.html"), 'okapi_base_url' => Settings::get('SITE_URL') . "okapi/", 'site_url' => Settings::get('SITE_URL'), 'site_name' => Okapi::get_normalized_site_name(), 'installations' => OkapiMenu::get_installations(), 'okapi_rev' => Okapi::$version_number, 'data_license_html' => Settings::get('DATA_LICENSE_URL') ? "<a href='" . Settings::get('DATA_LICENSE_URL') . "'>Data License</a>" : "Data License");
     $response = new OkapiHttpResponse();
     $response->content_type = "text/html; charset=utf-8";
     ob_start();
     include 'signup.tpl.php';
     $response->body = ob_get_clean();
     return $response;
 }
 public static function call()
 {
     $token_key = isset($_GET['oauth_token']) ? $_GET['oauth_token'] : '';
     $verifier = isset($_GET['oauth_verifier']) ? $_GET['oauth_verifier'] : '';
     $langpref = isset($_GET['langpref']) ? $_GET['langpref'] : Settings::get('SITELANG');
     $langprefs = explode("|", $langpref);
     $token = Db::select_row("\n            select\n                c.`key` as consumer_key,\n                c.name as consumer_name,\n                c.url as consumer_url,\n                t.verifier\n            from\n                okapi_consumers c,\n                okapi_tokens t\n            where\n                t.`key` = '" . mysql_real_escape_string($token_key) . "'\n                and t.consumer_key = c.`key`\n        ");
     if (!$token) {
         # Probably Request Token has expired or it was already used. We'll
         # just redirect to the Opencaching main page.
         return new OkapiRedirectResponse(Settings::get('SITE_URL'));
     }
     $vars = array('okapi_base_url' => Settings::get('SITE_URL') . "okapi/", 'token' => $token, 'verifier' => $verifier, 'site_name' => Okapi::get_normalized_site_name(), 'site_url' => Settings::get('SITE_URL'), 'site_logo' => Settings::get('SITE_LOGO'));
     $response = new OkapiHttpResponse();
     $response->content_type = "text/html; charset=utf-8";
     ob_start();
     Okapi::gettext_domain_init($langprefs);
     include 'authorized.tpl.php';
     $response->body = ob_get_clean();
     Okapi::gettext_domain_restore();
     return $response;
 }
Beispiel #9
0
 public static function call(OkapiRequest $request)
 {
     # The list of installations is periodically refreshed by contacting OKAPI
     # repository. This method usually displays the cached version of it.
     $cachekey = 'apisrv/installations';
     $backupkey = 'apisrv/installations-backup';
     $results = Cache::get($cachekey);
     if (!$results) {
         # Download the current list of OKAPI servers.
         try {
             $opts = array('http' => array('method' => "GET", 'timeout' => 5.0));
             $context = stream_context_create($opts);
             $xml = file_get_contents("http://opencaching-api.googlecode.com/svn/trunk/etc/installations.xml", false, $context);
             $doc = simplexml_load_string($xml);
             if (!$doc) {
                 throw new ErrorException();
                 # just to get to the catch block
             }
         } catch (ErrorException $e) {
             # Google failed on us. Try to respond with a backup list.
             $results = Cache::get($backupkey);
             if ($results) {
                 Cache::set($cachekey, $results, 12 * 3600);
                 # so to retry no earlier than after 12 hours
                 return Okapi::formatted_response($request, $results);
             }
             # Backup has expired (or have never been cached). If we're on a development
             # server then probably it's okay. In production this SHOULD NOT happen.
             $results = array(array('site_url' => Settings::get('SITE_URL'), 'site_name' => "Unable to retrieve!", 'okapi_base_url' => Settings::get('SITE_URL') . "okapi/"));
             Cache::set($cachekey, $results, 12 * 3600);
             # so to retry no earlier than after 12 hours
             return Okapi::formatted_response($request, $results);
         }
         $results = array();
         $i_was_included = false;
         foreach ($doc->installation as $inst) {
             $site_url = (string) $inst[0]['site_url'];
             if ($inst[0]['okapi_base_url']) {
                 $okapi_base_url = (string) $inst[0]['okapi_base_url'];
             } else {
                 $okapi_base_url = $site_url . "okapi/";
             }
             if ($inst[0]['site_name']) {
                 $site_name = (string) $inst[0]['site_name'];
             } else {
                 $site_name = Okapi::get_normalized_site_name($site_url);
             }
             $results[] = array('site_url' => $site_url, 'site_name' => $site_name, 'okapi_base_url' => $okapi_base_url);
             if ($site_url == Settings::get('SITE_URL')) {
                 $i_was_included = true;
             }
         }
         # If running on a local development installation, then include the local
         # installation URL.
         if (!$i_was_included) {
             $results[] = array('site_url' => Settings::get('SITE_URL'), 'site_name' => "DEVELSITE", 'okapi_base_url' => Settings::get('SITE_URL') . "okapi/");
             # Contact OKAPI developers in order to get added to the official sites list!
         }
         # Cache it for one day. Also, save a backup (valid for 30 days).
         Cache::set($cachekey, $results, 86400);
         Cache::set($backupkey, $results, 86400 * 30);
     }
     return Okapi::formatted_response($request, $results);
 }
Beispiel #10
0
 /**
  * Object to be used for forward-compatibility (see the attributes method).
  */
 public static function get_unknown_placeholder($acode)
 {
     return array('acode' => $acode, 'gc_equivs' => array(), 'internal_id' => null, 'names' => array('en' => "Unknown attribute"), 'descriptions' => array('en' => "This attribute ({$acode}) is unknown at " . Okapi::get_normalized_site_name() . ". It might not exist, or it may be a new attribute, recognized " . "only in newer OKAPI installations. Perhaps " . Okapi::get_normalized_site_name() . " needs to have its OKAPI updated?"), 'is_discontinued' => true);
 }
Beispiel #11
0
 public static function call()
 {
     $token_key = isset($_GET['oauth_token']) ? $_GET['oauth_token'] : '';
     $langpref = isset($_GET['langpref']) ? $_GET['langpref'] : Settings::get('SITELANG');
     $langprefs = explode("|", $langpref);
     $locales = array();
     foreach (Locales::$languages as $lang => $attrs) {
         $locales[$attrs['locale']] = $attrs;
     }
     # Current implementation of the "interactivity" parameter is: If developer
     # wants to "confirm_user", then just log out the current user before we
     # continue.
     $force_relogin = isset($_GET['interactivity']) && $_GET['interactivity'] == 'confirm_user';
     $token = Db::select_row("\n            select\n                t.`key` as `key`,\n                c.`key` as consumer_key,\n                c.name as consumer_name,\n                c.url as consumer_url,\n                t.callback,\n                t.verifier\n            from\n                okapi_consumers c,\n                okapi_tokens t\n            where\n                t.`key` = '" . Db::escape_string($token_key) . "'\n                and t.consumer_key = c.`key`\n                and t.user_id is null\n        ");
     $callback_concat_char = strpos($token['callback'], '?') === false ? "?" : "&";
     if (!$token) {
         # Probably Request Token has expired. This will be usually viewed
         # by the user, who knows nothing on tokens and OAuth. Let's be nice then!
         $vars = array('okapi_base_url' => Settings::get('SITE_URL') . "okapi/", 'token' => $token, 'token_expired' => true, 'site_name' => Okapi::get_normalized_site_name(), 'site_url' => Settings::get('SITE_URL'), 'site_logo' => Settings::get('SITE_LOGO'), 'locales' => $locales);
         $response = new OkapiHttpResponse();
         $response->content_type = "text/html; charset=utf-8";
         ob_start();
         $vars['locale_displayed'] = Okapi::gettext_domain_init($langprefs);
         include 'authorize.tpl.php';
         $response->body = ob_get_clean();
         Okapi::gettext_domain_restore();
         return $response;
     }
     # Determine which user is logged in to OC.
     require_once $GLOBALS['rootpath'] . "okapi/lib/oc_session.php";
     $OC_user_id = OCSession::get_user_id();
     # Ensure a user is logged in (or force re-login).
     if ($force_relogin || $OC_user_id == null) {
         # TODO: confirm_user should first ask the user if he's "the proper one",
         # and then offer to sign in as a different user.
         $login_page = 'login.php?';
         if ($OC_user_id !== null) {
             if (Settings::get('OC_BRANCH') == 'oc.de') {
                 # OCDE login.php?action=logout&target=... will NOT logout and
                 # then redirect to the target, but it will log out, prompt for
                 # login and then redirect to the target after logging in -
                 # that's exactly the relogin that we want.
                 $login_page .= 'action=logout&';
             } else {
                 # OCPL uses REAL MAGIC for session handling. I don't get ANY of it.
                 # The logout.php DOES NOT support the "target" parameter, so we
                 # can't just call it. The only thing that comes to mind is...
                 # Try to destroy EVERYTHING. (This still won't necessarilly work,
                 # because OC may store cookies in separate paths, but hopefully
                 # they won't).
                 if (isset($_SERVER['HTTP_COOKIE'])) {
                     $cookies = explode(';', $_SERVER['HTTP_COOKIE']);
                     foreach ($cookies as $cookie) {
                         $parts = explode('=', $cookie);
                         $name = trim($parts[0]);
                         setcookie($name, '', time() - 1000);
                         setcookie($name, '', time() - 1000, '/');
                         foreach (self::getPossibleCookieDomains() as $domain) {
                             setcookie($name, '', time() - 1000, '/', $domain);
                         }
                     }
                 }
                 # We should be logged out now. Let's login again.
             }
         }
         $after_login = "******" . ($langpref != Settings::get('SITELANG') ? "&langpref=" . $langpref : "");
         $login_url = Settings::get('SITE_URL') . $login_page . "target=" . urlencode($after_login) . "&langpref=" . $langpref;
         return new OkapiRedirectResponse($login_url);
     }
     # Check if this user has already authorized this Consumer. If he did,
     # then we will automatically authorize all subsequent Request Tokens
     # from this Consumer.
     $authorized = Db::select_value("\n            select 1\n            from okapi_authorizations\n            where\n                user_id = '" . Db::escape_string($OC_user_id) . "'\n                and consumer_key = '" . Db::escape_string($token['consumer_key']) . "'\n        ", 0);
     if (!$authorized) {
         if (isset($_POST['authorization_result'])) {
             # Not yet authorized, but user have just submitted the authorization form.
             # WRTODO: CSRF protection
             if ($_POST['authorization_result'] == 'granted') {
                 Db::execute("\n                        insert ignore into okapi_authorizations (consumer_key, user_id)\n                        values (\n                            '" . Db::escape_string($token['consumer_key']) . "',\n                            '" . Db::escape_string($OC_user_id) . "'\n                        );\n                    ");
                 $authorized = true;
             } else {
                 # User denied access. Nothing sensible to do now. Will try to report
                 # back to the Consumer application with an error.
                 if ($token['callback']) {
                     return new OkapiRedirectResponse($token['callback'] . $callback_concat_char . "error=access_denied" . "&oauth_token=" . $token['key']);
                 } else {
                     # Consumer did not provide a callback URL (oauth_callback=oob).
                     # We'll have to redirect to the Opencaching main page then...
                     return new OkapiRedirectResponse(Settings::get('SITE_URL') . "index.php");
                 }
             }
         } else {
             # Not yet authorized. Display an authorization request.
             $vars = array('okapi_base_url' => Settings::get('SITE_URL') . "okapi/", 'token' => $token, 'site_name' => Okapi::get_normalized_site_name(), 'site_url' => Settings::get('SITE_URL'), 'site_logo' => Settings::get('SITE_LOGO'), 'locales' => $locales);
             $response = new OkapiHttpResponse();
             $response->content_type = "text/html; charset=utf-8";
             ob_start();
             $vars['locale_displayed'] = Okapi::gettext_domain_init($langprefs);
             include 'authorize.tpl.php';
             $response->body = ob_get_clean();
             Okapi::gettext_domain_restore();
             return $response;
         }
     }
     # User granted access. Now we can authorize the Request Token.
     Db::execute("\n            update okapi_tokens\n            set user_id = '" . Db::escape_string($OC_user_id) . "'\n            where `key` = '" . Db::escape_string($token_key) . "';\n        ");
     # Redirect to the callback_url.
     if ($token['callback']) {
         return new OkapiRedirectResponse($token['callback'] . $callback_concat_char . "oauth_token=" . $token_key . "&oauth_verifier=" . $token['verifier']);
     } else {
         # Consumer did not provide a callback URL (probably the user is using a desktop
         # or mobile application). We'll just have to display the verifier to the user.
         return new OkapiRedirectResponse(Settings::get('SITE_URL') . "okapi/apps/authorized?oauth_token=" . $token_key . "&oauth_verifier=" . $token['verifier'] . "&langpref=" . $langpref);
     }
 }
 /**
  * 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);
 }
Beispiel #13
0
 /**
  * Return attribution note for the given geocache.
  *
  * The $lang parameter identifies the language of the cache description
  * to which the attribution note will be appended to (one cache may
  * have descriptions in multiple languages!).
  *
  * The $langpref parameter is *an array* of language preferences
  * extracted from the langpref parameter passed to the method by the
  * OKAPI Consumer.
  *
  * Both values ($lang and $langpref) will be taken into account when
  * generating the attribution note, but $lang will have a higher
  * priority than $langpref (we don't want to mix the languages in the
  * descriptions if we don't have to).
  *
  * $owner is in object describing the user, it has the same format as
  * defined in "geocache" method specs (see the "owner" field).
  *
  * The $type is either "full" or "static". Full attributions may contain
  * dates and are not suitable for the replicate module. Static attributions
  * don't change that frequently.
  */
 public static function get_cache_attribution_note($cache_id, $lang, array $langpref, $owner, $type)
 {
     $site_url = Settings::get('SITE_URL');
     $site_name = Okapi::get_normalized_site_name();
     $cache_url = $site_url . "viewcache.php?cacheid={$cache_id}";
     Okapi::gettext_domain_init(array_merge(array($lang), $langpref));
     if (Settings::get('OC_BRANCH') == 'oc.pl') {
         # This does not vary on $type (yet).
         $note = sprintf(_("This <a href='%s'>geocache</a> description comes from the <a href='%s'>%s</a> site."), $cache_url, $site_url, $site_name);
     } else {
         # OC.de wants the tld in lowercase here
         $site_name = ucfirst(strtolower($site_name));
         if ($type == 'full') {
             $note = sprintf(_("&copy; <a href='%s'>%s</a>, <a href='%s'>%s</a>, " . "<a href='http://creativecommons.org/licenses/by-nc-nd/3.0/de/deed.en'>CC-BY-NC-ND</a>, " . "as of %s; all log entries &copy; their authors"), $owner['profile_url'], $owner['username'], $cache_url, $site_name, strftime('%x'));
         } elseif ($type == 'static') {
             $note = sprintf(_("&copy; <a href='%s'>%s</a>, <a href='%s'>%s</a>, " . "<a href='http://creativecommons.org/licenses/by-nc-nd/3.0/de/deed.en'>CC-BY-NC-ND</a>; " . "all log entries &copy; their authors"), $owner['profile_url'], $owner['username'], $cache_url, $site_name);
         }
     }
     Okapi::gettext_domain_restore();
     return $note;
 }
Beispiel #14
0
 /**
  * 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 "&lt;b&gt;" 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("&amp;", "&amp;#38;", $formatted_comment);
             $formatted_comment = str_replace("&lt;", "&amp;#60;", $formatted_comment);
             $formatted_comment = str_replace("&gt;", "&amp;#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;
 }
Beispiel #15
0
 /**
  * Edit an log entry image and return its (new) position.
  * Throws CannotPublishException or BadRequest on errors.
  */
 private static function _call(OkapiRequest $request)
 {
     # Developers! Please notice the fundamental difference between throwing
     # CannotPublishException and the "standard" BadRequest/InvalidParam
     # exceptions. CannotPublishException will be caught by the service's
     # call() function and returns a message to be displayed to the user.
     require_once 'log_images_common.inc.php';
     # validate the 'image_uuid' parameter
     list($image_uuid, $log_internal_id) = LogImagesCommon::validate_image_uuid($request);
     # validate the 'caption', 'is_spoiler' and 'position' parameters
     $caption = $request->get_parameter('caption');
     if ($caption !== null && $caption == '') {
         throw new CannotPublishException(sprintf(_("Please enter an image caption."), Okapi::get_normalized_site_name()));
     }
     $is_spoiler = $request->get_parameter('is_spoiler');
     if ($is_spoiler !== null) {
         if (!in_array($is_spoiler, array('true', 'false'))) {
             throw new InvalidParam('is_spoiler');
         }
     }
     $position = LogImagesCommon::validate_position($request);
     if ($caption === null && $is_spoiler === null && $position === null) {
         # If no-params were allowed, what would be the success message?
         # It's more reasonable to assume that this was a developer's error.
         throw new BadRequest("At least one of the parameters 'caption', 'is_spoiler' and 'position' must be supplied");
     }
     $image_uuid_escaped = Db::escape_string($image_uuid);
     $log_entry_modified = false;
     # update caption
     if ($caption !== null) {
         Db::execute("\n                update pictures\n                set title = '" . Db::escape_string($caption) . "'\n                where uuid = '" . $image_uuid_escaped . "'\n            ");
         $log_entry_modified = true;
     }
     # update spoiler flag
     if ($is_spoiler !== null) {
         Db::execute("\n                update pictures\n                set spoiler = " . ($is_spoiler == 'true' ? 1 : 0) . "\n                where uuid = '" . $image_uuid_escaped . "'\n            ");
         $log_entry_modified = true;
     }
     # update position
     if ($position !== null) {
         if (Settings::get('OC_BRANCH') == 'oc.pl') {
             # OCPL as no arbitrary log picture ordering => ignore position parameter
             # and return the picture's current position.
             $image_uuids = Db::select_column("\n                    select uuid from pictures\n                    where object_type = 1 and object_id = '" . Db::escape_string($log_internal_id) . "'\n                    order by date_created\n                ");
             $position = array_search($image_uuid, $image_uuids);
         } else {
             list($position, $seq) = LogImagesCommon::prepare_position($log_internal_id, $position, 0);
             # For OCDE the pictures table is write locked now.
             $old_seq = DB::select_value("\n                    select seq from pictures where uuid = '" . $image_uuid_escaped . "'\n                ");
             if ($seq != $old_seq) {
                 # First move the edited picture to the end, to make space for rotating.
                 # Remember that we have no transactions at OC.de. If something goes wrong,
                 # the image will stay at the end of the list.
                 $max_seq = Db::select_value("\n                        select max(seq)\n                        from pictures\n                        where object_type = 1 and object_id = '" . Db::escape_string($log_internal_id) . "'\n                    ");
                 Db::query("\n                        update pictures\n                        set seq = '" . Db::escape_string($max_seq + 1) . "'\n                        where uuid = '" . $image_uuid_escaped . "'\n                    ");
                 # now move the pictures inbetween
                 if ($seq < $old_seq) {
                     Db::execute("\n                            update pictures\n                            set seq = seq + 1\n                            where\n                                object_type = 1\n                                and object_id = '" . Db::escape_string($log_internal_id) . "'\n                                and seq >= '" . Db::escape_string($seq) . "'\n                                and seq < '" . Db::escape_string($old_seq) . "'\n                            order by seq desc\n                        ");
                 } else {
                     Db::execute("\n                            update pictures\n                            set seq = seq - 1\n                            where\n                                object_type = 1\n                                and object_id = '" . Db::escape_string($log_internal_id) . "'\n                                and seq <= '" . Db::escape_string($seq) . "'\n                                and seq > '" . Db::escape_string($old_seq) . "'\n                            order by seq asc\n                        ");
                 }
                 # and finally move the edited picture into place
                 Db::query("\n                        update pictures\n                        set seq = '" . Db::escape_string($seq) . "'\n                        where uuid = '" . $image_uuid_escaped . "'\n                    ");
             }
             Db::execute('unlock tables');
             $log_entry_modified = true;
         }
     }
     if (Settings::get('OC_BRANCH') == 'oc.pl' && $log_entry_modified) {
         # OCDE touches the log entry via trigger, OCPL needs an explicit update.
         # This will also update okapi_syncbase.
         Db::query("\n                update cache_logs\n                set last_modified = NOW()\n                where id = '" . Db::escape_string($log_internal_id) . "'\n            ");
         # OCPL code currently does not update pictures.last_modified when
         # editing, but that is a bug, see
         # https://github.com/opencaching/opencaching-pl/issues/341.
     }
     return $position;
 }
Beispiel #16
0
 /**
  * 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);
 }
Beispiel #17
0
 public function __construct($options)
 {
     Okapi::init_internals();
     $this->init_request();
     #
     # Parsing options.
     #
     $DEBUG_AS_USERNAME = null;
     foreach ($options as $key => $value) {
         switch ($key) {
             case 'min_auth_level':
                 if (!in_array($value, array(0, 1, 2, 3))) {
                     throw new Exception("'min_auth_level' option has invalid value: {$value}");
                 }
                 $this->opt_min_auth_level = $value;
                 break;
             case 'token_type':
                 if (!in_array($value, array("request", "access"))) {
                     throw new Exception("'token_type' option has invalid value: {$value}");
                 }
                 $this->opt_token_type = $value;
                 break;
             case 'DEBUG_AS_USERNAME':
                 $DEBUG_AS_USERNAME = $value;
                 break;
             default:
                 throw new Exception("Unknown option: {$key}");
                 break;
         }
     }
     if ($this->opt_min_auth_level === null) {
         throw new Exception("Required 'min_auth_level' option is missing.");
     }
     if ($DEBUG_AS_USERNAME != null) {
         # Enables debugging Level 2 and Level 3 methods. Should not be committed
         # at any time! If run on production server, make it an error.
         if (!Settings::get('DEBUG')) {
             throw new Exception("Attempted to use DEBUG_AS_USERNAME in " . "non-debug environment. Accidental commit?");
         }
         # Lower required authentication to Level 0, to pass the checks.
         $this->opt_min_auth_level = 0;
     }
     #
     # Let's see if the request is signed. If it is, verify the signature.
     # It it's not, check if it isn't against the rules defined in the $options.
     #
     if ($this->get_parameter('oauth_signature')) {
         # User is using OAuth.
         # Check for duplicate keys in the parameters. (Datastore doesn't
         # do that on its own, it caused vague server errors - issue #307.)
         $this->get_parameter('oauth_consumer');
         $this->get_parameter('oauth_version');
         $this->get_parameter('oauth_token');
         $this->get_parameter('oauth_nonce');
         # Verify OAuth request.
         list($this->consumer, $this->token) = Okapi::$server->verify_request2($this->request, $this->opt_token_type, $this->opt_min_auth_level == 3);
         if ($this->get_parameter('consumer_key') && $this->get_parameter('consumer_key') != $this->get_parameter('oauth_consumer_key')) {
             throw new BadRequest("Inproper mixing of authentication types. You used both 'consumer_key' " . "and 'oauth_consumer_key' parameters (Level 1 and Level 2), but they do not match with " . "each other. Were you trying to hack me? ;)");
         }
         if ($this->opt_min_auth_level == 3 && !$this->token) {
             throw new BadRequest("This method requires a valid Token to be included (Level 3 " . "Authentication). You didn't provide one.");
         }
     } else {
         if ($this->opt_min_auth_level >= 2) {
             throw new BadRequest("This method requires OAuth signature (Level " . $this->opt_min_auth_level . " Authentication). You didn't sign your request.");
         } else {
             $consumer_key = $this->get_parameter('consumer_key');
             if ($consumer_key) {
                 $this->consumer = Okapi::$data_store->lookup_consumer($consumer_key);
                 if (!$this->consumer) {
                     throw new InvalidParam('consumer_key', "Consumer does not exist.");
                 }
             }
             if ($this->opt_min_auth_level == 1 && !$this->consumer) {
                 throw new BadRequest("This method requires the 'consumer_key' argument (Level 1 " . "Authentication). You didn't provide one.");
             }
         }
     }
     if (is_object($this->consumer) && $this->consumer->hasFlag(OkapiConsumer::FLAG_KEY_REVOKED)) {
         throw new InvalidParam('consumer_key', "Your application was denied access to the " . Okapi::get_normalized_site_name() . " site " . "(this consumer key has been revoked).");
     }
     if (is_object($this->consumer) && $this->consumer->hasFlag(OkapiConsumer::FLAG_SKIP_LIMITS)) {
         $this->skip_limits = true;
     }
     #
     # Prevent developers from accessing request parameters with PHP globals.
     # Remember, that OKAPI requests can be nested within other OKAPI requests!
     # Search the code for "new OkapiInternalRequest" to see examples.
     #
     $_GET = $_POST = $_REQUEST = null;
     # When debugging, simulate as if been run using a proper Level 3 Authentication.
     if ($DEBUG_AS_USERNAME != null) {
         # Note, that this will override any other valid authentication the
         # developer might have issued.
         $debug_user_id = Db::select_value("select user_id from user where username='******'DEBUG_AS_USERNAME']) . "'");
         if ($debug_user_id == null) {
             throw new Exception("Invalid user name in DEBUG_AS_USERNAME: '******'DEBUG_AS_USERNAME'] . "'");
         }
         $this->consumer = new OkapiDebugConsumer();
         $this->token = new OkapiDebugAccessToken($debug_user_id);
     }
     # Read the ETag.
     if (isset($_SERVER['HTTP_IF_NONE_MATCH'])) {
         $this->etag = $_SERVER['HTTP_IF_NONE_MATCH'];
     }
 }
Beispiel #18
0
 /**
  * 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 &lt; &gt; and &amp; (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("&amp;", "&amp;#38;", $formatted_comment);
         $formatted_comment = str_replace("&lt;", "&amp;#60;", $formatted_comment);
         $formatted_comment = str_replace("&gt;", "&amp;#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;
 }