File indexing completed on 2025-01-19 03:53:09

0001 /* ============================================================
0002  *
0003  * This file is a part of digiKam project
0004  * https://www.digikam.org
0005  *
0006  * Date        : 2018-05-20
0007  * Description : a tool to export images to Onedrive web service
0008  *
0009  * SPDX-FileCopyrightText: 2018      by Tarek Talaat <tarektalaat93 at gmail dot com>
0010  *
0011  * SPDX-License-Identifier: GPL-2.0-or-later
0012  *
0013  * ============================================================ */
0014 
0015 #include "odtalker.h"
0016 
0017 // Qt includes
0018 
0019 #include <QJsonDocument>
0020 #include <QJsonParseError>
0021 #include <QJsonObject>
0022 #include <QJsonValue>
0023 #include <QJsonArray>
0024 #include <QByteArray>
0025 #include <QFileInfo>
0026 #include <QWidget>
0027 #include <QMessageBox>
0028 #include <QApplication>
0029 #include <QMimeDatabase>
0030 #include <QDesktopServices>
0031 #include <QUrlQuery>
0032 
0033 // KDE includes
0034 
0035 #include <klocalizedstring.h>
0036 #include <kwindowconfig.h>
0037 
0038 // Local includes
0039 
0040 #include "digikam_debug.h"
0041 #include "digikam_version.h"
0042 #include "previewloadthread.h"
0043 #include "networkmanager.h"
0044 #include "webbrowserdlg.h"
0045 #include "wstoolutils.h"
0046 #include "odwindow.h"
0047 #include "odmpform.h"
0048 
0049 namespace DigikamGenericOneDrivePlugin
0050 {
0051 
0052 class Q_DECL_HIDDEN ODTalker::Private
0053 {
0054 public:
0055 
0056     enum State
0057     {
0058         OD_USERNAME = 0,
0059         OD_LISTFOLDERS,
0060         OD_CREATEFOLDER,
0061         OD_ADDPHOTO
0062     };
0063 
0064 public:
0065 
0066     explicit Private()
0067       : state   (OD_USERNAME),
0068         parent  (nullptr),
0069         netMngr (nullptr),
0070         reply   (nullptr),
0071         settings(nullptr),
0072         browser (nullptr)
0073     {
0074         clientId     = QLatin1String("4c20a541-2ca8-4b98-8847-a375e4d33f34");
0075         clientSecret = QLatin1String("wtdcaXADCZ0|tcDA7633|@*");
0076 
0077         authUrl      = QLatin1String("https://login.live.com/oauth20_authorize.srf");
0078         tokenUrl     = QLatin1String("https://login.live.com/oauth20_token.srf");
0079         scope        = QLatin1String("Files.ReadWrite User.Read");
0080         redirectUrl  = QLatin1String("https://login.live.com/oauth20_desktop.srf");
0081         serviceName  = QLatin1String("Onedrive");
0082         serviceTime  = QLatin1String("token_time");
0083         serviceKey   = QLatin1String("access_token");
0084     }
0085 
0086 public:
0087 
0088     QString                         clientId;
0089     QString                         clientSecret;
0090     QString                         authUrl;
0091     QString                         tokenUrl;
0092     QString                         scope;
0093     QString                         redirectUrl;
0094     QString                         accessToken;
0095     QString                         serviceName;
0096     QString                         serviceTime;
0097     QString                         serviceKey;
0098 
0099     QDateTime                       expiryTime;
0100 
0101     State                           state;
0102 
0103     QWidget*                        parent;
0104 
0105     QNetworkAccessManager*          netMngr;
0106     QNetworkReply*                  reply;
0107 
0108     QSettings*                      settings;
0109 
0110     WebBrowserDlg*                  browser;
0111 
0112     QList<QPair<QString, QString> > folderList;
0113     QList<QString>                  nextFolder;
0114 };
0115 
0116 ODTalker::ODTalker(QWidget* const parent)
0117     : d           (new Private)
0118 {
0119     d->parent   = parent;
0120     d->netMngr  = NetworkManager::instance()->getNetworkManager(this);
0121     d->settings = WSToolUtils::getOauthSettings(this);
0122 
0123     connect(this, SIGNAL(oneDriveLinkingFailed()),
0124             this, SLOT(slotLinkingFailed()));
0125 
0126     connect(this, SIGNAL(oneDriveLinkingSucceeded()),
0127             this, SLOT(slotLinkingSucceeded()));
0128 
0129     connect(d->netMngr, SIGNAL(finished(QNetworkReply*)),
0130             this, SLOT(slotFinished(QNetworkReply*)));
0131 }
0132 
0133 ODTalker::~ODTalker()
0134 {
0135     if (d->reply)
0136     {
0137         d->reply->abort();
0138     }
0139 
0140     WSToolUtils::removeTemporaryDir("onedrive");
0141 
0142     delete d;
0143 }
0144 
0145 void ODTalker::link()
0146 {
0147     Q_EMIT signalBusy(true);
0148 
0149     QUrl url(d->authUrl);
0150     QUrlQuery query(url);
0151     query.addQueryItem(QLatin1String("client_id"), d->clientId);
0152     query.addQueryItem(QLatin1String("scope"), d->scope);
0153     query.addQueryItem(QLatin1String("redirect_uri"), d->redirectUrl);
0154     query.addQueryItem(QLatin1String("response_type"), QLatin1String("token"));
0155     url.setQuery(query);
0156 
0157     delete d->browser;
0158 
0159     d->browser = new WebBrowserDlg(url, d->parent, true);
0160     d->browser->setModal(true);
0161 
0162     connect(d->browser, SIGNAL(urlChanged(QUrl)),
0163             this, SLOT(slotCatchUrl(QUrl)));
0164 
0165     connect(d->browser, SIGNAL(closeView(bool)),
0166             this, SIGNAL(signalBusy(bool)));
0167 
0168     d->browser->show();
0169 }
0170 
0171 void ODTalker::unLink()
0172 {
0173     d->accessToken = QString();
0174 
0175     d->settings->beginGroup(d->serviceName);
0176     d->settings->remove(QString());
0177     d->settings->endGroup();
0178 
0179     Q_EMIT oneDriveLinkingSucceeded();
0180 }
0181 
0182 void ODTalker::slotCatchUrl(const QUrl& url)
0183 {
0184     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Received URL from webview:" << url;
0185 
0186     QString   str = url.toString();
0187     QUrlQuery query(str.section(QLatin1Char('#'), -1, -1));
0188 
0189     if (query.hasQueryItem(QLatin1String("access_token")))
0190     {
0191         d->accessToken = query.queryItemValue(QLatin1String("access_token"));
0192         int seconds    = query.queryItemValue(QLatin1String("expires_in")).toInt();
0193         d->expiryTime  = QDateTime::currentDateTime().addSecs(seconds);
0194 
0195         writeSettings();
0196 
0197         qDebug(DIGIKAM_WEBSERVICES_LOG) << "Access token received";
0198         Q_EMIT oneDriveLinkingSucceeded();
0199     }
0200     else
0201     {
0202         Q_EMIT oneDriveLinkingFailed();
0203     }
0204 }
0205 
0206 void ODTalker::slotLinkingFailed()
0207 {
0208     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "LINK to Onedrive fail";
0209     Q_EMIT signalBusy(false);
0210 }
0211 
0212 void ODTalker::slotLinkingSucceeded()
0213 {
0214     if (d->accessToken.isEmpty())
0215     {
0216         qCDebug(DIGIKAM_WEBSERVICES_LOG) << "UNLINK to Onedrive";
0217         Q_EMIT signalBusy(false);
0218         return;
0219     }
0220 
0221     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "LINK to Onedrive";
0222 
0223     if (d->browser)
0224     {
0225         d->browser->close();
0226     }
0227 
0228     Q_EMIT signalLinkingSucceeded();
0229 }
0230 
0231 bool ODTalker::authenticated()
0232 {
0233     return (!d->accessToken.isEmpty());
0234 }
0235 
0236 void ODTalker::cancel()
0237 {
0238     if (d->reply)
0239     {
0240         d->reply->abort();
0241         d->reply = nullptr;
0242     }
0243 
0244     Q_EMIT signalBusy(false);
0245 }
0246 
0247 void ODTalker::createFolder(QString& path)
0248 {
0249     // path also has name of new folder so send path parameter accordingly
0250 
0251     QString name       = QUrl(path).fileName();
0252     QString folderPath = QUrl(path).adjusted(QUrl::RemoveFilename |
0253                                              QUrl::StripTrailingSlash).path();
0254 
0255     QUrl url;
0256 
0257     if (folderPath == QLatin1String("/"))
0258     {
0259         url = QUrl(QLatin1String("https://graph.microsoft.com/v1.0/me/drive/root/children"));
0260     }
0261     else
0262     {
0263         url = QUrl(QString::fromUtf8("https://graph.microsoft.com/v1.0/me/drive/root:/%1:/children").arg(folderPath));
0264     }
0265 
0266     QNetworkRequest netRequest(url);
0267     netRequest.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String("application/json"));
0268     netRequest.setRawHeader("Authorization", QString::fromLatin1("Bearer %1").arg(d->accessToken).toUtf8());
0269 
0270     QByteArray postData = QString::fromUtf8("{\"name\": \"%1\",\"folder\": {}}").arg(name).toUtf8();
0271     d->reply = d->netMngr->post(netRequest, postData);
0272 
0273     d->state = Private::OD_CREATEFOLDER;
0274     Q_EMIT signalBusy(true);
0275 }
0276 
0277 void ODTalker::getUserName()
0278 {
0279     QUrl url(QLatin1String("https://graph.microsoft.com/v1.0/me"));
0280 
0281     QNetworkRequest netRequest(url);
0282     netRequest.setRawHeader("Authorization", QString::fromLatin1("Bearer %1").arg(d->accessToken).toUtf8());
0283     netRequest.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String("application/json"));
0284 
0285     d->reply = d->netMngr->get(netRequest);
0286     d->state = Private::OD_USERNAME;
0287     Q_EMIT signalBusy(true);
0288 }
0289 
0290 /**
0291  * Get list of folders by parsing json sent by onedrive
0292  */
0293 void ODTalker::listFolders(const QString& folder)
0294 {
0295     QString nextFolder;
0296 
0297     if (folder.isEmpty())
0298     {
0299         d->folderList.clear();
0300         d->nextFolder.clear();
0301     }
0302     else
0303     {
0304         nextFolder = QLatin1Char(':') + folder + QLatin1Char(':');
0305     }
0306 
0307     QUrl url(QString::fromLatin1("https://graph.microsoft.com/v1.0/me/drive/root%1/"
0308                                  "children?select=name,folder,path,parentReference").arg(nextFolder));
0309 
0310     QNetworkRequest netRequest(url);
0311     netRequest.setRawHeader("Authorization", QString::fromLatin1("Bearer %1").arg(d->accessToken).toUtf8());
0312     netRequest.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String("application/json"));
0313 
0314     d->reply = d->netMngr->get(netRequest);
0315 
0316     d->state = Private::OD_LISTFOLDERS;
0317     Q_EMIT signalBusy(true);
0318 }
0319 
0320 bool ODTalker::addPhoto(const QString& imgPath, const QString& uploadFolder, bool rescale, int maxDim, int imageQuality)
0321 {
0322     if (d->reply)
0323     {
0324         d->reply->abort();
0325         d->reply = nullptr;
0326     }
0327 
0328     Q_EMIT signalBusy(true);
0329 
0330     ODMPForm form;
0331     QString path = imgPath;
0332 
0333     QMimeDatabase mimeDB;
0334 
0335     if (mimeDB.mimeTypeForFile(imgPath).name().startsWith(QLatin1String("image/")))
0336     {
0337         QImage image = PreviewLoadThread::loadHighQualitySynchronously(imgPath).copyQImage();
0338 
0339         if (image.isNull())
0340         {
0341             Q_EMIT signalBusy(false);
0342             return false;
0343         }
0344 
0345         path = WSToolUtils::makeTemporaryDir("onedrive").filePath(QFileInfo(imgPath)
0346                                              .baseName().trimmed() + QLatin1String(".jpg"));
0347 
0348         if (rescale && ((image.width() > maxDim) || (image.height() > maxDim)))
0349         {
0350             image = image.scaled(maxDim, maxDim, Qt::KeepAspectRatio, Qt::SmoothTransformation);
0351         }
0352 
0353         image.save(path, "JPEG", imageQuality);
0354 
0355         QScopedPointer<DMetadata> meta(new DMetadata);
0356 
0357         if (meta->load(imgPath))
0358         {
0359             meta->setItemDimensions(image.size());
0360             meta->setItemOrientation(DMetadata::ORIENTATION_NORMAL);
0361             meta->setMetadataWritingMode((int)DMetadata::WRITE_TO_FILE_ONLY);
0362             meta->save(path, true);
0363         }
0364     }
0365 
0366     if (!form.addFile(path))
0367     {
0368         Q_EMIT signalBusy(false);
0369         return false;
0370     }
0371 
0372     QString uploadPath = uploadFolder + QUrl(imgPath).fileName();
0373     QUrl url(QString::fromLatin1("https://graph.microsoft.com/v1.0/me/drive/root:%1:/content").arg(uploadPath));
0374 
0375     QNetworkRequest netRequest(url);
0376     netRequest.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String("application/octet-stream"));
0377     netRequest.setRawHeader("Authorization", QString::fromLatin1("Bearer %1").arg(d->accessToken).toUtf8());
0378 
0379     d->reply = d->netMngr->put(netRequest, form.formData());
0380 
0381     d->state = Private::OD_ADDPHOTO;
0382 
0383     return true;
0384 }
0385 
0386 void ODTalker::slotFinished(QNetworkReply* reply)
0387 {
0388     if (reply != d->reply)
0389     {
0390         return;
0391     }
0392 
0393     d->reply = nullptr;
0394 
0395     if (reply->error() != QNetworkReply::NoError)
0396     {
0397         if (d->state != Private::OD_CREATEFOLDER)
0398         {
0399             Q_EMIT signalTransferCancel();
0400             QMessageBox::critical(QApplication::activeWindow(),
0401                                   i18nc("@title:window", "Error"), reply->errorString());
0402 
0403             reply->deleteLater();
0404             return;
0405         }
0406     }
0407 
0408     QByteArray buffer = reply->readAll();
0409 
0410     switch (d->state)
0411     {
0412         case Private::OD_LISTFOLDERS:
0413             qCDebug(DIGIKAM_WEBSERVICES_LOG) << "In OD_LISTFOLDERS";
0414             parseResponseListFolders(buffer);
0415             break;
0416 
0417         case Private::OD_CREATEFOLDER:
0418             qCDebug(DIGIKAM_WEBSERVICES_LOG) << "In OD_CREATEFOLDER";
0419             parseResponseCreateFolder(buffer);
0420             break;
0421 
0422         case Private::OD_ADDPHOTO:
0423             qCDebug(DIGIKAM_WEBSERVICES_LOG) << "In OD_ADDPHOTO";
0424             parseResponseAddPhoto(buffer);
0425             break;
0426 
0427         case Private::OD_USERNAME:
0428             qCDebug(DIGIKAM_WEBSERVICES_LOG) << "In OD_USERNAME";
0429             parseResponseUserName(buffer);
0430             break;
0431 
0432         default:
0433             break;
0434     }
0435 
0436     reply->deleteLater();
0437 }
0438 
0439 void ODTalker::parseResponseAddPhoto(const QByteArray& data)
0440 {
0441     QJsonDocument doc      = QJsonDocument::fromJson(data);
0442     QJsonObject jsonObject = doc.object();
0443     bool success           = jsonObject.contains(QLatin1String("size"));
0444 
0445     if (!success)
0446     {
0447         Q_EMIT signalAddPhotoFailed(i18n("Failed to upload photo"));
0448     }
0449     else
0450     {
0451         Q_EMIT signalAddPhotoSucceeded();
0452     }
0453 }
0454 
0455 void ODTalker::parseResponseUserName(const QByteArray& data)
0456 {
0457     QJsonDocument doc = QJsonDocument::fromJson(data);
0458     QString name      = doc.object()[QLatin1String("displayName")].toString();
0459     Q_EMIT signalBusy(false);
0460     Q_EMIT signalSetUserName(name);
0461 }
0462 
0463 void ODTalker::parseResponseListFolders(const QByteArray& data)
0464 {
0465     QJsonParseError err;
0466     QJsonDocument doc = QJsonDocument::fromJson(data, &err);
0467 
0468     if (err.error != QJsonParseError::NoError)
0469     {
0470         Q_EMIT signalBusy(false);
0471         Q_EMIT signalListAlbumsFailed(i18n("Failed to list folders"));
0472         return;
0473     }
0474 
0475     QJsonObject jsonObject = doc.object();
0476 /*
0477     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Json: " << doc;
0478 */
0479     QJsonArray jsonArray   = jsonObject[QLatin1String("value")].toArray();
0480 
0481     if (d->folderList.isEmpty())
0482     {
0483         d->folderList.append(qMakePair(QLatin1String(""), QLatin1String("root")));
0484     }
0485 
0486     Q_FOREACH (const QJsonValue& value, jsonArray)
0487     {
0488         QString path;
0489         QString listName;
0490         QString folderPath;
0491         QString folderName;
0492         QJsonObject folder;
0493         QJsonObject parent;
0494 
0495         QJsonObject obj = value.toObject();
0496         folder          = obj[QLatin1String("folder")].toObject();
0497         parent          = obj[QLatin1String("parentReference")].toObject();
0498 
0499         if (!folder.isEmpty())
0500         {
0501             folderPath  = parent[QLatin1String("path")].toString();
0502             folderName  = obj[QLatin1String("name")].toString();
0503 
0504             path        = folderPath.section(QLatin1String("root:"), -1, -1) +
0505                                              QLatin1Char('/') + folderName;
0506             path        = QUrl(path).toString();
0507             listName    = path.section(QLatin1Char('/'), 1);
0508 
0509             d->folderList.append(qMakePair(path, listName));
0510 
0511             if (folder[QLatin1String("childCount")].toInt() > 0)
0512             {
0513                 d->nextFolder << path;
0514             }
0515         }
0516     }
0517 
0518     if (!d->nextFolder.isEmpty())
0519     {
0520         listFolders(d->nextFolder.takeLast());
0521     }
0522     else
0523     {
0524         std::sort(d->folderList.begin(), d->folderList.end());
0525 
0526         Q_EMIT signalBusy(false);
0527         Q_EMIT signalListAlbumsDone(d->folderList);
0528     }
0529 }
0530 
0531 void ODTalker::parseResponseCreateFolder(const QByteArray& data)
0532 {
0533     QJsonDocument doc1     = QJsonDocument::fromJson(data);
0534     QJsonObject jsonObject = doc1.object();
0535     bool fail              = jsonObject.contains(QLatin1String("error"));
0536 
0537     Q_EMIT signalBusy(false);
0538 
0539     if (fail)
0540     {
0541         QJsonParseError err;
0542         QJsonDocument doc2 = QJsonDocument::fromJson(data, &err);
0543         Q_EMIT signalCreateFolderFailed(jsonObject[QLatin1String("error_summary")].toString());
0544     }
0545     else
0546     {
0547         Q_EMIT signalCreateFolderSucceeded();
0548     }
0549 }
0550 
0551 void ODTalker::writeSettings()
0552 {
0553     d->settings->beginGroup(d->serviceName);
0554     d->settings->setValue(d->serviceTime, d->expiryTime);
0555     d->settings->setValue(d->serviceKey,  d->accessToken);
0556     d->settings->endGroup();
0557 }
0558 
0559 void ODTalker::readSettings()
0560 {
0561     d->settings->beginGroup(d->serviceName);
0562     d->expiryTime  = d->settings->value(d->serviceTime).toDateTime();
0563     d->accessToken = d->settings->value(d->serviceKey).toString();
0564     d->settings->endGroup();
0565 
0566     if      (d->accessToken.isEmpty())
0567     {
0568         qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Linking...";
0569         link();
0570     }
0571     else if (QDateTime::currentDateTime() > d->expiryTime)
0572     {
0573         qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Access token has expired";
0574         d->accessToken = QString();
0575         link();
0576     }
0577     else
0578     {
0579         qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Already Linked";
0580         Q_EMIT oneDriveLinkingSucceeded();
0581     }
0582 }
0583 
0584 } // namespace DigikamGenericOneDrivePlugin
0585 
0586 #include "moc_odtalker.cpp"