1 /* ============================================================
2  *
3  * This file is a part of digiKam project
4  * https://www.digikam.org
5  *
6  * Date        : 2018-07-03
7  * Description : Web Service authentication container.
8  *
9  * Copyright (C) 2018 by Thanh Trung Dinh <dinhthanhtrung1996 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 "wsauthentication.h"
24 
25 // Qt includes
26 
27 #include <QApplication>
28 #include <QMap>
29 #include <QMessageBox>
30 #include <QFile>
31 #include <QFileInfo>
32 #include <QScopedPointer>
33 
34 // KDE includes
35 
36 #include <klocalizedstring.h>
37 
38 // Local includes
39 
40 #include "digikam_debug.h"
41 #include "digikam_version.h"
42 #include "dmetadata.h"
43 #include "previewloadthread.h"
44 #include "wstoolutils.h"
45 #include "wstalker.h"
46 #include "dbtalker.h"
47 #include "fbtalker.h"
48 #include "fbnewalbumdlg.h"
49 #include "flickrtalker.h"
50 #include "gptalker.h"
51 #include "gdtalker.h"
52 #include "imgurtalker.h"
53 #include "smugtalker.h"
54 
55 namespace DigikamGenericUnifiedPlugin
56 {
57 
58 class Q_DECL_HIDDEN WSAuthentication::Private
59 {
60 public:
61 
Private()62     explicit Private()
63       : wizard(0),
64         iface(0),
65         talker(0),
66         ws(WSSettings::WebService::FLICKR),
67         albumDlg(0),
68         imagesCount(0),
69         imagesTotal(0)
70     {
71     }
72 
73     WSWizard*               wizard;
74     DInfoInterface*         iface;
75 
76     WSTalker*               talker;
77 
78     WSSettings::WebService  ws;
79     QString                 serviceName;
80 
81     WSNewAlbumDialog*       albumDlg;
82     QString                 currentAlbumId;
83     WSAlbum                 baseAlbum;
84 
85     QStringList             tmpPath;
86     QString                 tmpDir;
87     unsigned int            imagesCount;
88     unsigned int            imagesTotal;
89     QMap<QString, QString>  imagesCaption;
90 
91     QList<QUrl>             transferQueue;
92 };
93 
WSAuthentication(QWidget * const parent,DInfoInterface * const iface)94 WSAuthentication::WSAuthentication(QWidget* const parent, DInfoInterface* const iface)
95     : d(new Private())
96 {
97     d->wizard = dynamic_cast<WSWizard*>(parent);
98 
99     if (d->wizard)
100     {
101         d->iface = d->wizard->iface();
102     }
103     else
104     {
105         d->iface = iface;
106     }
107 
108     /* --------------------
109      * Temporary path to store images before uploading
110      */
111 
112     d->tmpPath.clear();
113     d->tmpDir = WSToolUtils::makeTemporaryDir(d->serviceName.toUtf8().data()).absolutePath() + QLatin1Char('/');
114 }
115 
~WSAuthentication()116 WSAuthentication::~WSAuthentication()
117 {
118     slotCancel();
119     delete d;
120 }
121 
createTalker(WSSettings::WebService ws,const QString & serviceName)122 void WSAuthentication::createTalker(WSSettings::WebService ws, const QString& serviceName)
123 {
124     d->ws          = ws;
125     d->serviceName = serviceName;
126     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "create " << serviceName << "talker";
127 
128     switch (ws)
129     {
130         case WSSettings::WebService::FLICKR:
131             //d->talker = new DigikamGenericFlickrPlugin::FlickrTalker(d->wizard, serviceName, d->iface);
132             break;
133         case WSSettings::WebService::DROPBOX:
134             //d->talker = new DigikamGenericDropBoxPlugin::DBTalker(d->wizard);
135             break;
136         case WSSettings::WebService::IMGUR:
137             //d->talker = new DigikamGenericImgUrPlugin::ImgurTalker(d->wizard);
138             break;
139         case WSSettings::WebService::FACEBOOK:
140             //d->albumDlg = new DigikamGenericFaceBookPlugin::FbNewAlbumDlg(d->wizard, d->serviceName);
141             //d->talker   = new DigikamGenericFaceBookPlugin::FbTalker(d->wizard, d->albumDlg);
142             break;
143         case WSSettings::WebService::SMUGMUG:
144             //d->talker = new DigikamGenericSmugMugPlugin::SmugTalker(d->iface, d->wizard);
145             break;
146         case WSSettings::WebService::GDRIVE:
147             //d->talker = new DigikamGenericGoogleServicesPlugin::GDTalker(d->wizard);
148             break;
149         case WSSettings::WebService::GPHOTO:
150             //d->talker = new DigikamGenericGoogleServicesPlugin::GPTalker(d->wizard);
151             break;
152     }
153 
154     connect(d->talker, SIGNAL(signalOpenBrowser(QUrl)),
155             this, SIGNAL(signalOpenBrowser(QUrl)));
156 
157     connect(d->talker, SIGNAL(signalCloseBrowser()),
158             this, SIGNAL(signalCloseBrowser()));
159 
160     connect(d->talker, SIGNAL(signalAuthenticationComplete(bool)),
161             this, SIGNAL(signalAuthenticationComplete(bool)));
162 
163     connect(this, SIGNAL(signalResponseTokenReceived(QMap<QString,QString>)),
164             d->talker, SLOT(slotResponseTokenReceived(QMap<QString,QString>)));
165 
166     connect(d->talker, SIGNAL(signalCreateAlbumDone(int,QString,QString)),
167             this, SIGNAL(signalCreateAlbumDone(int,QString,QString)));
168 
169     connect(d->talker, SIGNAL(signalListAlbumsDone(int,QString,QList<WSAlbum>)),
170             this, SLOT(slotListAlbumsDone(int,QString,QList<WSAlbum>)));
171 
172     connect(d->talker, SIGNAL(signalAddPhotoDone(int,QString)),
173             this, SLOT(slotAddPhotoDone(int,QString)));
174 }
175 
cancelTalker()176 void WSAuthentication::cancelTalker()
177 {
178     if (d->talker)
179     {
180         d->talker->cancel();
181     }
182 }
183 
webserviceName()184 QString WSAuthentication::webserviceName()
185 {
186     return d->serviceName;
187 }
188 
authenticate()189 void WSAuthentication::authenticate()
190 {
191     d->talker->authenticate();
192 }
193 
reauthenticate()194 void WSAuthentication::reauthenticate()
195 {
196     d->talker->reauthenticate();
197 }
198 
authenticated() const199 bool WSAuthentication::authenticated() const
200 {
201     return d->talker->linked();
202 }
203 
parseTreeFromListAlbums(const QList<WSAlbum> & albumsList,QMap<QString,AlbumSimplified> & albumTree,QStringList & rootAlbums)204 void WSAuthentication::parseTreeFromListAlbums(const QList <WSAlbum>& albumsList,
205                                                QMap<QString, AlbumSimplified>& albumTree,
206                                                QStringList& rootAlbums)
207 {
208     foreach (const WSAlbum& album, albumsList)
209     {
210         if (albumTree.contains(album.id))
211         {
212             albumTree[album.id].title      = album.title;
213             albumTree[album.id].uploadable = album.uploadable;
214         }
215         else
216         {
217             AlbumSimplified item(album.title, album.uploadable);
218             albumTree[album.id] = item;
219         }
220 
221         if (album.isRoot)
222         {
223             rootAlbums << album.id;
224         }
225         else
226         {
227             if (albumTree.contains(album.parentID))
228             {
229                 albumTree[album.parentID].childrenIDs << album.id;
230             }
231             else
232             {
233                 AlbumSimplified parentAlbum;
234                 parentAlbum.childrenIDs << album.id;
235                 albumTree[album.parentID] = parentAlbum;
236             }
237         }
238     }
239 }
240 
getImageCaption(const QString & fileName)241 QString WSAuthentication::getImageCaption(const QString& fileName)
242 {
243     DItemInfo info(d->iface->itemInfo(QUrl::fromLocalFile(fileName)));
244 
245     // If webservice doesn't support image titles, include it in descriptions if needed.
246 
247     QStringList descriptions = QStringList() << info.title() << info.comment();
248     descriptions.removeAll(QLatin1String(""));
249 
250     return descriptions.join(QLatin1String("\n\n"));
251 }
252 
prepareForUpload()253 void WSAuthentication::prepareForUpload()
254 {
255     d->transferQueue  = d->wizard->settings()->inputImages;
256     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "prepareForUpload invoked";
257 
258     if (d->transferQueue.isEmpty())
259     {
260         emit signalMessage(QLatin1String("transferQueue is empty"), true);
261         return;
262     }
263 
264     d->currentAlbumId = d->wizard->settings()->currentAlbumId;
265     d->imagesTotal    = d->transferQueue.count();
266     d->imagesCount    = 0;
267     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "upload request got album id from widget: " << d->currentAlbumId;
268 
269     if (d->wizard->settings()->imagesChangeProp)
270     {
271         foreach (const QUrl& imgUrl, d->transferQueue)
272         {
273             QString imgPath = imgUrl.toLocalFile();
274             QImage image = PreviewLoadThread::loadHighQualitySynchronously(imgPath).copyQImage();
275 
276             if (image.isNull())
277             {
278                 image.load(imgPath);
279             }
280 
281             if (image.isNull())
282             {
283                 emit d->talker->signalAddPhotoDone(666, i18n("Cannot open image at %1\n", imgPath));
284                 return;
285             }
286 
287             // get temporary file name
288             d->tmpPath << d->tmpDir + QFileInfo(imgPath).baseName().trimmed() + d->wizard->settings()->format();
289 
290             // rescale image if requested
291             int maxDim = d->wizard->settings()->imageSize;
292 
293             if (image.width() > maxDim || image.height() > maxDim)
294             {
295                 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Resizing to " << maxDim;
296                 image = image.scaled(maxDim, maxDim, Qt::KeepAspectRatio,
297                                      Qt::SmoothTransformation);
298             }
299 
300             qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Saving to temp file: " << d->tmpPath.last();
301             image.save(d->tmpPath.last(), "JPEG", d->wizard->settings()->imageCompression);
302 
303             // copy meta data to temporary image and get caption for image
304             QScopedPointer<DMetadata> meta(new DMetadata);
305             QString caption = QLatin1String("");
306 
307             if (meta->load(imgPath))
308             {
309                 meta->setItemDimensions(image.size());
310                 meta->setItemOrientation(MetaEngine::ORIENTATION_NORMAL);
311                 meta->setMetadataWritingMode((int)DMetadata::WRITE_TO_FILE_ONLY);
312                 meta->save(d->tmpPath.last(), true);
313                 caption = getImageCaption(imgPath);
314             }
315 
316             d->imagesCaption[imgPath] = caption;
317         }
318     }
319 }
320 
numberItemsUpload()321 unsigned int WSAuthentication::numberItemsUpload()
322 {
323     return d->imagesTotal;
324 }
325 
uploadNextPhoto()326 void WSAuthentication::uploadNextPhoto()
327 {
328     if (d->transferQueue.isEmpty())
329     {
330         emit signalDone();
331         return;
332     }
333 
334     /*
335      * This comparison is a little bit complicated and may seem unnecessary, but it will be useful later
336      * when we will be able to choose to change or not image properties for EACH image.
337      */
338     QString imgPath = d->transferQueue.first().toLocalFile();
339     QString tmpPath = d->tmpDir + QFileInfo(imgPath).baseName().trimmed() + d->wizard->settings()->format();
340 
341     if (!d->tmpPath.isEmpty() && tmpPath == d->tmpPath.first())
342     {
343         d->talker->addPhoto(tmpPath, d->currentAlbumId, d->imagesCaption[imgPath]);
344         d->tmpPath.removeFirst();
345     }
346     else
347     {
348         d->talker->addPhoto(imgPath, d->currentAlbumId, d->imagesCaption[imgPath]);
349     }
350 }
351 
startTransfer()352 void WSAuthentication::startTransfer()
353 {
354     uploadNextPhoto();
355 }
356 
slotCancel()357 void WSAuthentication::slotCancel()
358 {
359     // First we cancel talker
360     cancelTalker();
361 
362     // Then the folder containing all temporary photos to upload will be removed after all.
363     QDir tmpDir(d->tmpDir);
364     if (tmpDir.exists())
365     {
366         tmpDir.removeRecursively();
367     }
368 
369     emit signalProgress(0);
370 }
371 
slotNewAlbumRequest()372 void WSAuthentication::slotNewAlbumRequest()
373 {
374     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Slot create New Album";
375 
376     if (d->albumDlg->exec() == QDialog::Accepted)
377     {
378         qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Calling New Album method";
379         d->talker->createNewAlbum();
380     }
381 }
382 
slotListAlbumsRequest()383 void WSAuthentication::slotListAlbumsRequest()
384 {
385     d->talker->listAlbums();
386 }
387 
slotListAlbumsDone(int errCode,const QString & errMsg,const QList<WSAlbum> & albumsList)388 void WSAuthentication::slotListAlbumsDone(int errCode, const QString& errMsg, const QList<WSAlbum>& albumsList)
389 {
390     QString albumDebug = QLatin1String("");
391 
392     foreach (const WSAlbum &album, albumsList)
393     {
394         albumDebug.append(QString::fromLatin1("%1: %2\n").arg(album.id).arg(album.title));
395     }
396 
397     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Received albums (errCode = " << errCode << ", errMsg = "
398                                      << errMsg << "): " << albumDebug;
399 
400     if (errCode != 0)
401     {
402         QMessageBox::critical(QApplication::activeWindow(),
403                               i18n("%1 Call Failed",  d->serviceName),
404                               i18n("Code: %1. %2", errCode, errMsg));
405         return;
406     }
407 
408     QMap<QString, AlbumSimplified> albumTree;
409     QStringList rootAlbums;
410     parseTreeFromListAlbums(albumsList, albumTree, rootAlbums);
411 
412     emit signalListAlbumsDone(albumTree, rootAlbums, QLatin1String(""));
413 }
414 
slotAddPhotoDone(int errCode,const QString & errMsg)415 void WSAuthentication::slotAddPhotoDone(int errCode, const QString& errMsg)
416 {
417     if (errCode == 0)
418     {
419         emit signalMessage(QDir::toNativeSeparators(d->transferQueue.first().toLocalFile()), false);
420         d->transferQueue.removeFirst();
421 
422         d->imagesCount++;
423         emit signalProgress(d->imagesCount);
424     }
425     else
426     {
427         if (QMessageBox::question(d->wizard, i18n("Uploading Failed"),
428                 i18n("Failed to upload photo: %1\n"
429                 "Do you want to continue?", errMsg))
430             != QMessageBox::Yes)
431         {
432             d->transferQueue.clear();
433             return;
434         }
435     }
436 
437     uploadNextPhoto();
438 }
439 
440 } // namespace DigikamGenericUnifiedPlugin
441