subsurface/core/uploadDiveLogsDE.cpp
Berthold Stoeger 5eda1c0e39 parser: XML_PARSE_RECOVER to xmlReadMemory()
Due to changes in the handling of sensor-ids, invalid XMLs were
generated. In particular, these contained duplicate attributes
in the sample tags.

Even though these files shouldn't exist, let's try to parse
them anyway. Some data will be lost, but that's better than
not opening the file.

libxml2 can be told to try to recover from such petty(?) errors
by passing the XML_PARSE_RECOVER flag.

Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2021-09-03 09:30:34 -07:00

354 lines
9.2 KiB
C++

// SPDX-License-Identifier: GPL-2.0
#include "uploadDiveLogsDE.h"
#include <QDir>
#include <QDebug>
#include <QTemporaryFile>
#include <zip.h>
#include <errno.h>
#include "core/display.h"
#include "core/errorhelper.h"
#include "core/qthelper.h"
#include "core/dive.h"
#include "core/membuffer.h"
#include "core/divesite.h"
#include "core/cloudstorage.h"
#include "core/xmlparams.h"
#ifndef SUBSURFACE_MOBILE
#include "core/selection.h"
#endif // SUBSURFACE_MOBILE
#include "core/settings/qPrefCloudStorage.h"
uploadDiveLogsDE *uploadDiveLogsDE::instance()
{
static uploadDiveLogsDE *self = new uploadDiveLogsDE;
return self;
}
uploadDiveLogsDE::uploadDiveLogsDE():
reply(NULL),
multipart(NULL)
{
timeout.setSingleShot(true);
}
static QString makeTempFileName()
{
QTemporaryFile tmpfile;
tmpfile.setFileTemplate(QDir::tempPath() + "/divelogsde-upload.XXXXXXXX.dld");
tmpfile.open();
return tmpfile.fileName();
}
void uploadDiveLogsDE::doUpload(bool selected, const QString &userid, const QString &password)
{
QString err;
QString filename = makeTempFileName();
// Make zip file, with all dives, in divelogs.de format
if (!prepareDives(filename, selected)) {
emit uploadFinish(false, tr("Cannot prepare dives, none selected?"));
timeout.stop();
return;
}
// And upload it
uploadDives(filename, userid, password);
}
bool uploadDiveLogsDE::prepareDives(const QString &tempfile, bool selected)
{
static const char errPrefix[] = "divelog.de-upload:";
xsltStylesheetPtr xslt = NULL;
struct zip *zip;
emit uploadStatus(tr("building zip file to upload"));
xslt = get_stylesheet("divelogs-export.xslt");
if (!xslt) {
qDebug() << errPrefix << "missing stylesheet";
report_error(tr("Stylesheet to export to divelogs.de is not found").toUtf8());
return false;
}
// Prepare zip file
int error_code;
zip = zip_open(QFile::encodeName(QDir::toNativeSeparators(tempfile)), ZIP_CREATE, &error_code);
if (!zip) {
char buffer[1024];
zip_error_to_str(buffer, sizeof buffer, error_code, errno);
report_error(tr("Failed to create zip file for upload: %s").toUtf8(), buffer);
return false;
}
/* walk the dive list in chronological order */
int i;
struct dive *dive;
for_each_dive (i, dive) {
char filename[PATH_MAX];
int streamsize;
char *membuf;
xmlDoc *transformed;
struct zip_source *s;
struct membufferpp mb;
struct xml_params *params = alloc_xml_params();
/*
* Get the i'th dive in XML format so we can process it.
* We need to save to a file before we can reload it back into memory...
*/
if (selected && !dive->selected) {
free_xml_params(params);
continue;
}
/* add the dive */
struct dive_site *ds = dive->dive_site;
if (ds) {
put_format(&mb, "<divelog><divesites><site uuid='%8x' name='", ds->uuid);
put_quoted(&mb, ds->name, 1, 0);
put_format(&mb, "'");
put_location(&mb, &ds->location, " gps='", "'");
put_format(&mb, ">\n");
if (ds->taxonomy.nr) {
for (int j = 0; j < ds->taxonomy.nr; j++) {
struct taxonomy *t = &ds->taxonomy.category[j];
if (t->category != TC_NONE && t->category == prefs.geocoding.category[j] && t->value) {
put_format(&mb, " <geo cat='%d'", t->category);
put_format(&mb, " origin='%d' value='", t->origin);
put_quoted(&mb, t->value, 1, 0);
put_format(&mb, "'/>\n");
}
}
}
put_format(&mb, "</site>\n</divesites>\n");
}
save_one_dive_to_mb(&mb, dive, false);
if (ds) {
put_format(&mb, "</divelog>\n");
}
/*
* Parse the memory buffer into XML document and
* transform it to divelogs.de format, finally dumping
* the XML into a character buffer.
*/
xmlDoc *doc = xmlReadMemory(mb.buffer, mb.len, "divelog", NULL, XML_PARSE_HUGE | XML_PARSE_RECOVER);
if (!doc) {
qWarning() << errPrefix << "could not parse back into memory the XML file we've just created!";
report_error(tr("internal error").toUtf8());
zip_close(zip);
QFile::remove(tempfile);
xsltFreeStylesheet(xslt);
free_xml_params(params);
return false;
}
xml_params_add_int(params, "allcylinders", prefs.display_unused_tanks);
transformed = xsltApplyStylesheet(xslt, doc, xml_params_get(params));
free_xml_params(params);
if (!transformed) {
qWarning() << errPrefix << "XSLT transform failed for dive: " << i;
report_error(tr("Conversion of dive %1 to divelogs.de format failed").arg(i).toUtf8());
continue;
}
xmlDocDumpMemory(transformed, (xmlChar **)&membuf, &streamsize);
xmlFreeDoc(doc);
xmlFreeDoc(transformed);
/*
* Save the XML document into a zip file.
*/
snprintf(filename, PATH_MAX, "%d.xml", i + 1);
s = zip_source_buffer(zip, membuf, streamsize, 1); // frees membuffer!
if (s) {
int64_t ret = zip_add(zip, filename, s);
if (ret == -1)
qDebug() << errPrefix << "failed to include dive:" << i;
}
}
xsltFreeStylesheet(xslt);
if (zip_close(zip)) {
int ze, se;
#if LIBZIP_VERSION_MAJOR >= 1
zip_error_t *error = zip_get_error(zip);
ze = zip_error_code_zip(error);
se = zip_error_code_system(error);
#else
zip_error_get(zip, &ze, &se);
#endif
report_error(qPrintable(tr("error writing zip file: %s zip error %d system error %d - %s")),
qPrintable(QDir::toNativeSeparators(tempfile)), ze, se, zip_strerror(zip));
return false;
}
return true;
}
void uploadDiveLogsDE::cleanupTempFile()
{
if (tempFile.isOpen())
tempFile.remove();
}
void uploadDiveLogsDE::uploadDives(const QString &filename, const QString &userid, const QString &password)
{
QHttpPart part1, part2, part3;
static QNetworkRequest request;
QString args;
// Check if there is an earlier request open
if (reply != NULL && reply->isOpen()) {
reply->abort();
delete reply;
reply = NULL;
}
if (multipart != NULL) {
delete multipart;
multipart = NULL;
}
multipart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
emit uploadStatus(tr("Uploading dives"));
// prepare header with filename (of all dives) and pointer to file
args = "form-data; name=\"userfile\"; filename=\"" + filename + "\"";
part1.setRawHeader("Content-Disposition", args.toLatin1());
// open new file for reading
cleanupTempFile();
tempFile.setFileName(filename);
if (!tempFile.open(QIODevice::ReadOnly)) {
qDebug() << "ERROR opening zip file: " << filename;
return;
}
part1.setBodyDevice(&tempFile);
multipart->append(part1);
// Add userid
args = "form-data; name=\"user\"";
part2.setRawHeader("Content-Disposition", args.toLatin1());
part2.setBody(qPrefCloudStorage::divelogde_user().toUtf8());
multipart->append(part2);
// Add password
args = "form-data; name=\"pass\"";
part3.setRawHeader("Content-Disposition", args.toLatin1());
part3.setBody(qPrefCloudStorage::divelogde_pass().toUtf8());
multipart->append(part3);
// Prepare network request
request.setUrl(QUrl("https://divelogs.de/DivelogsDirectImport.php"));
request.setRawHeader("Accept", "text/xml, application/xml");
request.setRawHeader("User-Agent", getUserAgent().toUtf8());
// Execute async.
reply = manager()->post(request, multipart);
// connect signals from upload process
connect(reply, SIGNAL(finished()), this, SLOT(uploadFinishedSlot()));
connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this,
SLOT(uploadErrorSlot(QNetworkReply::NetworkError)));
connect(reply, SIGNAL(uploadProgress(qint64, qint64)), this,
SLOT(updateProgressSlot(qint64, qint64)));
connect(&timeout, SIGNAL(timeout()), this, SLOT(uploadTimeoutSlot()));
timeout.start(30000); // 30s
}
void uploadDiveLogsDE::updateProgressSlot(qint64 current, qint64 total)
{
if (!reply)
return;
if (total <= 0 || current <= 0)
return;
// Calculate percentage as 0.x (values between 0.0 and 1.0)
// And signal whoever wants to know
qreal percentage = (float)current / (float)total;
emit uploadProgress(percentage, 1.0);
// reset the timer: 30 seconds after we last got any data
timeout.start();
}
void uploadDiveLogsDE::uploadFinishedSlot()
{
QString err;
if (!reply)
return;
// check what the server sent us: it might contain
// an error condition, such as a failed login
QByteArray xmlData = reply->readAll();
reply->deleteLater();
reply = NULL;
cleanupTempFile();
char *resp = xmlData.data();
if (resp) {
char *parsed = strstr(resp, "<Login>");
if (parsed) {
if (strstr(resp, "<Login>succeeded</Login>")) {
if (strstr(resp, "<FileCopy>failed</FileCopy>")) {
report_error(tr("Upload failed").toUtf8());
return;
}
timeout.stop();
err = tr("Upload successful");
emit uploadFinish(true, err);
return;
}
timeout.stop();
err = tr("Login failed");
report_error(err.toUtf8());
emit uploadFinish(false, err);
return;
}
timeout.stop();
err = tr("Cannot parse response");
report_error(tr("Cannot parse response").toUtf8());
emit uploadFinish(false, err);
}
}
void uploadDiveLogsDE::uploadTimeoutSlot()
{
timeout.stop();
if (reply) {
reply->deleteLater();
reply = NULL;
}
cleanupTempFile();
QString err(tr("divelogs.de not responding"));
report_error(err.toUtf8());
emit uploadFinish(false, err);
}
void uploadDiveLogsDE::uploadErrorSlot(QNetworkReply::NetworkError error)
{
timeout.stop();
if (reply) {
reply->deleteLater();
reply = NULL;
}
cleanupTempFile();
QString err(tr("network error %1").arg(error));
report_error(err.toUtf8());
emit uploadFinish(false, err);
}