1 /* ============================================================
2  *
3  * This file is a part of digiKam project
4  * https://www.digikam.org
5  *
6  * Date        : 2018-05-20
7  * Description : a tool to export images to Box web service
8  *
9  * Copyright (C) 2018      by Tarek Talaat <tarektalaat93 at gmail dot com>
10  *
11  * This program is free software; you can redistribute it
12  * and/or modify it under the terms of the GNU General
13  * Public License as published by the Free Software Foundation;
14  * either version 2, or (at your option) any later version.
15  *
16  * This program is distributed in the hope that it will be useful,
17  * but WITHOUT ANY WARRANTY; without even the implied warranty of
18  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19  * GNU General Public License for more details.
20  *
21  * ============================================================ */
22 
23 #include "boxtalker.h"
24 
25 // Qt includes
26 
27 #include <QMimeDatabase>
28 #include <QJsonDocument>
29 #include <QJsonParseError>
30 #include <QJsonObject>
31 #include <QJsonValue>
32 #include <QJsonArray>
33 #include <QByteArray>
34 #include <QList>
35 #include <QPair>
36 #include <QFileInfo>
37 #include <QWidget>
38 #include <QSettings>
39 #include <QMessageBox>
40 #include <QApplication>
41 #include <QDesktopServices>
42 #include <QHttpMultiPart>
43 #include <QNetworkAccessManager>
44 
45 // KDE includes
46 
47 #include <klocalizedstring.h>
48 
49 // Local includes
50 
51 #include "digikam_debug.h"
52 #include "digikam_version.h"
53 #include "wstoolutils.h"
54 #include "boxwindow.h"
55 #include "boxitem.h"
56 #include "previewloadthread.h"
57 #include "o0settingsstore.h"
58 
59 namespace DigikamGenericBoxPlugin
60 {
61 
62 class Q_DECL_HIDDEN BOXTalker::Private
63 {
64 public:
65 
66     enum State
67     {
68         BOX_USERNAME = 0,
69         BOX_LISTFOLDERS,
70         BOX_CREATEFOLDER,
71         BOX_ADDPHOTO
72     };
73 
74 public:
75 
Private()76     explicit Private()
77       : clientId(QLatin1String("yvd43v8av9zgg9phig80m2dc3r7mks4t")),
78         clientSecret(QLatin1String("KJkuMjvzOKDMyp3oxweQBEYixg678Fh5")),
79         authUrl(QLatin1String("https://account.box.com/api/oauth2/authorize")),
80         tokenUrl(QLatin1String("https://api.box.com/oauth2/token")),
81         redirectUrl(QLatin1String("https://app.box.com")),
82         state(BOX_USERNAME),
83         parent(nullptr),
84         netMngr(nullptr),
85         reply(nullptr),
86         settings(nullptr),
87         o2(nullptr)
88     {
89     }
90 
91 public:
92 
93     QString                         clientId;
94     QString                         clientSecret;
95     QString                         authUrl;
96     QString                         tokenUrl;
97     QString                         redirectUrl;
98 
99     State                           state;
100 
101     QWidget*                        parent;
102 
103     QNetworkAccessManager*          netMngr;
104     QNetworkReply*                  reply;
105 
106     QSettings*                      settings;
107 
108     O2*                             o2;
109 
110     QList<QPair<QString, QString> > foldersList;
111 };
112 
BOXTalker(QWidget * const parent)113 BOXTalker::BOXTalker(QWidget* const parent)
114     : d(new Private)
115 {
116     d->parent  = parent;
117     d->netMngr = new QNetworkAccessManager(this);
118 
119     connect(this, SIGNAL(boxLinkingFailed()),
120             this, SLOT(slotLinkingFailed()));
121 
122     connect(this, SIGNAL(boxLinkingSucceeded()),
123             this, SLOT(slotLinkingSucceeded()));
124 
125     connect(d->netMngr, SIGNAL(finished(QNetworkReply*)),
126             this, SLOT(slotFinished(QNetworkReply*)));
127 
128     d->o2      = new O2(this);
129 
130     d->o2->setClientId(d->clientId);
131     d->o2->setClientSecret(d->clientSecret);
132     d->o2->setRefreshTokenUrl(d->tokenUrl);
133     d->o2->setRequestUrl(d->authUrl);
134     d->o2->setTokenUrl(d->tokenUrl);
135     d->o2->setLocalPort(8000);
136 
137     d->settings                  = WSToolUtils::getOauthSettings(this);
138     O0SettingsStore* const store = new O0SettingsStore(d->settings, QLatin1String(O2_ENCRYPTION_KEY), this);
139     store->setGroupKey(QLatin1String("Box"));
140     d->o2->setStore(store);
141 
142     connect(d->o2, SIGNAL(linkingFailed()),
143             this, SLOT(slotLinkingFailed()));
144 
145     connect(d->o2, SIGNAL(linkingSucceeded()),
146             this, SLOT(slotLinkingSucceeded()));
147 
148     connect(d->o2, SIGNAL(openBrowser(QUrl)),
149             this, SLOT(slotOpenBrowser(QUrl)));
150 }
151 
~BOXTalker()152 BOXTalker::~BOXTalker()
153 {
154     if (d->reply)
155     {
156         d->reply->abort();
157     }
158 
159     WSToolUtils::removeTemporaryDir("box");
160 
161     delete d;
162 }
163 
link()164 void BOXTalker::link()
165 {
166     emit signalBusy(true);
167     d->o2->link();
168 }
169 
unLink()170 void BOXTalker::unLink()
171 {
172     d->o2->unlink();
173     d->settings->beginGroup(QLatin1String("Box"));
174     d->settings->remove(QString());
175     d->settings->endGroup();
176 }
177 
slotLinkingFailed()178 void BOXTalker::slotLinkingFailed()
179 {
180     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "LINK to Box fail";
181     emit signalBusy(false);
182 }
183 
slotLinkingSucceeded()184 void BOXTalker::slotLinkingSucceeded()
185 {
186     if (!d->o2->linked())
187     {
188         qCDebug(DIGIKAM_WEBSERVICES_LOG) << "UNLINK to Box ok";
189         emit signalBusy(false);
190         return;
191     }
192 
193     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "LINK to Box ok";
194     emit signalLinkingSucceeded();
195 }
196 
authenticated()197 bool BOXTalker::authenticated()
198 {
199     return d->o2->linked();
200 }
201 
cancel()202 void BOXTalker::cancel()
203 {
204     if (d->reply)
205     {
206         d->reply->abort();
207         d->reply = nullptr;
208     }
209 
210     emit signalBusy(false);
211 }
212 
slotOpenBrowser(const QUrl & url)213 void BOXTalker::slotOpenBrowser(const QUrl& url)
214 {
215     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Open Browser...";
216     QDesktopServices::openUrl(url);
217 }
218 
createFolder(QString & path)219 void BOXTalker::createFolder(QString& path)
220 {
221     QString name       = path.section(QLatin1Char('/'), -1);
222     QString folderPath = path.section(QLatin1Char('/'), -2, -2);
223 
224     QString id;
225 
226     for (int i = 0 ; i < d->foldersList.size() ; ++i)
227     {
228         if (d->foldersList.value(i).second == folderPath)
229         {
230             id = d->foldersList.value(i).first;
231         }
232     }
233 
234     QUrl url(QLatin1String("https://api.box.com/2.0/folders"));
235     QNetworkRequest netRequest(url);
236     netRequest.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String("application/json"));
237     netRequest.setRawHeader("Authorization", QString::fromLatin1("Bearer %1").arg(d->o2->token()).toUtf8());
238 
239     QByteArray postData = QString::fromUtf8("{\"name\": \"%1\",\"parent\": {\"id\": \"%2\"}}").arg(name).arg(id).toUtf8();
240 
241     d->reply = d->netMngr->post(netRequest, postData);
242     d->state = Private::BOX_CREATEFOLDER;
243 
244     emit signalBusy(true);
245 }
246 
getUserName()247 void BOXTalker::getUserName()
248 {
249     QUrl url(QLatin1String("https://api.box.com/2.0/users/me"));
250 
251     QNetworkRequest netRequest(url);
252     netRequest.setRawHeader("Authorization", QString::fromLatin1("Bearer %1").arg(d->o2->token()).toUtf8());
253     netRequest.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String("application/json"));
254 
255     d->reply = d->netMngr->get(netRequest);
256     d->state = Private::BOX_USERNAME;
257 
258     emit signalBusy(true);
259 }
260 
listFolders(const QString &)261 void BOXTalker::listFolders(const QString& /*path*/)
262 {
263     QUrl url(QLatin1String("https://api.box.com/2.0/folders/0/items"));;
264 
265     QNetworkRequest netRequest(url);
266     netRequest.setRawHeader("Authorization", QString::fromLatin1("Bearer %1").arg(d->o2->token()).toUtf8());
267     netRequest.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String("application/json"));
268 
269     d->reply = d->netMngr->get(netRequest);
270     d->state = Private::BOX_LISTFOLDERS;
271 
272     emit signalBusy(true);
273 }
274 
addPhoto(const QString & imgPath,const QString & uploadFolder,bool rescale,int maxDim,int imageQuality)275 bool BOXTalker::addPhoto(const QString& imgPath, const QString& uploadFolder, bool rescale, int maxDim, int imageQuality)
276 {
277     if (d->reply)
278     {
279         d->reply->abort();
280         d->reply = nullptr;
281     }
282 
283     emit signalBusy(true);
284 
285     QMimeDatabase mimeDB;
286     QString path     = imgPath;
287     QString mimeType = mimeDB.mimeTypeForFile(path).name();
288 
289     if (mimeType.startsWith(QLatin1String("image/")))
290     {
291         QImage image = PreviewLoadThread::loadHighQualitySynchronously(imgPath).copyQImage();
292 
293         if (image.isNull())
294         {
295             emit signalBusy(false);
296             return false;
297         }
298 
299         path = WSToolUtils::makeTemporaryDir("box").filePath(QFileInfo(imgPath)
300                                              .baseName().trimmed() + QLatin1String(".jpg"));
301 
302         if (rescale && ((image.width() > maxDim) || (image.height() > maxDim)))
303         {
304             image = image.scaled(maxDim, maxDim, Qt::KeepAspectRatio, Qt::SmoothTransformation);
305         }
306 
307         image.save(path, "JPEG", imageQuality);
308 
309         QScopedPointer<DMetadata> meta(new DMetadata);
310 
311         if (meta->load(imgPath))
312         {
313             meta->setItemDimensions(image.size());
314             meta->setItemOrientation(DMetadata::ORIENTATION_NORMAL);
315             meta->setMetadataWritingMode((int)DMetadata::WRITE_TO_FILE_ONLY);
316             meta->save(path, true);
317         }
318     }
319 
320     QString id;
321 
322     for (int i = 0 ; i < d->foldersList.size() ; ++i)
323     {
324         if (d->foldersList.value(i).second == uploadFolder)
325         {
326             id = d->foldersList.value(i).first;
327         }
328     }
329 
330     QHttpMultiPart* const multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
331 
332     QHttpPart attributes;
333     QString attributesHeader  = QLatin1String("form-data; name=\"attributes\"");
334     attributes.setHeader(QNetworkRequest::ContentDispositionHeader, attributesHeader);
335 
336     QString postData = QLatin1String("{\"name\":\"") + QFileInfo(imgPath).fileName() + QLatin1Char('"') +
337                        QLatin1String(", \"parent\":{\"id\":\"") + id + QLatin1String("\"}}");
338     attributes.setBody(postData.toUtf8());
339     multiPart->append(attributes);
340 
341     QFile* const file = new QFile(path);
342 
343     if (!file)
344     {
345         return false;
346     }
347 
348     if (!file->open(QIODevice::ReadOnly))
349     {
350         return false;
351     }
352 
353     QHttpPart imagePart;
354     QString imagePartHeader = QLatin1String("form-data; name=\"file\"; filename=\"") +
355                               QFileInfo(imgPath).fileName() + QLatin1Char('"');
356 
357     imagePart.setHeader(QNetworkRequest::ContentDispositionHeader, imagePartHeader);
358     imagePart.setHeader(QNetworkRequest::ContentTypeHeader, mimeType);
359 
360     imagePart.setBodyDevice(file);
361     multiPart->append(imagePart);
362 
363     QUrl url(QString::fromLatin1("https://upload.box.com/api/2.0/files/content?access_token=%1").arg(d->o2->token()));
364 
365     QNetworkRequest netRequest(url);
366     QString content = QLatin1String("multipart/form-data;boundary=") + QString::fromUtf8(multiPart->boundary());
367     netRequest.setHeader(QNetworkRequest::ContentTypeHeader, content);
368     d->reply        = d->netMngr->post(netRequest, multiPart);
369 
370     // delete the multiPart and file with the reply
371 
372     multiPart->setParent(d->reply);
373 
374     d->state        = Private::BOX_ADDPHOTO;
375 
376     return true;
377 }
378 
slotFinished(QNetworkReply * reply)379 void BOXTalker::slotFinished(QNetworkReply* reply)
380 {
381     if (reply != d->reply)
382     {
383         return;
384     }
385 
386     d->reply = nullptr;
387 
388     if (reply->error() != QNetworkReply::NoError)
389     {
390         if (d->state != Private::BOX_CREATEFOLDER)
391         {
392             emit signalBusy(false);
393             QMessageBox::critical(QApplication::activeWindow(),
394                                   i18n("Error"), reply->errorString());
395             reply->deleteLater();
396             return;
397         }
398     }
399 
400     QByteArray buffer = reply->readAll();
401 
402     switch (d->state)
403     {
404         case Private::BOX_LISTFOLDERS:
405             qCDebug(DIGIKAM_WEBSERVICES_LOG) << "In BOX_LISTFOLDERS";
406             parseResponseListFolders(buffer);
407             break;
408 
409         case Private::BOX_CREATEFOLDER:
410             qCDebug(DIGIKAM_WEBSERVICES_LOG) << "In BOX_CREATEFOLDER";
411             parseResponseCreateFolder(buffer);
412             break;
413 
414         case Private::BOX_ADDPHOTO:
415             qCDebug(DIGIKAM_WEBSERVICES_LOG) << "In BOX_ADDPHOTO";
416             parseResponseAddPhoto(buffer);
417             break;
418 
419         case Private::BOX_USERNAME:
420             qCDebug(DIGIKAM_WEBSERVICES_LOG) << "In BOX_USERNAME";
421             parseResponseUserName(buffer);
422             break;
423 
424         default:
425             break;
426     }
427 
428     reply->deleteLater();
429 }
430 
parseResponseAddPhoto(const QByteArray & data)431 void BOXTalker::parseResponseAddPhoto(const QByteArray& data)
432 {
433     QJsonDocument doc      = QJsonDocument::fromJson(data);
434     QJsonObject jsonObject = doc.object();
435     bool success           = jsonObject.contains(QLatin1String("total_count"));
436     emit signalBusy(false);
437 
438     if (!success)
439     {
440         emit signalAddPhotoFailed(i18n("Failed to upload photo"));
441     }
442     else
443     {
444         emit signalAddPhotoSucceeded();
445     }
446 }
447 
parseResponseUserName(const QByteArray & data)448 void BOXTalker::parseResponseUserName(const QByteArray& data)
449 {
450     QJsonDocument doc = QJsonDocument::fromJson(data);
451     QString name      = doc.object()[QLatin1String("name")].toString();
452     emit signalBusy(false);
453     emit signalSetUserName(name);
454 }
455 
parseResponseListFolders(const QByteArray & data)456 void BOXTalker::parseResponseListFolders(const QByteArray& data)
457 {
458     QJsonParseError err;
459     QJsonDocument doc = QJsonDocument::fromJson(data, &err);
460 
461     if (err.error != QJsonParseError::NoError)
462     {
463         emit signalBusy(false);
464         emit signalListAlbumsFailed(i18n("Failed to list folders"));
465         return;
466     }
467 
468     QJsonObject jsonObject = doc.object();
469     QJsonArray jsonArray   = jsonObject[QLatin1String("entries")].toArray();
470 
471     d->foldersList.clear();
472     d->foldersList.append(qMakePair(QLatin1String("0"), QLatin1String("root")));
473 
474     foreach (const QJsonValue& value, jsonArray)
475     {
476         QString folderName;
477         QString type;
478         QString id;
479 
480         QJsonObject obj = value.toObject();
481         type            = obj[QLatin1String("type")].toString();
482 
483         if (type == QLatin1String("folder"))
484         {
485             folderName = obj[QLatin1String("name")].toString();
486             id         = obj[QLatin1String("id")].toString();
487             d->foldersList.append(qMakePair(id, folderName));
488         }
489     }
490 
491     emit signalBusy(false);
492     emit signalListAlbumsDone(d->foldersList);
493 }
494 
parseResponseCreateFolder(const QByteArray & data)495 void BOXTalker::parseResponseCreateFolder(const QByteArray& data)
496 {
497     QJsonDocument doc1     = QJsonDocument::fromJson(data);
498     QJsonObject jsonObject = doc1.object();
499     bool fail              = jsonObject.contains(QLatin1String("error"));
500 
501     emit signalBusy(false);
502 
503     if (fail)
504     {
505         QJsonParseError err;
506         QJsonDocument doc2 = QJsonDocument::fromJson(data, &err);
507         emit signalCreateFolderFailed(jsonObject[QLatin1String("error_summary")].toString());
508     }
509     else
510     {
511         emit signalCreateFolderSucceeded();
512     }
513 }
514 
515 } // namespace DigikamGenericBoxPlugin
516