// SPDX-License-Identifier: GPL-2.0 #include "desktop-widgets/subsurfacewebservices.h" #include "core/qthelper.h" #include "core/namecmp.h" #include "core/webservice.h" #include "core/settings/qPrefCloudStorage.h" #include "desktop-widgets/mainwindow.h" #include "commands/command.h" #include "core/device.h" #include "core/divelist.h" // For IMPORT_MERGE_ALL_TRIPS #include "core/divelog.h" #include "core/errorhelper.h" #include "core/file.h" #include "desktop-widgets/mapwidget.h" #include "desktop-widgets/tab-widgets/maintab.h" #include "core/selection.h" #include "core/cloudstorage.h" #include "core/subsurface-string.h" #include "core/uploadDiveLogsDE.h" #include "core/settings/qPrefCloudStorage.h" #include #include #include #include #include #include #include #include #include #ifdef Q_OS_UNIX #include // for dup(2) #endif #include #ifndef PATH_MAX #define PATH_MAX 4096 #endif WebServices::WebServices(QWidget *parent) : QDialog(parent, QFlag(0)), 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(); } 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) { #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) connect(reply, &QNetworkReply::finished, this, &WebServices::downloadFinished); connect(reply, &QNetworkReply::errorOccurred, this, &WebServices::downloadError); connect(reply, &QNetworkReply::downloadProgress, this, &WebServices::updateProgress); #else 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))); #endif 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); } // # // # // # 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: * * * DD.MM.YYYY hh:mm * [repeat ] * * */ QXmlStreamReader reader(xmlData); const QString invalidXmlError = gettextFromC::tr("Invalid response from server"); bool seenDiveDates = false; DiveListResult result; result.idCount = 0; if (reader.readNextStartElement() && nameCmp(reader, "DiveDateReader") != 0) { result.errorCondition = invalidXmlError; result.errorDetails = gettextFromC::tr("Expected XML tag 'DiveDateReader', got instead '%1") .arg(reader.name().toString()); goto out; } while (reader.readNextStartElement()) { if (nameCmp(reader, "DiveDates") != 0) { if (nameCmp(reader, "Login") == 0) { 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 seenDiveDates = true; while (reader.readNextStartElement()) { if (nameCmp(reader, "date") != 0) { // qDebug() << "Skipping" << reader.name(); continue; } auto 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 = gettextFromC::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 = gettextFromC::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()); return self; } void DivelogsDeWebServices::downloadDives() { uploadMode = false; resetState(); hideUpload(); exec(); } void DivelogsDeWebServices::prepareDivesForUpload(bool selected) { // this is called when the user selects the divelogs.de radiobutton // Remember if all dives or selected dives are to be uploaded useSelectedDives = selected; // Adjust UI 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(); } DivelogsDeWebServices::DivelogsDeWebServices(QWidget *parent) : WebServices(parent), uploadMode(false) { // should DivelogDE user and pass be stored in the prefs struct or something? ui.userID->setText(qPrefCloudStorage::divelogde_user()); ui.password->setText(qPrefCloudStorage::divelogde_pass()); 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() { qPrefCloudStorage::set_divelogde_user(ui.userID->text()); qPrefCloudStorage::set_divelogde_pass(ui.password->text()); 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); // do upload in shared backend connect(uploadDiveLogsDE::instance(), SIGNAL(uploadFinish(bool, const QString &)), this, SLOT(uploadFinished(bool, const QString &))); connect(uploadDiveLogsDE::instance(), SIGNAL(uploadProgress(qreal, qreal)), this, SLOT(updateProgress(qreal, qreal))); connect(uploadDiveLogsDE::instance(), SIGNAL(uploadStatus(const QString &)), this, SLOT(uploadStatus(const QString &))); uploadDiveLogsDE::instance()->doUpload(useSelectedDives, qPrefCloudStorage::divelogde_user(), qPrefCloudStorage::divelogde_pass()); } 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().replace("+", "%2b")); reply = manager()->post(request, body.query(QUrl::FullyEncoded).toLatin1()); #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) connect(reply, &QNetworkReply::finished, this, &DivelogsDeWebServices::listDownloadFinished); connect(reply, &QNetworkReply::errorOccurred, this, &DivelogsDeWebServices::downloadError); #else connect(reply, SIGNAL(finished()), this, SLOT(listDownloadFinished())); connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError))); #endif 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().replace("+", "%2b")); 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%1").arg(QString::fromLocal8Bit(strerror(errno)))); 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(bool success, const QString &text) { 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(text); } void DivelogsDeWebServices::setStatusText(int) { } void DivelogsDeWebServices::downloadError(QNetworkReply::NetworkError) { resetState(); ui.status->setText(tr("Error: %1").arg(reply->errorString())); reply->deleteLater(); reply = NULL; } void DivelogsDeWebServices::updateProgress(qreal current, qreal total) { ui.progressBar->setRange(0, lrint(total)); ui.progressBar->setValue(lrint(current)); ui.status->setText(tr("Transferring data...")); } void DivelogsDeWebServices::uploadStatus(const QString &text) { ui.status->setText(text); } 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 */ struct divelog log; parse_file(QFile::encodeName(zipFile.fileName()), &log); Command::importDives(&log, IMPORT_MERGE_ALL_TRIPS, QStringLiteral("divelogs.de")); /* store last entered user/pass in config */ qPrefCloudStorage::set_divelogde_user(ui.userID->text()); qPrefCloudStorage::set_divelogde_pass(ui.password->text()); 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; } }