summaryrefslogtreecommitdiffstats
path: root/desktop-widgets/subsurfacewebservices.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'desktop-widgets/subsurfacewebservices.cpp')
-rw-r--r--desktop-widgets/subsurfacewebservices.cpp1121
1 files changed, 1121 insertions, 0 deletions
diff --git a/desktop-widgets/subsurfacewebservices.cpp b/desktop-widgets/subsurfacewebservices.cpp
new file mode 100644
index 000000000..ee079cc48
--- /dev/null
+++ b/desktop-widgets/subsurfacewebservices.cpp
@@ -0,0 +1,1121 @@
+#include "subsurfacewebservices.h"
+#include "helpers.h"
+#include "webservice.h"
+#include "mainwindow.h"
+#include "usersurvey.h"
+#include "divelist.h"
+#include "globe.h"
+#include "maintab.h"
+#include "display.h"
+#include "membuffer.h"
+#include <errno.h>
+
+#include <QDir>
+#include <QHttpMultiPart>
+#include <QMessageBox>
+#include <QSettings>
+#include <QXmlStreamReader>
+#include <qdesktopservices.h>
+#include <QShortcut>
+#include <QDebug>
+
+#ifdef Q_OS_UNIX
+#include <unistd.h> // for dup(2)
+#endif
+
+#include <QUrlQuery>
+
+#ifndef PATH_MAX
+#define PATH_MAX 4096
+#endif
+
+struct dive_table gps_location_table;
+
+// we don't overwrite any existing GPS info in the dive
+// so get the dive site and if there is none or there is one without GPS fix, add it
+static void copy_gps_location(struct dive *from, struct dive *to)
+{
+ struct dive_site *ds = get_dive_site_for_dive(to);
+ if (!ds || !dive_site_has_gps_location(ds)) {
+ struct dive_site *gds = get_dive_site_for_dive(from);
+ if (!ds) {
+ // simply link to the one created for the fake dive
+ to->dive_site_uuid = gds->uuid;
+ } else {
+ ds->latitude = gds->latitude;
+ ds->longitude = gds->longitude;
+ if (same_string(ds->name, ""))
+ ds->name = copy_string(gds->name);
+ }
+ }
+}
+
+#define SAME_GROUP 6 * 3600 // six hours
+//TODO: C Code. static functions are not good if we plan to have a test for them.
+static bool merge_locations_into_dives(void)
+{
+ int i, j, tracer=0, changed=0;
+ struct dive *gpsfix, *nextgpsfix, *dive;
+
+ sort_table(&gps_location_table);
+
+ for_each_dive (i, dive) {
+ if (!dive_has_gps_location(dive)) {
+ for (j = tracer; (gpsfix = get_dive_from_table(j, &gps_location_table)) !=NULL; j++) {
+ if (time_during_dive_with_offset(dive, gpsfix->when, SAME_GROUP)) {
+ if (verbose)
+ qDebug() << "processing gpsfix @" << get_dive_date_string(gpsfix->when) <<
+ "which is withing six hours of dive from" <<
+ get_dive_date_string(dive->when) << "until" <<
+ get_dive_date_string(dive->when + dive->duration.seconds);
+ /*
+ * If position is fixed during dive. This is the good one.
+ * Asign and mark position, and end gps_location loop
+ */
+ if (time_during_dive_with_offset(dive, gpsfix->when, 0)) {
+ if (verbose)
+ qDebug() << "gpsfix is during the dive, pick that one";
+ copy_gps_location(gpsfix, dive);
+ changed++;
+ tracer = j;
+ break;
+ } else {
+ /*
+ * If it is not, check if there are more position fixes in SAME_GROUP range
+ */
+ if ((nextgpsfix = get_dive_from_table(j + 1, &gps_location_table)) &&
+ time_during_dive_with_offset(dive, nextgpsfix->when, SAME_GROUP)) {
+ if (verbose)
+ qDebug() << "look at the next gps fix @" << get_dive_date_string(nextgpsfix->when);
+ /* first let's test if this one is during the dive */
+ if (time_during_dive_with_offset(dive, nextgpsfix->when, 0)) {
+ if (verbose)
+ qDebug() << "which is during the dive, pick that one";
+ copy_gps_location(nextgpsfix, dive);
+ changed++;
+ tracer = j + 1;
+ break;
+ }
+ /* we know the gps fixes are sorted; if they are both before the dive, ignore the first,
+ * if theay are both after the dive, take the first,
+ * if the first is before and the second is after, take the closer one */
+ if (nextgpsfix->when < dive->when) {
+ if (verbose)
+ qDebug() << "which is closer to the start of the dive, do continue with that";
+ continue;
+ } else if (gpsfix->when > dive->when + dive->duration.seconds) {
+ if (verbose)
+ qDebug() << "which is even later after the end of the dive, so pick the previous one";
+ copy_gps_location(gpsfix, dive);
+ changed++;
+ tracer = j;
+ break;
+ } else {
+ /* ok, gpsfix is before, nextgpsfix is after */
+ if (dive->when - gpsfix->when <= nextgpsfix->when - (dive->when + dive->duration.seconds)) {
+ if (verbose)
+ qDebug() << "pick the one before as it's closer to the start";
+ copy_gps_location(gpsfix, dive);
+ changed++;
+ tracer = j;
+ break;
+ } else {
+ if (verbose)
+ qDebug() << "pick the one after as it's closer to the start";
+ copy_gps_location(nextgpsfix, dive);
+ changed++;
+ tracer = j + 1;
+ break;
+ }
+ }
+ /*
+ * If no more positions in range, the actual is the one. Asign, mark and end loop.
+ */
+ } else {
+ if (verbose)
+ qDebug() << "which seems to be the best one for this dive, so pick it";
+ copy_gps_location(gpsfix, dive);
+ changed++;
+ tracer = j;
+ break;
+ }
+ }
+ } else {
+ /* If position is out of SAME_GROUP range and in the future, mark position for
+ * next dive iteration and end the gps_location loop
+ */
+ if (gpsfix->when >= dive->when + dive->duration.seconds + SAME_GROUP) {
+ tracer = j;
+ break;
+ }
+ }
+ }
+ }
+ }
+ return changed > 0;
+}
+
+// TODO: This looks like should be ported to C code. or a big part of it.
+bool DivelogsDeWebServices::prepare_dives_for_divelogs(const QString &tempfile, const bool selected)
+{
+ static const char errPrefix[] = "divelog.de-upload:";
+ if (!amount_selected) {
+ report_error(tr("no dives were selected").toUtf8());
+ return false;
+ }
+
+ xsltStylesheetPtr xslt = NULL;
+ struct zip *zip;
+
+ 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;
+ }
+
+
+ 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) {
+ FILE *f;
+ char filename[PATH_MAX];
+ int streamsize;
+ const char *membuf;
+ xmlDoc *transformed;
+ struct zip_source *s;
+ struct membuffer mb = { 0 };
+
+ /*
+ * 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)
+ continue;
+ /* make sure the buffer is empty and add the dive */
+ mb.len = 0;
+
+ struct dive_site *ds = get_dive_site_by_uuid(dive->dive_site_uuid);
+
+ if (ds) {
+ put_format(&mb, "<divelog><divesites><site uuid='%8x' name='", dive->dive_site_uuid);
+ put_quoted(&mb, ds->name, 1, 0);
+ put_format(&mb, "'");
+ if (ds->latitude.udeg || ds->longitude.udeg) {
+ put_degrees(&mb, ds->latitude, " gps='", " ");
+ put_degrees(&mb, ds->longitude, "", "'");
+ }
+ put_format(&mb, "/>\n</divesites>\n");
+ }
+
+ save_one_dive_to_mb(&mb, dive);
+
+ if (ds) {
+ put_format(&mb, "</divelog>\n");
+ }
+ membuf = mb_cstring(&mb);
+ streamsize = strlen(membuf);
+ /*
+ * 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(membuf, streamsize, "divelog", NULL, 0);
+ if (!doc) {
+ qWarning() << errPrefix << "could not parse back into memory the XML file we've just created!";
+ report_error(tr("internal error").toUtf8());
+ goto error_close_zip;
+ }
+ free((void *)membuf);
+
+ transformed = xsltApplyStylesheet(xslt, doc, NULL);
+ 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);
+ 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;
+
+error_close_zip:
+ zip_close(zip);
+ QFile::remove(tempfile);
+ xsltFreeStylesheet(xslt);
+ return false;
+}
+
+WebServices::WebServices(QWidget *parent, Qt::WindowFlags f) : QDialog(parent, f), reply(0)
+{
+ ui.setupUi(this);
+ connect(ui.buttonBox, SIGNAL(clicked(QAbstractButton *)), this, SLOT(buttonClicked(QAbstractButton *)));
+ connect(ui.download, SIGNAL(clicked(bool)), this, SLOT(startDownload()));
+ connect(ui.upload, SIGNAL(clicked(bool)), this, SLOT(startUpload()));
+ connect(&timeout, SIGNAL(timeout()), this, SLOT(downloadTimedOut()));
+ ui.buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false);
+ timeout.setSingleShot(true);
+ defaultApplyText = ui.buttonBox->button(QDialogButtonBox::Apply)->text();
+ userAgent = getUserAgent();
+}
+
+void WebServices::hidePassword()
+{
+ ui.password->hide();
+ ui.passLabel->hide();
+}
+
+void WebServices::hideUpload()
+{
+ ui.upload->hide();
+ ui.download->show();
+}
+
+void WebServices::hideDownload()
+{
+ ui.download->hide();
+ ui.upload->show();
+}
+
+QNetworkAccessManager *WebServices::manager()
+{
+ static QNetworkAccessManager *manager = new QNetworkAccessManager(qApp);
+ return manager;
+}
+
+void WebServices::downloadTimedOut()
+{
+ if (!reply)
+ return;
+
+ reply->deleteLater();
+ reply = NULL;
+ resetState();
+ ui.status->setText(tr("Operation timed out"));
+}
+
+void WebServices::updateProgress(qint64 current, qint64 total)
+{
+ if (!reply)
+ return;
+ if (total == -1) {
+ total = INT_MAX / 2 - 1;
+ }
+ if (total >= INT_MAX / 2) {
+ // over a gigabyte!
+ if (total >= Q_INT64_C(1) << 47) {
+ total >>= 16;
+ current >>= 16;
+ }
+ total >>= 16;
+ current >>= 16;
+ }
+ ui.progressBar->setRange(0, total);
+ ui.progressBar->setValue(current);
+ ui.status->setText(tr("Transferring data..."));
+
+ // reset the timer: 30 seconds after we last got any data
+ timeout.start();
+}
+
+void WebServices::connectSignalsForDownload(QNetworkReply *reply)
+{
+ connect(reply, SIGNAL(finished()), this, SLOT(downloadFinished()));
+ connect(reply, SIGNAL(error(QNetworkReply::NetworkError)),
+ this, SLOT(downloadError(QNetworkReply::NetworkError)));
+ connect(reply, SIGNAL(downloadProgress(qint64, qint64)), this,
+ SLOT(updateProgress(qint64, qint64)));
+
+ timeout.start(30000); // 30s
+}
+
+void WebServices::resetState()
+{
+ ui.download->setEnabled(true);
+ ui.upload->setEnabled(true);
+ ui.userID->setEnabled(true);
+ ui.password->setEnabled(true);
+ ui.progressBar->reset();
+ ui.progressBar->setRange(0, 1);
+ ui.status->setText(QString());
+ ui.buttonBox->button(QDialogButtonBox::Apply)->setText(defaultApplyText);
+}
+
+// #
+// #
+// # Subsurface Web Service Implementation.
+// #
+// #
+
+SubsurfaceWebServices::SubsurfaceWebServices(QWidget *parent, Qt::WindowFlags f) : WebServices(parent, f)
+{
+ QSettings s;
+ if (!prefs.save_userid_local || !*prefs.userid)
+ ui.userID->setText(s.value("subsurface_webservice_uid").toString().toUpper());
+ else
+ ui.userID->setText(prefs.userid);
+ hidePassword();
+ hideUpload();
+ ui.progressBar->setFormat(tr("Enter User ID and click Download"));
+ ui.progressBar->setRange(0, 1);
+ ui.progressBar->setValue(-1);
+ ui.progressBar->setAlignment(Qt::AlignCenter);
+ ui.saveUidLocal->setChecked(prefs.save_userid_local);
+ QShortcut *close = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), this);
+ connect(close, SIGNAL(activated()), this, SLOT(close()));
+ QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this);
+ connect(quit, SIGNAL(activated()), parent, SLOT(close()));
+}
+
+void SubsurfaceWebServices::buttonClicked(QAbstractButton *button)
+{
+ ui.buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false);
+ switch (ui.buttonBox->buttonRole(button)) {
+ case QDialogButtonBox::ApplyRole: {
+ int i;
+ struct dive *d;
+ struct dive_site *ds;
+ bool changed = false;
+ clear_table(&gps_location_table);
+ QByteArray url = tr("Webservice").toLocal8Bit();
+ parse_xml_buffer(url.data(), downloadedData.data(), downloadedData.length(), &gps_location_table, NULL);
+ // make sure we mark all the dive sites that were created
+ for (i = 0; i < gps_location_table.nr; i++) {
+ d = get_dive_from_table(i, &gps_location_table);
+ ds = get_dive_site_by_uuid(d->dive_site_uuid);
+ if (ds)
+ ds->notes = strdup("SubsurfaceWebservice");
+ }
+ /* now merge the data in the gps_location table into the dive_table */
+ if (merge_locations_into_dives()) {
+ changed = true;
+ mark_divelist_changed(true);
+ MainWindow::instance()->information()->updateDiveInfo();
+ }
+
+ /* store last entered uid in config */
+ QSettings s;
+ QString qDialogUid = ui.userID->text().toUpper();
+ bool qSaveUid = ui.saveUidLocal->checkState();
+ set_save_userid_local(qSaveUid);
+ if (qSaveUid) {
+ QString qSettingUid = s.value("subsurface_webservice_uid").toString();
+ QString qFileUid = QString(prefs.userid);
+ bool s_eq_d = (qSettingUid == qDialogUid);
+ bool d_eq_f = (qDialogUid == qFileUid);
+ if (!d_eq_f || s_eq_d)
+ s.setValue("subsurface_webservice_uid", qDialogUid);
+ set_userid(qDialogUid.toLocal8Bit().data());
+ } else {
+ s.setValue("subsurface_webservice_uid", qDialogUid);
+ }
+ s.sync();
+ hide();
+ close();
+ resetState();
+ /* and now clean up and remove all the extra dive sites that were created */
+ QSet<uint32_t> usedUuids;
+ for_each_dive(i, d) {
+ if (d->dive_site_uuid)
+ usedUuids.insert(d->dive_site_uuid);
+ }
+ for_each_dive_site(i, ds) {
+ if (!usedUuids.contains(ds->uuid) && same_string(ds->notes, "SubsurfaceWebservice")) {
+ delete_dive_site(ds->uuid);
+ i--; // otherwise we skip one site
+ }
+ }
+#ifndef NO_MARBLE
+ // finally now that all the extra GPS fixes that weren't used have been deleted
+ // we can update the globe
+ if (changed) {
+ GlobeGPS::instance()->repopulateLabels();
+ GlobeGPS::instance()->centerOnDiveSite(get_dive_site_by_uuid(current_dive->dive_site_uuid));
+ }
+#endif
+
+ } break;
+ case QDialogButtonBox::RejectRole:
+ if (reply != NULL && reply->isOpen()) {
+ reply->abort();
+ delete reply;
+ reply = NULL;
+ }
+ resetState();
+ break;
+ case QDialogButtonBox::HelpRole:
+ QDesktopServices::openUrl(QUrl("http://api.hohndel.org"));
+ break;
+ default:
+ break;
+ }
+}
+
+void SubsurfaceWebServices::startDownload()
+{
+ QUrl url("http://api.hohndel.org/api/dive/get/");
+ QUrlQuery query;
+ query.addQueryItem("login", ui.userID->text().toUpper());
+ url.setQuery(query);
+
+ QNetworkRequest request;
+ request.setUrl(url);
+ request.setRawHeader("Accept", "text/xml");
+ request.setRawHeader("User-Agent", userAgent.toUtf8());
+ reply = manager()->get(request);
+ ui.status->setText(tr("Connecting..."));
+ ui.progressBar->setEnabled(true);
+ ui.progressBar->setRange(0, 0); // this makes the progressbar do an 'infinite spin'
+ ui.download->setEnabled(false);
+ ui.buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false);
+ connectSignalsForDownload(reply);
+}
+
+void SubsurfaceWebServices::downloadFinished()
+{
+ if (!reply)
+ return;
+
+ ui.progressBar->setRange(0, 1);
+ ui.progressBar->setValue(1);
+ ui.progressBar->setFormat("%p%");
+ downloadedData = reply->readAll();
+
+ ui.download->setEnabled(true);
+ ui.status->setText(tr("Download finished"));
+
+ uint resultCode = download_dialog_parse_response(downloadedData);
+ setStatusText(resultCode);
+ if (resultCode == DD_STATUS_OK) {
+ ui.buttonBox->button(QDialogButtonBox::Apply)->setEnabled(true);
+ }
+ reply->deleteLater();
+ reply = NULL;
+}
+
+void SubsurfaceWebServices::downloadError(QNetworkReply::NetworkError)
+{
+ resetState();
+ ui.status->setText(tr("Download error: %1").arg(reply->errorString()));
+ reply->deleteLater();
+ reply = NULL;
+}
+
+void SubsurfaceWebServices::setStatusText(int status)
+{
+ QString text;
+ switch (status) {
+ case DD_STATUS_ERROR_CONNECT:
+ text = tr("Connection error: ");
+ break;
+ case DD_STATUS_ERROR_ID:
+ text = tr("Invalid user identifier!");
+ break;
+ case DD_STATUS_ERROR_PARSE:
+ text = tr("Cannot parse response!");
+ break;
+ case DD_STATUS_OK:
+ text = tr("Download successful");
+ break;
+ }
+ ui.status->setText(text);
+}
+
+//TODO: C-Code.
+/* requires that there is a <download> or <error> tag under the <root> tag */
+void SubsurfaceWebServices::download_dialog_traverse_xml(xmlNodePtr node, unsigned int *download_status)
+{
+ xmlNodePtr cur_node;
+ for (cur_node = node; cur_node; cur_node = cur_node->next) {
+ if ((!strcmp((const char *)cur_node->name, (const char *)"download")) &&
+ (!strcmp((const char *)xmlNodeGetContent(cur_node), (const char *)"ok"))) {
+ *download_status = DD_STATUS_OK;
+ return;
+ } else if (!strcmp((const char *)cur_node->name, (const char *)"error")) {
+ *download_status = DD_STATUS_ERROR_ID;
+ return;
+ }
+ }
+}
+
+// TODO: C-Code
+unsigned int SubsurfaceWebServices::download_dialog_parse_response(const QByteArray &xml)
+{
+ xmlNodePtr root;
+ xmlDocPtr doc = xmlParseMemory(xml.data(), xml.length());
+ unsigned int status = DD_STATUS_ERROR_PARSE;
+
+ if (!doc)
+ return DD_STATUS_ERROR_PARSE;
+ root = xmlDocGetRootElement(doc);
+ if (!root) {
+ status = DD_STATUS_ERROR_PARSE;
+ goto end;
+ }
+ if (root->children)
+ download_dialog_traverse_xml(root->children, &status);
+end:
+ xmlFreeDoc(doc);
+ return status;
+}
+
+// #
+// #
+// # Divelogs DE Web Service Implementation.
+// #
+// #
+
+struct DiveListResult {
+ QString errorCondition;
+ QString errorDetails;
+ QByteArray idList; // comma-separated, suitable to be sent in the fetch request
+ int idCount;
+};
+
+static DiveListResult parseDiveLogsDeDiveList(const QByteArray &xmlData)
+{
+ /* XML format seems to be:
+ * <DiveDateReader version="1.0">
+ * <DiveDates>
+ * <date diveLogsId="nnn" lastModified="YYYY-MM-DD hh:mm:ss">DD.MM.YYYY hh:mm</date>
+ * [repeat <date></date>]
+ * </DiveDates>
+ * </DiveDateReader>
+ */
+ QXmlStreamReader reader(xmlData);
+ const QString invalidXmlError = QObject::tr("Invalid response from server");
+ bool seenDiveDates = false;
+ DiveListResult result;
+ result.idCount = 0;
+
+ if (reader.readNextStartElement() && reader.name() != "DiveDateReader") {
+ result.errorCondition = invalidXmlError;
+ result.errorDetails =
+ QObject::tr("Expected XML tag 'DiveDateReader', got instead '%1")
+ .arg(reader.name().toString());
+ goto out;
+ }
+
+ while (reader.readNextStartElement()) {
+ if (reader.name() != "DiveDates") {
+ if (reader.name() == "Login") {
+ QString status = reader.readElementText();
+ // qDebug() << "Login status:" << status;
+
+ // Note: there has to be a better way to determine a successful login...
+ if (status == "failed") {
+ result.errorCondition = "Login failed";
+ goto out;
+ }
+ } else {
+ // qDebug() << "Skipping" << reader.name();
+ }
+ continue;
+ }
+
+ // process <DiveDates>
+ seenDiveDates = true;
+ while (reader.readNextStartElement()) {
+ if (reader.name() != "date") {
+ // qDebug() << "Skipping" << reader.name();
+ continue;
+ }
+ QStringRef id = reader.attributes().value("divelogsId");
+ // qDebug() << "Found" << reader.name() << "with id =" << id;
+ if (!id.isEmpty()) {
+ result.idList += id.toLatin1();
+ result.idList += ',';
+ ++result.idCount;
+ }
+
+ reader.skipCurrentElement();
+ }
+ }
+
+ // chop the ending comma, if any
+ result.idList.chop(1);
+
+ if (!seenDiveDates) {
+ result.errorCondition = invalidXmlError;
+ result.errorDetails = QObject::tr("Expected XML tag 'DiveDates' not found");
+ }
+
+out:
+ if (reader.hasError()) {
+ // if there was an XML error, overwrite the result or other error conditions
+ result.errorCondition = invalidXmlError;
+ result.errorDetails = QObject::tr("Malformed XML response. Line %1: %2")
+ .arg(reader.lineNumber())
+ .arg(reader.errorString());
+ }
+ return result;
+}
+
+DivelogsDeWebServices *DivelogsDeWebServices::instance()
+{
+ static DivelogsDeWebServices *self = new DivelogsDeWebServices(MainWindow::instance());
+ self->setAttribute(Qt::WA_QuitOnClose, false);
+ return self;
+}
+
+void DivelogsDeWebServices::downloadDives()
+{
+ uploadMode = false;
+ resetState();
+ hideUpload();
+ exec();
+}
+
+void DivelogsDeWebServices::prepareDivesForUpload(bool selected)
+{
+ /* generate a random filename and create/open that file with zip_open */
+ QString filename = QDir::tempPath() + "/import-" + QString::number(qrand() % 99999999) + ".dld";
+ if (prepare_dives_for_divelogs(filename, selected)) {
+ QFile f(filename);
+ if (f.open(QIODevice::ReadOnly)) {
+ uploadDives((QIODevice *)&f);
+ f.close();
+ f.remove();
+ return;
+ } else {
+ report_error("Failed to open upload file %s\n", qPrintable(filename));
+ }
+ } else {
+ report_error("Failed to create upload file %s\n", qPrintable(filename));
+ }
+ MainWindow::instance()->getNotificationWidget()->showNotification(get_error_string(), KMessageWidget::Error);
+}
+
+void DivelogsDeWebServices::uploadDives(QIODevice *dldContent)
+{
+ QHttpMultiPart mp(QHttpMultiPart::FormDataType);
+ QHttpPart part;
+ QFile *f = (QFile *)dldContent;
+ QFileInfo fi(*f);
+ QString args("form-data; name=\"userfile\"; filename=\"" + fi.absoluteFilePath() + "\"");
+ part.setRawHeader("Content-Disposition", args.toLatin1());
+ part.setBodyDevice(dldContent);
+ mp.append(part);
+
+ multipart = &mp;
+ hideDownload();
+ resetState();
+ uploadMode = true;
+ ui.buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(true);
+ ui.buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false);
+ ui.buttonBox->button(QDialogButtonBox::Apply)->setText(tr("Done"));
+ exec();
+
+ multipart = NULL;
+ if (reply != NULL && reply->isOpen()) {
+ reply->abort();
+ delete reply;
+ reply = NULL;
+ }
+}
+
+DivelogsDeWebServices::DivelogsDeWebServices(QWidget *parent, Qt::WindowFlags f) : WebServices(parent, f),
+ multipart(NULL),
+ uploadMode(false)
+{
+ QSettings s;
+ ui.userID->setText(s.value("divelogde_user").toString());
+ ui.password->setText(s.value("divelogde_pass").toString());
+ ui.saveUidLocal->hide();
+ hideUpload();
+ QShortcut *close = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), this);
+ connect(close, SIGNAL(activated()), this, SLOT(close()));
+ QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this);
+ connect(quit, SIGNAL(activated()), parent, SLOT(close()));
+}
+
+void DivelogsDeWebServices::startUpload()
+{
+ QSettings s;
+ s.setValue("divelogde_user", ui.userID->text());
+ s.setValue("divelogde_pass", ui.password->text());
+ s.sync();
+
+ ui.status->setText(tr("Uploading dive list..."));
+ ui.progressBar->setRange(0, 0); // this makes the progressbar do an 'infinite spin'
+ ui.upload->setEnabled(false);
+ ui.userID->setEnabled(false);
+ ui.password->setEnabled(false);
+
+ QNetworkRequest request;
+ request.setUrl(QUrl("https://divelogs.de/DivelogsDirectImport.php"));
+ request.setRawHeader("Accept", "text/xml, application/xml");
+ request.setRawHeader("User-Agent", userAgent.toUtf8());
+
+ QHttpPart part;
+ part.setRawHeader("Content-Disposition", "form-data; name=\"user\"");
+ part.setBody(ui.userID->text().toUtf8());
+ multipart->append(part);
+
+ part.setRawHeader("Content-Disposition", "form-data; name=\"pass\"");
+ part.setBody(ui.password->text().toUtf8());
+ multipart->append(part);
+
+ reply = manager()->post(request, multipart);
+ connect(reply, SIGNAL(finished()), this, SLOT(uploadFinished()));
+ connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this,
+ SLOT(uploadError(QNetworkReply::NetworkError)));
+ connect(reply, SIGNAL(uploadProgress(qint64, qint64)), this,
+ SLOT(updateProgress(qint64, qint64)));
+
+ timeout.start(30000); // 30s
+}
+
+void DivelogsDeWebServices::startDownload()
+{
+ ui.status->setText(tr("Downloading dive list..."));
+ ui.progressBar->setRange(0, 0); // this makes the progressbar do an 'infinite spin'
+ ui.download->setEnabled(false);
+ ui.userID->setEnabled(false);
+ ui.password->setEnabled(false);
+
+ QNetworkRequest request;
+ request.setUrl(QUrl("https://divelogs.de/xml_available_dives.php"));
+ request.setRawHeader("Accept", "text/xml, application/xml");
+ request.setRawHeader("User-Agent", userAgent.toUtf8());
+ request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
+
+ QUrlQuery body;
+ body.addQueryItem("user", ui.userID->text());
+ body.addQueryItem("pass", ui.password->text());
+
+ reply = manager()->post(request, body.query(QUrl::FullyEncoded).toLatin1());
+ connect(reply, SIGNAL(finished()), this, SLOT(listDownloadFinished()));
+ connect(reply, SIGNAL(error(QNetworkReply::NetworkError)),
+ this, SLOT(downloadError(QNetworkReply::NetworkError)));
+
+ timeout.start(30000); // 30s
+}
+
+void DivelogsDeWebServices::listDownloadFinished()
+{
+ if (!reply)
+ return;
+ QByteArray xmlData = reply->readAll();
+ reply->deleteLater();
+ reply = NULL;
+
+ // parse the XML data we downloaded
+ DiveListResult diveList = parseDiveLogsDeDiveList(xmlData);
+ if (!diveList.errorCondition.isEmpty()) {
+ // error condition
+ resetState();
+ ui.status->setText(diveList.errorCondition);
+ return;
+ }
+
+ ui.status->setText(tr("Downloading %1 dives...").arg(diveList.idCount));
+
+ QNetworkRequest request;
+ request.setUrl(QUrl("https://divelogs.de/DivelogsDirectExport.php"));
+ request.setRawHeader("Accept", "application/zip, */*");
+ request.setRawHeader("User-Agent", userAgent.toUtf8());
+ request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
+
+ QUrlQuery body;
+ body.addQueryItem("user", ui.userID->text());
+ body.addQueryItem("pass", ui.password->text());
+ body.addQueryItem("ids", diveList.idList);
+
+ reply = manager()->post(request, body.query(QUrl::FullyEncoded).toLatin1());
+ connect(reply, SIGNAL(readyRead()), this, SLOT(saveToZipFile()));
+ connectSignalsForDownload(reply);
+}
+
+void DivelogsDeWebServices::saveToZipFile()
+{
+ if (!zipFile.isOpen()) {
+ zipFile.setFileTemplate(QDir::tempPath() + "/import-XXXXXX.dld");
+ zipFile.open();
+ }
+
+ zipFile.write(reply->readAll());
+}
+
+void DivelogsDeWebServices::downloadFinished()
+{
+ if (!reply)
+ return;
+
+ ui.download->setEnabled(true);
+ ui.status->setText(tr("Download finished - %1").arg(reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString()));
+ reply->deleteLater();
+ reply = NULL;
+
+ int errorcode;
+ zipFile.seek(0);
+#if defined(Q_OS_UNIX) && defined(LIBZIP_VERSION_MAJOR)
+ int duppedfd = dup(zipFile.handle());
+ struct zip *zip = NULL;
+ if (duppedfd >= 0) {
+ zip = zip_fdopen(duppedfd, 0, &errorcode);
+ if (!zip)
+ ::close(duppedfd);
+ } else {
+ QMessageBox::critical(this, tr("Problem with download"),
+ tr("The archive could not be opened:\n"));
+ return;
+ }
+#else
+ struct zip *zip = zip_open(QFile::encodeName(zipFile.fileName()), 0, &errorcode);
+#endif
+ if (!zip) {
+ char buf[512];
+ zip_error_to_str(buf, sizeof(buf), errorcode, errno);
+ QMessageBox::critical(this, tr("Corrupted download"),
+ tr("The archive could not be opened:\n%1").arg(QString::fromLocal8Bit(buf)));
+ zipFile.close();
+ return;
+ }
+ // now allow the user to cancel or accept
+ ui.buttonBox->button(QDialogButtonBox::Apply)->setEnabled(true);
+
+ zip_close(zip);
+ zipFile.close();
+#if defined(Q_OS_UNIX) && defined(LIBZIP_VERSION_MAJOR)
+ ::close(duppedfd);
+#endif
+}
+
+void DivelogsDeWebServices::uploadFinished()
+{
+ if (!reply)
+ return;
+
+ ui.progressBar->setRange(0, 1);
+ ui.upload->setEnabled(true);
+ ui.userID->setEnabled(true);
+ ui.password->setEnabled(true);
+ ui.buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(false);
+ ui.buttonBox->button(QDialogButtonBox::Apply)->setEnabled(true);
+ ui.buttonBox->button(QDialogButtonBox::Apply)->setText(tr("Done"));
+ ui.status->setText(tr("Upload finished"));
+
+ // 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;
+ 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>")) {
+ ui.status->setText(tr("Upload failed"));
+ return;
+ }
+ ui.status->setText(tr("Upload successful"));
+ return;
+ }
+ ui.status->setText(tr("Login failed"));
+ return;
+ }
+ ui.status->setText(tr("Cannot parse response"));
+ }
+}
+
+void DivelogsDeWebServices::setStatusText(int status)
+{
+}
+
+void DivelogsDeWebServices::downloadError(QNetworkReply::NetworkError)
+{
+ resetState();
+ ui.status->setText(tr("Error: %1").arg(reply->errorString()));
+ reply->deleteLater();
+ reply = NULL;
+}
+
+void DivelogsDeWebServices::uploadError(QNetworkReply::NetworkError error)
+{
+ downloadError(error);
+}
+
+void DivelogsDeWebServices::buttonClicked(QAbstractButton *button)
+{
+ ui.buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false);
+ switch (ui.buttonBox->buttonRole(button)) {
+ case QDialogButtonBox::ApplyRole: {
+ /* in 'uploadMode' button is called 'Done' and closes the dialog */
+ if (uploadMode) {
+ hide();
+ close();
+ resetState();
+ break;
+ }
+ /* parse file and import dives */
+ parse_file(QFile::encodeName(zipFile.fileName()));
+ process_dives(true, false);
+ MainWindow::instance()->refreshDisplay();
+
+ /* store last entered user/pass in config */
+ QSettings s;
+ s.setValue("divelogde_user", ui.userID->text());
+ s.setValue("divelogde_pass", ui.password->text());
+ s.sync();
+ hide();
+ close();
+ resetState();
+ } break;
+ case QDialogButtonBox::RejectRole:
+ // these two seem to be causing a crash:
+ // reply->deleteLater();
+ resetState();
+ break;
+ case QDialogButtonBox::HelpRole:
+ QDesktopServices::openUrl(QUrl("http://divelogs.de"));
+ break;
+ default:
+ break;
+ }
+}
+
+UserSurveyServices::UserSurveyServices(QWidget *parent, Qt::WindowFlags f) : WebServices(parent, f)
+{
+
+}
+
+QNetworkReply* UserSurveyServices::sendSurvey(QString values)
+{
+ QNetworkRequest request;
+ request.setUrl(QString("http://subsurface-divelog.org/survey?%1").arg(values));
+ request.setRawHeader("Accept", "text/xml");
+ request.setRawHeader("User-Agent", userAgent.toUtf8());
+ reply = manager()->get(request);
+ return reply;
+}
+
+CloudStorageAuthenticate::CloudStorageAuthenticate(QObject *parent) :
+ QObject(parent),
+ reply(NULL)
+{
+ userAgent = getUserAgent();
+}
+
+#define CLOUDURL QString(prefs.cloud_base_url)
+#define CLOUDBACKENDSTORAGE CLOUDURL + "/storage"
+#define CLOUDBACKENDVERIFY CLOUDURL + "/verify"
+#define CLOUDBACKENDUPDATE CLOUDURL + "/update"
+
+QNetworkReply* CloudStorageAuthenticate::backend(QString email, QString password, QString pin, QString newpasswd)
+{
+ QString payload(email + " " + password);
+ QUrl requestUrl;
+ if (pin == "" && newpasswd == "") {
+ requestUrl = QUrl(CLOUDBACKENDSTORAGE);
+ } else if (newpasswd != "") {
+ requestUrl = QUrl(CLOUDBACKENDUPDATE);
+ payload += " " + newpasswd;
+ } else {
+ requestUrl = QUrl(CLOUDBACKENDVERIFY);
+ payload += " " + pin;
+ }
+ QNetworkRequest *request = new QNetworkRequest(requestUrl);
+ request->setRawHeader("Accept", "text/xml, text/plain");
+ request->setRawHeader("User-Agent", userAgent.toUtf8());
+ request->setHeader(QNetworkRequest::ContentTypeHeader, "text/plain");
+ reply = WebServices::manager()->post(*request, qPrintable(payload));
+ connect(reply, SIGNAL(finished()), this, SLOT(uploadFinished()));
+ connect(reply, SIGNAL(sslErrors(QList<QSslError>)), this, SLOT(sslErrors(QList<QSslError>)));
+ connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this,
+ SLOT(uploadError(QNetworkReply::NetworkError)));
+ return reply;
+}
+
+void CloudStorageAuthenticate::uploadFinished()
+{
+ static QString myLastError;
+
+ QString cloudAuthReply(reply->readAll());
+ qDebug() << "Completed connection with cloud storage backend, response" << cloudAuthReply;
+ if (cloudAuthReply == "[VERIFIED]" || cloudAuthReply == "[OK]") {
+ prefs.cloud_verification_status = CS_VERIFIED;
+ NotificationWidget *nw = MainWindow::instance()->getNotificationWidget();
+ if (nw->getNotificationText() == myLastError)
+ nw->hideNotification();
+ myLastError.clear();
+ } else if (cloudAuthReply == "[VERIFY]") {
+ prefs.cloud_verification_status = CS_NEED_TO_VERIFY;
+ } else if (cloudAuthReply == "[PASSWDCHANGED]") {
+ free(prefs.cloud_storage_password);
+ prefs.cloud_storage_password = prefs.cloud_storage_newpassword;
+ prefs.cloud_storage_newpassword = NULL;
+ emit passwordChangeSuccessful();
+ return;
+ } else {
+ prefs.cloud_verification_status = CS_INCORRECT_USER_PASSWD;
+ myLastError = cloudAuthReply;
+ report_error("%s", qPrintable(cloudAuthReply));
+ MainWindow::instance()->getNotificationWidget()->showNotification(get_error_string(), KMessageWidget::Error);
+ }
+ emit finishedAuthenticate();
+}
+
+void CloudStorageAuthenticate::uploadError(QNetworkReply::NetworkError error)
+{
+ qDebug() << "Received error response from cloud storage backend:" << reply->errorString();
+}
+
+void CloudStorageAuthenticate::sslErrors(QList<QSslError> errorList)
+{
+ if (verbose) {
+ qDebug() << "Received error response trying to set up https connection with cloud storage backend:";
+ Q_FOREACH (QSslError err, errorList) {
+ qDebug() << err.errorString();
+ }
+ }
+ QSslConfiguration conf = reply->sslConfiguration();
+ QSslCertificate cert = conf.peerCertificate();
+ QByteArray hexDigest = cert.digest().toHex();
+ if (reply->url().toString().contains(prefs.cloud_base_url) &&
+ hexDigest == "13ff44c62996cfa5cd69d6810675490e") {
+ if (verbose)
+ qDebug() << "Overriding SSL check as I recognize the certificate digest" << hexDigest;
+ reply->ignoreSslErrors();
+ } else {
+ if (verbose)
+ qDebug() << "got invalid SSL certificate with hex digest" << hexDigest;
+ }
+}