From 033d16898041ef4474ca4b1129e5fb82df865199 Mon Sep 17 00:00:00 2001 From: Hugo Dimpfelmoser Date: Sat, 25 Nov 2023 10:39:29 +0100 Subject: [PATCH] issue #601 upload fieldnotes --- okapi/core/OkapiServiceRunner.php | 1 + .../upload_fieldnotes_file/WebService.php | 182 ++++++++++++++++++ .../draftlogs/upload_fieldnotes_file/docs.xml | 72 +++++++ 3 files changed, 255 insertions(+) create mode 100644 okapi/services/draftlogs/upload_fieldnotes_file/WebService.php create mode 100644 okapi/services/draftlogs/upload_fieldnotes_file/docs.xml diff --git a/okapi/core/OkapiServiceRunner.php b/okapi/core/OkapiServiceRunner.php index a1d0abba..4480668c 100644 --- a/okapi/core/OkapiServiceRunner.php +++ b/okapi/core/OkapiServiceRunner.php @@ -43,6 +43,7 @@ class OkapiServiceRunner 'services/caches/formatters/garmin', 'services/caches/formatters/ggz', 'services/caches/map/tile', + 'services/draftlogs/upload_fieldnotes_file', 'services/logs/capabilities', 'services/logs/delete', 'services/logs/edit', diff --git a/okapi/services/draftlogs/upload_fieldnotes_file/WebService.php b/okapi/services/draftlogs/upload_fieldnotes_file/WebService.php new file mode 100644 index 00000000..c408805d --- /dev/null +++ b/okapi/services/draftlogs/upload_fieldnotes_file/WebService.php @@ -0,0 +1,182 @@ + 3 + ); + } + + public static function call(OkapiRequest $request) + { + $result = array( + 'success' => false // if the installation doesn't support it + ); + + if (Settings::get('OC_BRANCH') == 'oc.de') + { + + $field_notes = $request->get_parameter('field_notes'); + if (!$field_notes) throw new ParamMissing('field_notes'); + + $notes = self::parse_notes($field_notes); + if ($notes['success'] === false) throw new InvalidParam('field_notes', "Input data not recognized."); + + foreach ($notes['records'] as $n) + { + $geocache = OkapiServiceRunner::call( + 'services/caches/geocache', + new OkapiInternalRequest($request->consumer, $request->token, array( + 'cache_code' => $n['code'], + 'fields' => 'internal_id' + )) + ); + $user_id = $request->token->user_id; + $geocache_id = $geocache['internal_id']; + $type = Okapi::logtypename2id($n['type']); + $text = $n['log']; + $dateString = strtotime($n['date']); + if ($dateString === false) + { + throw new InvalidParam('Date field in logi record', "Input data not recognized."); + } + else + { + $date = date("Y-m-d H:i:s", $dateString); + } + Db::query(" + insert into field_note ( + user_id, geocache_id, type, date, text + ) values ( + '".Db::escape_string($user_id)."', + '".Db::escape_string($geocache_id)."', + '".Db::escape_string($type)."', + '".Db::escape_string($date)."', + '".Db::escape_string($text)."' + ) + "); + + } + $result = array( + 'success' => true, + 'totalRecords' => $notes['totalRecords'], + 'processedRecords' => $notes['processedRecords'] + ); + //$result = json_encode($notes, JSON_PRETTY_PRINT); // debug + } + return Okapi::formatted_response($request, $result); + } + + // ------------------------------------------------------------------ + + private static function parse_notes($field_notes) + { + $decoded_field_notes = base64_decode($field_notes, true); + if ($decoded_field_notes === false) return false; + + $multiline = self::fieldNotesTxtArea2Array($decoded_field_notes); + $submittable_logtype_names = Okapi::get_submittable_logtype_names(); + $records = []; + $totalRecords = 0; + $processedRecords = 0; + + foreach ($multiline as $line) { + $totalRecords++; + $line = trim($line); + $fields = self::CSVtoArray($line); + + $code = $fields[0]; + $date = $fields[1]; + $type = $fields[2]; + + if (!in_array($type, $submittable_logtype_names)) continue; + + $log = nl2br($fields[3]); + + $records[] = [ + 'code' => $code, + 'date' => $date, + 'type' => $type, + 'log' => $log, + ]; + $processedRecords++; + } + return ['success' => true, 'records' => $records, 'totalRecords' => $totalRecords, 'processedRecords' => $processedRecords]; + } + + + // ------------------------------------------------------------------ + + private static function fieldNotesTxtArea2Array($fieldnotes) + { + $output = []; + $buffer = ''; + $start = true; + + $lines = explode("\n", $fieldnotes); + $lines = array_filter($lines); // Drop empty lines + + foreach ($lines as $line) { + if ($start) { + $buffer = $line; + $start = false; + } else { + if (strpos($line, 'OC') !== 0) { + $buffer .= "\n" . $line; + } else { + $output[] = trim($buffer); + $buffer = $line; + } + } + } + + if (!$start) { + $output[] = trim($buffer); + } + + return $output; + } + + // ------------------------------------------------------------------ + + private static function CSVtoArray($text) + { + $ret = ['']; + $i = 0; + $p = ''; + $s = true; + + foreach (str_split($text) as $l) { + if ('"' === $l) { + $s = !$s; + if ('"' === $p) { + $ret[$i] .= '"'; + $l = '-'; + } elseif ('' === $p) { + $l = '-'; + } + } elseif ($s && ',' === $l) { + $l = $ret[++$i] = ''; + } else { + $ret[$i] .= $l; + } + $p = $l; + } + + return $ret; + } +} diff --git a/okapi/services/draftlogs/upload_fieldnotes_file/docs.xml b/okapi/services/draftlogs/upload_fieldnotes_file/docs.xml new file mode 100644 index 00000000..bc6bc5a4 --- /dev/null +++ b/okapi/services/draftlogs/upload_fieldnotes_file/docs.xml @@ -0,0 +1,72 @@ + + Upload Fieldnotes + 630 + +

Upload a set of one or more fieldnotes records.

+
+ +

This method allows you to upload a series of fieldnotes in the CSV format. + Fieldnotes are draft versions of log entries. Once uploaded, users will be able + to review, edit, and submit them via the Opencaching site.

+ +

Fieldnotes do not have a CSV header in the file that might describe the columns.

+

Each record describes a geocache log consisting of four fields:

+
    +
  1. Geocache Code
  2. +
  3. Date
  4. +
  5. Log Type
  6. +
  7. Log Text
  8. +
+

The first three fields are string entities that don't have line control characters in them, + the Log Text field is different and a bit difficult as it may spread over muliple lines + and it may contain quote characters. In order to preserve the structure + of the records, the field_notes parameter must be passed as a + base64 encoded utf8 string. UTF-16LE, UTF-16BE with or without + BOM are not supported. +

+

The second field Date should be in ISO 8601 format (currently any format + acceptable by PHP's strtotime function also will do, but most of them don't handle + time zones properly, try to use ISO 8601!).

+

Since the log type is passed as a string, its value must match the + values supported by the platform (case sensitive!):

+
+[
+    "Found it",
+    "Didn't find it",
+    "Comment",
+    "Attended",
+    "Will attend",
+    "Archived",
+    "Ready to search",
+    "Temporarily unavailable"
+]
+        
+

Note: This service method is not supported on all installations

+
+ + +

A dictionary of the following structure:

+
    +
  • success - true
  • +
  • totalRecords - number of records in field_notes
  • +
  • processedRecords - number of records inserted into the database
  • +
+

processedRecords may be less than totalRecords (it may even be zero) and that + is the case for the following reason: Fieldnotes files are created from + Geocaching client applications on Smartphones. These applications support multiple + geocaching platforms from which opencaching is only one of them. Conseqently the + Fieldnotes file is a "hybrid file" which contains records for multiple different + platforms. For instance for geocaching.com logs the records start with GC.... + while of opencaching the log records start with OC..... The client application + uploads one and the same Fielnotes file to all platforms and it is the platform's + task to filter out what matches their objects. Geocaching.com will discard "OC logs" + and opencaching must discard "GC logs" in that file.

+

In addition, in that hybrid file there will be Log Type, a string that + inevitably has a different definition for the various platforms. For instance, what + is called a "Write note" log on geocaching.com is recognized s "Comments" on + opencaching platforms. Consequently fieldnotes records which have a log type which + is not understood by the opencaching platform will be discarded without notice.

+

It is the responsibility of the client application to assign the correct log type + string when the offline log is created

+
+