diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c1266f2e..309c0d3f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -102,7 +102,7 @@ jobs: - name: Install dependencies run: | sudo apt-get update -qq - sudo apt-get -y install gdb libgdal-dev libproj-dev qt6-base-dev qt6-base-dev-tools qt6-tools-dev build-essential libgl1-mesa-dev cmake git libexiv2-dev libqt6svg* libqt6core5compat* qt6-l10n-tools qt6-tools-dev-tools protobuf-compiler + sudo apt-get -y install gdb libgdal-dev libproj-dev qt6-base-dev qt6-base-dev-tools qt6-tools-dev build-essential libgl1-mesa-dev cmake git libexiv2-dev libqt6svg* libqt6networkauth* libqt6core5compat* qt6-l10n-tools qt6-tools-dev-tools protobuf-compiler - name: Build run: | ./ci/travis-linux-script.sh @@ -117,7 +117,7 @@ jobs: - name: Install dependencies run: | sudo apt-get update -qq - sudo apt-get -y install gdb libgdal-dev libproj-dev qt6-base-dev qt6-base-dev-tools qt6-tools-dev build-essential libgl1-mesa-dev cmake git libexiv2-dev libqt6svg* libqt6core5compat* qt6-l10n-tools qt6-tools-dev-tools protobuf-compiler + sudo apt-get -y install gdb libgdal-dev libproj-dev qt6-base-dev qt6-base-dev-tools qt6-tools-dev build-essential libgl1-mesa-dev cmake git libexiv2-dev libqt6svg* libqt6networkauth* libqt6core5compat* qt6-l10n-tools qt6-tools-dev-tools protobuf-compiler sudo apt-get -y install python3-pip libglib2.0-dev bash dash squashfs-tools zsync fakeroot sudo pip install git+https://github.com/AppImageCrafters/appimage-builder.git - name: Build diff --git a/CMakeLists.txt b/CMakeLists.txt index e107439d..aa134cfa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -48,7 +48,7 @@ set(CMAKE_AUTORCC ON) ############################################## # Find Qt dependencies ############################################## -set(MERKAARTOR_QT_LIBS Svg Network Xml Core Gui Concurrent PrintSupport Widgets Test) +set(MERKAARTOR_QT_LIBS Svg Network NetworkAuth Xml Core Gui Concurrent PrintSupport Widgets Test) set(MERKAARTOR_QT_TOOLS LinguistTools) find_package(QT NAMES Qt5 Qt6 REQUIRED COMPONENTS Core) message(STATUS " * Qt version found: ${QT_VERSION}") @@ -264,6 +264,10 @@ src/Utils/SlippyMapWidget.cpp src/Utils/SlippyMapWidget.h src/Utils/Utils.h src/Utils/OsmLink.cpp +src/Utils/OsmServer.cpp +src/Utils/OsmOAuth2Flow.cpp +src/Utils/OAuth2OOBDialog.ui +src/Utils/OAuth2OOBDialog.cpp src/Utils/ShortcutOverrideFilter.cpp src/Utils/LineF.h src/Utils/SelectionDialog.ui diff --git a/HACKING.md b/HACKING.md index 3842a162..5be3dabf 100644 --- a/HACKING.md +++ b/HACKING.md @@ -83,4 +83,16 @@ The TCL framework also had the convention of "I" for second-level initialisation of objects after creation (possibly due to limitations in the compiler support of C++ features - I don't remember). +## Logging +Some of the (newer) classes use QLoggingCategory for log filtering. Debug logs are disabled by default, but can be enabled at runtime using environment variable: + +``` +QT_LOGGING_RULES="merk..debug=true" +``` + +Or for all classes: + +``` +QT_LOGGING_RULES="merk.*.debug=true" +``` diff --git a/ci/travis-linux-install.sh b/ci/travis-linux-install.sh index ec64ced8..9cca9fa8 100755 --- a/ci/travis-linux-install.sh +++ b/ci/travis-linux-install.sh @@ -9,4 +9,4 @@ sudo echo "deb http://archive.ubuntu.com/ubuntu ${TRAVIS_DIST} main universe res sudo apt-add-repository 'deb https://apt.kitware.com/ubuntu/ focal main' sudo apt-add-repository -y ${QT_REPO} sudo apt-get update -qq -sudo apt-get -y install gdb libgdal-dev libproj-dev qt${QT_PREFIX}base qt${QT_PREFIX}tools qt${QT_PREFIX}svg build-essential libgl1-mesa-dev cmake git libexiv2-dev protobuf-compiler +sudo apt-get -y install gdb libgdal-dev libproj-dev qt${QT_PREFIX}base qt${QT_PREFIX}tools qt${QT_PREFIX}svg qt${QT_PREFIX}networkauth-no-lgpl build-essential libgl1-mesa-dev cmake git libexiv2-dev protobuf-compiler diff --git a/cmake/AppImageBuilder.yml.in b/cmake/AppImageBuilder.yml.in index 40b68199..14e66e40 100644 --- a/cmake/AppImageBuilder.yml.in +++ b/cmake/AppImageBuilder.yml.in @@ -30,6 +30,7 @@ AppDir: - libqt6dbus6 - libqt6svg6 - libqt6core5compat6 + - libqt6networkauth6 - qt6-qpa-plugins files: @@ -49,5 +50,6 @@ AppDir: AppImage: update-information: None sign-key: None + comp: xz arch: x86_64 comp: xz diff --git a/src/Docks/InfoDock.cpp b/src/Docks/InfoDock.cpp index 744e880a..86682147 100644 --- a/src/Docks/InfoDock.cpp +++ b/src/Docks/InfoDock.cpp @@ -80,7 +80,7 @@ void InfoDock::on_anchorClicked(const QUrl & link) // } else { // QMessageBox::warning(Main,QApplication::translate("Downloader","Download failed"),QApplication::translate("Downloader","Unexpected http status code (%1)").arg(theDownloader.resultCode())); // } - QUrl theUrl(M_PREFS->getOsmWebsite()+link.path()); + QUrl theUrl(M_PREFS->getOsmServer()->getServerInfo().Url+link.path()); QDesktopServices::openUrl(theUrl); } diff --git a/src/ImportExport/ImportOSM.cpp b/src/ImportExport/ImportOSM.cpp index 3caf958d..94da5850 100644 --- a/src/ImportExport/ImportOSM.cpp +++ b/src/ImportExport/ImportOSM.cpp @@ -343,7 +343,7 @@ static bool downloadToResolve(const QList& Resolution, QWidget* aParen for (int i=0; igetOsmApiUrl()+theDownloader->getURLToFetchFull(Resolution[i]); + QUrl URL = theDownloader->getURLToFetchFull(Resolution[i]); Lbl->setText(QApplication::translate("Downloader","Downloading unresolved %1 of %2").arg(i+1).arg(Resolution.size())); if (theDownloader->go(URL)) { diff --git a/src/Layers/Layer.cpp b/src/Layers/Layer.cpp index 1337fc7c..5cbbeb5d 100644 --- a/src/Layers/Layer.cpp +++ b/src/Layers/Layer.cpp @@ -831,7 +831,7 @@ LayerWidget* SpecialLayer::newWidget(void) void SpecialLayer::refreshLayer() { if (m_type == Layer::MapDustLayer) { - g_Merk_MainWindow->on_layersMapdustAction_triggered(); + /* FIXME: Remove mapdust type */ } } diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index f7830fa0..96169e51 100755 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -102,6 +102,7 @@ #include "gdal_version.h" #include "Utils/SlippyMapWidget.h" +#include "Utils/OsmServer.h" namespace { @@ -2018,6 +2019,8 @@ void MainWindow::on_fileUploadAction_triggered() return; } + /* TODO: Check if the server list is empty? */ + /* while (M_PREFS->getOsmUser().isEmpty()) { int ret = QMessageBox::warning(this, tr("Upload OSM"), tr("You don't seem to have specified your\n" "OpenStreetMap username and password.\nDo you want to do this now?"), QMessageBox::Yes | QMessageBox::No); @@ -2026,8 +2029,10 @@ void MainWindow::on_fileUploadAction_triggered() } else return; } + */ on_editPropertiesAction_triggered(); - syncOSM(M_PREFS->getOsmApiUrl(), M_PREFS->getOsmUser(), M_PREFS->getOsmPassword()); + // TODO: Replace this call to use a the OsmServer object instead of individual parameters. + syncOSM(M_PREFS->getOsmServer()); theDocument->history().updateActions(); theDirty->updateList(); @@ -2067,28 +2072,6 @@ void MainWindow::on_fileDownloadMoreAction_triggered() emit content_changed(); } -void MainWindow::on_layersMapdustAction_triggered() -{ - SpecialLayer* sl = NULL; - for (int i=0; ilayerSize(); ++i) { - if (theDocument->getLayer(i)->classType() == Layer::MapDustLayer) { - sl = dynamic_cast(theDocument->getLayer(i)); - while (sl->size()) - { - sl->deleteFeature(sl->get(0)); - } - } - } - - createProgressDialog(); - - if (!::downloadMapdust(this, theView->viewport(), theDocument, sl)) { - QMessageBox::warning(this, tr("Error downloading MapDust"), tr("The MapDust bugs could not be downloaded")); - } - - deleteProgressDialog(); -} - void MainWindow::downloadFeatures(const QList& aDownloadList) { createProgressDialog(); @@ -4294,7 +4277,7 @@ bool MainWindow::hasUnsavedChanges() return true; } -void MainWindow::syncOSM(const QString& aWeb, const QString& aUser, const QString& aPwd) +void MainWindow::syncOSM(OsmServer server) { #ifndef FRISIUS_BUILD if (checkForConflicts(theDocument)) { @@ -4305,10 +4288,10 @@ void MainWindow::syncOSM(const QString& aWeb, const QString& aUser, const QStrin DirtyListBuild Future; theDocument->history().buildDirtyList(Future); DirtyListDescriber Describer(theDocument,Future); - ChangesetInfo changesetInfo = {aWeb, "", ""}; + ChangesetInfo changesetInfo = {server->getServerInfo().Url, "", ""}; if (Describer.showChanges(this, changesetInfo) && Describer.tasks()) { Future.resetUpdates(); - DirtyListExecutorOSC Exec(theDocument,Future,changesetInfo,aWeb,aUser,aPwd,Describer.tasks()); + DirtyListExecutorOSC Exec(theDocument,Future,changesetInfo,server,Describer.tasks()); if (Exec.executeChanges(this)) { if (M_PREFS->getAutoHistoryCleanup() && !theDocument->getDirtyOrOriginLayer()->getDirtySize()) theDocument->history().cleanup(); diff --git a/src/MainWindow.h b/src/MainWindow.h index 3e40e210..9b2ce1e3 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -9,6 +9,8 @@ #include #include +#include "OsmServer.h" + namespace Ui { class MainWindow; } @@ -77,7 +79,6 @@ public slots: virtual void on_layersNewImageAction_triggered(); virtual void on_layersNewDrawingAction_triggered(); virtual void on_layersNewFilterAction_triggered(); - virtual void on_layersMapdustAction_triggered(); virtual void on_fileNewAction_triggered(); virtual void on_fileDownloadAction_triggered(); @@ -340,7 +341,7 @@ private slots: void startBusyCursor(); void endBusyCursor(); - void syncOSM(const QString &aWeb, const QString &aUser, const QString &aPwd); + void syncOSM(OsmServer srv); protected: void closeEvent(QCloseEvent * event); virtual QMenu * createPopupMenu (); diff --git a/src/Preferences/MerkaartorPreferences.cpp b/src/Preferences/MerkaartorPreferences.cpp index a2d79df3..25c7a16c 100644 --- a/src/Preferences/MerkaartorPreferences.cpp +++ b/src/Preferences/MerkaartorPreferences.cpp @@ -227,6 +227,7 @@ Tool::Tool() namespace { + QSettings* getSettings() { if (!g_Merk_Portable) { return new QSettings(); @@ -253,9 +254,7 @@ MerkaartorPreferences::MerkaartorPreferences() version = Sets->value("version/version", "0").toString(); } - connect(&httpRequest, SIGNAL(authenticationRequired(QNetworkReply*, QAuthenticator*)), this, SLOT(on_authenticationRequired(QNetworkReply*, QAuthenticator*))); connect(&httpRequest, SIGNAL(finished(QNetworkReply*)),this,SLOT(on_requestFinished(QNetworkReply*))); - connect(&httpRequest, SIGNAL(sslErrors(QNetworkReply *, const QList&)), this, SLOT(on_sslErrors(QNetworkReply*, const QList&))); #ifdef USE_LIBPROXY // Initialise libproxy @@ -308,6 +307,7 @@ void MerkaartorPreferences::save(bool UserPwdChanged) void MerkaartorPreferences::toOsmPref() { + /* FIXME: Refactor. qDebug(lc_MerkaartorPreferences) << "MerkaartorPreferences::toOsmPref"; if (getOfflineMode()) return; @@ -356,10 +356,12 @@ void MerkaartorPreferences::toOsmPref() putOsmPref(it.key(), it.value()); } + */ } void MerkaartorPreferences::fromOsmPref() { + /* FIXME: Refactor. if (getOfflineMode()) return; qDebug(lc_MerkaartorPreferences) << "Requesting preferences from OSM server."; @@ -373,35 +375,23 @@ void MerkaartorPreferences::fromOsmPref() httpRequest.setProxy(getProxy(osmWeb)); OsmPrefLoadReply = httpRequest.get(req); -} - -void MerkaartorPreferences::on_authenticationRequired( QNetworkReply *reply, QAuthenticator *auth ) { - static QNetworkReply *lastReply = NULL; - - /* Only provide authentication the first time we see this reply, to avoid - * infinite loop providing the same credentials. */ - if (lastReply != reply) { - lastReply = reply; - qDebug(lc_MerkaartorPreferences) << "Authentication required and provided."; - auth->setUser(getOsmUser()); - auth->setPassword(getOsmPassword()); - } -} - -void MerkaartorPreferences::on_sslErrors(QNetworkReply *reply, const QList& errors) { - Q_UNUSED(reply); - qDebug(lc_MerkaartorPreferences) << "We stumbled upon some SSL errors: "; - foreach ( QSslError error, errors ) { - qDebug(lc_MerkaartorPreferences) << "1:"; - qDebug(lc_MerkaartorPreferences) << error.errorString(); - } + */ } void MerkaartorPreferences::on_requestFinished ( QNetworkReply *reply ) { int error = reply->error(); + + if (reply != OsmPrefLoadReply) { + qWarning(lc_MerkaartorPreferences) << "Ignoring foreign network request with URL: " << reply->url(); + return; + } + if (error != QNetworkReply::NoError) { - //qDebug(lc_MerkaartorPreferences) << "Received response with code " << error << "(" << reply->errorString() << ")"; + qDebug(lc_MerkaartorPreferences) << "Error: " << reply->errorString(); + qDebug(lc_MerkaartorPreferences) << "Received response with code " << error; + qDebug(lc_MerkaartorPreferences) << "Url was:" << reply->url(); + switch (error) { case QNetworkReply::HostNotFoundError: qWarning() << "MerkaartorPreferences: Host not found, preferences won't be synchronized with your profile."; @@ -420,9 +410,6 @@ void MerkaartorPreferences::on_requestFinished ( QNetworkReply *reply ) } } - if (reply != OsmPrefLoadReply) - return; - qDebug(lc_MerkaartorPreferences) << "Reading preferences from online profile."; QDomDocument aOsmPrefDoc; @@ -508,6 +495,7 @@ void MerkaartorPreferences::on_requestFinished ( QNetworkReply *reply ) void MerkaartorPreferences::putOsmPref(const QString& k, const QString& v) { + /* FIXME: Refactor. qDebug(lc_MerkaartorPreferences) << "Saving OSM preference online: " << k << "=" << v; QUrl osmWeb(getOsmApiUrl()+QString("/user/preferences/%1").arg(k)); @@ -518,10 +506,12 @@ void MerkaartorPreferences::putOsmPref(const QString& k, const QString& v) httpRequest.setProxy(getProxy(osmWeb)); OsmPrefSaveReply = httpRequest.put(req, ba); + */ } void MerkaartorPreferences::deleteOsmPref(const QString& k) { + /* FIXME: Refactor. qDebug(lc_MerkaartorPreferences) << "Deleting OSM preference online: " << k; QUrl osmWeb(getOsmApiUrl()+QString("/user/preferences/%1").arg(k)); @@ -530,6 +520,7 @@ void MerkaartorPreferences::deleteOsmPref(const QString& k) httpRequest.setProxy(getProxy(osmWeb)); OsmPrefSaveReply = httpRequest.sendCustomRequest(req,"DELETE"); + */ } void MerkaartorPreferences::initialize() @@ -628,6 +619,17 @@ void MerkaartorPreferences::initialize() parentDashes << 1 << 5; + bool serversMigrated = false; + for (auto &srv: theOsmServers) { + serversMigrated |= migrateOsmServerInfo(srv); + } + if (serversMigrated) { + QMessageBox::information(nullptr, + tr("Osm Server Migration"), + tr("Some OSM server information has been migrated to the new format. Please visit preferences and login to the servers.")); + + } + //Ensure we have a CacheDir value in QSettings if (!g_Merk_Ignore_Preferences) Sets->setValue("backgroundImage/CacheDir", Sets->value("backgroundImage/CacheDir", HOMEDIR + "/BackgroundCache")); @@ -948,94 +950,20 @@ M_PARAM_IMPLEMENT_BOOL(HideToolbarLabels, interface, false) /* DATA */ -QString MerkaartorPreferences::getOsmWebsite() const -{ - QString s; - if (!g_Merk_Ignore_Preferences && !g_Merk_Reset_Preferences) - s = Sets->value("osm/Website", "www.openstreetmap.org").toString(); - else - s = "www.openstreetmap.org"; - -#if QT_VERSION >= 0x040600 && !defined(FORCE_46) - QUrl u = QUrl::fromUserInput(s); -#else - // convenience for creating a valid URL - // fails miserably if QString s already contains a schema - QString h = s; // intermediate host - QString p; // intermediate path - - int slashpos = s.indexOf('/'); - if (slashpos >= 1) // there's a path element in s - { - h = s.left(slashpos); - p = s.right(s.size() - 1 - slashpos); - } +M_PARAM_IMPLEMENT_INT(OsmServerIndex, osm, 0) - QUrl u; - u.setHost(h); - u.setScheme("http"); - u.setPath(p); -#endif - - if (!u.path().isEmpty()) - u.setPath(QString()); - - return u.toString(); -} - - -QString MerkaartorPreferences::getOsmApiUrl() const -{ - QUrl u(getOsmWebsite()); - if (u.path().isEmpty()) - u.setPath("/api/" + apiVersion()); - - return u.toString(); -} - -void MerkaartorPreferences::setOsmWebsite(const QString & theValue) -{ - if (!g_Merk_Ignore_Preferences) - Sets->setValue("osm/Website", theValue); +std::shared_ptr MerkaartorPreferences::getOsmServer() { + /* TODO: Each call creates a new server. */ + OsmServerList* servers = getOsmServers(); + return makeOsmServer((*servers)[getOsmServerIndex()], httpRequest); } M_PARAM_IMPLEMENT_STRING(XapiUrl, osm, "http://www.overpass-api.de/api/xapi_meta?") M_PARAM_IMPLEMENT_STRING(NominatimUrl, osm, "http://nominatim.openstreetmap.org/search") M_PARAM_IMPLEMENT_BOOL(AutoHistoryCleanup, data, true); - -QString MerkaartorPreferences::getOsmUser() const -{ - if (!g_Merk_Ignore_Preferences && !g_Merk_Reset_Preferences) - return Sets->value("osm/User").toString(); - else - return QString(); -} - -void MerkaartorPreferences::setOsmUser(const QString & theValue) -{ - if (!g_Merk_Ignore_Preferences) - Sets->setValue("osm/User", theValue); -} - -QString MerkaartorPreferences::getOsmPassword() const -{ - if (!g_Merk_Ignore_Preferences && !g_Merk_Reset_Preferences) - return Sets->value("osm/Password").toString(); - else - return QString(); -} - -void MerkaartorPreferences::setOsmPassword(const QString & theValue) -{ - if (!g_Merk_Ignore_Preferences) - Sets->setValue("osm/Password", theValue); -} - M_PARAM_IMPLEMENT_DOUBLE(MaxDistNodes, data, 0.0); - M_PARAM_IMPLEMENT_BOOL(AutoSaveDoc, data, false); M_PARAM_IMPLEMENT_BOOL(AutoExtractTracks, data, false); - M_PARAM_IMPLEMENT_INT(DirectionalArrowsVisible, visual, 1); RendererOptions MerkaartorPreferences::getRenderOptions() @@ -1662,11 +1590,13 @@ void MerkaartorPreferences::loadOsmServers() int size = Sets->beginReadArray("OsmServers"); for (int i = 0; i < size; ++i) { Sets->setArrayIndex(i); - OsmServer server; + OsmServerInfo server; server.Selected = Sets->value("selected").toBool(); + server.Type = OsmServerInfo::typeFromString(Sets->value("type").toString()); server.Url = Sets->value("url").toString(); server.User = Sets->value("user").toString(); server.Password = Sets->value("password").toString(); + server.CfgVersion = Sets->value("version").toInt(); theOsmServers.append(server); } Sets->endArray(); @@ -1680,9 +1610,11 @@ void MerkaartorPreferences::saveOsmServers() for (int i = 0; i < theOsmServers.size(); ++i) { Sets->setArrayIndex(i); Sets->setValue("selected", theOsmServers.at(i).Selected); + Sets->setValue("type", OsmServerInfo::typeToString(theOsmServers.at(i).Type)); Sets->setValue("url", theOsmServers.at(i).Url); Sets->setValue("user", theOsmServers.at(i).User); Sets->setValue("password", theOsmServers.at(i).Password); + Sets->setValue("version", theOsmServers.at(i).CfgVersion); } Sets->endArray(); } diff --git a/src/Preferences/MerkaartorPreferences.h b/src/Preferences/MerkaartorPreferences.h index cb8fe64d..8f11bbf7 100644 --- a/src/Preferences/MerkaartorPreferences.h +++ b/src/Preferences/MerkaartorPreferences.h @@ -37,6 +37,8 @@ #include "IRenderer.h" +#include "OsmServer.h" + #include "build-metadata.hpp" class MainWindow; @@ -146,15 +148,8 @@ class Tool typedef QMap ToolList; typedef QMapIterator ToolListIterator; -struct OsmServer -{ - bool Selected; - QString Url; - QString User; - QString Password; -}; -typedef QList OsmServerList; -typedef QListIterator OsmServerIterator; +typedef QList OsmServerList; +typedef QListIterator OsmServerIterator; // Outside of merkaartorpreferences, because initializing it will need translations // Classic chicken & egg problem. @@ -239,19 +234,21 @@ Q_OBJECT M_PARAM_DECLARE_BOOL(HideToolbarLabels) /* Data */ - void setOsmWebsite(const QString & theValue); - QString getOsmWebsite() const; - QString getOsmApiUrl() const; +// void setOsmWebsite(const QString & theValue); +// QString getOsmWebsite() const; +// QString getOsmApiUrl() const; + M_PARAM_DECLARE_INT(OsmServerIndex) + std::shared_ptr getOsmServer(); M_PARAM_DECLARE_STRING(XapiUrl) M_PARAM_DECLARE_STRING(NominatimUrl) M_PARAM_DECLARE_BOOL(AutoHistoryCleanup) - void setOsmUser(const QString & theValue); - QString getOsmUser() const; - - void setOsmPassword(const QString & theValue); - QString getOsmPassword() const; +// void setOsmUser(const QString & theValue); +// QString getOsmUser() const; +// +// void setOsmPassword(const QString & theValue); +// QString getOsmPassword() const; M_PARAM_DECLARE_DOUBLE(MaxDistNodes) @@ -491,8 +488,6 @@ Q_OBJECT private slots: void on_requestFinished ( QNetworkReply *reply ); - void on_authenticationRequired( QNetworkReply *reply, QAuthenticator *auth ); - void on_sslErrors(QNetworkReply *reply, const QList& errors); signals: void bookmarkChanged(); diff --git a/src/Preferences/OsmServerWidget.ui b/src/Preferences/OsmServerWidget.ui index 4004cd1c..1f0fd199 100644 --- a/src/Preferences/OsmServerWidget.ui +++ b/src/Preferences/OsmServerWidget.ui @@ -7,14 +7,23 @@ 0 0 552 - 69 + 195 Form - + + 0 + + + 0 + + + 0 + + 0 @@ -27,7 +36,36 @@ - + + + + 0 + 0 + + + + IBeamCursor + + + true + + + true + + + true + + + + https://www.openstreetmap.org/ + + + + + https://master.apis.dev.openstreetmap.org/ + + + @@ -54,59 +92,125 @@ - + - - - Qt::Horizontal - - - - 57 - 20 - - - - - - - - User: - - - - - - - - + - Pwd: + Authentication type - - - QLineEdit::Password + + + 0 + + + Username and password (deprecated) + + + + + OAuth2 Redirect (redirect via 127.0.0.1:1337) + + + + + OAuth2 OOB (manually copy code) + + + + + + 1 + + + + + + + User: + + + + + + + + + + Pwd: + + + + + + + QLineEdit::Password + + + + + + + + + + + Login state: Unknown + + + + + + + Qt::Horizontal + + + + 309 + 20 + + + + + + + + Login + + + + + + + - rbOsmServerSelected - edOsmServerUrl - label_14 - edOsmServerUser - label_13 - edOsmServerPwd - tbOsmServerAdd - tbOsmServerDel - label_14 - + + + authType + currentIndexChanged(int) + credentials + setCurrentIndex(int) + + + 275 + 43 + + + 275 + 77 + + + + diff --git a/src/Preferences/PreferencesDialog.cpp b/src/Preferences/PreferencesDialog.cpp index cb061fd7..b0639cb4 100644 --- a/src/Preferences/PreferencesDialog.cpp +++ b/src/Preferences/PreferencesDialog.cpp @@ -16,6 +16,7 @@ #include "Document.h" #include "Feature.h" #include "PropertiesDock.h" +#include "OsmServer.h" #include "BoundaryIcon.h" #include @@ -25,11 +26,38 @@ #include #include +// TODO: Move the class to it's own file. +class OsmServerWidget : public QWidget, public Ui::OsmServerWidget +{ + Q_OBJECT + +public: + OsmServerWidget(QWidget * parent = 0, Qt::WindowFlags f = Qt::Widget); + + OsmServerInfo getOsmServerInfo() const; + void setOsmServerInfo(OsmServerInfo srv); + QNetworkAccessManager* m_nm; /* TODO: Inject. */ + +public slots: + void on_tbOsmServerAdd_clicked(); + void on_tbOsmServerDel_clicked(); + void on_rbOsmServerSelected_clicked(); + void on_tbOAuth2Login_clicked(); + void on_httpAuthAuthenticated(); + void on_httpAuthFailed(); + +private: + std::shared_ptr m_impl; +}; + +#include "PreferencesDialog.moc" + OsmServerWidget::OsmServerWidget(QWidget * parent, Qt::WindowFlags f) : QWidget(parent, f) { setupUi(this); setAttribute(Qt::WA_DeleteOnClose); + m_nm = new QNetworkAccessManager(this); } void OsmServerWidget::on_tbOsmServerAdd_clicked() @@ -71,6 +99,24 @@ void OsmServerWidget::on_tbOsmServerDel_clicked() } } +OsmServerInfo OsmServerWidget::getOsmServerInfo() const { + OsmServerInfo srv; + srv.Url = edOsmServerUrl->currentText(); + srv.Type = static_cast(authType->currentIndex()); + srv.User = edOsmServerUser->text(); + srv.Password = edOsmServerPwd->text(); + srv.Selected = rbOsmServerSelected->isChecked(); + return srv; +} + +void OsmServerWidget::setOsmServerInfo(OsmServerInfo srv) { + edOsmServerUrl->setCurrentText(srv.Url); + authType->setCurrentIndex(static_cast(srv.Type)); + edOsmServerUser->setText(srv.User); + edOsmServerPwd->setText(srv.Password); + rbOsmServerSelected->setChecked(srv.Selected); +} + void OsmServerWidget::on_rbOsmServerSelected_clicked() { QLayout* lay = parentWidget()->layout(); @@ -86,6 +132,34 @@ void OsmServerWidget::on_rbOsmServerSelected_clicked() rbOsmServerSelected->setChecked(true); } +void OsmServerWidget::on_tbOAuth2Login_clicked() { + qDebug() << "Start login..."; + + OsmServerInfo srv = getOsmServerInfo(); + m_impl = makeOsmServer(srv, *m_nm); + + connect(&*m_impl, &IOsmServerImpl::authenticated, this, &OsmServerWidget::on_httpAuthAuthenticated); + connect(&*m_impl, &IOsmServerImpl::failed, this, &OsmServerWidget::on_httpAuthFailed); + + this->lbLoginState->setText(tr("Authenticating")); + this->tbOAuth2Login->setEnabled(false); + + m_impl->authenticate(); +} + +void OsmServerWidget::on_httpAuthAuthenticated() { + qDebug() << "Authenticated!"; + setOsmServerInfo(m_impl->getServerInfo()); + this->lbLoginState->setText(tr("Authenticated")); + this->tbOAuth2Login->setEnabled(true); +} + +void OsmServerWidget::on_httpAuthFailed() { + qDebug() << "Failed!"; + this->lbLoginState->setText(tr("Failed")); + this->tbOAuth2Login->setEnabled(true); +} + PreferencesDialog::PreferencesDialog(QWidget* parent) : QDialog(parent) { @@ -177,22 +251,14 @@ void PreferencesDialog::loadPrefs() if (!theOsmServers->size()) { OsmServerWidget* wOSmServer = new OsmServerWidget(grpOSM); - wOSmServer->edOsmServerUrl->setText(M_PREFS->getOsmApiUrl()); - wOSmServer->edOsmServerUser->setText(M_PREFS->getOsmUser()); - wOSmServer->edOsmServerPwd->setText(M_PREFS->getOsmPassword()); wOSmServer->rbOsmServerSelected->setChecked(true); wOSmServer->tbOsmServerDel->setEnabled(false); OsmServersLayout->addWidget(wOSmServer); } else { - foreach(OsmServer srv, *theOsmServers) { + foreach(OsmServerInfo srv, *theOsmServers) { OsmServerWidget* wOSmServer = new OsmServerWidget(grpOSM); - - wOSmServer->edOsmServerUrl->setText(srv.Url); - wOSmServer->edOsmServerUser->setText(srv.User); - wOSmServer->edOsmServerPwd->setText(srv.Password); - wOSmServer->rbOsmServerSelected->setChecked(srv.Selected); - + wOSmServer->setOsmServerInfo(srv); OsmServersLayout->addWidget(wOSmServer); } } @@ -341,16 +407,10 @@ void PreferencesDialog::savePrefs() if (!wOsmServer) continue; - OsmServer srv; - srv.Url = wOsmServer->edOsmServerUrl->text(); - srv.User = wOsmServer->edOsmServerUser->text(); - srv.Password = wOsmServer->edOsmServerPwd->text(); - srv.Selected = wOsmServer->rbOsmServerSelected->isChecked(); + OsmServerInfo srv = wOsmServer->getOsmServerInfo(); - if (srv.Selected && (srv.Url != M_PREFS->getOsmApiUrl() || srv.User != M_PREFS->getOsmUser() || srv.Password != M_PREFS->getOsmPassword())) { - M_PREFS->setOsmWebsite(srv.Url); - M_PREFS->setOsmUser(srv.User); - M_PREFS->setOsmPassword(srv.Password); + if (srv.Selected) { + M_PREFS->setOsmServerIndex(i); OsmDataChanged = true; } diff --git a/src/Preferences/PreferencesDialog.h b/src/Preferences/PreferencesDialog.h index 9f667bd0..740f87d0 100644 --- a/src/Preferences/PreferencesDialog.h +++ b/src/Preferences/PreferencesDialog.h @@ -24,19 +24,6 @@ @author cbro */ -class OsmServerWidget : public QWidget, public Ui::OsmServerWidget -{ - Q_OBJECT - -public: - OsmServerWidget(QWidget * parent = 0, Qt::WindowFlags f = Qt::Widget); - -public slots: - void on_tbOsmServerAdd_clicked(); - void on_tbOsmServerDel_clicked(); - void on_rbOsmServerSelected_clicked(); -}; - class PreferencesDialog : public QDialog, public Ui::PreferencesDialog { Q_OBJECT diff --git a/src/Preferences/PreferencesDialog.ui b/src/Preferences/PreferencesDialog.ui index 30977e1c..f341ee97 100644 --- a/src/Preferences/PreferencesDialog.ui +++ b/src/Preferences/PreferencesDialog.ui @@ -8,7 +8,7 @@ 0 0 689 - 487 + 592 @@ -21,7 +21,7 @@ QTabWidget::North - 3 + 5 @@ -899,7 +899,7 @@ - OSM API (URL is, e.g., "https://www.openstreetmap.org/api/0.6") + OSM API (do not include /api/0.6/ specifier) @@ -1121,7 +1121,16 @@ 4 - + + 0 + + + 0 + + + 0 + + 0 @@ -1149,7 +1158,16 @@ 4 - + + 0 + + + 0 + + + 0 + + 0 diff --git a/src/Sync/DirtyListExecutorOSC.cpp b/src/Sync/DirtyListExecutorOSC.cpp index 1a9bfea6..74766940 100644 --- a/src/Sync/DirtyListExecutorOSC.cpp +++ b/src/Sync/DirtyListExecutorOSC.cpp @@ -34,10 +34,10 @@ DirtyListExecutorOSC::DirtyListExecutorOSC(Document* aDoc, const DirtyListBuild& { } -DirtyListExecutorOSC::DirtyListExecutorOSC(Document* aDoc, const DirtyListBuild& aFuture, const ChangesetInfo& info, const QString& aWeb, const QString& aUser, const QString& aPwd, int aTasks) -: DirtyListVisit(aDoc, aFuture, false), changesetInfo(info), Tasks(aTasks), Done(0), Web(aWeb), User(aUser), Pwd(aPwd), theDownloader(0) +DirtyListExecutorOSC::DirtyListExecutorOSC(Document* aDoc, const DirtyListBuild& aFuture, const ChangesetInfo& info, OsmServer server, int aTasks) +: DirtyListVisit(aDoc, aFuture, false), changesetInfo(info), Tasks(aTasks), Done(0), server(server), theDownloader(0) { - theDownloader = new Downloader(User, Pwd); + theDownloader = new Downloader(server); } DirtyListExecutorOSC::~DirtyListExecutorOSC() @@ -55,7 +55,7 @@ int DirtyListExecutorOSC::sendRequest(const QString& Method, const QString& URL, QMessageBox::StandardButton theChoice = QMessageBox::Retry; while (theChoice == QMessageBox::Retry) { - QUrl theUrl(Web+URL); + QUrl theUrl(URL); if (!theDownloader->request(Method,theUrl,Data)) { qDebug() << QString("Upload error: request (%1); Server message is '%2'").arg(theDownloader->resultCode()).arg(theDownloader->resultText()); @@ -322,7 +322,7 @@ bool DirtyListExecutorOSC::stop() QEventLoop L; L.processEvents(QEventLoop::ExcludeUserInputEvents); URL = theDownloader->getURLToCloseChangeSet(ChangeSetId); - QUrl theUrl(Web+URL); + QUrl theUrl(URL); theDownloader->setAnimator(NULL, NULL, NULL, false); if (!theDownloader->request("PUT",theUrl,DataIn)) { QMessageBox::warning(NULL, tr("Changeset could not be closed."), tr("An unknown error has occurred. It might already be closed, or will be closed automatically. If you want to be sure, please, check manually on the osm.org website.")); diff --git a/src/Sync/DirtyListExecutorOSC.h b/src/Sync/DirtyListExecutorOSC.h index 0cc30d6a..57923095 100644 --- a/src/Sync/DirtyListExecutorOSC.h +++ b/src/Sync/DirtyListExecutorOSC.h @@ -17,6 +17,7 @@ #include #include +#include "Utils/OsmServer.h" class Downloader; @@ -26,7 +27,7 @@ class DirtyListExecutorOSC : public QObject, public DirtyListVisit public: DirtyListExecutorOSC(Document* aDoc, const DirtyListBuild& aFuture); - DirtyListExecutorOSC(Document* aDoc, const DirtyListBuild& aFuture, const ChangesetInfo& info, const QString& aWeb, const QString& aUser, const QString& aPwd, int aTasks); + DirtyListExecutorOSC(Document* aDoc, const DirtyListBuild& aFuture, const ChangesetInfo& info, OsmServer server, int aTasks); virtual ~DirtyListExecutorOSC(); void OscCreate(Feature* F); @@ -57,7 +58,7 @@ class DirtyListExecutorOSC : public QObject, public DirtyListVisit Ui::SyncListDialog Ui; int Tasks, Done; QProgressDialog* Progress; - QString Web,User,Pwd; + OsmServer server; Downloader* theDownloader; QString ChangeSetId; QString LastAction; diff --git a/src/Sync/DownloadOSM.cpp b/src/Sync/DownloadOSM.cpp index b1be43e7..00257c2b 100644 --- a/src/Sync/DownloadOSM.cpp +++ b/src/Sync/DownloadOSM.cpp @@ -30,13 +30,12 @@ /* DOWNLOADER */ -Downloader::Downloader(const QString& aUser, const QString& aPwd) -: User(aUser), Password(aPwd), +Downloader::Downloader(OsmServer server) +: server(server), currentReply(0),Error(false), AnimatorLabel(0), AnimatorBar(0), AnimationTimer(0) { - //IdAuth = Request.setUser(User.toUtf8(), Password.toUtf8()); - connect(&netManager,SIGNAL(finished(QNetworkReply*)),this,SLOT(on_requestFinished(QNetworkReply*))); - connect(&netManager,SIGNAL(authenticationRequired(QNetworkReply*,QAuthenticator*)), this,SLOT(on_authenticationRequired(QNetworkReply*,QAuthenticator*))); + /* FIXME: Handle finished request. */ + //connect(&netManager,SIGNAL(finished(QNetworkReply*)),this,SLOT(on_requestFinished(QNetworkReply*))); } @@ -73,8 +72,6 @@ void Downloader::on_Cancel_clicked() Loop.exit(QDialog::Rejected); } -#include "QTextBrowser" - bool Downloader::go(const QUrl& url) { return request("GET", url, QString()); } @@ -82,17 +79,28 @@ bool Downloader::go(const QUrl& url) { bool Downloader::request(const QString& theMethod, const QUrl& url, const QString& theData) { if (Error) return false; - qDebug() << "Downloader::request:" << url; + qDebug() << "Downloader::request:" << server->baseUrl().resolved(url); - netManager.setProxy(M_PREFS->getProxy(url)); - QNetworkRequest req(url); + QNetworkRequest req(server->baseUrl().resolved(url)); req.setRawHeader(QByteArray("Content-Type"), QByteArray("text/xml")); req.setRawHeader(QByteArray("User-Agent"), USER_AGENT.toLatin1()); QByteArray dataArray(theData.toUtf8()); - QBuffer dataBuffer(&dataArray); - currentReply = netManager.sendCustomRequest(req, theMethod.toLatin1(), &dataBuffer); + currentReply = server->sendRequest(req, theMethod.toLatin1(), dataArray); + connect(currentReply, &QNetworkReply::finished, this, [this] { + qDebug() << "Downloader::request: received response"; + if (currentReply->error()) { + Error = true; + qDebug() << "Downloader::request: received response with code" + << currentReply->error() << ", message" << currentReply->errorString(); + qDebug() << "Body: " << currentReply->readAll(); + Loop.exit(QDialog::Rejected); + } else { + Loop.exit(QDialog::Accepted); + } + } + ); if (AnimationTimer) { AnimationTimer->start(200); @@ -130,32 +138,11 @@ bool Downloader::request(const QString& theMethod, const QUrl& url, const QStrin return !Error; } -void Downloader::on_authenticationRequired( QNetworkReply *reply, QAuthenticator *auth) { - static QNetworkReply *lastReply = NULL; - - /* Only provide authentication the first time we see this reply, to avoid - * infinite loop providing the same credentials. */ - if (lastReply != reply) { - lastReply = reply; - qDebug() << "Downloader authentication required and provided."; - auth->setUser(User); - auth->setPassword(Password); - } -} - QByteArray& Downloader::content() { return Content; } -void Downloader::on_requestFinished( QNetworkReply *reply) -{ - if (reply->error()) - Error = true; - if ( (reply == currentReply) && Loop.isRunning() ) - Loop.exit(QDialog::Accepted); -} - void Downloader::progress(qint64 done, qint64 total) { if (AnimatorLabel && AnimatorBar) @@ -196,22 +183,22 @@ const QString &Downloader::locationText() QString Downloader::getURLToOpenChangeSet() { - return QString("/changeset/create"); + return QString("/api/0.6/changeset/create"); } QString Downloader::getURLToCloseChangeSet(const QString& Id) { - return QString("/changeset/%1/close").arg(Id); + return QString("/api/0.6/changeset/%1/close").arg(Id); } QString Downloader::getURLToUploadDiff(QString changesetId) { - return QString("/changeset/%1/upload").arg(changesetId); + return QString("/api/0.6/changeset/%1/upload").arg(changesetId); } QString Downloader::getURLToFetch(const QString &What) { - QString URL = QString("/%1?%2="); + QString URL = QString("/api/0.6/%1?%2="); return URL.arg(What).arg(What); } @@ -221,13 +208,13 @@ QString Downloader::getURLToFetchFull(IFeature::FId id) QString URL; if (id.type & IFeature::Point) { type = "node"; - URL = QString("/%1/%2"); + URL = QString("/api/0.6/%1/%2"); } else { if (id.type & IFeature::LineString) type = "way"; if (id.type & IFeature::OsmRelation) type = "relation"; - URL = QString("/%1/%2/full"); + URL = QString("/api/0.6/%1/%2/full"); } return URL.arg(type).arg(id.numId); @@ -240,43 +227,43 @@ QString Downloader::getURLToFetchFull(Feature* aFeature) QString Downloader::getURLToFetch(const QString &What, const QString& Id) { - QString URL = QString("/%1/%2"); + QString URL = QString("/api/0.6/%1/%2"); return URL.arg(What).arg(Id); } QString Downloader::getURLToCreate(const QString &What) { - QString URL = QString("/%1/create"); + QString URL = QString("/api/0.6/%1/create"); return URL.arg(What); } QString Downloader::getURLToUpdate(const QString &What, const QString& Id) { - QString URL = QString("/%1/%2"); + QString URL = QString("/api/0.6/%1/%2"); return URL.arg(What).arg(Id); } QString Downloader::getURLToDelete(const QString &What, const QString& Id) { - QString URL = QString("/%1/%2"); + QString URL = QString("/api/0.6/%1/%2"); return URL.arg(What).arg(Id); } QString Downloader::getURLToMap() { - QString URL("/map?bbox=%1,%2,%3,%4"); + QString URL("/api/0.6/map?bbox=%1,%2,%3,%4"); return URL; } QString Downloader::getURLToTrackPoints() { - QString URL = QString("/trackpoints?bbox=%1,%2,%3,%4&page=%5"); + QString URL = QString("/api/0.6/trackpoints?bbox=%1,%2,%3,%4&page=%5"); return URL; } -bool downloadOSM(QWidget* aParent, const QUrl& theUrl, const QString& aUser, const QString& aPassword, Document* theDocument, Layer* theLayer) +bool downloadOSM(QWidget* aParent, const QUrl& theUrl, OsmServer server, Document* theDocument, Layer* theLayer) { - Downloader Rcv(aUser, aPassword); + Downloader Rcv(server); IProgressWindow* aProgressWindow = dynamic_cast(aParent); if (aProgressWindow) { @@ -319,6 +306,8 @@ bool downloadOSM(QWidget* aParent, const QUrl& theUrl, const QString& aUser, con case 301: case 302: case 307: { + qDebug() << "FIXME: Relocation to: " << Rcv.locationText(); + /* QString aWeb = Rcv.locationText(); if (!aWeb.isEmpty()) { QUrl aURL(aWeb); @@ -330,6 +319,7 @@ bool downloadOSM(QWidget* aParent, const QUrl& theUrl, const QString& aUser, con QMessageBox::warning(aParent,QApplication::translate("Downloader","Download failed"), msg); return false; } + */ break; } case 401: @@ -342,19 +332,19 @@ bool downloadOSM(QWidget* aParent, const QUrl& theUrl, const QString& aUser, con QMessageBox::warning(aParent,QApplication::translate("Downloader","Download failed"), msg); return false; } - Downloader Down(aUser, aPassword); + Downloader Down(server); bool OK = importOSM(aParent, Rcv.content(), theDocument, theLayer, &Down); return OK; } -bool downloadOSM(QWidget* aParent, const QString& aWeb, const QString& aUser, const QString& aPassword, const CoordBox& aBox , Document* theDocument, Layer* theLayer) +bool downloadOSM(QWidget* aParent, OsmServer server, const CoordBox& aBox , Document* theDocument, Layer* theLayer) { if (checkForConflicts(theDocument)) { QMessageBox::warning(aParent,QApplication::translate("Downloader","Unresolved conflicts"), QApplication::translate("Downloader","Please resolve existing conflicts first")); return false; } - Downloader Rcv(aUser, aPassword); + Downloader Rcv(server); QString URL = Rcv.getURLToMap(); if ((fabs(aBox.bottomLeft().x()) < 180.0 && fabs(aBox.topRight().x()) > 180.0) @@ -371,20 +361,20 @@ bool downloadOSM(QWidget* aParent, const QString& aWeb, const QString& aUser, co q2.setRight(-180*sign); q2.setLeft(q2.left()+360); } - return downloadOSM(aParent, aWeb, aUser, aPassword, q1, theDocument, theLayer) - && downloadOSM(aParent, aWeb, aUser, aPassword, q2, theDocument, theLayer); + return downloadOSM(aParent, server, q1, theDocument, theLayer) + && downloadOSM(aParent, server, q2, theDocument, theLayer); } else { /* Normal code path */ URL = URL.arg(aBox.bottomLeft().x(), 0, 'f').arg(aBox.bottomLeft().y(), 0, 'f').arg(aBox.topRight().x(), 0, 'f').arg(aBox.topRight().y(), 0, 'f'); - QUrl theUrl(aWeb+URL); - return downloadOSM(aParent, theUrl, aUser, aPassword, theDocument, theLayer); + QUrl theUrl(URL); + return downloadOSM(aParent, theUrl, server, theDocument, theLayer); } } -bool downloadTracksFromOSM(QWidget* Main, const QString& aWeb, const QString& aUser, const QString& aPassword, const CoordBox& aBox , Document* theDocument) +bool downloadTracksFromOSM(QWidget* Main, OsmServer server, const CoordBox& aBox , Document* theDocument) { - Downloader theDownloader(aUser, aPassword); + Downloader theDownloader(server); QList theTracklayers; //TrackMapLayer* trackLayer = new TrackMapLayer(QApplication::translate("Downloader","Downloaded tracks")); //theDocument->add(trackLayer); @@ -416,7 +406,7 @@ bool downloadTracksFromOSM(QWidget* Main, const QString& aWeb, const QString& aU arg(aBox.topRight().x()). arg(aBox.topRight().y()). arg(Page); - QUrl theUrl(aWeb+URL); + QUrl theUrl(URL); if (!theDownloader.go(theUrl)) return false; if (theDownloader.resultCode() != 200) @@ -473,23 +463,18 @@ bool downloadFeatures(MainWindow* Main, const QList& idList , Doc theLayer = (Layer*)theDocument->getLastDownloadLayer(); } - QString osmWebsite, osmUser, osmPwd; - - osmWebsite = M_PREFS->getOsmApiUrl(); - osmUser = M_PREFS->getOsmUser(); - osmPwd = M_PREFS->getOsmPassword(); if (Main) Main->view()->setUpdatesEnabled(false); bool OK = true; - Downloader Rcv(osmUser, osmPwd); + Downloader Rcv(M_PREFS->getOsmServer()); for (int i=0; igetOsmServer(), theDocument, theLayer); } if (Main) @@ -508,76 +493,6 @@ bool downloadFeatures(MainWindow* Main, const QList& idList , Doc return OK; } -bool downloadMapdust(MainWindow* Main, const CoordBox& aBox, Document* theDocument, SpecialLayer* theLayer) -{ - QUrl url; - - url.setUrl(M_PREFS->getMapdustUrl()); - - if (Main) - Main->view()->setUpdatesEnabled(false); - - Downloader theDownloader("", ""); - - SpecialLayer* trackLayer = theLayer; - if (!trackLayer) { - trackLayer = new SpecialLayer(QApplication::translate("Downloader","MapDust"), Layer::MapDustLayer); - trackLayer->setUploadable(false); - theDocument->add(trackLayer); - } - - IProgressWindow* aProgressWindow = dynamic_cast(Main); - if (!aProgressWindow) - return false; - - QProgressDialog* dlg = aProgressWindow->getProgressDialog(); - dlg->setWindowTitle(QApplication::translate("Downloader","Parsing...")); - - QProgressBar* Bar = aProgressWindow->getProgressBar(); - Bar->setTextVisible(false); - Bar->setMaximum(11); - - QLabel* Lbl = aProgressWindow->getProgressLabel(); - Lbl->setText(QApplication::translate("Downloader","Parsing XML")); - - if (dlg) - dlg->show(); - - theDownloader.setAnimator(dlg,Lbl,Bar,true); - Lbl->setText(QApplication::translate("Downloader","Downloading points")); - -#if QT_VERSION >= QT_VERSION_CHECK(5,0,0) - QUrlQuery theQuery(url); -#define theQuery theQuery -#else -#define theQuery url -#endif - theQuery.addQueryItem("t", COORD2STRING(aBox.topRight().y())); - theQuery.addQueryItem("l", COORD2STRING(aBox.bottomLeft().x())); - theQuery.addQueryItem("b", COORD2STRING(aBox.bottomLeft().y())); - theQuery.addQueryItem("r", COORD2STRING(aBox.topRight().x())); -#if QT_VERSION >= QT_VERSION_CHECK(5,0,0) - url.setQuery(theQuery); -#endif -#undef theQuery - - if (!theDownloader.go(url)) - return false; - if (theDownloader.resultCode() != 200) - return false; - QByteArray Ar(theDownloader.content()); - ImportExportGdal gdal(theDocument); - bool OK = gdal.import(trackLayer, Ar, false); - - if (Main) - Main->view()->setUpdatesEnabled(true); - if (OK) { - if (Main) - Main->invalidateView(); - } - return OK; -} - bool downloadMoreOSM(MainWindow* Main, const CoordBox& aBox , Document* theDocument) { Layer* theLayer; @@ -587,17 +502,12 @@ bool downloadMoreOSM(MainWindow* Main, const CoordBox& aBox , Document* theDocum } else theLayer = (Layer*)theDocument->getLastDownloadLayer(); - QString osmWebsite, osmUser, osmPwd; - - osmWebsite = M_PREFS->getOsmApiUrl(); - osmUser = M_PREFS->getOsmUser(); - osmPwd = M_PREFS->getOsmPassword(); - qDebug() << "Requesting more from " << osmWebsite; + OsmServer server = M_PREFS->getOsmServer(); Main->view()->setUpdatesEnabled(false); bool OK = true; - OK = downloadOSM(Main,osmWebsite,osmUser,osmPwd,aBox,theDocument,theLayer); + OK = downloadOSM(Main,server,aBox,theDocument,theLayer); Main->view()->setUpdatesEnabled(true); if (OK) { @@ -623,9 +533,7 @@ bool downloadOSM(MainWindow* Main, const CoordBox& aBox , Document* theDocument) QDialog * dlg = new QDialog(Main); - osmWebsite = M_PREFS->getOsmApiUrl(); - osmUser = M_PREFS->getOsmUser(); - osmPwd = M_PREFS->getOsmPassword(); + OsmServer server = M_PREFS->getOsmServer(); Ui::DownloadMapDialog ui; ui.setupUi(dlg); @@ -667,20 +575,18 @@ bool downloadOSM(MainWindow* Main, const CoordBox& aBox , Document* theDocument) else if (ui.FromLink->isChecked()) { QString link = ui.Link->text(); - if (link.contains("/api/")) { + if (link.contains("/api/0.6/")) { directAPI=true; directUrl = link; } else if (link.contains("/browse/")) { QString tag("/browse/"); int ix = link.lastIndexOf(tag) + tag.length(); - directUrl = M_PREFS->getOsmApiUrl(); if (!directUrl.endsWith("/")) directUrl += "/"; directUrl += link.right(link.length() - ix); if (!directUrl.endsWith("/")) directUrl += "/"; directUrl += "full"; directAPI=true; } else if (link.startsWith("way") || link.startsWith("node") || link.startsWith("relation")) { - directUrl = M_PREFS->getOsmApiUrl(); if (!directUrl.endsWith("/")) directUrl += "/"; directUrl += link; directAPI=true; @@ -694,7 +600,6 @@ bool downloadOSM(MainWindow* Main, const CoordBox& aBox , Document* theDocument) else if (ui.FromXapi->isChecked()) { directAPI = true; - directUrl = M_PREFS->getXapiUrl(); //if (!directUrl.endsWith("/")) directUrl += "/"; directUrl += ui.edXapiUrl->text(); } @@ -711,12 +616,12 @@ bool downloadOSM(MainWindow* Main, const CoordBox& aBox , Document* theDocument) if (directAPI) { if (ui.FromXapi->isChecked()) theLayer->setUploadable(false); - OK = downloadOSM(Main,QUrl(QUrl::fromEncoded(directUrl.toLatin1())),osmUser,osmPwd,theDocument,theLayer); + OK = downloadOSM(Main,QUrl(QUrl::fromEncoded(directUrl.toLatin1())),server,theDocument,theLayer); } else - OK = downloadOSM(Main,osmWebsite,osmUser,osmPwd,Clip,theDocument,theLayer); + OK = downloadOSM(Main,server,Clip,theDocument,theLayer); if (OK && ui.IncludeTracks->isChecked()) - OK = downloadTracksFromOSM(Main,osmWebsite,osmUser,osmPwd, Clip,theDocument); + OK = downloadTracksFromOSM(Main,server, Clip,theDocument); Main->view()->setUpdatesEnabled(true); if (OK) { diff --git a/src/Sync/DownloadOSM.h b/src/Sync/DownloadOSM.h index 70a8081b..bf156ad7 100644 --- a/src/Sync/DownloadOSM.h +++ b/src/Sync/DownloadOSM.h @@ -20,6 +20,7 @@ class SpecialLayer; #include #include #include +#include "Utils/OsmServer.h" #include "IFeature.h" @@ -28,7 +29,7 @@ class Downloader : public QObject Q_OBJECT public: - Downloader(const QString& aUser, const QString& aPwd); + Downloader(OsmServer server); bool request(const QString& theMethod, const QUrl& URL, const QString& Out); bool go(const QUrl& url); @@ -53,14 +54,11 @@ class Downloader : public QObject public slots: void progress( qint64 done, qint64 total ); - void on_requestFinished( QNetworkReply *reply); - void on_authenticationRequired( QNetworkReply *reply, QAuthenticator *auth); void animate(); void on_Cancel_clicked(); private: - QNetworkAccessManager netManager; - QString User, Password; + OsmServer server; QByteArray Content; int Result; QString LocationText; @@ -80,7 +78,6 @@ bool downloadMoreOSM(MainWindow* Main, const CoordBox& aBox , Document* theDocum bool downloadFeatures(MainWindow* Main, const QList& aDownloadList , Document* theDocument); bool downloadFeature(MainWindow* Main, const IFeature::FId& id, Document* theDocument, Layer* theLayer=NULL); bool downloadFeatures(MainWindow* Main, const QList& aDownloadList, Document* theDocument, Layer* theLayer=NULL); -bool downloadMapdust(MainWindow* Main, const CoordBox& aBox, Document* theDocument, SpecialLayer* theLayer=NULL); bool checkForConflicts(Document* theDocument); diff --git a/src/Utils/OAuth2OOBDialog.cpp b/src/Utils/OAuth2OOBDialog.cpp new file mode 100644 index 00000000..2b718227 --- /dev/null +++ b/src/Utils/OAuth2OOBDialog.cpp @@ -0,0 +1,32 @@ +#include "OAuth2OOBDialog.h" + +#include +#include +#include +#include +#include + + +OAuth2OOBDialog::OAuth2OOBDialog(QWidget *parent) + : QDialog(parent) +{ + ui.setupUi(this); + + connect(ui.btnCopy, &QPushButton::clicked, this, [this](){ + QGuiApplication::clipboard()->setText(ui.loginUrl->text()); + }); + + connect(ui.btnOpen, &QPushButton::clicked, this, [this](){ + QDesktopServices::openUrl(QUrl(ui.loginUrl->text())); + }); +} + +QString OAuth2OOBDialog::getAuthCode(const QString url) { + OAuth2OOBDialog dialog; + dialog.ui.loginUrl->setText(url); + if (dialog.exec() == QDialog::Accepted) { + return dialog.ui.loginCode->text(); + } else { + return QString(); + } +} diff --git a/src/Utils/OAuth2OOBDialog.h b/src/Utils/OAuth2OOBDialog.h new file mode 100644 index 00000000..8b2d24c6 --- /dev/null +++ b/src/Utils/OAuth2OOBDialog.h @@ -0,0 +1,20 @@ +#ifndef __OAUTH2OOBDIALOG_H__ +#define __OAUTH2OOBDIALOG_H__ + +#include +#include "ui_OAuth2OOBDialog.h" + +class OAuth2OOBDialog : public QDialog +{ + Q_OBJECT + + public: + OAuth2OOBDialog(QWidget *parent = 0); + + static QString getAuthCode(const QString url); + + private: + Ui::OAuth2OOBDialog ui; +}; + +#endif diff --git a/src/Utils/OAuth2OOBDialog.ui b/src/Utils/OAuth2OOBDialog.ui new file mode 100644 index 00000000..78d600e1 --- /dev/null +++ b/src/Utils/OAuth2OOBDialog.ui @@ -0,0 +1,144 @@ + + + OAuth2OOBDialog + + + + 0 + 0 + 586 + 131 + + + + + 0 + 0 + + + + OAuth2 OOB + + + + + + + 0 + 0 + + + + Open the following URL in a browser. After logging in, paste the displayed authentication code below. + + + + + + + 5 + + + + + Login URL + + + + + + + + + true + + + true + + + + + + + + 0 + 0 + + + + Copy + + + + + + + Open + + + + + + + + + Authentication code + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + buttonBox + + + + + buttonBox + rejected() + OAuth2OOBDialog + reject() + + + 325 + 188 + + + 286 + 274 + + + + + buttonBox + accepted() + OAuth2OOBDialog + accept() + + + 292 + 224 + + + 292 + 122 + + + + + diff --git a/src/Utils/OsmOAuth2Flow.cpp b/src/Utils/OsmOAuth2Flow.cpp new file mode 100644 index 00000000..9a3fc407 --- /dev/null +++ b/src/Utils/OsmOAuth2Flow.cpp @@ -0,0 +1,10 @@ +#include "OsmOAuth2Flow.h" + +OsmOAuth2Flow::OsmOAuth2Flow(QObject* parent) + : QOAuth2AuthorizationCodeFlow(parent) +{ } + +void OsmOAuth2Flow::requestAccessToken(const QString& code) { + return QOAuth2AuthorizationCodeFlow::requestAccessToken(code); +} + diff --git a/src/Utils/OsmOAuth2Flow.h b/src/Utils/OsmOAuth2Flow.h new file mode 100644 index 00000000..32f94de7 --- /dev/null +++ b/src/Utils/OsmOAuth2Flow.h @@ -0,0 +1,16 @@ +#ifndef __OSMOAUTH2FLOW_H__ +#define __OSMOAUTH2FLOW_H__ + +#include +#include + +class OsmOAuth2Flow : public QOAuth2AuthorizationCodeFlow +{ + Q_OBJECT + + public: + OsmOAuth2Flow(QObject* parent = nullptr); + void requestAccessToken(const QString& code); +}; + +#endif // __OSMOAUTH2FLOW_H__ diff --git a/src/Utils/OsmServer.cpp b/src/Utils/OsmServer.cpp new file mode 100644 index 00000000..c62bd749 --- /dev/null +++ b/src/Utils/OsmServer.cpp @@ -0,0 +1,355 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "OsmServer.h" +#include "OsmOAuth2Flow.h" +#include "OAuth2OOBDialog.h" + +#include +// +//FIXME: The client ID and secret are not really a secret, since there is no +//way to hide them in a desktop application. In case of abuse, the client ID +//can be revoked by osm.org. It might be a good idea to make it configurable, +//so that the user can enter their own client ID and secret in this case. +// +//Client ID wv3ui28EyHjH0c4C1Wuz6_I-o47ithPAOt7Qt1ov9Ps +//Client Secret evCjgZOGTRL70ezsXs3VbxG0ugjJ5hq7pFQMB6toBcM // FIXME: The secret is not needed. Why? +//Make sure to save this secret - it will not be accessible again +//Permissions +// +// Read user preferences (read_prefs) +// Modify user preferences (write_prefs) +// Create diary entries, comments and make friends (write_diary) +// Modify the map (write_api) +// Read private GPS traces (read_gpx) +// Upload GPS traces (write_gpx) +// Modify notes (write_notes) +// +//Redirect URIs +// +// http://127.0.0.1:1337/ + +QLoggingCategory lc_OsmServer("merk.OsmServer"); + +class OsmServerImplBasic : public IOsmServerImpl { + public: + OsmServerImplBasic(OsmServerInfo& info, QNetworkAccessManager& manager) + : m_info(info), m_manager(manager) { + } + + QUrl baseUrl() const { + return m_info.Url; + } + + OsmServerInfo const getServerInfo() const { return m_info; } + + virtual QNetworkReply* get(const QUrl &url) { + return m_manager.get(QNetworkRequest(baseUrl().resolved(url))); + } + + virtual QNetworkReply* put(const QUrl &url, const QByteArray &data) { + return m_manager.put(QNetworkRequest(baseUrl().resolved(url)), data); + } + + virtual QNetworkReply* deleteResource(const QUrl &url) { + return m_manager.deleteResource(QNetworkRequest(baseUrl().resolved(url))); + } + + virtual QNetworkReply* sendRequest(QNetworkRequest &request, const QByteArray &verb, const QByteArray &data) { + return m_manager.sendCustomRequest(request, verb, data); + } + + virtual void authenticate() { + QUrl base(m_info.Url); + auto reply = m_manager.get(QNetworkRequest(base.resolved(QUrl("api/0.6/user/details")))); + connect(reply, &QNetworkReply::finished, [=]() { + reply->deleteLater(); + + if (reply->error() != QNetworkReply::NoError) { + qCritical() << "Error:" << reply->errorString(); + emit failed(Error::Unauthorized, reply->errorString()); + } else { + qDebug(lc_OsmServer) << reply->readAll(); + emit authenticated(); + } + }); + } + + private slots: + void on_authenticationRequired( QNetworkReply *reply, QAuthenticator *auth ); + void on_sslErrors(QNetworkReply *reply, const QList& errors); + + private: + OsmServerInfo m_info; + QNetworkAccessManager& m_manager; +}; + +class OsmServerImplOAuth2 : public IOsmServerImpl { + public: + OsmServerImplOAuth2(OsmServerInfo& info, QNetworkAccessManager& manager); + + QUrl baseUrl() const { + return m_info.Url; + } + + OsmServerInfo const getServerInfo() const { return m_info; } + + virtual QNetworkReply* get(const QUrl &url) { + qDebug(lc_OsmServer) << "get: " << baseUrl().resolved(url); + return m_oauth2.get(baseUrl().resolved(url)); + } + + virtual QNetworkReply* put(const QUrl &url, const QByteArray &data) { + qDebug(lc_OsmServer) << "put: " << baseUrl().resolved(url); + return m_oauth2.put(baseUrl().resolved(url), data); + } + + virtual QNetworkReply* deleteResource(const QUrl &url) { + qDebug(lc_OsmServer) << "delete: " << baseUrl().resolved(url); + return m_oauth2.deleteResource(baseUrl().resolved(url)); + } + + virtual QNetworkReply* sendRequest(QNetworkRequest &request, const QByteArray &verb, const QByteArray &data) { + qDebug(lc_OsmServer) << "custom request" << request.url() << " with verb" << verb << "and data" << data; + m_oauth2.prepareRequest(&request, data); + return m_manager.sendCustomRequest(request, verb, data); + } + + virtual void authenticate(); + + private: + OsmServerInfo m_info; + QNetworkAccessManager& m_manager; + OsmOAuth2Flow m_oauth2; + + QString generateCodeVerifier(); +}; + +std::shared_ptr makeOsmServer(OsmServerInfo& info, QNetworkAccessManager& manager) { + if (info.Type == OsmServerInfo::AuthType::Basic) { + return std::make_shared(info, manager); + } else if (info.Type == OsmServerInfo::AuthType::OAuth2Redirect) { + // OsmServerImplOAuth2 will inspect info.Type and choose appropriately + return std::make_shared(info, manager); + } else if (info.Type == OsmServerInfo::AuthType::OAuth2OOB) { + // OsmServerImplOAuth2 will inspect info.Type and choose appropriately + return std::make_shared(info, manager); + } else { + qWarning(lc_OsmServer) << "Error creating OsmServer: Unknown AuthType" << static_cast(info.Type); + return nullptr; + } +} + +/*********************************************************************** + * OsmServerBasic + **********************************************************************/ + +void OsmServerImplBasic::on_authenticationRequired( QNetworkReply *reply, QAuthenticator *auth ) { + static QNetworkReply *lastReply = NULL; + + /* Only provide authentication the first time we see this reply, to avoid + * infinite loop providing the same credentials. */ + if (lastReply != reply) { + lastReply = reply; + qDebug(/*lc_MerkaartorPreferences */) << "Authentication required and provided."; + auth->setUser(m_info.User); + auth->setPassword(m_info.Password); + } +} + +void OsmServerImplBasic::on_sslErrors(QNetworkReply *reply, const QList& errors) { + Q_UNUSED(reply); + qDebug(/*lc_MerkaartorPreferences*/) << "We stumbled upon some SSL errors: "; + foreach ( QSslError error, errors ) { + qDebug(/*lc_MerkaartorPreferences*/) << "1:"; + qDebug(/*lc_MerkaartorPreferences*/) << error.errorString(); + } +} + +/*********************************************************************** + * OsmServerOAuth2 + **********************************************************************/ + +QString OsmServerImplOAuth2::generateCodeVerifier() { + std::random_device rd; + std::mt19937 gen(rd()); + int bytes = 32; + std::uniform_int_distribution dis(0, 255); + QString result; + for (int i = 0; i < bytes; i++) { + result.append(QString::number(dis(gen), 16)); + } + return result; +} + +OsmServerImplOAuth2::OsmServerImplOAuth2(OsmServerInfo& info, QNetworkAccessManager& manager) + : m_info(info), m_manager(manager),m_oauth2(&manager) +{ + m_oauth2.setScope("read_prefs write_prefs write_api read_gpx write_gpx write_notes"); + //m_oauth2.setScope("read_prefs%20write_prefs%20write_api%20read_gpx%20write_gpx%20write_notes"); + QString host = QUrl(m_info.Url).host(); + if (host == "master.apis.dev.openstreetmap.org") { + m_oauth2.setClientIdentifier("Ydpjx_nAy3fnGN5X1LiFwqToueYvwV0Nl1_IUUi7H3I"); + m_oauth2.setClientIdentifierSharedKey("SuWJYZr9hjCmZH4PaMrvB48aVR_vgIw4D2VUEKPlR4c"); + } else { + if (host != "openstreetmap.org") { + qWarning(lc_OsmServer) << "Unknown OSM API host " << host << ", assuming alias to openstreetmap.org"; + } + m_oauth2.setClientIdentifier("wv3ui28EyHjH0c4C1Wuz6_I-o47ithPAOt7Qt1ov9Ps"); + m_oauth2.setClientIdentifierSharedKey("evCjgZOGTRL70ezsXs3VbxG0ugjJ5hq7pFQMB6toBcM"); + } + m_oauth2.setToken(m_info.Password); +} + +void OsmServerImplOAuth2::authenticate() { + m_oauth2.setAuthorizationUrl(baseUrl().resolved(QUrl("oauth2/authorize"))); + m_oauth2.setAccessTokenUrl(baseUrl().resolved(QUrl("oauth2/token"))); + qDebug(lc_OsmServer) << "Base URL: " << baseUrl(); + qDebug(lc_OsmServer) << "Authorization URL: " << m_oauth2.authorizationUrl(); + qDebug(lc_OsmServer) << "Access Token URL: " << m_oauth2.accessTokenUrl(); + + QString redirect; + + QAbstractOAuthReplyHandler* replyHandler = nullptr; + if (m_info.Type == OsmServerInfo::AuthType::OAuth2Redirect) { + replyHandler = new QOAuthHttpServerReplyHandler(QHostAddress("127.0.0.1"), 1337, this); + QTimer::singleShot(60000, replyHandler, [=]() { + qWarning(lc_OsmServer) << "OAuth2 reply handler timed out."; + replyHandler->deleteLater(); + emit failed(Error::Timeout, tr("OAuth2 reply handler timed out.")); + }); + + m_oauth2.setReplyHandler(replyHandler); + redirect = "http://127.0.0.1:1337/"; + } else { + replyHandler = new QOAuthOobReplyHandler(this); + m_oauth2.setReplyHandler(replyHandler); + redirect = "urn:ietf:wg:oauth:2.0:oob"; + } + + + QString codeVerifier = generateCodeVerifier(); + QString codeChallenge = QString(QCryptographicHash::hash(codeVerifier.toUtf8(), QCryptographicHash::Sha256).toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals)); + + connect(&m_oauth2, &QOAuth2AuthorizationCodeFlow::statusChanged, [=]( + QAbstractOAuth::Status status) { + qDebug(lc_OsmServer) << "OAuth status changed to " << int(status); + if (status == QAbstractOAuth::Status::Granted) { + qDebug(lc_OsmServer) << "Granted, token: " << m_oauth2.token(); + m_info.Password = m_oauth2.token(); + emit authenticated(); + replyHandler->deleteLater(); + auto reply = get(QUrl("/api/0.6/user/details")); + connect(reply, &QNetworkReply::finished, [=]() { + reply->deleteLater(); + + if (reply->error() != QNetworkReply::NoError) { + qCritical() << "Error:" << reply->error() << reply->errorString(); + return; + } + + qDebug(lc_OsmServer) << reply->readAll(); + }); + } else if (status == QAbstractOAuth::Status::TemporaryCredentialsReceived) { + qDebug(lc_OsmServer) << "Temporary credentials."; + } else { + qWarning(lc_OsmServer) << "Status is not granted: " << int(status); + } + }); + + connect(&m_oauth2, &QOAuth2AuthorizationCodeFlow::error, replyHandler, [this, replyHandler]() { + qDebug(lc_OsmServer) << "QOAuth2AuthorizationCodeFlow::error raised."; + replyHandler->deleteLater(); + emit failed(Error::ReplyError, tr("QOAuth2AuthorizationCodeFlow::error raised.")); + }); + +#if QT_VERSION > QT_VERSION_CHECK(6, 6, 0) + // TODO: Is there a way to check in Qt5? + connect(replyHandler, &QOAuthHttpServerReplyHandler::tokenRequestErrorOccurred, this, [this, replyHandler](QAbstractOAuth::Error error, const QString& errorString) { + qDebug(lc_OsmServer) << "Token request error occurred: " << int(error) << errorString; + replyHandler->deleteLater(); + emit failed(Error::TokenRequestError, tr("Token request failed.") + "\n" + errorString); + }); +#endif + + m_oauth2.setModifyParametersFunction([codeVerifier,codeChallenge,redirect](QAbstractOAuth::Stage stage, auto*params){ + /* Note: params is QMap for Qt5 and QMultiMap for Qt6 */ + if (params) { + qDebug(lc_OsmServer) << "Stage: " << int(stage) << "with params" << *params; + switch (stage) { + case QAbstractOAuth::Stage::RequestingAuthorization: + qDebug(lc_OsmServer) << "Requesting Authorization."; + params->insert("code_challenge", codeChallenge); + params->insert("code_challenge_method", "S256"); + // FIXME: Can be replaced by ->replace when Qt 6.x is minimum supported version + params->remove("redirect_uri"); + params->insert("redirect_uri", redirect); + break; + case QAbstractOAuth::Stage::RequestingAccessToken: + qDebug(lc_OsmServer) << "Requesting Access Token."; + params->insert("code_verifier", codeVerifier); + //Note: technically, we no longer redirect, but OSM server rejects the token request unless we provide a matching redirect_uri + params->remove("redirect_uri"); + params->insert("redirect_uri", redirect); + break; + default: + qDebug(lc_OsmServer) << "default stage."; + } + qDebug(lc_OsmServer) << "Stage: " << int(stage) << "with params" << *params; + } + }); + + + if (m_info.Type == OsmServerInfo::AuthType::OAuth2Redirect) { + connect(&m_oauth2, &QOAuth2AuthorizationCodeFlow::authorizeWithBrowser, &QDesktopServices::openUrl); + } else if (m_info.Type == OsmServerInfo::AuthType::OAuth2OOB) { + connect(&m_oauth2, &QOAuth2AuthorizationCodeFlow::authorizeWithBrowser, this, [this](const QUrl& url){ + qDebug(lc_OsmServer) << "Login URL: " << url; + QString code = OAuth2OOBDialog::getAuthCode(url.toString()); + if (!code.isNull()) { + qDebug(lc_OsmServer) << "Received code: " << code; + m_oauth2.requestAccessToken(code.trimmed()); + qDebug(lc_OsmServer) << m_oauth2.accessTokenUrl(); + } else { + qDebug(lc_OsmServer) << "Cancelled by user."; + emit failed(Error::Cancelled, tr("Cancelled by user.")); + } + }); + } else { + qWarning(lc_OsmServer) << "Unknown OAuth type."; + } + + m_oauth2.grant(); +} + +/** Migration logic */ + +bool migrateOsmServerInfo(OsmServerInfo& info) { + if (info.CfgVersion <= 0) { + /* Version 0 is pre-oAuth and old URL format. Fixup the URL and change to OAuth redirect. Ask user to migrate.*/ + auto newUrl = QString(info.Url).replace("http://", "https://").replace("/api/0.6", ""); + QMessageBox msgBox; + msgBox.setStyleSheet("QLabel{min-width: 500px;}"); + msgBox.setText(QObject::tr("Migrate OSM Server")); + msgBox.setInformativeText(QObject::tr("OSM.org now requires OAuth2 for authentication. This brings in a URL change. Do you want to migrate?\n\nCurrent URL: %1\nNew URL: %2").arg(info.Url, newUrl)); + msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); + msgBox.setDefaultButton(QMessageBox::Yes); + switch (msgBox.exec()) { + case QMessageBox::Yes: + info.Url = newUrl; + info.Type = OsmServerInfo::AuthType::OAuth2Redirect; + info.CfgVersion = 1; + return true; + default: + case QMessageBox::No: + return false; + } + } + return false; +} diff --git a/src/Utils/OsmServer.h b/src/Utils/OsmServer.h new file mode 100644 index 00000000..1b23ff72 --- /dev/null +++ b/src/Utils/OsmServer.h @@ -0,0 +1,91 @@ +#ifndef __OSMSERVER_H__ +#define __OSMSERVER_H__ + +#include +#include +#include + +#include + +class QNetworkAccessManager; + +// A dataclass for OSM server information +struct OsmServerInfo +{ + /* Note: Types here correspond to the UI combo box. Keep those in sync. */ + enum class AuthType : int{ + Basic, + OAuth2Redirect, //password field is the token if OAuth2* is used + OAuth2OOB, + }; + + static AuthType typeFromString(const QString& type) { + if (type == "basic") return AuthType::Basic; + if (type == "oauth2redirect") return AuthType::OAuth2Redirect; + if (type == "oauth2oob") return AuthType::OAuth2OOB; + qWarning() << "Error parsing AuthType: Unknown AuthType" << type; + return AuthType::Basic; + } + + static QString typeToString(AuthType type) { + switch (type) { + case AuthType::OAuth2Redirect: return "oauth2redirect"; + case AuthType::OAuth2OOB: return "oauth2oob"; + case AuthType::Basic: return "basic"; + default: + qWarning() << "Error encoding AuthType: Unknown AuthType" << static_cast(type); + return "basic"; + } + } + + bool Selected; + AuthType Type; + QString Url; + QString User; + QString Password; + int CfgVersion = 1; +}; + +class IOsmServerImpl : public QObject { + Q_OBJECT + + public: + virtual QNetworkReply* get(const QUrl &url) = 0; + virtual QNetworkReply* put(const QUrl &url, const QByteArray &data) = 0; + virtual QNetworkReply* deleteResource(const QUrl &url) = 0; + virtual QNetworkReply* sendRequest(QNetworkRequest &request, const QByteArray &verb, const QByteArray &data) = 0; + virtual void authenticate() = 0; + + virtual OsmServerInfo const getServerInfo() const = 0; + + virtual QUrl baseUrl() const { + return QUrl(getServerInfo().Url); + } + + virtual QUrl apiUrl() const { + return QUrl(getServerInfo().Url).resolved(QUrl("/api/0.6")); + } + + + enum class Error { + NoError, + Unauthorized, + Timeout, + ReplyError, + TokenRequestError, + Cancelled, + }; + signals: + void authenticated(); + void failed(Error error, QString errorString); +}; + +Q_DECLARE_INTERFACE(IOsmServerImpl, "InterfaceIOsmServerImpl") + +using OsmServer = std::shared_ptr; + +OsmServer makeOsmServer(OsmServerInfo& info, QNetworkAccessManager& manager); + +bool migrateOsmServerInfo(OsmServerInfo& info); + +#endif diff --git a/src/common/GotoDialog.cpp b/src/common/GotoDialog.cpp index 7fd03d7b..93318884 100644 --- a/src/common/GotoDialog.cpp +++ b/src/common/GotoDialog.cpp @@ -69,7 +69,7 @@ GotoDialog::GotoDialog(MapView* aView, QWidget *parent) .arg(QString::number(OsmZoom)) ); coordOsmApi->setText( QString("%1/map?bbox=%2,%3,%4,%5") - .arg(M_PREFS->getOsmApiUrl()) + .arg(M_PREFS->getOsmServer()->apiUrl().toString()) .arg(COORD2STRING(theViewport.bottomLeft().x())) .arg(COORD2STRING(theViewport.bottomLeft().y())) .arg(COORD2STRING(theViewport.topRight().x()))