Skip to content

Commit

Permalink
issue opencaching#601 upload fieldnotes
Browse files Browse the repository at this point in the history
  • Loading branch information
hxdimpf committed Nov 26, 2023
1 parent 34975e3 commit 36b7e8a
Show file tree
Hide file tree
Showing 3 changed files with 227 additions and 0 deletions.
1 change: 1 addition & 0 deletions okapi/core/OkapiServiceRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
151 changes: 151 additions & 0 deletions okapi/services/draftlogs/upload_fieldnotes_file/WebService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
<?php

namespace okapi\services\draftlogs\upload_fieldnotes_file;

use okapi\core\Exception\InvalidParam;
use okapi\core\Exception\ParamMissing;
use okapi\core\Db;
use okapi\core\Okapi;
use okapi\core\OkapiServiceRunner;
use okapi\core\Request\OkapiInternalRequest;
use okapi\core\Request\OkapiRequest;
use okapi\services\logs\LogsCommon;
use okapi\Settings;

class WebService
{
public static function options()
{
return array(
'min_auth_level' => 3
);
}

public static function call(OkapiRequest $request)
{
if (Settings::get('OC_BRANCH') != 'oc.de')
throw new BadRequest('This method is not supported in this OKAPI installation. See the has_draftlogs field in services/apisrv/installation method.');

$field_notes = $request->get_parameter('field_notes');
if (!$field_notes) throw new ParamMissing('field_notes');

$notes = self::parse_notes($field_notes);

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'
))
);

try {
$type = Okapi::logtypename2id($n['type']);
} catch (\Exception $e) {
throw new InvalidParam('Type', 'Invalid log type provided.');
}

$dateString = strtotime($n['date']);
if ($dateString === false) {
throw new InvalidParam('`Date` field in log record', "Input data not recognized.");
} else {
$date = date("Y-m-d H:i:s", $dateString);
}

$user_id = $request->token->user_id;
$geocache_id = $geocache['internal_id'];
$text = $n['log'];

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']
);
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) throw new InvalidParam('field_notes', "Input data is not properly base64 encoded.");

$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 = str_getcsv($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;
}
}
75 changes: 75 additions & 0 deletions okapi/services/draftlogs/upload_fieldnotes_file/docs.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<xml>
<brief>Upload Fieldnotes</brief>
<issue-id>630</issue-id>
<desc>
<p>Upload a set of one or more fieldnotes records.</p>
</desc>
<req name='field_notes' infotags='ocde-specific'>
<p>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.</p>

<p>Fieldnotes do not have a CSV header in the file that might describe the columns.</p>
<p>Each record describes a geocache log consisting of four fields:</p>
<ol>
<li>Geocache Code</li>
<li>Date</li>
<li>Log Type</li>
<li>Log Text</li>
</ol>
<p>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 <i>field_notes</i> parameter must be passed as a
<b>base64 encoded utf8 string</b>. UTF-16LE, UTF-16BE with or without
BOM are not supported.
</p>
<p>The second field <i>Date</i> 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!).</p>
<p>Since the log type is passed as a string, its value must match the
values supported by the platform (case sensitive!):</p>
<pre>
[
"Found it",
"Didn't find it",
"Comment",
"Attended",
"Will attend",
"Archived",
"Ready to search",
"Temporarily unavailable"
]
</pre>
<p>Note: This service method is not supported on all installations</p>
</req>
<common-format-params/>
<returns>
<p>A dictionary of the following structure:</p>
<ul>
<li>success - true</li>
<li>totalRecords - number of records in <i>field_notes</i></li>
<li>processedRecords - number of records inserted into the database</li>
</ul>
<p>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 <b>GC....</b>
while on opencaching.de the log records start with <b>OC....</b>. Other opencaching
platforms use other codes, for instance opencaching.pl uses <b>OP...</b>.</p>
<p>
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.de discards everything that doesn't start with "OC."</p>

<p>In addition, in that hybrid file there will be <i>Log Type</i>, 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 as "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.</p>
<p>It is the responsibility of the client application to assign the correct Log Type
string when the offline log is created.</p>
</returns>
</xml>

0 comments on commit 36b7e8a

Please sign in to comment.