diff options
Diffstat (limited to 'desktop-widgets/subsurfacewebservices.cpp')
-rw-r--r-- | desktop-widgets/subsurfacewebservices.cpp | 1121 |
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 = ∓ + 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; + } +} |