1 /* ============================================================
2  *
3  * This file is a part of digiKam project
4  * https://www.digikam.org
5  *
6  * Date        : 2014-09-30
7  * Description : a tool to export items to Piwigo web service
8  *
9  * Copyright (C) 2003-2005 by Renchi Raju <renchi dot raju at gmail dot com>
10  * Copyright (C) 2006      by Colin Guthrie <kde at colin dot guthr dot ie>
11  * Copyright (C) 2006-2021 by Gilles Caulier <caulier dot gilles at gmail dot com>
12  * Copyright (C) 2008      by Andrea Diamantini <adjam7 at gmail dot com>
13  * Copyright (C) 2010-2019 by Frederic Coiffier <frederic dot coiffier at free dot com>
14  *
15  * This program is free software; you can redistribute it
16  * and/or modify it under the terms of the GNU General
17  * Public License as published by the Free Software Foundation;
18  * either version 2, or (at your option) any later version.
19  *
20  * This program is distributed in the hope that it will be useful,
21  * but WITHOUT ANY WARRANTY; without even the implied warranty of
22  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
23  * GNU General Public License for more details.
24  *
25  * ============================================================ */
26 
27 #include "piwigotalker.h"
28 
29 // Qt includes
30 
31 #include <QByteArray>
32 #include <QImage>
33 #include <QRegExp>
34 #include <QXmlStreamReader>
35 #include <QFileInfo>
36 #include <QMessageBox>
37 #include <QApplication>
38 #include <QCryptographicHash>
39 #include <QUuid>
40 
41 // KDE includes
42 
43 #include <klocalizedstring.h>
44 
45 // Local includes
46 
47 #include "dmetadata.h"
48 #include "digikam_debug.h"
49 #include "piwigoitem.h"
50 #include "digikam_version.h"
51 #include "wstoolutils.h"
52 #include "previewloadthread.h"
53 
54 namespace DigikamGenericPiwigoPlugin
55 {
56 
57 class Q_DECL_HIDDEN PiwigoTalker::Private
58 {
59 public:
60 
Private()61     explicit Private()
62       : parent      (nullptr),
63         state       (GE_LOGOUT),
64         netMngr     (nullptr),
65         reply       (nullptr),
66         loggedIn    (false),
67         chunkId     (0),
68         nbOfChunks  (0),
69         version     (-1),
70         albumId     (0),
71         photoId     (0),
72         iface       (nullptr)
73     {
74     }
75 
76     QWidget*               parent;
77     State                  state;
78     QString                cookie;
79     QUrl                   url;
80     QNetworkAccessManager* netMngr;
81     QNetworkReply*         reply;
82     bool                   loggedIn;
83     QByteArray             talker_buffer;
84     uint                   chunkId;
85     uint                   nbOfChunks;
86     int                    version;
87 
88     QByteArray             md5sum;
89     QString                path;
90     QString                tmpPath;    ///< If set, contains a temporary file which must be deleted
91     int                    albumId;
92     int                    photoId;    ///< Filled when the photo already exist
93     QString                comment;    ///< Synchronized with Piwigo comment
94     QString                title;      ///< Synchronized with Piwigo name
95     QString                author;     ///< Synchronized with Piwigo author
96     QDateTime              date;       ///< Synchronized with Piwigo date
97     DInfoInterface*        iface;
98 };
99 
100 QString PiwigoTalker::s_authToken = QLatin1String("");
101 
PiwigoTalker(DInfoInterface * const iface,QWidget * const parent)102 PiwigoTalker::PiwigoTalker(DInfoInterface* const iface, QWidget* const parent)
103     : d(new Private)
104 {
105     d->parent  = parent;
106     d->iface   = iface;
107     d->netMngr = new QNetworkAccessManager(this);
108 
109     connect(d->netMngr, SIGNAL(finished(QNetworkReply*)),
110             this, SLOT(slotFinished(QNetworkReply*)));
111 }
112 
~PiwigoTalker()113 PiwigoTalker::~PiwigoTalker()
114 {
115     cancel();
116     WSToolUtils::removeTemporaryDir("piwigo");
117 
118     delete d;
119 }
120 
cancel()121 void PiwigoTalker::cancel()
122 {
123     deleteTemporaryFile();
124 
125     if (d->reply)
126     {
127         d->reply->abort();
128         d->reply = nullptr;
129     }
130 }
131 
getAuthToken()132 QString PiwigoTalker::getAuthToken()
133 {
134     return s_authToken;
135 }
136 
computeMD5Sum(const QString & filepath)137 QByteArray PiwigoTalker::computeMD5Sum(const QString& filepath)
138 {
139     QFile file(filepath);
140 
141     if (!file.open(QIODevice::ReadOnly))
142     {
143         qCDebug(DIGIKAM_WEBSERVICES_LOG) << "File open error:" << filepath;
144         return QByteArray();
145     }
146 
147     QByteArray md5sum = QCryptographicHash::hash(file.readAll(), QCryptographicHash::Md5);
148     file.close();
149 
150     return md5sum;
151 }
152 
loggedIn() const153 bool PiwigoTalker::loggedIn() const
154 {
155     return d->loggedIn;
156 }
157 
login(const QUrl & url,const QString & name,const QString & passwd)158 void PiwigoTalker::login(const QUrl& url, const QString& name, const QString& passwd)
159 {
160     d->url   = url;
161     d->state = GE_LOGIN;
162     d->talker_buffer.resize(0);
163 
164     // Add the page to the URL
165 
166     if (!d->url.url().endsWith(QLatin1String(".php")))
167     {
168         d->url.setPath(d->url.path() + QLatin1Char('/') + QLatin1String("ws.php"));
169     }
170 
171     s_authToken = QLatin1String(QUuid::createUuid().toByteArray().toBase64());
172 
173     QStringList qsl;
174     qsl.append(QLatin1String("password=") + QString::fromUtf8(passwd.toUtf8().toPercentEncoding()));
175     qsl.append(QLatin1String("method=pwg.session.login"));
176     qsl.append(QLatin1String("username=") + QString::fromUtf8(name.toUtf8().toPercentEncoding()));
177     QString dataParameters = qsl.join(QLatin1Char('&'));
178     QByteArray buffer;
179     buffer.append(dataParameters.toUtf8());
180 
181     QNetworkRequest netRequest(d->url);
182     netRequest.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String("application/x-www-form-urlencoded"));
183     netRequest.setRawHeader("Authorization", s_authToken.toLatin1());
184 
185     d->reply = d->netMngr->post(netRequest, buffer);
186 
187     emit signalBusy(true);
188 }
189 
listAlbums()190 void PiwigoTalker::listAlbums()
191 {
192     d->state = GE_LISTALBUMS;
193     d->talker_buffer.resize(0);
194 
195     QStringList qsl;
196     qsl.append(QLatin1String("method=pwg.categories.getList"));
197     qsl.append(QLatin1String("recursive=true"));
198     QString dataParameters = qsl.join(QLatin1Char('&'));
199     QByteArray buffer;
200     buffer.append(dataParameters.toUtf8());
201 
202     QNetworkRequest netRequest(d->url);
203     netRequest.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String("application/x-www-form-urlencoded"));
204     netRequest.setRawHeader("Authorization", s_authToken.toLatin1());
205 
206     d->reply = d->netMngr->post(netRequest, buffer);
207 
208     emit signalBusy(true);
209 }
210 
addPhoto(int albumId,const QString & mediaPath,bool rescale,int maxWidth,int maxHeight,int quality)211 bool PiwigoTalker::addPhoto(int   albumId,
212                             const QString& mediaPath,
213                             bool  rescale,
214                             int   maxWidth,
215                             int   maxHeight,
216                             int   quality)
217 {
218     d->state       = GE_CHECKPHOTOEXIST;
219     d->talker_buffer.resize(0);
220 
221     d->path        = mediaPath;           // By default, d->path contains the original file
222     d->tmpPath     = QLatin1String("");   // By default, no temporary file (except with rescaling)
223     d->albumId     = albumId;
224 
225     d->md5sum      = computeMD5Sum(mediaPath);
226 
227     qCDebug(DIGIKAM_WEBSERVICES_LOG) << mediaPath << " " << d->md5sum.toHex();
228 
229     if (mediaPath.endsWith(QLatin1String(".mp4"))  || mediaPath.endsWith(QLatin1String(".MP4")) ||
230         mediaPath.endsWith(QLatin1String(".ogg"))  || mediaPath.endsWith(QLatin1String(".OGG")) ||
231         mediaPath.endsWith(QLatin1String(".webm")) || mediaPath.endsWith(QLatin1String(".WEBM")))
232     {
233         // Video management
234         // Nothing to do
235     }
236     else
237     {
238         // Image management
239 
240         QImage image = PreviewLoadThread::loadHighQualitySynchronously(mediaPath).copyQImage();
241 
242         if (image.isNull())
243         {
244             image.load(mediaPath);
245         }
246 
247         if (image.isNull())
248         {
249             // Invalid image
250             return false;
251         }
252 
253         if (!rescale)
254         {
255             qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Upload the original version: " << d->path;
256         }
257         else
258         {
259             // Rescale the image
260 
261             if ((image.width() > maxWidth) || (image.height() > maxHeight))
262             {
263                 image = image.scaled(maxWidth, maxHeight, Qt::KeepAspectRatio, Qt::SmoothTransformation);
264             }
265 
266             d->path = WSToolUtils::makeTemporaryDir("piwigo")
267                                                     .filePath(QUrl::fromLocalFile(mediaPath).fileName());
268             d->tmpPath = d->path;
269             image.save(d->path, "JPEG", quality);
270 
271             qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Upload a resized version: " << d->path ;
272 
273             // Restore all metadata with EXIF
274             // in the resized version
275 
276             QScopedPointer<DMetadata> meta(new DMetadata);
277 
278             if (meta->load(mediaPath))
279             {
280                 meta->setItemDimensions(image.size());
281                 meta->setItemOrientation(MetaEngine::ORIENTATION_NORMAL);
282                 meta->setMetadataWritingMode((int)DMetadata::WRITE_TO_FILE_ONLY);
283                 meta->save(d->path, true);
284             }
285             else
286             {
287                 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Image " << mediaPath << " has no exif data";
288             }
289         }
290     }
291 
292     // Metadata management
293 
294     // Complete name and comment for summary sending
295 
296     QFileInfo fi(mediaPath);
297     d->title   = fi.completeBaseName();
298     d->comment = QString();
299     d->author  = QString();
300 
301 #if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0))
302 
303     d->date    = fi.birthTime();
304 
305 #else
306 
307     d->date    = fi.created();
308 
309 #endif
310 
311     // Look in the host database
312 
313     DItemInfo info(d->iface->itemInfo(QUrl::fromLocalFile(mediaPath)));
314 
315     if (!info.title().isEmpty())
316     {
317         d->title = info.title();
318     }
319 
320     if (!info.comment().isEmpty())
321     {
322         d->comment = info.comment();
323     }
324 
325     if (!info.creators().isEmpty())
326     {
327         d->author = info.creators().join(QLatin1String(" / "));
328     }
329 
330     if (!info.dateTime().isNull())
331     {
332         d->date = info.dateTime();
333     }
334 
335     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Title: "   << d->title;
336     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Comment: " << d->comment;
337     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Author: "  << d->author;
338     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Date: "    << d->date;
339 
340     QStringList qsl;
341     qsl.append(QLatin1String("method=pwg.images.exist"));
342     qsl.append(QLatin1String("md5sud->list=") + QLatin1String(d->md5sum.toHex()));
343     QString dataParameters = qsl.join(QLatin1Char('&'));
344     QByteArray buffer;
345     buffer.append(dataParameters.toUtf8());
346 
347     QNetworkRequest netRequest(d->url);
348     netRequest.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String("application/x-www-form-urlencoded"));
349     netRequest.setRawHeader("Authorization", s_authToken.toLatin1());
350 
351     d->reply = d->netMngr->post(netRequest, buffer);
352 
353     emit signalProgressInfo(i18n("Check if %1 already exists", QUrl(mediaPath).fileName()));
354 
355     emit signalBusy(true);
356 
357     return true;
358 }
359 
slotFinished(QNetworkReply * reply)360 void PiwigoTalker::slotFinished(QNetworkReply* reply)
361 {
362     if (reply != d->reply)
363     {
364         return;
365     }
366 
367     d->reply     = nullptr;
368     State state = d->state; // Can change in the treatment itself, so we cache it
369 
370     if (reply->error() != QNetworkReply::NoError)
371     {
372         if      (state == GE_LOGIN)
373         {
374             emit signalLoginFailed(reply->errorString());
375             qCDebug(DIGIKAM_WEBSERVICES_LOG) << reply->errorString();
376         }
377         else if (state == GE_GETVERSION)
378         {
379             qCDebug(DIGIKAM_WEBSERVICES_LOG) << reply->errorString();
380 
381             // Version isn't mandatory and errors can be ignored
382             // As login succeeded, albums can be listed
383 
384             listAlbums();
385         }
386         else if ((state == GE_CHECKPHOTOEXIST) || (state == GE_GETINFO)       ||
387                  (state == GE_SETINFO)         || (state == GE_ADDPHOTOCHUNK) ||
388                  (state == GE_ADDPHOTOSUMMARY))
389         {
390             deleteTemporaryFile();
391             emit signalAddPhotoFailed(reply->errorString());
392         }
393         else
394         {
395             QMessageBox::critical(QApplication::activeWindow(),
396                                   i18n("Error"), reply->errorString());
397         }
398 
399         emit signalBusy(false);
400         reply->deleteLater();
401         return;
402     }
403 
404     d->talker_buffer.append(reply->readAll());
405 
406     switch (state)
407     {
408         case (GE_LOGIN):
409             parseResponseLogin(d->talker_buffer);
410             break;
411 
412         case (GE_GETVERSION):
413             parseResponseGetVersion(d->talker_buffer);
414             break;
415 
416         case (GE_LISTALBUMS):
417             parseResponseListAlbums(d->talker_buffer);
418             break;
419 
420         case (GE_CHECKPHOTOEXIST):
421             parseResponseDoesPhotoExist(d->talker_buffer);
422             break;
423 
424         case (GE_GETINFO):
425             parseResponseGetInfo(d->talker_buffer);
426             break;
427 
428         case (GE_SETINFO):
429             parseResponseSetInfo(d->talker_buffer);
430             break;
431 
432         case (GE_ADDPHOTOCHUNK):
433             // Support for Web API >= 2.4
434             parseResponseAddPhotoChunk(d->talker_buffer);
435             break;
436 
437         case (GE_ADDPHOTOSUMMARY):
438             parseResponseAddPhotoSummary(d->talker_buffer);
439             break;
440 
441         default:   // GE_LOGOUT
442             break;
443     }
444 
445     if ((state == GE_GETVERSION) && d->loggedIn)
446     {
447         listAlbums();
448     }
449 
450     emit signalBusy(false);
451     reply->deleteLater();
452 }
453 
parseResponseLogin(const QByteArray & data)454 void PiwigoTalker::parseResponseLogin(const QByteArray& data)
455 {
456     QXmlStreamReader ts(data);
457     QString line;
458     bool foundResponse = false;
459     d->loggedIn        = false;
460 
461     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "parseResponseLogin: " << QString::fromUtf8(data);
462 
463     while (!ts.atEnd())
464     {
465         ts.readNext();
466 
467         if (ts.isStartElement())
468         {
469             foundResponse = true;
470 
471             if ((ts.name() == QLatin1String("rsp")) &&
472                 (ts.attributes().value(QLatin1String("stat")) == QLatin1String("ok")))
473             {
474                 d->loggedIn = true;
475 
476                 /** Request Version */
477 
478                 d->state          = GE_GETVERSION;
479                 d->talker_buffer.resize(0);
480                 d->version        = -1;
481 
482                 QByteArray buffer = "method=pwg.getVersion";
483 
484                 QNetworkRequest netRequest(d->url);
485                 netRequest.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String("application/x-www-form-urlencoded"));
486                 netRequest.setRawHeader("Authorization", s_authToken.toLatin1());
487 
488                 d->reply = d->netMngr->post(netRequest, buffer);
489 
490                 emit signalBusy(true);
491 
492                 return;
493             }
494         }
495     }
496 
497     if (!foundResponse)
498     {
499         emit signalLoginFailed(i18n("Piwigo URL probably incorrect"));
500         return;
501     }
502 
503     if (!d->loggedIn)
504     {
505         emit signalLoginFailed(i18n("Incorrect username or password specified"));
506     }
507 }
508 
parseResponseGetVersion(const QByteArray & data)509 void PiwigoTalker::parseResponseGetVersion(const QByteArray& data)
510 {
511     QXmlStreamReader ts(data);
512     QString line;
513     QRegExp verrx(QLatin1String(".?(\\d+)\\.(\\d+).*"));
514 
515     bool foundResponse = false;
516 
517     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "parseResponseGetVersion: " << QString::fromUtf8(data);
518 
519     while (!ts.atEnd())
520     {
521         ts.readNext();
522 
523         if (ts.isStartElement())
524         {
525             foundResponse = true;
526 
527             if ((ts.name() == QLatin1String("rsp")) &&
528                 (ts.attributes().value(QLatin1String("stat")) == QLatin1String("ok")))
529             {
530                 QString v = ts.readElementText();
531 
532                 if (verrx.exactMatch(v))
533                 {
534                     QStringList qsl = verrx.capturedTexts();
535                     d->version      = qsl[1].toInt() * 100 + qsl[2].toInt();
536                     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Version: " << d->version;
537                     break;
538                 }
539             }
540         }
541     }
542 
543     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "foundResponse : " << foundResponse;
544 
545     if (d->version < PIWIGO_VER_2_4)
546     {
547         d->loggedIn = false;
548         emit signalLoginFailed(i18n("Upload to Piwigo version inferior to 2.4 is no longer supported"));
549         return;
550     }
551 }
552 
parseResponseListAlbums(const QByteArray & data)553 void PiwigoTalker::parseResponseListAlbums(const QByteArray& data)
554 {
555     QString str        = QString::fromUtf8(data);
556     QXmlStreamReader ts(data);
557     QString line;
558     bool foundResponse = false;
559     bool success       = false;
560 
561     typedef QList<PiwigoAlbum> PiwigoAlbumList;
562     PiwigoAlbumList albumList;
563     PiwigoAlbumList::iterator iter = albumList.begin();
564 
565     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "parseResponseListAlbums";
566 
567     while (!ts.atEnd())
568     {
569         ts.readNext();
570 
571         if (ts.isEndElement() && (ts.name() == QLatin1String("categories")))
572         {
573             break;
574         }
575 
576         if (ts.isStartElement())
577         {
578             if ((ts.name() == QLatin1String("rsp")) &&
579                 (ts.attributes().value(QLatin1String("stat")) == QLatin1String("ok")))
580             {
581                 foundResponse = true;
582             }
583 
584             if (ts.name() == QLatin1String("categories"))
585             {
586                 success = true;
587             }
588 
589             if (ts.name() == QLatin1String("category"))
590             {
591                 PiwigoAlbum album;
592                 album.m_refNum       = ts.attributes().value(QLatin1String("id")).toString().toInt();
593                 album.m_parentRefNum = -1;
594 
595                 qCDebug(DIGIKAM_WEBSERVICES_LOG) << album.m_refNum << "\n";
596 
597                 iter = albumList.insert(iter, album);
598             }
599 
600             if (ts.name() == QLatin1String("name"))
601             {
602                 (*iter).m_name = ts.readElementText();
603                 qCDebug(DIGIKAM_WEBSERVICES_LOG) << (*iter).m_name << "\n";
604             }
605 
606             if (ts.name() == QLatin1String("uppercats"))
607             {
608                 QString uppercats   = ts.readElementText();
609                 QStringList catlist = uppercats.split(QLatin1Char(','));
610 
611                 if ((catlist.size() > 1) && (catlist.at((uint)catlist.size() - 2).toInt() != (*iter).m_refNum))
612                 {
613                     (*iter).m_parentRefNum = catlist.at((uint)catlist.size() - 2).toInt();
614                     qCDebug(DIGIKAM_WEBSERVICES_LOG) << (*iter).m_parentRefNum << "\n";
615                 }
616             }
617         }
618     }
619 
620     if (!foundResponse)
621     {
622         emit signalError(i18n("Invalid response received from remote Piwigo"));
623         return;
624     }
625 
626     if (!success)
627     {
628         emit signalError(i18n("Failed to list albums"));
629         return;
630     }
631 
632     // We need parent albums to come first for rest of the code to work
633 
634     std::sort(albumList.begin(), albumList.end());
635 
636     emit signalAlbums(albumList);
637 }
638 
parseResponseDoesPhotoExist(const QByteArray & data)639 void PiwigoTalker::parseResponseDoesPhotoExist(const QByteArray& data)
640 {
641     QString str        = QString::fromUtf8(data);
642     QXmlStreamReader ts(data);
643     QString line;
644     bool foundResponse = false;
645     bool success       = false;
646 
647     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "parseResponseDoesPhotoExist: " << QString::fromUtf8(data);
648 
649     while (!ts.atEnd())
650     {
651         ts.readNext();
652 
653         if (ts.name() == QLatin1String("rsp"))
654         {
655             foundResponse = true;
656 
657             if (ts.attributes().value(QLatin1String("stat")) == QLatin1String("ok"))
658             {
659                 success = true;
660             }
661 
662             // Originally, first versions of Piwigo 2.4.x returned an invalid XML as the element started with a digit
663             // New versions are corrected (starting with _) : This code works with both versions
664 
665             QRegExp md5rx(QLatin1String("_?([a-f0-9]+)>([0-9]+)</.+"));
666 
667             ts.readNext();
668 
669             if (md5rx.exactMatch(QString::fromUtf8(data.mid(ts.characterOffset()))))
670             {
671                 QStringList qsl1 = md5rx.capturedTexts();
672 
673                 if (qsl1[1] == QLatin1String(d->md5sum.toHex()))
674                 {
675                     d->photoId = qsl1[2].toInt();
676                     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "d->photoId: " << d->photoId;
677 
678                     emit signalProgressInfo(i18n("Photo '%1' already exists.", d->title));
679 
680                     d->state   = GE_GETINFO;
681                     d->talker_buffer.resize(0);
682 
683                     QStringList qsl2;
684                     qsl2.append(QLatin1String("method=pwg.images.getInfo"));
685                     qsl2.append(QLatin1String("image_id=") + QString::number(d->photoId));
686                     QString dataParameters = qsl2.join(QLatin1Char('&'));
687                     QByteArray buffer;
688                     buffer.append(dataParameters.toUtf8());
689 
690                     QNetworkRequest netRequest(d->url);
691                     netRequest.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String("application/x-www-form-urlencoded"));
692                     netRequest.setRawHeader("Authorization", s_authToken.toLatin1());
693 
694                     d->reply = d->netMngr->post(netRequest, buffer);
695 
696                     return;
697                 }
698             }
699         }
700     }
701 
702     if (!foundResponse)
703     {
704         emit signalAddPhotoFailed(i18n("Invalid response received from remote Piwigo"));
705         return;
706     }
707 
708     if (!success)
709     {
710         emit signalAddPhotoFailed(i18n("Failed to upload photo"));
711         return;
712     }
713 
714     if (d->version >= PIWIGO_VER_2_4)
715     {
716         QFileInfo fi(d->path);
717 
718         d->state      = GE_ADDPHOTOCHUNK;
719         d->talker_buffer.resize(0);
720 
721         // Compute the number of chunks for the image
722 
723         d->nbOfChunks = (fi.size() / CHUNK_MAX_SIZE) + 1;
724         d->chunkId    = 0;
725 
726         addNextChunk();
727     }
728     else
729     {
730         emit signalAddPhotoFailed(i18n("Upload to Piwigo version inferior to 2.4 is no longer supported"));
731         return;
732     }
733 }
734 
parseResponseGetInfo(const QByteArray & data)735 void PiwigoTalker::parseResponseGetInfo(const QByteArray& data)
736 {
737     QString str        = QString::fromUtf8(data);
738     QXmlStreamReader ts(data);
739     QString line;
740     bool foundResponse = false;
741     bool success       = false;
742     QList<int> categories;
743 
744     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "parseResponseGetInfo: " << QString::fromUtf8(data);
745 
746     while (!ts.atEnd())
747     {
748         ts.readNext();
749 
750         if (ts.isStartElement())
751         {
752             if (ts.name() == QLatin1String("rsp"))
753             {
754                 foundResponse = true;
755 
756                 if (ts.attributes().value(QLatin1String("stat")) == QLatin1String("ok"))
757                 {
758                     success = true;
759                 }
760             }
761 
762             if (ts.name() == QLatin1String("category"))
763             {
764                 if (ts.attributes().hasAttribute(QLatin1String("id")))
765                 {
766                     QString id(ts.attributes().value(QLatin1String("id")).toString());
767                     categories.append(id.toInt());
768                 }
769             }
770         }
771     }
772 
773     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "success : " << success;
774 
775     if (!foundResponse)
776     {
777         emit signalAddPhotoFailed(i18n("Invalid response received from remote Piwigo"));
778         return;
779     }
780 
781     if (categories.contains(d->albumId))
782     {
783         emit signalAddPhotoFailed(i18n("Photo '%1' already exists in this album.", d->title));
784         return;
785     }
786     else
787     {
788         categories.append(d->albumId);
789     }
790 
791     d->state = GE_SETINFO;
792     d->talker_buffer.resize(0);
793 
794     QStringList qsl_cat;
795 
796     for (int i = 0 ; i < categories.size() ; ++i)
797     {
798         qsl_cat.append(QString::number(categories.at(i)));
799     }
800 
801     QStringList qsl;
802     qsl.append(QLatin1String("method=pwg.images.setInfo"));
803     qsl.append(QLatin1String("image_id=") + QString::number(d->photoId));
804     qsl.append(QLatin1String("categories=") + QString::fromUtf8(qsl_cat.join(QLatin1Char(';')).toUtf8().toPercentEncoding()));
805     QString dataParameters = qsl.join(QLatin1Char('&'));
806     QByteArray buffer;
807     buffer.append(dataParameters.toUtf8());
808 
809     QNetworkRequest netRequest(d->url);
810     netRequest.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String("application/x-www-form-urlencoded"));
811     netRequest.setRawHeader("Authorization", s_authToken.toLatin1());
812 
813     d->reply = d->netMngr->post(netRequest, buffer);
814 
815     return;
816 }
817 
parseResponseSetInfo(const QByteArray & data)818 void PiwigoTalker::parseResponseSetInfo(const QByteArray& data)
819 {
820     QString str        = QString::fromUtf8(data);
821     QXmlStreamReader ts(data);
822     QString line;
823     bool foundResponse = false;
824     bool success       = false;
825 
826     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "parseResponseSetInfo: " << QString::fromUtf8(data);
827 
828     while (!ts.atEnd())
829     {
830         ts.readNext();
831 
832         if (ts.isStartElement())
833         {
834             if (ts.name() == QLatin1String("rsp"))
835             {
836                 foundResponse = true;
837 
838                 if (ts.attributes().value(QLatin1String("stat")) == QLatin1String("ok"))
839                 {
840                     success = true;
841                 }
842 
843                 break;
844             }
845         }
846     }
847 
848     if (!foundResponse)
849     {
850         emit signalAddPhotoFailed(i18n("Invalid response received from remote Piwigo"));
851 
852         return;
853     }
854 
855     if (!success)
856     {
857         emit signalAddPhotoFailed(i18n("Failed to upload photo"));
858 
859         return;
860     }
861 
862     deleteTemporaryFile();
863 
864     emit signalAddPhotoSucceeded();
865 }
866 
addNextChunk()867 void PiwigoTalker::addNextChunk()
868 {
869     QFile imagefile(d->path);
870 
871     if (!imagefile.open(QIODevice::ReadOnly))
872     {
873         emit signalProgressInfo(i18n("Error : Cannot open photo: %1", QUrl(d->path).fileName()));
874         return;
875     }
876 
877     d->chunkId++; // We start with chunk 1
878 
879     imagefile.seek((d->chunkId - 1) * CHUNK_MAX_SIZE);
880 
881     d->talker_buffer.resize(0);
882     QStringList qsl;
883     qsl.append(QLatin1String("method=pwg.images.addChunk"));
884     qsl.append(QLatin1String("original_sum=") + QLatin1String(d->md5sum.toHex()));
885     qsl.append(QLatin1String("position=") + QString::number(d->chunkId));
886     qsl.append(QLatin1String("type=file"));
887     qsl.append(QLatin1String("data=") + QString::fromUtf8(imagefile.read(CHUNK_MAX_SIZE).toBase64().toPercentEncoding()));
888     QString dataParameters = qsl.join(QLatin1Char('&'));
889     QByteArray buffer;
890     buffer.append(dataParameters.toUtf8());
891 
892     imagefile.close();
893 
894     QNetworkRequest netRequest(d->url);
895     netRequest.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String("application/x-www-form-urlencoded"));
896     netRequest.setRawHeader("Authorization", s_authToken.toLatin1());
897 
898     d->reply = d->netMngr->post(netRequest, buffer);
899 
900     emit signalProgressInfo(i18n("Upload the chunk %1/%2 of %3", d->chunkId, d->nbOfChunks, QUrl(d->path).fileName()));
901 }
902 
parseResponseAddPhotoChunk(const QByteArray & data)903 void PiwigoTalker::parseResponseAddPhotoChunk(const QByteArray& data)
904 {
905     QString str        = QString::fromUtf8(data);
906     QXmlStreamReader ts(data);
907     QString line;
908     bool foundResponse = false;
909     bool success       = false;
910 
911     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "parseResponseAddPhotoChunk: " << QString::fromUtf8(data);
912 
913     while (!ts.atEnd())
914     {
915         ts.readNext();
916 
917         if (ts.isStartElement())
918         {
919             if (ts.name() == QLatin1String("rsp"))
920             {
921                 foundResponse = true;
922 
923                 if (ts.attributes().value(QLatin1String("stat")) == QLatin1String("ok"))
924                 {
925                     success = true;
926                 }
927 
928                 break;
929             }
930         }
931     }
932 
933     if (!foundResponse || !success)
934     {
935         emit signalProgressInfo(i18n("Warning : The full size photo cannot be uploaded."));
936     }
937 
938     if (d->chunkId < d->nbOfChunks)
939     {
940         addNextChunk();
941     }
942     else
943     {
944         addPhotoSummary();
945     }
946 }
947 
addPhotoSummary()948 void PiwigoTalker::addPhotoSummary()
949 {
950     d->state = GE_ADDPHOTOSUMMARY;
951     d->talker_buffer.resize(0);
952 
953     QStringList qsl;
954     qsl.append(QLatin1String("method=pwg.images.add"));
955     qsl.append(QLatin1String("original_sum=") + QLatin1String(d->md5sum.toHex()));
956     qsl.append(QLatin1String("original_filename=") + QString::fromUtf8(QUrl(d->path).fileName().toUtf8().toPercentEncoding()));
957     qsl.append(QLatin1String("name=") + QString::fromUtf8(d->title.toUtf8().toPercentEncoding()));
958 
959     if (!d->author.isEmpty())
960     {
961         qsl.append(QLatin1String("author=") + QString::fromUtf8(d->author.toUtf8().toPercentEncoding()));
962     }
963 
964     if (!d->comment.isEmpty())
965     {
966         qsl.append(QLatin1String("comment=") + QString::fromUtf8(d->comment.toUtf8().toPercentEncoding()));
967     }
968 
969     qsl.append(QLatin1String("categories=") + QString::number(d->albumId));
970     qsl.append(QLatin1String("file_sum=") + QLatin1String(computeMD5Sum(d->path).toHex()));
971     qsl.append(QLatin1String("date_creation=") +
972                QString::fromUtf8(d->date.toString(QLatin1String("yyyy-MM-dd hh:mm:ss")).toUtf8().toPercentEncoding()));
973 
974     //qsl.append("tag_ids="); // TODO Implement this function
975 
976     QString dataParameters = qsl.join(QLatin1Char('&'));
977     QByteArray buffer;
978     buffer.append(dataParameters.toUtf8());
979 
980     QNetworkRequest netRequest(d->url);
981     netRequest.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String("application/x-www-form-urlencoded"));
982     netRequest.setRawHeader("Authorization", s_authToken.toLatin1());
983 
984     d->reply = d->netMngr->post(netRequest, buffer);
985 
986     emit signalProgressInfo(i18n("Upload the metadata of %1", QUrl(d->path).fileName()));
987 }
988 
parseResponseAddPhotoSummary(const QByteArray & data)989 void PiwigoTalker::parseResponseAddPhotoSummary(const QByteArray& data)
990 {
991     QString str        = QString::fromUtf8(data);
992     QXmlStreamReader ts(data.mid(data.indexOf("<?xml")));
993     QString line;
994     bool foundResponse = false;
995     bool success       = false;
996 
997     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "parseResponseAddPhotoSummary: " << QString::fromUtf8(data);
998 
999     while (!ts.atEnd())
1000     {
1001         ts.readNext();
1002 
1003         if (ts.isStartElement())
1004         {
1005             if (ts.name() == QLatin1String("rsp"))
1006             {
1007                 foundResponse = true;
1008 
1009                 if (ts.attributes().value(QLatin1String("stat")) == QLatin1String("ok"))
1010                 {
1011                    success = true;
1012                 }
1013 
1014                 break;
1015             }
1016         }
1017     }
1018 
1019     if (!foundResponse)
1020     {
1021         emit signalAddPhotoFailed(i18n("Invalid response received from remote Piwigo (%1)", QString::fromUtf8(data)));
1022 
1023         return;
1024     }
1025 
1026     if (!success)
1027     {
1028         emit signalAddPhotoFailed(i18n("Failed to upload photo"));
1029 
1030         return;
1031     }
1032 
1033     deleteTemporaryFile();
1034 
1035     emit signalAddPhotoSucceeded();
1036 }
1037 
deleteTemporaryFile()1038 void PiwigoTalker::deleteTemporaryFile()
1039 {
1040     if (d->tmpPath.size())
1041     {
1042         QFile(d->tmpPath).remove();
1043         d->tmpPath = QLatin1String("");
1044     }
1045 }
1046 
1047 } // namespace DigikamGenericPiwigoPlugin
1048