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; }
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); }
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; }
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); }
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; }
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); }
/** * 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); }
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); }
/** * 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(_("© <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 © their authors"), $owner['profile_url'], $owner['username'], $cache_url, $site_name, strftime('%x')); } elseif ($type == 'static') { $note = sprintf(_("© <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 © their authors"), $owner['profile_url'], $owner['username'], $cache_url, $site_name); } } Okapi::gettext_domain_restore(); return $note; }
/** * Publish a new log entry and return log entry uuid. Throws * CannotPublishException or BadRequest on errors. */ private static function _call(OkapiRequest $request) { # Developers! Please notice the fundamental difference between throwing # CannotPublishException and standard BadRequest/InvalidParam exceptions! # Notice, that this is "_call" method, not the usual "call" (see below # for "call"). $cache_code = $request->get_parameter('cache_code'); if (!$cache_code) { throw new ParamMissing('cache_code'); } $logtype = $request->get_parameter('logtype'); if (!$logtype) { throw new ParamMissing('logtype'); } if (!in_array($logtype, array('Found it', "Didn't find it", 'Comment', 'Will attend', 'Attended'))) { throw new InvalidParam('logtype', "'{$logtype}' in not a valid logtype code."); } $comment = $request->get_parameter('comment'); if (!$comment) { $comment = ""; } $comment_format = $request->get_parameter('comment_format'); if (!$comment_format) { $comment_format = "auto"; } if (!in_array($comment_format, array('auto', 'html', 'plaintext'))) { throw new InvalidParam('comment_format', $comment_format); } $tmp = $request->get_parameter('when'); if ($tmp) { $when = strtotime($tmp); if (!$when) { throw new InvalidParam('when', "'{$tmp}' is not in a valid format or is not a valid date."); } if ($when > time() + 5 * 60) { throw new CannotPublishException(_("You are trying to publish a log entry with a date in future. " . "Cache log entries are allowed to be published in the past, but NOT in the future.")); } } else { $when = time(); } $on_duplicate = $request->get_parameter('on_duplicate'); if (!$on_duplicate) { $on_duplicate = "silent_success"; } if (!in_array($on_duplicate, array('silent_success', 'user_error', 'continue'))) { throw new InvalidParam('on_duplicate', "Unknown option: '{$on_duplicate}'."); } $rating = $request->get_parameter('rating'); if ($rating !== null && !in_array($rating, array(1, 2, 3, 4, 5))) { throw new InvalidParam('rating', "If present, it must be an integer in the 1..5 scale."); } if ($rating && $logtype != 'Found it' && $logtype != 'Attended') { throw new BadRequest("Rating is allowed only for 'Found it' and 'Attended' logtypes."); } if ($rating !== null && Settings::get('OC_BRANCH') == 'oc.de') { # We will remove the rating request and change the success message # (which will be returned IF the rest of the query will meet all the # requirements). self::$success_message .= " " . sprintf(_("However, your cache rating was ignored, because %s does not have a rating system."), Okapi::get_normalized_site_name()); $rating = null; } $recommend = $request->get_parameter('recommend'); if (!$recommend) { $recommend = 'false'; } if (!in_array($recommend, array('true', 'false'))) { throw new InvalidParam('recommend', "Unknown option: '{$recommend}'."); } $recommend = $recommend == 'true'; if ($recommend && $logtype != 'Found it') { if ($logtype != 'Attended') { throw new BadRequest("Recommending is allowed only for 'Found it' and 'Attended' logs."); } else { if (Settings::get('OC_BRANCH') == 'oc.pl') { # We will remove the recommendation request and change the success message # (which will be returned IF the rest of the query will meet all the # requirements). self::$success_message .= " " . sprintf(_("However, your cache recommendation was ignored, because %s does not allow recommending event caches."), Okapi::get_normalized_site_name()); $recommend = null; } } } $needs_maintenance = $request->get_parameter('needs_maintenance'); if (!$needs_maintenance) { $needs_maintenance = 'false'; } if (!in_array($needs_maintenance, array('true', 'false'))) { throw new InvalidParam('needs_maintenance', "Unknown option: '{$needs_maintenance}'."); } $needs_maintenance = $needs_maintenance == 'true'; if ($needs_maintenance && !Settings::get('SUPPORTS_LOGTYPE_NEEDS_MAINTENANCE')) { # If not supported, just ignore it. self::$success_message .= " " . sprintf(_("However, your \"needs maintenance\" flag was ignored, because %s does not support this feature."), Okapi::get_normalized_site_name()); $needs_maintenance = false; } # Check if cache exists and retrieve cache internal ID (this will throw # a proper exception on invalid cache_code). Also, get the user object. $cache = OkapiServiceRunner::call('services/caches/geocache', new OkapiInternalRequest($request->consumer, null, array('cache_code' => $cache_code, 'fields' => 'internal_id|status|owner|type|req_passwd'))); $user = OkapiServiceRunner::call('services/users/by_internal_id', new OkapiInternalRequest($request->consumer, $request->token, array('internal_id' => $request->token->user_id, 'fields' => 'is_admin|uuid|internal_id|caches_found|rcmds_given'))); # Various integrity checks. if ($cache['type'] == 'Event') { if (!in_array($logtype, array('Will attend', 'Attended', 'Comment'))) { throw new CannotPublishException(_('This cache is an Event cache. You cannot "Find" it (but you can attend it, or comment on it)!')); } } else { if (in_array($logtype, array('Will attend', 'Attended'))) { throw new CannotPublishException(_('This cache is NOT an Event cache. You cannot "Attend" it (but you can find it, or comment on it)!')); } else { if (!in_array($logtype, array('Found it', "Didn't find it", 'Comment'))) { throw new Exception("Unknown log entry - should be documented here."); } } } if ($logtype == 'Comment' && strlen(trim($comment)) == 0) { throw new CannotPublishException(_("Your have to supply some text for your comment.")); } # Password check. if (($logtype == 'Found it' || $logtype == 'Attended') && $cache['req_passwd']) { $valid_password = Db::select_value("\n select logpw\n from caches\n where cache_id = '" . mysql_real_escape_string($cache['internal_id']) . "'\n "); $supplied_password = $request->get_parameter('password'); if (!$supplied_password) { throw new CannotPublishException(_("This cache requires a password. You didn't provide one!")); } if (strtolower($supplied_password) != strtolower($valid_password)) { throw new CannotPublishException(_("Invalid password!")); } } # Prepare our comment to be inserted into the database. This may require # some reformatting which depends on the current OC installation. if (Settings::get('OC_BRANCH') == 'oc.de') { # OCDE stores all comments in HTML format, while the 'text_html' field # indicates their *original* format as delivered by the user. This # allows processing the 'text' field contents without caring about the # original format, while still being able to re-create the comment in # its original form. It requires us to HTML-encode plaintext comments # and to indicate this by setting 'html_text' to FALSE. # # For user-supplied HTML comments, OCDE requires us to do additional # HTML purification prior to the insertion into the database. if ($comment_format == 'plaintext') { $formatted_comment = htmlspecialchars($comment, ENT_QUOTES); $formatted_comment = nl2br($formatted_comment); $value_for_text_html_field = 0; } else { if ($comment_format == 'auto') { # 'Auto' is for backward compatibility. Before the "comment_format" # was introduced, OKAPI used a weird format in between (it allowed # HTML, but applied nl2br too). $formatted_comment = nl2br($comment); } else { $formatted_comment = $comment; } # NOTICE: We are including EXTERNAL OCDE library here! This # code does not belong to OKAPI! $opt['rootpath'] = $GLOBALS['rootpath']; $opt['html_purifier'] = Settings::get('OCDE_HTML_PURIFIER_SETTINGS'); require_once $GLOBALS['rootpath'] . 'lib2/OcHTMLPurifier.class.php'; $purifier = new \OcHTMLPurifier($opt); $formatted_comment = $purifier->purify($formatted_comment); $value_for_text_html_field = 1; } } else { # OCPL is even weirder. It also stores HTML-lized comments in the database # (it doesn't really matter if 'text_html' field is set to FALSE). OKAPI must # save it in HTML either way. However, escaping plain-text doesn't work! # If we put "<b>" in, it still gets converted to "<b>" before display! # NONE of this process is documented within OCPL code. OKAPI uses a dirty # "hack" to save PLAINTEXT comments (let us hope the hack will remain valid). # # OCPL doesn't require HTML purification prior to the database insertion. # HTML seems to be purified dynamically, before it is displayed. if ($comment_format == 'plaintext') { $formatted_comment = htmlspecialchars($comment, ENT_QUOTES); $formatted_comment = nl2br($formatted_comment); $formatted_comment = str_replace("&", "&#38;", $formatted_comment); $formatted_comment = str_replace("<", "&#60;", $formatted_comment); $formatted_comment = str_replace(">", "&#62;", $formatted_comment); $value_for_text_html_field = 0; // WRTODO: get rid of } elseif ($comment_format == 'auto') { $formatted_comment = nl2br($comment); $value_for_text_html_field = 1; } else { $formatted_comment = $comment; $value_for_text_html_field = 1; } } unset($comment); # Duplicate detection. if ($on_duplicate != 'continue') { # Attempt to find a log entry made by the same user, for the same cache, with # the same date, type, comment, etc. Note, that these are not ALL the fields # we could check, but should work ok in most cases. Also note, that we # DO NOT guarantee that duplicate detection will succeed. If it doesn't, # nothing bad happens (user will just post two similar log entries). # Keep this simple! $duplicate_uuid = Db::select_value("\n select uuid\n from cache_logs\n where\n user_id = '" . mysql_real_escape_string($request->token->user_id) . "'\n and cache_id = '" . mysql_real_escape_string($cache['internal_id']) . "'\n and type = '" . mysql_real_escape_string(Okapi::logtypename2id($logtype)) . "'\n and date = from_unixtime('" . mysql_real_escape_string($when) . "')\n and text = '" . mysql_real_escape_string($formatted_comment) . "'\n " . (Settings::get('OC_BRANCH') == 'oc.pl' ? "and deleted = 0" : "") . "\n limit 1\n "); if ($duplicate_uuid != null) { if ($on_duplicate == 'silent_success') { # Act as if the log has been submitted successfully. return $duplicate_uuid; } elseif ($on_duplicate == 'user_error') { throw new CannotPublishException(_("You have already submitted a log entry with exactly the same contents.")); } } } # Check if already found it (and make sure the user is not the owner). # # OCPL forbids logging 'Found it' or "Didn't find" for an already found cache, # while OCDE allows all kinds of duplicate logs. if (Settings::get('OC_BRANCH') == 'oc.pl' && ($logtype == 'Found it' || $logtype == "Didn't find it")) { $has_already_found_it = Db::select_value("\n select 1\n from cache_logs\n where\n user_id = '" . mysql_real_escape_string($user['internal_id']) . "'\n and cache_id = '" . mysql_real_escape_string($cache['internal_id']) . "'\n and type = '" . mysql_real_escape_string(Okapi::logtypename2id("Found it")) . "'\n and " . (Settings::get('OC_BRANCH') == 'oc.pl' ? "deleted = 0" : "true") . "\n "); if ($has_already_found_it) { throw new CannotPublishException(_("You have already submitted a \"Found it\" log entry once. Now you may submit \"Comments\" only!")); } if ($user['uuid'] == $cache['owner']['uuid']) { throw new CannotPublishException(_("You are the owner of this cache. You may submit \"Comments\" only!")); } } # Check if the user has already rated the cache. BTW: I don't get this one. # If we already know, that the cache was NOT found yet, then HOW could the # user submit a rating for it? Anyway, I will stick to the procedure # found in log.php. On the bright side, it's fail-safe. if ($rating) { $has_already_rated = Db::select_value("\n select 1\n from scores\n where\n user_id = '" . mysql_real_escape_string($user['internal_id']) . "'\n and cache_id = '" . mysql_real_escape_string($cache['internal_id']) . "'\n "); if ($has_already_rated) { throw new CannotPublishException(_("You have already rated this cache once. Your rating cannot be changed.")); } } # If user wants to recommend... if ($recommend) { # Do the same "fail-safety" check as we did for the rating. $already_recommended = Db::select_value("\n select 1\n from cache_rating\n where\n user_id = '" . mysql_real_escape_string($user['internal_id']) . "'\n and cache_id = '" . mysql_real_escape_string($cache['internal_id']) . "'\n "); if ($already_recommended) { throw new CannotPublishException(_("You have already recommended this cache once.")); } # Check the number of recommendations. $founds = $user['caches_found'] + 1; // +1, because he'll find THIS ONE in a moment, right? # Note: caches_found includes event attendance on both, OCDE and OCPL. # Though OCPL does not allow recommending events, for each 10 event # attendances the user may recommend a non-event cache. $rcmds_left = floor($founds / 10.0) - $user['rcmds_given']; if ($rcmds_left <= 0) { throw new CannotPublishException(_("You don't have any recommendations to give. Find more caches first!")); } } # If user checked the "needs_maintenance" flag, we will shuffle things a little... if ($needs_maintenance) { # If we're here, then we also know that the "Needs maintenance" log type is supported # by this OC site. However, it's a separate log type, so we might have to submit # two log types together: if ($logtype == 'Comment') { # If user submits a "Comment", we'll just change its type to "Needs maintenance". # Only one log entry will be issued. $logtype = 'Needs maintenance'; $second_logtype = null; $second_formatted_comment = null; } elseif ($logtype == 'Found it') { # If "Found it", then we'll issue two log entries: one "Found it" with the # original comment, and second one "Needs maintenance" with empty comment. $second_logtype = 'Needs maintenance'; $second_formatted_comment = ""; } elseif ($logtype == "Didn't find it") { # If "Didn't find it", then we'll issue two log entries, but this time # we'll do this the other way around. The first "Didn't find it" entry # will have an empty comment. We will move the comment to the second # "Needs maintenance" log entry. (It's okay for this behavior to change # in the future, but it seems natural to me.) $second_logtype = 'Needs maintenance'; $second_formatted_comment = $formatted_comment; $formatted_comment = ""; } else { if ($logtype == 'Will attend' || $logtype == 'Attended') { # OC branches which know maintenance logs do not allow them on event caches. throw new CannotPublishException(_("Event caches cannot \"need maintenance\".")); } else { throw new Exception(); } } } else { # User didn't check the "Needs maintenance" flag OR "Needs maintenance" log type # isn't supported by this server. $second_logtype = null; $second_formatted_comment = null; } # Finally! Insert the rows into the log entries table. Update # cache stats and user stats. $log_uuid = self::insert_log_row($request->consumer->key, $cache['internal_id'], $user['internal_id'], $logtype, $when, $formatted_comment, $value_for_text_html_field); self::increment_cache_stats($cache['internal_id'], $when, $logtype); self::increment_user_stats($user['internal_id'], $logtype); if ($second_logtype != null) { # Reminder: This will never be called while SUPPORTS_LOGTYPE_NEEDS_MAINTENANCE is off. self::insert_log_row($request->consumer->key, $cache['internal_id'], $user['internal_id'], $second_logtype, $when + 1, $second_formatted_comment, $value_for_text_html_field); self::increment_cache_stats($cache['internal_id'], $when + 1, $second_logtype); self::increment_user_stats($user['internal_id'], $second_logtype); } # Save the rating. if ($rating) { # This code will be called for OCPL branch only. Earlier, we made sure, # to set $rating to null, if we're running on OCDE. # OCPL has a little strange way of storing cumulative rating. Instead # of storing the sum of all ratings, OCPL stores the computed average # and update it using multiple floating-point operations. Moreover, # the "score" field in the database is on the -3..3 scale (NOT 1..5), # and the translation made at retrieval time is DIFFERENT than the # one made here (both of them are non-linear). Also, once submitted, # the rating can never be changed. It surely feels quite inconsistent, # but presumably has some deep logic into it. See also here (Polish): # http://wiki.opencaching.pl/index.php/Oceny_skrzynek switch ($rating) { case 1: $db_score = -2.0; break; case 2: $db_score = -0.5; break; case 3: $db_score = 0.7; break; case 4: $db_score = 1.7; break; case 5: $db_score = 3.0; break; default: throw new Exception(); } Db::execute("\n update caches\n set\n score = (score*votes + '" . mysql_real_escape_string($db_score) . "')/(votes + 1),\n votes = votes + 1\n where cache_id = '" . mysql_real_escape_string($cache['internal_id']) . "'\n "); Db::execute("\n insert into scores (user_id, cache_id, score)\n values (\n '" . mysql_real_escape_string($user['internal_id']) . "',\n '" . mysql_real_escape_string($cache['internal_id']) . "',\n '" . mysql_real_escape_string($db_score) . "'\n );\n "); } # Save recommendation. if ($recommend) { if (Db::field_exists('cache_rating', 'rating_date')) { Db::execute("\n insert into cache_rating (user_id, cache_id, rating_date)\n values (\n '" . mysql_real_escape_string($user['internal_id']) . "',\n '" . mysql_real_escape_string($cache['internal_id']) . "',\n from_unixtime('" . mysql_real_escape_string($when) . "')\n );\n "); } else { Db::execute("\n insert into cache_rating (user_id, cache_id)\n values (\n '" . mysql_real_escape_string($user['internal_id']) . "',\n '" . mysql_real_escape_string($cache['internal_id']) . "'\n );\n "); } } # We need to delete the copy of stats-picture for this user. Otherwise, # the legacy OC code won't detect that the picture needs to be refreshed. $filepath = Okapi::get_var_dir() . '/images/statpics/statpic' . $user['internal_id'] . '.jpg'; if (file_exists($filepath)) { unlink($filepath); } # Success. Return the uuid. return $log_uuid; }
/** * 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; }
/** * 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); }
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']; } }
/** * Publish a new log entry and return log entry uuid. Throws * CannotPublishException or BadRequest on errors. */ private static function _call(OkapiRequest $request) { # Developers! Please notice the fundamental difference between throwing # CannotPublishException and the "standard" BadRequest/InvalidParam # exceptions. You're reading the "_call" method now (see below for # "call"). $cache_code = $request->get_parameter('cache_code'); if (!$cache_code) { throw new ParamMissing('cache_code'); } $logtype = $request->get_parameter('logtype'); if (!$logtype) { throw new ParamMissing('logtype'); } if (!in_array($logtype, array('Found it', "Didn't find it", 'Comment', 'Will attend', 'Attended'))) { throw new InvalidParam('logtype', "'{$logtype}' in not a valid logtype code."); } $comment = $request->get_parameter('comment'); if (!$comment) { $comment = ""; } $comment_format = $request->get_parameter('comment_format'); if (!$comment_format) { $comment_format = "auto"; } if (!in_array($comment_format, array('auto', 'html', 'plaintext'))) { throw new InvalidParam('comment_format', $comment_format); } $tmp = $request->get_parameter('when'); if ($tmp) { $when = strtotime($tmp); if ($when < 1) { throw new InvalidParam('when', "'{$tmp}' is not in a valid format or is not a valid date."); } if ($when > time() + 5 * 60) { throw new CannotPublishException(_("You are trying to publish a log entry with a date in " . "future. Cache log entries are allowed to be published in " . "the past, but NOT in the future.")); } } else { $when = time(); } $on_duplicate = $request->get_parameter('on_duplicate'); if (!$on_duplicate) { $on_duplicate = "silent_success"; } if (!in_array($on_duplicate, array('silent_success', 'user_error', 'continue'))) { throw new InvalidParam('on_duplicate', "Unknown option: '{$on_duplicate}'."); } $rating = $request->get_parameter('rating'); if ($rating !== null && !in_array($rating, array(1, 2, 3, 4, 5))) { throw new InvalidParam('rating', "If present, it must be an integer in the 1..5 scale."); } if ($rating && $logtype != 'Found it' && $logtype != 'Attended') { throw new BadRequest("Rating is allowed only for 'Found it' and 'Attended' logtypes."); } if ($rating !== null && Settings::get('OC_BRANCH') == 'oc.de') { # We will remove the rating request and change the success message # (which will be returned IF the rest of the query will meet all the # requirements). self::$success_message .= " " . sprintf(_("However, your cache rating was ignored, because %s does not " . "have a rating system."), Okapi::get_normalized_site_name()); $rating = null; } $recommend = $request->get_parameter('recommend'); if (!$recommend) { $recommend = 'false'; } if (!in_array($recommend, array('true', 'false'))) { throw new InvalidParam('recommend', "Unknown option: '{$recommend}'."); } $recommend = $recommend == 'true'; if ($recommend && $logtype != 'Found it') { if ($logtype != 'Attended') { throw new BadRequest("Recommending is allowed only for 'Found it' and 'Attended' logs."); } else { if (Settings::get('OC_BRANCH') == 'oc.pl') { # We will remove the recommendation request and change the success message # (which will be returned IF the rest of the query will meet all the # requirements). self::$success_message .= " " . sprintf(_("However, your cache recommendation was ignored, because " . "%s does not allow recommending event caches."), Okapi::get_normalized_site_name()); $recommend = null; } } } # We'll parse both 'needs_maintenance' and 'needs_maintenance2' here, but # we'll use only the $needs_maintenance2 variable afterwards. $needs_maintenance = $request->get_parameter('needs_maintenance'); $needs_maintenance2 = $request->get_parameter('needs_maintenance2'); if ($needs_maintenance && $needs_maintenance2) { throw new BadRequest("You cannot use both of these parameters at the same time: " . "needs_maintenance and needs_maintenance2."); } if (!$needs_maintenance2) { $needs_maintenance2 = 'null'; } # Parse $needs_maintenance and get rid of it. if ($needs_maintenance) { if ($needs_maintenance == 'true') { $needs_maintenance2 = 'true'; } else { if ($needs_maintenance == 'false') { $needs_maintenance2 = 'null'; } else { throw new InvalidParam('needs_maintenance', "Unknown option: '{$needs_maintenance}'."); } } } unset($needs_maintenance); # At this point, $needs_maintenance2 is set exactly as the user intended # it to be set. if (!in_array($needs_maintenance2, array('null', 'true', 'false'))) { throw new InvalidParam('needs_maintenance2', "Unknown option: '{$needs_maintenance2}'."); } if ($needs_maintenance2 == 'false' && Settings::get('OC_BRANCH') == 'oc.pl') { # If not supported, just ignore it. self::$success_message .= " " . sprintf(_("However, your \"does not need maintenance\" flag was ignored, because " . "%s does not yet support this feature."), Okapi::get_normalized_site_name()); $needs_maintenance2 = 'null'; } # Check if cache exists and retrieve cache internal ID (this will throw # a proper exception on invalid cache_code). Also, get the user object. $cache = OkapiServiceRunner::call('services/caches/geocache', new OkapiInternalRequest($request->consumer, null, array('cache_code' => $cache_code, 'fields' => 'internal_id|status|owner|type|req_passwd'))); $user = OkapiServiceRunner::call('services/users/by_internal_id', new OkapiInternalRequest($request->consumer, $request->token, array('internal_id' => $request->token->user_id, 'fields' => 'is_admin|uuid|internal_id|caches_found|rcmds_given'))); # Various integrity checks. if ($cache['type'] == 'Event') { if (!in_array($logtype, array('Will attend', 'Attended', 'Comment'))) { throw new CannotPublishException(_('This cache is an Event cache. You cannot "Find" it (but ' . 'you can attend it, or comment on it)!')); } } else { if (in_array($logtype, array('Will attend', 'Attended'))) { throw new CannotPublishException(_('This cache is NOT an Event cache. You cannot "Attend" it ' . '(but you can find it, or comment on it)!')); } else { if (!in_array($logtype, array('Found it', "Didn't find it", 'Comment'))) { throw new Exception("Unknown log entry - should be documented here."); } } } if ($logtype == 'Comment' && strlen(trim($comment)) == 0) { throw new CannotPublishException(_("Your have to supply some text for your comment.")); } # Password check. if (($logtype == 'Found it' || $logtype == 'Attended') && $cache['req_passwd']) { $valid_password = Db::select_value("\n select logpw\n from caches\n where cache_id = '" . Db::escape_string($cache['internal_id']) . "'\n "); $supplied_password = $request->get_parameter('password'); if (!$supplied_password) { throw new CannotPublishException(_("This cache requires a password. You didn't provide one!")); } if (strtolower($supplied_password) != strtolower($valid_password)) { throw new CannotPublishException(_("Invalid password!")); } } # Prepare our comment to be inserted into the database. This may require # some reformatting which depends on the current OC installation. # OC sites store all comments in HTML format, while the 'text_html' field # indicates their *original* format as delivered by the user. This # allows processing the 'text' field contents without caring about the # original format, while still being able to re-create the comment in # its original form. if ($comment_format == 'plaintext') { # This code is identical to the plaintext processing in OC code, # including a space handling bug: Multiple consecutive spaces will # get semantically lost in the generated HTML. $formatted_comment = htmlspecialchars($comment, ENT_COMPAT); $formatted_comment = nl2br($formatted_comment); if (Settings::get('OC_BRANCH') == 'oc.de') { $value_for_text_html_field = 0; } else { # 'text_html' = 0 (false) is broken in OCPL code and has been # deprecated; OCPL code was changed to always set it to 1 (true). # For OKAPI, the value has been changed from 0 to 1 with commit # cb7d222, after an email discussion with Harrie Klomp. This is # an ID of the appropriate email thread: # # Message-ID: <*****@*****.**> $value_for_text_html_field = 1; } } elseif ($comment_format == 'auto') { # 'Auto' is for backward compatibility. Before the "comment_format" # was introduced, OKAPI used a weird format in between (it allowed # HTML, but applied nl2br too). $formatted_comment = nl2br($comment); $value_for_text_html_field = 1; } else { $formatted_comment = $comment; # For user-supplied HTML comments, OC sites require us to do # additional HTML purification prior to the insertion into the # database. if (Settings::get('OC_BRANCH') == 'oc.de') { # NOTICE: We are including EXTERNAL OCDE library here! This # code does not belong to OKAPI! $opt['rootpath'] = $GLOBALS['rootpath']; $opt['html_purifier'] = Settings::get('OCDE_HTML_PURIFIER_SETTINGS'); require_once $GLOBALS['rootpath'] . 'lib2/OcHTMLPurifier.class.php'; $purifier = new \OcHTMLPurifier($opt); $formatted_comment = $purifier->purify($formatted_comment); } else { # TODO: Add OCPL HTML filtering. # See https://github.com/opencaching/okapi/issues/412. } $value_for_text_html_field = 1; } if (Settings::get('OC_BRANCH') == 'oc.pl') { # The HTML processing in OCPL code is broken. Effectively, it # will decode < > and & (and maybe other things?) # before display so that text contents may be interpreted as HTML. # We work around this by applying a double-encoding for & < >: $formatted_comment = str_replace("&", "&#38;", $formatted_comment); $formatted_comment = str_replace("<", "&#60;", $formatted_comment); $formatted_comment = str_replace(">", "&#62;", $formatted_comment); # Note: This problem also exists when submitting logs on OCPL websites. # If you e.g. enter "<text>" in the editor, it will get lost. # See https://github.com/opencaching/opencaching-pl/issues/469. } unset($comment); # Prevent bug #367. Start the transaction and lock all the rows of this # (user, cache) pair. In theory, we want to lock even smaller number of # rows here (user, cache, type=1), but this wouldn't work, because there's # no index for this. # # http://stackoverflow.com/questions/17068686/ Db::execute("start transaction"); Db::select_column("\n select 1\n from cache_logs\n where\n user_id = '" . Db::escape_string($request->token->user_id) . "'\n and cache_id = '" . Db::escape_string($cache['internal_id']) . "'\n for update\n "); # Duplicate detection. if ($on_duplicate != 'continue') { # Attempt to find a log entry made by the same user, for the same cache, with # the same date, type, comment, etc. Note, that these are not ALL the fields # we could check, but should work ok in most cases. Also note, that we # DO NOT guarantee that duplicate detection will succeed. If it doesn't, # nothing bad happens (user will just post two similar log entries). # Keep this simple! $duplicate_uuid = Db::select_value("\n select uuid\n from cache_logs\n where\n user_id = '" . Db::escape_string($request->token->user_id) . "'\n and cache_id = '" . Db::escape_string($cache['internal_id']) . "'\n and type = '" . Db::escape_string(Okapi::logtypename2id($logtype)) . "'\n and date = from_unixtime('" . Db::escape_string($when) . "')\n and text = '" . Db::escape_string($formatted_comment) . "'\n " . (Settings::get('OC_BRANCH') == 'oc.pl' ? "and deleted = 0" : "") . "\n limit 1\n "); if ($duplicate_uuid != null) { if ($on_duplicate == 'silent_success') { # Act as if the log has been submitted successfully. return $duplicate_uuid; } elseif ($on_duplicate == 'user_error') { throw new CannotPublishException(_("You have already submitted a log entry with exactly " . "the same contents.")); } } } # Check if already found it (and make sure the user is not the owner). # # OCPL forbids logging 'Found it' or "Didn't find" for an already found cache, # while OCDE allows all kinds of duplicate logs. if (Settings::get('OC_BRANCH') == 'oc.pl' && ($logtype == 'Found it' || $logtype == "Didn't find it")) { $has_already_found_it = Db::select_value("\n select 1\n from cache_logs\n where\n user_id = '" . Db::escape_string($user['internal_id']) . "'\n and cache_id = '" . Db::escape_string($cache['internal_id']) . "'\n and type = '" . Db::escape_string(Okapi::logtypename2id("Found it")) . "'\n and " . (Settings::get('OC_BRANCH') == 'oc.pl' ? "deleted = 0" : "true") . "\n "); if ($has_already_found_it) { throw new CannotPublishException(_("You have already submitted a \"Found it\" log entry once. " . "Now you may submit \"Comments\" only!")); } if ($user['uuid'] == $cache['owner']['uuid']) { throw new CannotPublishException(_("You are the owner of this cache. You may submit " . "\"Comments\" only!")); } } # Check if the user has already rated the cache. BTW: I don't get this one. # If we already know, that the cache was NOT found yet, then HOW could the # user submit a rating for it? Anyway, I will stick to the procedure # found in log.php. On the bright side, it's fail-safe. if ($rating) { $has_already_rated = Db::select_value("\n select 1\n from scores\n where\n user_id = '" . Db::escape_string($user['internal_id']) . "'\n and cache_id = '" . Db::escape_string($cache['internal_id']) . "'\n "); if ($has_already_rated) { throw new CannotPublishException(_("You have already rated this cache once. Your rating " . "cannot be changed.")); } } # If user wants to recommend... if ($recommend) { # Do the same "fail-safety" check as we did for the rating. $already_recommended = Db::select_value("\n select 1\n from cache_rating\n where\n user_id = '" . Db::escape_string($user['internal_id']) . "'\n and cache_id = '" . Db::escape_string($cache['internal_id']) . "'\n "); if ($already_recommended) { throw new CannotPublishException(_("You have already recommended this cache once.")); } # Check the number of recommendations. $founds = $user['caches_found'] + 1; // +1, because he'll find THIS ONE in a moment # Note: caches_found includes the number of attended events (both on # OCDE and OCPL). OCPL does not allow recommending events, but the # number of attended events influences $rcmds_left the same way a # normal "Fount it" log does. $rcmds_left = floor($founds / 10.0) - $user['rcmds_given']; if ($rcmds_left <= 0) { throw new CannotPublishException(_("You don't have any recommendations to give. Find more " . "caches first!")); } } # If user checked the "needs_maintenance(2)" flag for OCPL, we will shuffle things # a little... if (Settings::get('OC_BRANCH') == 'oc.pl' && $needs_maintenance2 == 'true') { # If we're here, then we also know that the "Needs maintenance" log # type is supported by this OC site. However, it's a separate log # type, so we might have to submit two log types together: if ($logtype == 'Comment') { # If user submits a "Comment", we'll just change its type to # "Needs maintenance". Only one log entry will be issued. $logtype = 'Needs maintenance'; $second_logtype = null; $second_formatted_comment = null; } elseif ($logtype == 'Found it') { # If "Found it", then we'll issue two log entries: one "Found # it" with the original comment, and second one "Needs # maintenance" with empty comment. $second_logtype = 'Needs maintenance'; $second_formatted_comment = ""; } elseif ($logtype == "Didn't find it") { # If "Didn't find it", then we'll issue two log entries, but this time # we'll do this the other way around. The first "Didn't find it" entry # will have an empty comment. We will move the comment to the second # "Needs maintenance" log entry. (It's okay for this behavior to change # in the future, but it seems natural to me.) $second_logtype = 'Needs maintenance'; $second_formatted_comment = $formatted_comment; $formatted_comment = ""; } else { if ($logtype == 'Will attend' || $logtype == 'Attended') { # OC branches which allow maintenance logs, still don't allow them on # event caches. throw new CannotPublishException(_("Event caches cannot \"need maintenance\".")); } else { throw new Exception(); } } } else { # User didn't check the "Needs maintenance" flag OR "Needs maintenance" # log type isn't supported by this server. $second_logtype = null; $second_formatted_comment = null; } # Finally! Insert the rows into the log entries table. Update # cache stats and user stats. $log_uuids = array(self::insert_log_row($request->consumer->key, $cache['internal_id'], $user['internal_id'], $logtype, $when, $formatted_comment, $value_for_text_html_field, $needs_maintenance2)); self::increment_cache_stats($cache['internal_id'], $when, $logtype); self::increment_user_stats($user['internal_id'], $logtype); if ($second_logtype != null) { # Reminder: This will only be called for OCPL branch. $log_uuids[] = self::insert_log_row($request->consumer->key, $cache['internal_id'], $user['internal_id'], $second_logtype, $when + 1, $second_formatted_comment, $value_for_text_html_field, 'null'); self::increment_cache_stats($cache['internal_id'], $when + 1, $second_logtype); self::increment_user_stats($user['internal_id'], $second_logtype); } # Save the rating. if ($rating) { # This code will be called for OCPL branch only. Earlier, we made sure, # to set $rating to null, if we're running on OCDE. # OCPL has a little strange way of storing cumulative rating. Instead # of storing the sum of all ratings, OCPL stores the computed average # and update it using multiple floating-point operations. Moreover, # the "score" field in the database is on the -3..3 scale (NOT 1..5), # and the translation made at retrieval time is DIFFERENT than the # one made here (both of them are non-linear). Also, once submitted, # the rating can never be changed. It surely feels quite inconsistent, # but presumably has some deep logic into it. See also here (Polish): # http://wiki.opencaching.pl/index.php/Oceny_skrzynek switch ($rating) { case 1: $db_score = -2.0; break; case 2: $db_score = -0.5; break; case 3: $db_score = 0.7; break; case 4: $db_score = 1.7; break; case 5: $db_score = 3.0; break; default: throw new Exception(); } Db::execute("\n update caches\n set\n score = (\n score*votes + '" . Db::escape_string($db_score) . "'\n ) / (votes + 1),\n votes = votes + 1\n where cache_id = '" . Db::escape_string($cache['internal_id']) . "'\n "); Db::execute("\n insert into scores (user_id, cache_id, score)\n values (\n '" . Db::escape_string($user['internal_id']) . "',\n '" . Db::escape_string($cache['internal_id']) . "',\n '" . Db::escape_string($db_score) . "'\n );\n "); } # Save recommendation. if ($recommend) { if (Db::field_exists('cache_rating', 'rating_date')) { Db::execute("\n insert into cache_rating (user_id, cache_id, rating_date)\n values (\n '" . Db::escape_string($user['internal_id']) . "',\n '" . Db::escape_string($cache['internal_id']) . "',\n from_unixtime('" . Db::escape_string($when) . "')\n );\n "); } else { Db::execute("\n insert into cache_rating (user_id, cache_id)\n values (\n '" . Db::escape_string($user['internal_id']) . "',\n '" . Db::escape_string($cache['internal_id']) . "'\n );\n "); } } # Finalize the transaction. Db::execute("commit"); # We need to delete the copy of stats-picture for this user. Otherwise, # the legacy OC code won't detect that the picture needs to be refreshed. $filepath = Okapi::get_var_dir() . '/images/statpics/statpic' . $user['internal_id'] . '.jpg'; if (file_exists($filepath)) { unlink($filepath); } # Success. Return the uuids. return $log_uuids; }