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"