File indexing completed on 2025-01-05 03:53:24

0001 /* ============================================================
0002  *
0003  * This file is a part of digiKam project
0004  * https://www.digikam.org
0005  *
0006  * Date        : 2013-11-18
0007  * Description : a tool to export images to Dropbox web service
0008  *
0009  * SPDX-FileCopyrightText: 2013      by Pankaj Kumar <me at panks dot me>
0010  * SPDX-FileCopyrightText: 2018-2019 by Maik Qualmann <metzpinguin at gmail dot com>
0011  *
0012  * SPDX-License-Identifier: GPL-2.0-or-later
0013  *
0014  * ============================================================ */
0015 
0016 #include <dbtalker.h>
0017 
0018 // Qt includes
0019 
0020 #include <QMimeDatabase>
0021 #include <QJsonDocument>
0022 #include <QJsonParseError>
0023 #include <QJsonObject>
0024 #include <QJsonValue>
0025 #include <QJsonArray>
0026 #include <QByteArray>
0027 #include <QFileInfo>
0028 #include <QWidget>
0029 #include <QSettings>
0030 #include <QMessageBox>
0031 #include <QApplication>
0032 #include <QDesktopServices>
0033 
0034 // KDE includes
0035 
0036 #include <klocalizedstring.h>
0037 
0038 // Local includes
0039 
0040 #include "digikam_debug.h"
0041 #include "digikam_version.h"
0042 #include "previewloadthread.h"
0043 #include "wstoolutils.h"
0044 #include "dmetadata.h"
0045 #include "dbwindow.h"
0046 #include "dbmpform.h"
0047 #include "dbitem.h"
0048 #include "o2.h"
0049 #include "o0globals.h"
0050 #include "o0settingsstore.h"
0051 #include "networkmanager.h"
0052 
0053 using namespace Digikam;
0054 
0055 namespace DigikamGenericDropBoxPlugin
0056 {
0057 
0058 class Q_DECL_HIDDEN DBTalker::Private
0059 {
0060 public:
0061 
0062     enum State
0063     {
0064         DB_USERNAME = 0,
0065         DB_LISTFOLDERS,
0066         DB_CREATEFOLDER,
0067         DB_ADDPHOTO
0068     };
0069 
0070 public:
0071 
0072     explicit Private(QWidget* const p)
0073     {
0074         apikey   = QLatin1String("mv2pk07ym9bx3r8");
0075         secret   = QLatin1String("f33sflc8jhiozqu");
0076 
0077         authUrl  = QLatin1String("https://www.dropbox.com/oauth2/authorize");
0078         tokenUrl = QLatin1String("https://api.dropboxapi.com/oauth2/token");
0079 
0080         state    = DB_USERNAME;
0081         settings = nullptr;
0082         netMngr  = nullptr;
0083         reply    = nullptr;
0084         o2       = nullptr;
0085         parent   = p;
0086     }
0087 
0088 public:
0089 
0090     QString                         apikey;
0091     QString                         secret;
0092     QString                         authUrl;
0093     QString                         tokenUrl;
0094     QList<QPair<QString, QString> > folderList;
0095 
0096     QWidget*                        parent;
0097     QNetworkAccessManager*          netMngr;
0098 
0099     QNetworkReply*                  reply;
0100 
0101     QSettings*                      settings;
0102 
0103     State                           state;
0104 
0105     O2*                             o2;
0106 };
0107 
0108 DBTalker::DBTalker(QWidget* const parent)
0109     : d           (new Private(parent))
0110 {
0111     d->netMngr = NetworkManager::instance()->getNetworkManager(this);
0112 
0113     connect(d->netMngr, SIGNAL(finished(QNetworkReply*)),
0114             this, SLOT(slotFinished(QNetworkReply*)));
0115 
0116     d->o2      = new O2(this);
0117 
0118     d->o2->setClientId(d->apikey);
0119     d->o2->setClientSecret(d->secret);
0120     d->o2->setRefreshTokenUrl(d->tokenUrl);
0121     d->o2->setRequestUrl(d->authUrl);
0122     d->o2->setTokenUrl(d->tokenUrl);
0123     d->o2->setLocalPort(8000);
0124 
0125     d->settings                  = WSToolUtils::getOauthSettings(this);
0126     O0SettingsStore* const store = new O0SettingsStore(d->settings, QLatin1String(O2_ENCRYPTION_KEY), this);
0127     store->setGroupKey(QLatin1String("Dropbox"));
0128     d->o2->setStore(store);
0129 
0130     connect(d->o2, SIGNAL(linkingFailed()),
0131             this, SLOT(slotLinkingFailed()));
0132 
0133     connect(d->o2, SIGNAL(linkingSucceeded()),
0134             this, SLOT(slotLinkingSucceeded()));
0135 
0136     connect(d->o2, SIGNAL(openBrowser(QUrl)),
0137             this, SLOT(slotOpenBrowser(QUrl)));
0138 }
0139 
0140 DBTalker::~DBTalker()
0141 {
0142     if (d->reply)
0143     {
0144         d->reply->abort();
0145     }
0146 
0147     WSToolUtils::removeTemporaryDir("dropbox");
0148 
0149     delete d;
0150 }
0151 
0152 void DBTalker::link()
0153 {
0154     Q_EMIT signalBusy(true);
0155     d->o2->link();
0156 }
0157 
0158 void DBTalker::unLink()
0159 {
0160     d->o2->unlink();
0161 
0162     d->settings->beginGroup(QLatin1String("Dropbox"));
0163     d->settings->remove(QString());
0164     d->settings->endGroup();
0165 }
0166 
0167 void DBTalker::reauthenticate()
0168 {
0169     d->o2->unlink();
0170 
0171     // Wait until user account is unlinked completely
0172     while (authenticated());
0173 
0174     d->o2->link();
0175 }
0176 
0177 void DBTalker::slotLinkingFailed()
0178 {
0179     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "LINK to Dropbox fail";
0180     Q_EMIT signalBusy(false);
0181 }
0182 
0183 void DBTalker::slotLinkingSucceeded()
0184 {
0185     if (!d->o2->linked())
0186     {
0187         qCDebug(DIGIKAM_WEBSERVICES_LOG) << "UNLINK to Dropbox ok";
0188         Q_EMIT signalBusy(false);
0189         return;
0190     }
0191 
0192     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "LINK to Dropbox ok";
0193     Q_EMIT signalLinkingSucceeded();
0194 }
0195 
0196 void DBTalker::slotOpenBrowser(const QUrl& url)
0197 {
0198     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Open Browser...";
0199     QDesktopServices::openUrl(url);
0200 }
0201 
0202 bool DBTalker::authenticated()
0203 {
0204     return d->o2->linked();
0205 }
0206 
0207 /** Creates folder at specified path
0208  */
0209 void DBTalker::createFolder(const QString& path)
0210 {
0211     //path also has name of new folder so send path parameter accordingly
0212     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "createFolder:" << path;
0213 
0214     QUrl url(QLatin1String("https://api.dropboxapi.com/2/files/create_folder_v2"));
0215 
0216     QNetworkRequest netRequest(url);
0217     netRequest.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String(O2_MIME_TYPE_JSON));
0218     netRequest.setRawHeader("Authorization", QString::fromLatin1("Bearer %1").arg(d->o2->token()).toUtf8());
0219 
0220     QByteArray postData = QString::fromUtf8("{\"path\": \"%1\"}").arg(path).toUtf8();
0221 
0222     d->reply = d->netMngr->post(netRequest, postData);
0223 
0224     d->state = Private::DB_CREATEFOLDER;
0225     Q_EMIT signalBusy(true);
0226 }
0227 
0228 /** Get username of dropbox user
0229  */
0230 void DBTalker::getUserName()
0231 {
0232     QUrl url(QLatin1String("https://api.dropboxapi.com/2/users/get_current_account"));
0233 
0234     QNetworkRequest netRequest(url);
0235     netRequest.setRawHeader("Authorization", QString::fromLatin1("Bearer %1").arg(d->o2->token()).toUtf8());
0236 
0237     d->reply = d->netMngr->post(netRequest, QByteArray());
0238 
0239     d->state = Private::DB_USERNAME;
0240     Q_EMIT signalBusy(true);
0241 }
0242 
0243 /** Get list of folders by parsing json sent by dropbox
0244  */
0245 void DBTalker::listFolders(const QString& cursor)
0246 {
0247     QUrl url(QLatin1String("https://api.dropboxapi.com/2/files/list_folder"));
0248     QByteArray postData;
0249 
0250     if (cursor.isEmpty())
0251     {
0252         d->folderList.clear();
0253         postData = QString::fromUtf8("{\"path\": \"\",\"recursive\": true}").toUtf8();
0254     }
0255     else
0256     {
0257         url.setPath(url.path() + QLatin1String("/continue"));
0258         postData = QString::fromUtf8("{\"cursor\": \"%1\"}").arg(cursor).toUtf8();
0259     }
0260 
0261     QNetworkRequest netRequest(url);
0262     netRequest.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String(O2_MIME_TYPE_JSON));
0263     netRequest.setRawHeader("Authorization", QString::fromLatin1("Bearer %1").arg(d->o2->token()).toUtf8());
0264 
0265     d->reply = d->netMngr->post(netRequest, postData);
0266 
0267     d->state = Private::DB_LISTFOLDERS;
0268     Q_EMIT signalBusy(true);
0269 }
0270 
0271 bool DBTalker::addPhoto(const QString& imgPath, const QString& uploadFolder,
0272                         bool original, bool rescale, int maxDim, int imageQuality)
0273 {
0274     if (d->reply)
0275     {
0276         d->reply->abort();
0277         d->reply = nullptr;
0278     }
0279 
0280     Q_EMIT signalBusy(true);
0281 
0282     QString path = imgPath;
0283 
0284     QMimeDatabase mimeDB;
0285 
0286     if (!original && mimeDB.mimeTypeForFile(imgPath).name().startsWith(QLatin1String("image/")))
0287     {
0288         QImage image = PreviewLoadThread::loadHighQualitySynchronously(imgPath).copyQImage();
0289 
0290         if (image.isNull())
0291         {
0292             image.load(imgPath);
0293         }
0294 
0295         if (image.isNull())
0296         {
0297             Q_EMIT signalBusy(false);
0298             return false;
0299         }
0300 
0301         path = WSToolUtils::makeTemporaryDir("dropbox").filePath(QFileInfo(imgPath)
0302                                              .baseName().trimmed() + QLatin1String(".jpg"));
0303 
0304         if (rescale && (image.width() > maxDim || image.height() > maxDim))
0305         {
0306             image = image.scaled(maxDim, maxDim, Qt::KeepAspectRatio, Qt::SmoothTransformation);
0307         }
0308 
0309         image.save(path, "JPEG", imageQuality);
0310 
0311         QScopedPointer<DMetadata> meta(new DMetadata);
0312 
0313         if (meta->load(imgPath))
0314         {
0315             meta->setItemDimensions(image.size());
0316             meta->setItemOrientation(DMetadata::ORIENTATION_NORMAL);
0317             meta->setMetadataWritingMode((int)DMetadata::WRITE_TO_FILE_ONLY);
0318             meta->save(path, true);
0319         }
0320     }
0321 
0322     DBMPForm form;
0323 
0324     if (!form.addFile(path))
0325     {
0326         Q_EMIT signalBusy(false);
0327         return false;
0328     }
0329 
0330     QString uploadPath = uploadFolder + QUrl(QUrl::fromLocalFile(imgPath)).fileName();
0331     QUrl url(QLatin1String("https://content.dropboxapi.com/2/files/upload"));
0332 
0333     QNetworkRequest netRequest(url);
0334     netRequest.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String("application/octet-stream"));
0335     netRequest.setRawHeader("Authorization", QString::fromLatin1("Bearer %1").arg(d->o2->token()).toUtf8());
0336 
0337     QByteArray postData = QString::fromUtf8("{\"path\": \"%1\",\"mode\": \"add\"}").arg(uploadPath).toUtf8();
0338     netRequest.setRawHeader("Dropbox-API-Arg", postData);
0339 
0340     d->reply = d->netMngr->post(netRequest, form.formData());
0341 
0342     d->state = Private::DB_ADDPHOTO;
0343     return true;
0344 }
0345 
0346 void DBTalker::cancel()
0347 {
0348     if (d->reply)
0349     {
0350         d->reply->abort();
0351         d->reply = nullptr;
0352     }
0353 
0354     Q_EMIT signalBusy(false);
0355 }
0356 
0357 void DBTalker::slotFinished(QNetworkReply* reply)
0358 {
0359     if (reply != d->reply)
0360     {
0361         return;
0362     }
0363 
0364     d->reply = nullptr;
0365 
0366     if (reply->error() != QNetworkReply::NoError)
0367     {
0368         if (d->state != Private::DB_CREATEFOLDER)
0369         {
0370             Q_EMIT signalBusy(false);
0371             QMessageBox::critical(QApplication::activeWindow(),
0372                                   i18nc("@title:window", "Error"), reply->errorString());
0373 
0374             reply->deleteLater();
0375             return;
0376         }
0377     }
0378 
0379     QByteArray buffer = reply->readAll();
0380 
0381     switch (d->state)
0382     {
0383         case Private::DB_LISTFOLDERS:
0384             qCDebug(DIGIKAM_WEBSERVICES_LOG) << "In DB_LISTFOLDERS";
0385             parseResponseListFolders(buffer);
0386             break;
0387         case Private::DB_CREATEFOLDER:
0388             qCDebug(DIGIKAM_WEBSERVICES_LOG) << "In DB_CREATEFOLDER";
0389             parseResponseCreateFolder(buffer);
0390             break;
0391         case Private::DB_ADDPHOTO:
0392             qCDebug(DIGIKAM_WEBSERVICES_LOG) << "In DB_ADDPHOTO";
0393             parseResponseAddPhoto(buffer);
0394             break;
0395         case Private::DB_USERNAME:
0396             qCDebug(DIGIKAM_WEBSERVICES_LOG) << "In DB_USERNAME";
0397             parseResponseUserName(buffer);
0398             break;
0399         default:
0400             break;
0401     }
0402 
0403     reply->deleteLater();
0404 }
0405 
0406 void DBTalker::parseResponseAddPhoto(const QByteArray& data)
0407 {
0408     QJsonDocument doc      = QJsonDocument::fromJson(data);
0409     QJsonObject jsonObject = doc.object();
0410     bool success           = jsonObject.contains(QLatin1String("size"));
0411     Q_EMIT signalBusy(false);
0412 
0413     if (!success)
0414     {
0415         Q_EMIT signalAddPhotoFailed(i18n("Failed to upload photo"));
0416     }
0417     else
0418     {
0419         Q_EMIT signalAddPhotoSucceeded();
0420     }
0421 }
0422 
0423 void DBTalker::parseResponseUserName(const QByteArray& data)
0424 {
0425     QJsonDocument doc      = QJsonDocument::fromJson(data);
0426     QJsonObject jsonObject = doc.object()[QLatin1String("name")].toObject();
0427 
0428     QString name           = jsonObject[QLatin1String("display_name")].toString();
0429 
0430     Q_EMIT signalBusy(false);
0431     Q_EMIT signalSetUserName(name);
0432 }
0433 
0434 void DBTalker::parseResponseListFolders(const QByteArray& data)
0435 {
0436     QJsonParseError err;
0437     QJsonDocument doc = QJsonDocument::fromJson(data, &err);
0438 
0439     if (err.error != QJsonParseError::NoError)
0440     {
0441         Q_EMIT signalBusy(false);
0442         Q_EMIT signalListAlbumsFailed(i18n("Failed to list folders"));
0443         return;
0444     }
0445 
0446     QJsonObject jsonObject = doc.object();
0447     QJsonArray jsonArray   = jsonObject[QLatin1String("entries")].toArray();
0448 
0449     if (d->folderList.isEmpty())
0450     {
0451         d->folderList.append(qMakePair(QLatin1String(""), QLatin1String("root")));
0452     }
0453 
0454     Q_FOREACH (const QJsonValue& value, jsonArray)
0455     {
0456         QString path;
0457         QString folder;
0458 
0459         QJsonObject obj = value.toObject();
0460         path            = obj[QLatin1String("path_display")].toString();
0461         folder          = obj[QLatin1String(".tag")].toString();
0462 
0463         if (folder == QLatin1String("folder"))
0464         {
0465             qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Path is" << path;
0466             QString listName = path.section(QLatin1Char('/'), 1);
0467             d->folderList.append(qMakePair(path, listName));
0468         }
0469     }
0470 
0471     if (jsonObject[QLatin1String("has_more")].toBool())
0472     {
0473         QString cursor = jsonObject[QLatin1String("cursor")].toString();
0474 
0475         if (!cursor.isEmpty())
0476         {
0477             listFolders(cursor);
0478             return;
0479         }
0480     }
0481 
0482     std::sort(d->folderList.begin(), d->folderList.end());
0483 
0484     Q_EMIT signalBusy(false);
0485     Q_EMIT signalListAlbumsDone(d->folderList);
0486 }
0487 
0488 void DBTalker::parseResponseCreateFolder(const QByteArray& data)
0489 {
0490     QJsonDocument doc      = QJsonDocument::fromJson(data);
0491     QJsonObject jsonObject = doc.object();
0492     bool fail              = jsonObject.contains(QLatin1String("error"));
0493 
0494     Q_EMIT signalBusy(false);
0495 
0496     if (fail)
0497     {
0498         Q_EMIT signalCreateFolderFailed(jsonObject[QLatin1String("error_summary")].toString());
0499     }
0500     else
0501     {
0502         Q_EMIT signalCreateFolderSucceeded();
0503     }
0504 }
0505 
0506 } // namespace DigikamGenericDropBoxPlugin
0507 
0508 #include "moc_dbtalker.cpp"