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"