File indexing completed on 2025-01-05 03:53:23
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 Box 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 "boxtalker.h" 0016 0017 // Qt includes 0018 0019 #include <QMimeDatabase> 0020 #include <QJsonDocument> 0021 #include <QJsonParseError> 0022 #include <QJsonObject> 0023 #include <QJsonValue> 0024 #include <QJsonArray> 0025 #include <QByteArray> 0026 #include <QFileInfo> 0027 #include <QWidget> 0028 #include <QSettings> 0029 #include <QMessageBox> 0030 #include <QApplication> 0031 #include <QDesktopServices> 0032 #include <QHttpMultiPart> 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 "wstoolutils.h" 0043 #include "boxwindow.h" 0044 #include "previewloadthread.h" 0045 #include "o0settingsstore.h" 0046 #include "networkmanager.h" 0047 0048 namespace DigikamGenericBoxPlugin 0049 { 0050 0051 class Q_DECL_HIDDEN BOXTalker::Private 0052 { 0053 public: 0054 0055 enum State 0056 { 0057 BOX_USERNAME = 0, 0058 BOX_LISTFOLDERS, 0059 BOX_CREATEFOLDER, 0060 BOX_ADDPHOTO 0061 }; 0062 0063 public: 0064 0065 explicit Private() 0066 : clientId(QLatin1String("yvd43v8av9zgg9phig80m2dc3r7mks4t")), 0067 clientSecret(QLatin1String("KJkuMjvzOKDMyp3oxweQBEYixg678Fh5")), 0068 authUrl(QLatin1String("https://account.box.com/api/oauth2/authorize")), 0069 tokenUrl(QLatin1String("https://api.box.com/oauth2/token")), 0070 redirectUrl(QLatin1String("https://app.box.com")), 0071 state(BOX_USERNAME), 0072 parent(nullptr), 0073 netMngr(nullptr), 0074 reply(nullptr), 0075 settings(nullptr), 0076 o2(nullptr) 0077 { 0078 } 0079 0080 public: 0081 0082 QString clientId; 0083 QString clientSecret; 0084 QString authUrl; 0085 QString tokenUrl; 0086 QString redirectUrl; 0087 0088 State state; 0089 0090 QWidget* parent; 0091 0092 QNetworkAccessManager* netMngr; 0093 QNetworkReply* reply; 0094 0095 QSettings* settings; 0096 0097 O2* o2; 0098 0099 QList<QPair<QString, QString> > foldersList; 0100 }; 0101 0102 BOXTalker::BOXTalker(QWidget* const parent) 0103 : d(new Private) 0104 { 0105 d->parent = parent; 0106 d->netMngr = NetworkManager::instance()->getNetworkManager(this); 0107 0108 connect(this, SIGNAL(boxLinkingFailed()), 0109 this, SLOT(slotLinkingFailed())); 0110 0111 connect(this, SIGNAL(boxLinkingSucceeded()), 0112 this, SLOT(slotLinkingSucceeded())); 0113 0114 connect(d->netMngr, SIGNAL(finished(QNetworkReply*)), 0115 this, SLOT(slotFinished(QNetworkReply*))); 0116 0117 d->o2 = new O2(this); 0118 0119 d->o2->setClientId(d->clientId); 0120 d->o2->setClientSecret(d->clientSecret); 0121 d->o2->setRefreshTokenUrl(d->tokenUrl); 0122 d->o2->setRequestUrl(d->authUrl); 0123 d->o2->setTokenUrl(d->tokenUrl); 0124 d->o2->setLocalPort(8000); 0125 0126 d->settings = WSToolUtils::getOauthSettings(this); 0127 O0SettingsStore* const store = new O0SettingsStore(d->settings, QLatin1String(O2_ENCRYPTION_KEY), this); 0128 store->setGroupKey(QLatin1String("Box")); 0129 d->o2->setStore(store); 0130 0131 connect(d->o2, SIGNAL(linkingFailed()), 0132 this, SLOT(slotLinkingFailed())); 0133 0134 connect(d->o2, SIGNAL(linkingSucceeded()), 0135 this, SLOT(slotLinkingSucceeded())); 0136 0137 connect(d->o2, SIGNAL(openBrowser(QUrl)), 0138 this, SLOT(slotOpenBrowser(QUrl))); 0139 } 0140 0141 BOXTalker::~BOXTalker() 0142 { 0143 if (d->reply) 0144 { 0145 d->reply->abort(); 0146 } 0147 0148 WSToolUtils::removeTemporaryDir("box"); 0149 0150 delete d; 0151 } 0152 0153 void BOXTalker::link() 0154 { 0155 Q_EMIT signalBusy(true); 0156 d->o2->link(); 0157 } 0158 0159 void BOXTalker::unLink() 0160 { 0161 d->o2->unlink(); 0162 d->settings->beginGroup(QLatin1String("Box")); 0163 d->settings->remove(QString()); 0164 d->settings->endGroup(); 0165 } 0166 0167 void BOXTalker::slotLinkingFailed() 0168 { 0169 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "LINK to Box fail"; 0170 Q_EMIT signalBusy(false); 0171 } 0172 0173 void BOXTalker::slotLinkingSucceeded() 0174 { 0175 if (!d->o2->linked()) 0176 { 0177 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "UNLINK to Box ok"; 0178 Q_EMIT signalBusy(false); 0179 return; 0180 } 0181 0182 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "LINK to Box ok"; 0183 Q_EMIT signalLinkingSucceeded(); 0184 } 0185 0186 bool BOXTalker::authenticated() 0187 { 0188 return d->o2->linked(); 0189 } 0190 0191 void BOXTalker::cancel() 0192 { 0193 if (d->reply) 0194 { 0195 d->reply->abort(); 0196 d->reply = nullptr; 0197 } 0198 0199 Q_EMIT signalBusy(false); 0200 } 0201 0202 void BOXTalker::slotOpenBrowser(const QUrl& url) 0203 { 0204 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Open Browser..."; 0205 QDesktopServices::openUrl(url); 0206 } 0207 0208 void BOXTalker::createFolder(const QString& path) 0209 { 0210 QString name = path.section(QLatin1Char('/'), -1); 0211 QString folderPath = path.section(QLatin1Char('/'), -2, -2); 0212 0213 QString id; 0214 0215 for (int i = 0 ; i < d->foldersList.size() ; ++i) 0216 { 0217 if (d->foldersList.value(i).second == folderPath) 0218 { 0219 id = d->foldersList.value(i).first; 0220 } 0221 } 0222 0223 QUrl url(QLatin1String("https://api.box.com/2.0/folders")); 0224 QNetworkRequest netRequest(url); 0225 netRequest.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String("application/json")); 0226 netRequest.setRawHeader("Authorization", QString::fromLatin1("Bearer %1").arg(d->o2->token()).toUtf8()); 0227 0228 QByteArray postData = QString::fromUtf8("{\"name\": \"%1\",\"parent\": {\"id\": \"%2\"}}").arg(name).arg(id).toUtf8(); 0229 0230 d->reply = d->netMngr->post(netRequest, postData); 0231 d->state = Private::BOX_CREATEFOLDER; 0232 0233 Q_EMIT signalBusy(true); 0234 } 0235 0236 void BOXTalker::getUserName() 0237 { 0238 QUrl url(QLatin1String("https://api.box.com/2.0/users/me")); 0239 0240 QNetworkRequest netRequest(url); 0241 netRequest.setRawHeader("Authorization", QString::fromLatin1("Bearer %1").arg(d->o2->token()).toUtf8()); 0242 netRequest.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String("application/json")); 0243 0244 d->reply = d->netMngr->get(netRequest); 0245 d->state = Private::BOX_USERNAME; 0246 0247 Q_EMIT signalBusy(true); 0248 } 0249 0250 void BOXTalker::listFolders(const QString& /*path*/) 0251 { 0252 QUrl url(QLatin1String("https://api.box.com/2.0/folders/0/items"));; 0253 0254 QNetworkRequest netRequest(url); 0255 netRequest.setRawHeader("Authorization", QString::fromLatin1("Bearer %1").arg(d->o2->token()).toUtf8()); 0256 netRequest.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String("application/json")); 0257 0258 d->reply = d->netMngr->get(netRequest); 0259 d->state = Private::BOX_LISTFOLDERS; 0260 0261 Q_EMIT signalBusy(true); 0262 } 0263 0264 bool BOXTalker::addPhoto(const QString& imgPath, const QString& uploadFolder, bool rescale, int maxDim, int imageQuality) 0265 { 0266 if (d->reply) 0267 { 0268 d->reply->abort(); 0269 d->reply = nullptr; 0270 } 0271 0272 Q_EMIT signalBusy(true); 0273 0274 QMimeDatabase mimeDB; 0275 QString path = imgPath; 0276 QString mimeType = mimeDB.mimeTypeForFile(path).name(); 0277 0278 if (mimeType.startsWith(QLatin1String("image/"))) 0279 { 0280 QImage image = PreviewLoadThread::loadHighQualitySynchronously(imgPath).copyQImage(); 0281 0282 if (image.isNull()) 0283 { 0284 Q_EMIT signalBusy(false); 0285 return false; 0286 } 0287 0288 path = WSToolUtils::makeTemporaryDir("box").filePath(QFileInfo(imgPath) 0289 .baseName().trimmed() + QLatin1String(".jpg")); 0290 0291 if (rescale && ((image.width() > maxDim) || (image.height() > maxDim))) 0292 { 0293 image = image.scaled(maxDim, maxDim, Qt::KeepAspectRatio, Qt::SmoothTransformation); 0294 } 0295 0296 image.save(path, "JPEG", imageQuality); 0297 0298 QScopedPointer<DMetadata> meta(new DMetadata); 0299 0300 if (meta->load(imgPath)) 0301 { 0302 meta->setItemDimensions(image.size()); 0303 meta->setItemOrientation(DMetadata::ORIENTATION_NORMAL); 0304 meta->setMetadataWritingMode((int)DMetadata::WRITE_TO_FILE_ONLY); 0305 meta->save(path, true); 0306 } 0307 } 0308 0309 QString id; 0310 0311 for (int i = 0 ; i < d->foldersList.size() ; ++i) 0312 { 0313 if (d->foldersList.value(i).second == uploadFolder) 0314 { 0315 id = d->foldersList.value(i).first; 0316 } 0317 } 0318 0319 QHttpMultiPart* const multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType); 0320 0321 QHttpPart attributes; 0322 QString attributesHeader = QLatin1String("form-data; name=\"attributes\""); 0323 attributes.setHeader(QNetworkRequest::ContentDispositionHeader, attributesHeader); 0324 0325 QString postData = QLatin1String("{\"name\":\"") + QFileInfo(imgPath).fileName() + QLatin1Char('"') + 0326 QLatin1String(", \"parent\":{\"id\":\"") + id + QLatin1String("\"}}"); 0327 attributes.setBody(postData.toUtf8()); 0328 multiPart->append(attributes); 0329 0330 QFile* const file = new QFile(path); 0331 0332 if (!file) 0333 { 0334 return false; 0335 } 0336 0337 if (!file->open(QIODevice::ReadOnly)) 0338 { 0339 return false; 0340 } 0341 0342 QHttpPart imagePart; 0343 QString imagePartHeader = QLatin1String("form-data; name=\"file\"; filename=\"") + 0344 QFileInfo(imgPath).fileName() + QLatin1Char('"'); 0345 0346 imagePart.setHeader(QNetworkRequest::ContentDispositionHeader, imagePartHeader); 0347 imagePart.setHeader(QNetworkRequest::ContentTypeHeader, mimeType); 0348 0349 imagePart.setBodyDevice(file); 0350 multiPart->append(imagePart); 0351 0352 QUrl url(QString::fromLatin1("https://upload.box.com/api/2.0/files/content?access_token=%1").arg(d->o2->token())); 0353 0354 QNetworkRequest netRequest(url); 0355 QString content = QLatin1String("multipart/form-data;boundary=") + QString::fromUtf8(multiPart->boundary()); 0356 netRequest.setHeader(QNetworkRequest::ContentTypeHeader, content); 0357 d->reply = d->netMngr->post(netRequest, multiPart); 0358 0359 // delete the multiPart and file with the reply 0360 0361 multiPart->setParent(d->reply); 0362 0363 d->state = Private::BOX_ADDPHOTO; 0364 0365 return true; 0366 } 0367 0368 void BOXTalker::slotFinished(QNetworkReply* reply) 0369 { 0370 if (reply != d->reply) 0371 { 0372 return; 0373 } 0374 0375 d->reply = nullptr; 0376 0377 if (reply->error() != QNetworkReply::NoError) 0378 { 0379 if (d->state != Private::BOX_CREATEFOLDER) 0380 { 0381 Q_EMIT signalBusy(false); 0382 QMessageBox::critical(QApplication::activeWindow(), 0383 i18nc("@title:window", "Error"), reply->errorString()); 0384 reply->deleteLater(); 0385 return; 0386 } 0387 } 0388 0389 QByteArray buffer = reply->readAll(); 0390 0391 switch (d->state) 0392 { 0393 case Private::BOX_LISTFOLDERS: 0394 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "In BOX_LISTFOLDERS"; 0395 parseResponseListFolders(buffer); 0396 break; 0397 0398 case Private::BOX_CREATEFOLDER: 0399 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "In BOX_CREATEFOLDER"; 0400 parseResponseCreateFolder(buffer); 0401 break; 0402 0403 case Private::BOX_ADDPHOTO: 0404 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "In BOX_ADDPHOTO"; 0405 parseResponseAddPhoto(buffer); 0406 break; 0407 0408 case Private::BOX_USERNAME: 0409 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "In BOX_USERNAME"; 0410 parseResponseUserName(buffer); 0411 break; 0412 0413 default: 0414 break; 0415 } 0416 0417 reply->deleteLater(); 0418 } 0419 0420 void BOXTalker::parseResponseAddPhoto(const QByteArray& data) 0421 { 0422 QJsonDocument doc = QJsonDocument::fromJson(data); 0423 QJsonObject jsonObject = doc.object(); 0424 bool success = jsonObject.contains(QLatin1String("total_count")); 0425 Q_EMIT signalBusy(false); 0426 0427 if (!success) 0428 { 0429 Q_EMIT signalAddPhotoFailed(i18n("Failed to upload photo")); 0430 } 0431 else 0432 { 0433 Q_EMIT signalAddPhotoSucceeded(); 0434 } 0435 } 0436 0437 void BOXTalker::parseResponseUserName(const QByteArray& data) 0438 { 0439 QJsonDocument doc = QJsonDocument::fromJson(data); 0440 QString name = doc.object()[QLatin1String("name")].toString(); 0441 Q_EMIT signalBusy(false); 0442 Q_EMIT signalSetUserName(name); 0443 } 0444 0445 void BOXTalker::parseResponseListFolders(const QByteArray& data) 0446 { 0447 QJsonParseError err; 0448 QJsonDocument doc = QJsonDocument::fromJson(data, &err); 0449 0450 if (err.error != QJsonParseError::NoError) 0451 { 0452 Q_EMIT signalBusy(false); 0453 Q_EMIT signalListAlbumsFailed(i18n("Failed to list folders")); 0454 return; 0455 } 0456 0457 QJsonObject jsonObject = doc.object(); 0458 QJsonArray jsonArray = jsonObject[QLatin1String("entries")].toArray(); 0459 0460 d->foldersList.clear(); 0461 d->foldersList.append(qMakePair(QLatin1String("0"), QLatin1String("root"))); 0462 0463 Q_FOREACH (const QJsonValue& value, jsonArray) 0464 { 0465 QString folderName; 0466 QString type; 0467 QString id; 0468 0469 QJsonObject obj = value.toObject(); 0470 type = obj[QLatin1String("type")].toString(); 0471 0472 if (type == QLatin1String("folder")) 0473 { 0474 folderName = obj[QLatin1String("name")].toString(); 0475 id = obj[QLatin1String("id")].toString(); 0476 d->foldersList.append(qMakePair(id, folderName)); 0477 } 0478 } 0479 0480 Q_EMIT signalBusy(false); 0481 Q_EMIT signalListAlbumsDone(d->foldersList); 0482 } 0483 0484 void BOXTalker::parseResponseCreateFolder(const QByteArray& data) 0485 { 0486 QJsonDocument doc1 = QJsonDocument::fromJson(data); 0487 QJsonObject jsonObject = doc1.object(); 0488 bool fail = jsonObject.contains(QLatin1String("error")); 0489 0490 Q_EMIT signalBusy(false); 0491 0492 if (fail) 0493 { 0494 QJsonParseError err; 0495 QJsonDocument doc2 = QJsonDocument::fromJson(data, &err); 0496 Q_EMIT signalCreateFolderFailed(jsonObject[QLatin1String("error_summary")].toString()); 0497 } 0498 else 0499 { 0500 Q_EMIT signalCreateFolderSucceeded(); 0501 } 0502 } 0503 0504 } // namespace DigikamGenericBoxPlugin 0505 0506 #include "moc_boxtalker.cpp"