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