File indexing completed on 2025-01-05 03:53:42

0001 /* ============================================================
0002  *
0003  * This file is a part of digiKam project
0004  * https://www.digikam.org
0005  *
0006  * Date        : 2008-12-01
0007  * Description : a tool to export images to Smugmug web service
0008  *
0009  * SPDX-FileCopyrightText: 2008-2009 by Luka Renko <lure at kubuntu dot org>
0010  * SPDX-FileCopyrightText: 2008-2024 by Gilles Caulier <caulier dot gilles at gmail dot com>
0011  * SPDX-FileCopyrightText: 2018      by Thanh Trung Dinh <dinhthanhtrung1996 at gmail dot com>
0012  *
0013  * SPDX-License-Identifier: GPL-2.0-or-later
0014  *
0015  * ============================================================ */
0016 
0017 #include "smugtalker.h"
0018 
0019 // Qt includes
0020 
0021 #include <QByteArray>
0022 #include <QDomDocument>
0023 #include <QDomElement>
0024 #include <QJsonDocument>
0025 #include <QJsonParseError>
0026 #include <QJsonObject>
0027 #include <QJsonValue>
0028 #include <QJsonArray>
0029 #include <QTextDocument>
0030 #include <QFile>
0031 #include <QFileInfo>
0032 #include <QMessageBox>
0033 #include <QApplication>
0034 #include <QDesktopServices>
0035 #include <QCryptographicHash>
0036 #include <QUrlQuery>
0037 #include <QNetworkReply>
0038 
0039 // KDE includes
0040 
0041 #include <klocalizedstring.h>
0042 
0043 // Local includes
0044 
0045 #include "digikam_debug.h"
0046 #include "digikam_version.h"
0047 #include "smugmpform.h"
0048 
0049 // OAuth2 library includes
0050 
0051 #if defined(Q_CC_CLANG)
0052 #   pragma clang diagnostic push
0053 #   pragma clang diagnostic ignored "-Wextra-semi"
0054 #endif
0055 
0056 #include "wstoolutils.h"
0057 #include "networkmanager.h"
0058 #include "o0settingsstore.h"
0059 
0060 #if defined(Q_CC_CLANG)
0061 #   pragma clang diagnostic pop
0062 #endif
0063 
0064 using namespace Digikam;
0065 
0066 namespace DigikamGenericSmugPlugin
0067 {
0068 
0069 class Q_DECL_HIDDEN SmugTalker::Private
0070 {
0071 
0072 public:
0073 
0074     enum State
0075     {
0076         SMUG_LOGIN = 0,
0077         SMUG_LOGOUT,
0078         SMUG_LISTALBUMS,
0079         SMUG_LISTPHOTOS,
0080         SMUG_LISTALBUMTEMPLATES,
0081         SMUG_CREATEALBUM,
0082         SMUG_ADDPHOTO,
0083         SMUG_GETPHOTO
0084 /*
0085         SMUG_LISTCATEGORIES,
0086         SMUG_LISTSUBCATEGORIES,
0087 */
0088     };
0089 
0090 public:
0091 
0092     explicit Private()
0093       : parent(nullptr),
0094         userAgent(QString::fromLatin1("digiKam/%1 (digikamdeveloper@gmail.com)").arg(digiKamVersion())),
0095         apiURL(QLatin1String("https://api.smugmug.com%1")),
0096         uploadUrl(QLatin1String("https://upload.smugmug.com/")),
0097         requestTokenUrl(QLatin1String("https://api.smugmug.com/services/oauth/1.0a/getRequestToken")),
0098         authUrl(QLatin1String("https://api.smugmug.com/services/oauth/1.0a/authorize")),
0099         accessTokenUrl(QLatin1String("https://api.smugmug.com/services/oauth/1.0a/getAccessToken")),
0100         apiVersion(QLatin1String("v2")),
0101         apikey(QLatin1String("xKp43CXF8MHgjhgGdgdgfgc7cWjqQcck")),
0102         clientSecret(QLatin1String("3CKcLcWx64Rm8HVRwX3bf4HCtJpnGrwnk9xSn4DK8wRhGLVsRBBFktD95W4HTRHD")),
0103         iface(nullptr),
0104         netMngr(nullptr),
0105         reply(nullptr),
0106         state(SMUG_LOGOUT),
0107         settings(nullptr),
0108         requestor(nullptr),
0109         o1(nullptr)
0110     {
0111     }
0112 
0113 public:
0114 
0115     QWidget*               parent;
0116 
0117     QString                userAgent;
0118 
0119     QString                apiURL;
0120     QString                uploadUrl;
0121     QString                requestTokenUrl;
0122     QString                authUrl;
0123     QString                accessTokenUrl;
0124 
0125     QString                apiVersion;
0126     QString                apikey;
0127     QString                clientSecret;
0128     QString                sessionID;
0129 
0130     SmugUser               user;
0131     DInfoInterface*        iface;
0132 
0133     QNetworkAccessManager* netMngr;
0134 
0135     QNetworkReply*         reply;
0136 
0137     State                  state;
0138 
0139     QSettings*             settings;
0140     O1Requestor*           requestor;
0141     O1SmugMug*             o1;
0142 };
0143 
0144 SmugTalker::SmugTalker(DInfoInterface* const iface, QWidget* const parent)
0145     : d(new Private)
0146 {
0147     d->parent  = parent;
0148     d->iface   = iface;
0149     d->netMngr = NetworkManager::instance()->getNetworkManager(this);
0150 
0151     connect(d->netMngr, SIGNAL(finished(QNetworkReply*)),
0152             this, SLOT(slotFinished(QNetworkReply*)));
0153 
0154     // Init
0155 
0156     d->o1      = new O1SmugMug(this, d->netMngr);
0157 
0158     // Config for authentication flow
0159 
0160     d->o1->setRequestTokenUrl(QUrl(d->requestTokenUrl));
0161     d->o1->setAuthorizeUrl(QUrl(d->authUrl));
0162     d->o1->setAccessTokenUrl(QUrl(d->accessTokenUrl));
0163     d->o1->setLocalPort(8000);
0164 
0165     // Application credentials
0166 
0167     d->o1->setClientId(d->apikey);
0168     d->o1->setClientSecret(d->clientSecret);
0169 
0170     // Set userAgent to work around error :
0171     // O1::onTokenRequestError: 201 "Error transferring requestTokenUrl() - server replied: Forbidden" "Bad bot"
0172 
0173     d->o1->setUserAgent(d->userAgent.toUtf8());
0174 
0175     // Setting to store oauth config
0176 
0177     d->settings                  = WSToolUtils::getOauthSettings(this);
0178     O0SettingsStore* const store = new O0SettingsStore(d->settings, QLatin1String(O2_ENCRYPTION_KEY), this);
0179     store->setGroupKey(QLatin1String("Smugmug"));
0180     d->o1->setStore(store);
0181 
0182     // Connect signaux slots
0183 
0184     connect(d->o1, SIGNAL(linkingFailed()),
0185             this, SLOT(slotLinkingFailed()));
0186 
0187     connect(this, SIGNAL(signalLinkingSucceeded()),
0188             this, SLOT(slotLinkingSucceeded()));
0189 
0190     connect(d->o1, SIGNAL(linkingSucceeded()),
0191             this, SLOT(slotLinkingSucceeded()));
0192 
0193     connect(d->o1, SIGNAL(openBrowser(QUrl)),
0194             this, SLOT(slotOpenBrowser(QUrl)));
0195 
0196     d->requestor = new O1Requestor(d->netMngr, d->o1, this);
0197 }
0198 
0199 SmugTalker::~SmugTalker()
0200 {
0201 /*  We shouldn't logout without user's consent
0202 
0203     if (loggedIn())
0204     {
0205         logout();
0206 
0207         while (d->reply && d->reply->isRunning())
0208         {
0209             qApp->processEvents();
0210         }
0211     }
0212 */
0213 
0214     if (d->reply)
0215         d->reply->abort();
0216 
0217     delete d;
0218 }
0219 
0220 /**
0221  * TODO: Porting to O2
0222  */
0223 void SmugTalker::link()
0224 {
0225     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "LINK to Smug ";
0226     d->o1->link();
0227 }
0228 
0229 void SmugTalker::unlink()
0230 {
0231     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "UNLINK to Smug ";
0232     d->o1->unlink();
0233 }
0234 
0235 void SmugTalker::removeUserAccount(const QString& /*userName*/)
0236 {
0237 /*
0238     if (userName.startsWith(d->serviceName))
0239     {
0240         d->settings->beginGroup(userName);
0241         d->settings->remove(QString());
0242         d->settings->endGroup();
0243     }
0244 */
0245 }
0246 
0247 bool SmugTalker::loggedIn() const
0248 {
0249     return d->o1->linked();
0250 }
0251 
0252 void SmugTalker::slotLinkingFailed()
0253 {
0254     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "LINK to Smug fail";
0255     Q_EMIT signalBusy(false);
0256     getLoginedUser();
0257 }
0258 
0259 void SmugTalker::slotLinkingSucceeded()
0260 {
0261     if (!d->o1->linked())
0262     {
0263         qCDebug(DIGIKAM_WEBSERVICES_LOG) << "UNLINK to Smug ok";
0264 
0265         // Remove user account
0266 
0267         removeUserAccount(d->user.nickName);
0268 
0269         // Clear user field
0270 
0271         d->user.clear();
0272 
0273         // Set state to SMUG_LOGOUT
0274 
0275         d->state = Private::SMUG_LOGOUT;
0276 
0277         Q_EMIT signalBusy(false);
0278         return;
0279     }
0280 
0281     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "LINK to Smug ok";
0282 
0283     getLoginedUser();
0284 }
0285 
0286 void SmugTalker::slotOpenBrowser(const QUrl& url)
0287 {
0288     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Open Browser...";
0289     QDesktopServices::openUrl(url);
0290 }
0291 
0292 void SmugTalker::slotCloseBrowser()
0293 {
0294     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Close Browser...";
0295 }
0296 
0297 SmugUser SmugTalker::getUser() const
0298 {
0299     return d->user;
0300 }
0301 
0302 /**
0303  * (Trung)
0304  * There are some characters not valid for album title (e.g "_")
0305  * and for url (e.g "-") that are not mentioned on the API page,
0306  * so if found, that has to be treated here
0307  */
0308 QString SmugTalker::createAlbumName(const QString& word)
0309 {
0310     QString w(word);
0311 
0312     // First we remove space at beginning and end
0313 
0314     w = w.trimmed();
0315 
0316     // We replace all character "_" with space
0317 
0318     w = w.replace(QLatin1Char('_'), QLatin1Char(' '));
0319 
0320     // Then we replace first letter with its uppercase
0321 
0322     w.replace(0, 1, w[0].toUpper());
0323 
0324     qCDebug(DIGIKAM_WEBSERVICES_LOG) << w;
0325 
0326     return w;
0327 }
0328 
0329 QString SmugTalker::createAlbumUrl(const QString& name)
0330 {
0331     QString n(name);
0332 
0333     // First we create a valid name
0334 
0335     n = createAlbumName(n);
0336 
0337     // Then we replace space with "-"
0338 
0339     QStringList words = n.split(QLatin1Char(' '));
0340     n = words.join(QLatin1Char('-'));
0341 
0342     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "url name : " << n;
0343 
0344     return n;
0345 }
0346 
0347 void SmugTalker::cancel()
0348 {
0349     if (d->reply)
0350     {
0351         d->reply->abort();
0352         d->reply = nullptr;
0353     }
0354 
0355     Q_EMIT signalBusy(false);
0356 }
0357 
0358 void SmugTalker::login()
0359 {
0360     if (d->reply)
0361     {
0362         d->reply->abort();
0363         d->reply = nullptr;
0364     }
0365 
0366     Q_EMIT signalBusy(true);
0367     Q_EMIT signalLoginProgress(1, 4, i18n("Logging in to SmugMug service..."));
0368 
0369     // Build authentication url
0370 
0371     O1SmugMug::AuthorizationUrlBuilder builder;
0372     builder.setAccess(O1SmugMug::AccessFull);
0373     builder.setPermissions(O1SmugMug::PermissionsModify);
0374     d->o1->initAuthorizationUrl(builder);
0375 
0376     if (!d->o1->linked())
0377     {
0378         link();
0379     }
0380     else
0381     {
0382         Q_EMIT signalLinkingSucceeded();
0383     }
0384 }
0385 
0386 void SmugTalker::getLoginedUser()
0387 {
0388     QUrl url(d->apiURL.arg(QLatin1String("/api/v2!authuser")));
0389     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "url = " << url.url();
0390 
0391     QList<O0RequestParameter> reqParams = QList<O0RequestParameter>();
0392 
0393     QNetworkRequest netRequest(url);
0394     netRequest.setRawHeader("Accept", "application/json");
0395     netRequest.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String("application/json"));
0396     netRequest.setHeader(QNetworkRequest::UserAgentHeader,   d->userAgent);
0397 
0398     d->reply = d->requestor->get(netRequest, reqParams);
0399 
0400     d->state = Private::SMUG_LOGIN;
0401 }
0402 
0403 void SmugTalker::logout()
0404 {
0405     if (d->reply)
0406     {
0407         d->reply->abort();
0408         d->reply = nullptr;
0409     }
0410 
0411     Q_EMIT signalBusy(true);
0412 
0413     unlink();
0414 }
0415 
0416 void SmugTalker::listAlbums(const QString& /*nickName*/)
0417 {
0418     if (d->reply)
0419     {
0420         d->reply->abort();
0421         d->reply = nullptr;
0422     }
0423 
0424     Q_EMIT signalBusy(true);
0425 
0426     QUrl url(d->apiURL.arg(QString::fromLatin1("%1!albums").arg(d->user.userUri)));
0427     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "url = " << url.url();
0428 
0429     QList<O0RequestParameter> reqParams = QList<O0RequestParameter>();
0430 
0431     QNetworkRequest netRequest(url);
0432     netRequest.setRawHeader("Accept", "application/json");
0433     netRequest.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String("application/json"));
0434     netRequest.setHeader(QNetworkRequest::UserAgentHeader,   d->userAgent);
0435 
0436     d->reply = d->requestor->get(netRequest, reqParams);
0437 
0438     d->state = Private::SMUG_LISTALBUMS;
0439 }
0440 
0441 void SmugTalker::listPhotos(const qint64 /*albumID*/,
0442                             const QString& albumKey,
0443                             const QString& /*albumPassword*/,
0444                             const QString& /*sitePassword*/)
0445 {
0446     if (d->reply)
0447     {
0448         d->reply->abort();
0449         d->reply = nullptr;
0450     }
0451 
0452     Q_EMIT signalBusy(true);
0453 
0454     QUrl url(d->apiURL.arg(QString::fromLatin1("/api/v2/album/%1!images").arg(albumKey)));
0455     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "list photo " << url.url();
0456 
0457     QList<O0RequestParameter> reqParams = QList<O0RequestParameter>();
0458 
0459     QNetworkRequest netRequest(url);
0460     netRequest.setRawHeader("Accept", "application/json");
0461     netRequest.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String("application/json"));
0462     netRequest.setHeader(QNetworkRequest::UserAgentHeader,   d->userAgent);
0463 
0464     d->reply = d->requestor->get(netRequest, reqParams);
0465 
0466     d->state = Private::SMUG_LISTPHOTOS;
0467 }
0468 
0469 void SmugTalker::listAlbumTmpl()
0470 {
0471     if (d->reply)
0472     {
0473         d->reply->abort();
0474         d->reply = nullptr;
0475     }
0476 
0477     Q_EMIT signalBusy(true);
0478 
0479     QUrl url(d->apiURL.arg(QString::fromLatin1("%1!albumtemplates").arg(d->user.userUri)));
0480     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "url to listAlbumTmpl " << url.url();
0481 
0482     QList<O0RequestParameter> reqParams = QList<O0RequestParameter>();
0483 
0484     QNetworkRequest netRequest(url);
0485     netRequest.setRawHeader("Accept", "application/json");
0486     netRequest.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String("application/json"));
0487     netRequest.setHeader(QNetworkRequest::UserAgentHeader,   d->userAgent);
0488 
0489     d->reply = d->requestor->get(netRequest, reqParams);
0490 
0491     d->state = Private::SMUG_LISTALBUMTEMPLATES;
0492 }
0493 
0494 /* Categories deprecated in API v2
0495 
0496 void SmugTalker::listCategories()
0497 {
0498     if (d->reply)
0499     {
0500         d->reply->abort();
0501         d->reply = 0;
0502     }
0503 
0504     Q_EMIT signalBusy(true);
0505 
0506     QUrl url(d->apiURL);
0507     QUrlQuery q;
0508     q.addQueryItem(QLatin1String("method"),    QLatin1String("smugmug.categories.get"));
0509     q.addQueryItem(QLatin1String("SessionID"), d->sessionID);
0510     url.setQuery(q);
0511 
0512     QNetworkRequest netRequest(url);
0513     netRequest.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String("application/x-www-form-urlencoded"));
0514     netRequest.setHeader(QNetworkRequest::UserAgentHeader,   d->userAgent);
0515 
0516     d->reply = d->netMngr->get(netRequest);
0517 
0518     d->state = Private::SMUG_LISTCATEGORIES;
0519 }
0520 
0521 void SmugTalker::listSubCategories(qint64 categoryID)
0522 {
0523     if (d->reply)
0524     {
0525         d->reply->abort();
0526         d->reply = 0;
0527     }
0528 
0529     Q_EMIT signalBusy(true);
0530 
0531     QUrl url(d->apiURL);
0532     QUrlQuery q;
0533     q.addQueryItem(QLatin1String("method"),     QLatin1String("smugmug.subcategories.get"));
0534     q.addQueryItem(QLatin1String("SessionID"),  d->sessionID);
0535     q.addQueryItem(QLatin1String("CategoryID"), QString::number(categoryID));
0536     url.setQuery(q);
0537 
0538     QNetworkRequest netRequest(url);
0539     netRequest.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String("application/x-www-form-urlencoded"));
0540     netRequest.setHeader(QNetworkRequest::UserAgentHeader,   d->userAgent);
0541 
0542     d->reply = d->netMngr->get(netRequest);
0543 
0544     d->state = Private::SMUG_LISTSUBCATEGORIES;
0545 }
0546 */
0547 
0548 void SmugTalker::createAlbum(const SmugAlbum& album)
0549 {
0550     if (d->reply)
0551     {
0552         d->reply->abort();
0553         d->reply = nullptr;
0554     }
0555 
0556     Q_EMIT signalBusy(true);
0557 
0558     QUrl url(d->apiURL.arg(QString::fromLatin1("%1!albums").arg(d->user.folderUri)));
0559     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "url to post " << url.url();
0560 
0561     QList<O0RequestParameter> reqParams = QList<O0RequestParameter>();
0562 
0563     /**
0564      * Something must to be remembered here is that we HAVE TO start a name with upper case !!!
0565      * And url name with '-' instead of space
0566      */
0567 
0568     QByteArray data;
0569     data += "{\"Name\": \"";
0570     data += createAlbumName(album.title).toUtf8();
0571     data += "\",\"UrlName\":\"";
0572     data += createAlbumUrl(album.title).toUtf8();
0573     data += "\",\"Privacy\":\"Public\"}";
0574     qCDebug(DIGIKAM_WEBSERVICES_LOG) << data;
0575 
0576     QNetworkRequest netRequest(url);
0577     netRequest.setRawHeader("Accept", "application/json");
0578     netRequest.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String("application/json"));
0579     netRequest.setHeader(QNetworkRequest::UserAgentHeader,   d->userAgent);
0580 
0581     d->reply = d->requestor->post(netRequest, reqParams, data);
0582 
0583     d->state = Private::SMUG_CREATEALBUM;
0584 }
0585 
0586 bool SmugTalker::addPhoto(const  QString& imgPath,
0587                           qint64 /*albumID*/,
0588                           const  QString& albumKey,
0589                           const  QString& caption)
0590 {
0591     if (d->reply)
0592     {
0593         d->reply->abort();
0594         d->reply = nullptr;
0595     }
0596 
0597     Q_EMIT signalBusy(true);
0598 
0599     QString imgName = QFileInfo(imgPath).fileName();
0600 
0601     // load temporary image to buffer
0602 
0603     QFile imgFile(imgPath);
0604 
0605     if (!imgFile.open(QIODevice::ReadOnly))
0606     {
0607         Q_EMIT signalBusy(false);
0608         return false;
0609     }
0610 
0611     QByteArray imgData = imgFile.readAll();
0612     imgFile.close();
0613 
0614     SmugMPForm form;
0615 
0616     if (!caption.isEmpty())
0617     {
0618         form.addPair(QLatin1String("Caption"), caption);
0619     }
0620 
0621     if (!form.addFile(imgName, imgPath))
0622     {
0623         return false;
0624     }
0625 
0626     form.finish();
0627 
0628     QString customHdr;
0629     QUrl url(d->uploadUrl);
0630     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "url to upload " << url.url();
0631 
0632     QList<O0RequestParameter> reqParams = QList<O0RequestParameter>();
0633 
0634     QNetworkRequest netRequest(url);
0635     netRequest.setHeader(QNetworkRequest::ContentTypeHeader, form.contentType());
0636     netRequest.setHeader(QNetworkRequest::UserAgentHeader,   d->userAgent);
0637     netRequest.setRawHeader("X-Smug-Caption", caption.toUtf8());
0638     netRequest.setRawHeader("X-Smug-FileName", imgName.toUtf8());
0639     netRequest.setRawHeader("X-Smug-AlbumUri", QString::fromLatin1("/api/v2/album/%1").arg(albumKey).toUtf8());
0640     netRequest.setRawHeader("X-Smug-ResponseType", "JSON");
0641     netRequest.setRawHeader("X-Smug-Version",   d->apiVersion.toLatin1());
0642 
0643     d->reply = d->requestor->post(netRequest, reqParams, form.formData());
0644 
0645     d->state = Private::SMUG_ADDPHOTO;
0646 
0647     return true;
0648 }
0649 
0650 void SmugTalker::getPhoto(const QString& imgPath)
0651 {
0652     if (d->reply)
0653     {
0654         d->reply->abort();
0655         d->reply = nullptr;
0656     }
0657 
0658     Q_EMIT signalBusy(true);
0659 
0660     QUrl url(imgPath);
0661 
0662     QUrlQuery q;
0663     q.addQueryItem(QLatin1String("APIKey"), d->apikey);
0664 
0665     url.setQuery(q);
0666     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "download link for image " << url.url();
0667 
0668     QNetworkRequest netRequest(url);
0669     netRequest.setHeader(QNetworkRequest::UserAgentHeader, d->userAgent);
0670 
0671     d->reply = d->netMngr->get(netRequest);
0672 
0673     d->state = Private::SMUG_GETPHOTO;
0674 }
0675 
0676 QString SmugTalker::errorToText(int errCode, const QString& errMsg) const
0677 {
0678     QString transError;
0679     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "errorToText: " << errCode << ": " << errMsg;
0680 
0681     switch (errCode)
0682     {
0683         case 0:
0684             transError = QString();
0685             break;
0686 
0687         case 1:
0688             transError = i18n("Login failed");
0689             break;
0690 
0691         case 4:
0692             transError = i18n("Invalid user/nick/password");
0693             break;
0694 
0695         case 18:
0696             transError = i18n("Invalid API key");
0697             break;
0698 
0699         default:
0700             transError = errMsg;
0701             break;
0702     }
0703 
0704     return transError;
0705 }
0706 
0707 void SmugTalker::slotFinished(QNetworkReply* reply)
0708 {
0709     if (reply != d->reply)
0710     {
0711         return;
0712     }
0713 
0714     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "error code : " << reply->error() << "error text " << reply->errorString();
0715 
0716     d->reply = nullptr;
0717 
0718     if (reply->error() != QNetworkReply::NoError)
0719     {
0720         if      (d->state == Private::SMUG_LOGIN)
0721         {
0722             d->sessionID.clear();
0723             d->user.clear();
0724 
0725             Q_EMIT signalBusy(false);
0726             Q_EMIT signalLoginDone(reply->error(), reply->errorString());
0727             qCDebug(DIGIKAM_WEBSERVICES_LOG) << "error code : " << reply->error() << "error text " << reply->errorString();
0728         }
0729         else if (d->state == Private::SMUG_ADDPHOTO)
0730         {
0731             Q_EMIT signalBusy(false);
0732             Q_EMIT signalAddPhotoDone(reply->error(), reply->errorString());
0733         }
0734         else if (d->state == Private::SMUG_GETPHOTO)
0735         {
0736             Q_EMIT signalBusy(false);
0737             Q_EMIT signalGetPhotoDone(reply->error(), reply->errorString(), QByteArray());
0738         }
0739         else
0740         {
0741             Q_EMIT signalBusy(false);
0742             QMessageBox::critical(QApplication::activeWindow(),
0743                                 i18nc("@title:window", "Error"), reply->errorString());
0744         }
0745 
0746         reply->deleteLater();
0747         return;
0748     }
0749 
0750     QByteArray buffer = reply->readAll();
0751 
0752     switch (d->state)
0753     {
0754         case (Private::SMUG_LOGIN):
0755             parseResponseLogin(buffer);
0756             break;
0757 
0758         case (Private::SMUG_LISTALBUMS):
0759             parseResponseListAlbums(buffer);
0760             break;
0761 
0762         case (Private::SMUG_LISTPHOTOS):
0763             parseResponseListPhotos(buffer);
0764             break;
0765 
0766         case (Private::SMUG_LISTALBUMTEMPLATES):
0767             parseResponseListAlbumTmpl(buffer);
0768             break;
0769 /*
0770         case (Private::SMUG_LISTCATEGORIES):
0771             parseResponseListCategories(buffer);
0772             break;
0773 
0774         case (Private::SMUG_LISTSUBCATEGORIES):
0775             parseResponseListSubCategories(buffer);
0776             break;
0777 */
0778         case (Private::SMUG_CREATEALBUM):
0779             parseResponseCreateAlbum(buffer);
0780             break;
0781 
0782         case (Private::SMUG_ADDPHOTO):
0783             parseResponseAddPhoto(buffer);
0784             break;
0785 
0786         case (Private::SMUG_GETPHOTO):
0787 
0788             // all we get is data of the image
0789 
0790             Q_EMIT signalBusy(false);
0791             Q_EMIT signalGetPhotoDone(0, QString(), buffer);
0792             break;
0793 
0794         default: // Private::SMUG_LOGIN
0795             break;
0796     }
0797 
0798     reply->deleteLater();
0799 }
0800 
0801 void SmugTalker::parseResponseLogin(const QByteArray& data)
0802 {
0803     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "parseResponseLogin";
0804     QJsonParseError err;
0805     QJsonDocument doc = QJsonDocument::fromJson(data, &err);
0806 
0807     Q_EMIT signalLoginProgress(3);
0808 
0809     if (err.error != QJsonParseError::NoError)
0810     {
0811         qCDebug(DIGIKAM_WEBSERVICES_LOG) << "failed to parse to json";
0812         qCDebug(DIGIKAM_WEBSERVICES_LOG) << "errCode " << err.error;
0813         Q_EMIT signalLoginDone(err.error, errorToText(err.error, err.errorString()));
0814         Q_EMIT signalBusy(false);
0815         return;
0816     }
0817 
0818     QJsonObject jsonObject  = doc.object();
0819     QJsonObject response    = jsonObject[QLatin1String("Response")].toObject();
0820     QJsonObject userObject  = response[QLatin1String("User")].toObject();
0821     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "json object " << userObject;
0822 
0823     d->user.displayName     = userObject[QLatin1String("Name")].toString();
0824     d->user.nickName        = userObject[QLatin1String("NickName")].toString();
0825     d->user.userUri         = userObject[QLatin1String("Uri")].toString();
0826 
0827     QJsonObject Uris        = userObject[QLatin1String("Uris")].toObject();
0828     QJsonObject node        = Uris[QLatin1String("Node")].toObject();
0829     QJsonObject folder      = Uris[QLatin1String("Folder")].toObject();
0830 
0831     d->user.nodeUri         = node[QLatin1String("Uri")].toString();
0832     d->user.folderUri       = folder[QLatin1String("Uri")].toString();
0833 
0834     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "json data parse : " << d->user.displayName << "+ " << d->user.nodeUri;
0835 
0836     Q_EMIT signalLoginProgress(4);
0837     Q_EMIT signalBusy(false);
0838     Q_EMIT signalLoginDone(0, QString());
0839 }
0840 
0841 /* Not necessary anymore
0842 
0843 void SmugTalker::parseResponseLogout(const QByteArray& data)
0844 {
0845     int errCode = -1;
0846     QString errMsg;
0847 
0848     QDomDocument doc(QLatin1String("logout"));
0849 
0850     if (!doc.setContent(data))
0851         return;
0852 
0853     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Parse Logout response:" << QT_ENDL << data;
0854 
0855     QDomElement e = doc.documentElement();
0856 
0857     for (QDomNode node = e.firstChild(); !node.isNull(); node = node.nextSibling())
0858     {
0859         if (!node.isElement())
0860         {
0861             continue;
0862         }
0863 
0864         e = node.toElement();
0865 
0866         if      (e.tagName() == QLatin1String("Logout"))
0867         {
0868             errCode = 0;
0869         }
0870         else if (e.tagName() == QLatin1String("err"))
0871         {
0872             errCode = e.attribute(QLatin1String("code")).toInt();
0873             errMsg  = e.attribute(QLatin1String("msg"));
0874             qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Error:" << errCode << errMsg;
0875         }
0876     }
0877 
0878     // consider we are logged out in any case
0879     d->sessionID.clear();
0880     d->user.clear();
0881 
0882     Q_EMIT signalBusy(false);
0883 }
0884 */
0885 
0886 /**
0887  * A multi-part put response (which we get now) looks like:
0888  * <?xml version="1.0" encoding="utf-8"?>
0889  * <rsp stat="ok">
0890  *    <method>smugmug.images.upload</method>
0891  *    <ImageID>884775096</ImageID>
0892  *    <ImageKey>L7aq5</ImageKey>
0893  *    <ImageURL>http://froody.smugmug.com/Other/Test/12372176_y7yNq#884775096_L7aq5</ImageURL>          // krazy:exclude=insecurenet
0894  * </rsp>
0895  *
0896  * A simple put response (which we used to get) looks like:
0897  * <?xml version="1.0" encoding="utf-8"?>
0898  * <rsp stat="ok">
0899  *    <method>smugmug.images.upload</method>
0900  *    <Image id="884790545" Key="seeQa" URL="http://froody.smugmug.com/Other/Test/12372176_y7yNq#884790545_seeQa"/>     // krazy:exclude=insecurenet
0901  * </rsp>
0902  *
0903  * Since all we care about is success or not, we can just check the rsp stat.
0904  */
0905 
0906 void SmugTalker::parseResponseAddPhoto(const QByteArray& data)
0907 {
0908     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "parseResponseAddPhoto";
0909 
0910     QJsonParseError err;
0911     QJsonDocument doc = QJsonDocument::fromJson(data, &err);
0912 
0913     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "json doc " << doc;
0914 
0915     if (err.error != QJsonParseError::NoError)
0916     {
0917         Q_EMIT signalBusy(false);
0918         Q_EMIT signalAddPhotoDone(err.error, errorToText(err.error, err.errorString()));
0919         return;
0920     }
0921 
0922     Q_EMIT signalBusy(false);
0923     Q_EMIT signalAddPhotoDone(err.error, errorToText(err.error, err.errorString()));
0924 }
0925 
0926 void SmugTalker::parseResponseCreateAlbum(const QByteArray& data)
0927 {
0928     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "parseResponseCreateAlbum";
0929 
0930     QJsonParseError err;
0931     QJsonDocument doc = QJsonDocument::fromJson(data, &err);
0932 
0933     if (err.error != QJsonParseError::NoError)
0934     {
0935         Q_EMIT signalBusy(false);
0936         Q_EMIT signalCreateAlbumDone(err.error, err.errorString(), 0, QString());
0937         return;
0938     }
0939 
0940     QJsonObject jsonObject  = doc.object();
0941     QJsonObject response    = jsonObject[QLatin1String("Response")].toObject();
0942     QJsonObject album       = response[QLatin1String("Album")].toObject();
0943     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "json data : " << jsonObject;
0944 
0945     QString newAlbumKey     = album[QLatin1String("AlbumKey")].toString();
0946 
0947     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "newAlbumKey " << newAlbumKey;
0948 
0949     Q_EMIT signalBusy(false);
0950     Q_EMIT signalCreateAlbumDone(0, errorToText(0, QString()), 0, newAlbumKey);
0951 }
0952 
0953 void SmugTalker::parseResponseListAlbums(const QByteArray& data)
0954 {
0955     QJsonParseError err;
0956     QJsonDocument doc = QJsonDocument::fromJson(data, &err);
0957 
0958     if (err.error != QJsonParseError::NoError)
0959     {
0960         Q_EMIT signalBusy(false);
0961         Q_EMIT signalListAlbumsDone(err.error,i18n("Failed to list albums"), QList<SmugAlbum>());
0962         return;
0963     }
0964 
0965     QJsonObject jsonObject  = doc.object();
0966     QJsonObject response    = jsonObject[QLatin1String("Response")].toObject();
0967     QJsonArray jsonArray    = response[QLatin1String("Album")].toArray();
0968     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "parseResponseListAlbum : " << jsonObject;
0969 
0970     QList<SmugAlbum> albumList;
0971 
0972     Q_FOREACH (const QJsonValue& value, jsonArray)
0973     {
0974         QJsonObject obj = value.toObject();
0975 
0976         SmugAlbum album;
0977 
0978         album.nodeID        = obj[QLatin1String("NodeID")].toString();
0979         album.name          = obj[QLatin1String("Name")].toString();
0980         album.key           = obj[QLatin1String("AlbumKey")].toString();
0981         album.title         = obj[QLatin1String("Title")].toString();
0982         album.description   = obj[QLatin1String("Description")].toString();
0983         album.keywords      = obj[QLatin1String("Keywords")].toString();
0984         album.canShare      = obj[QLatin1String("CanShare")].toBool();
0985         album.passwordHint  = obj[QLatin1String("PasswordHint")].toString();
0986         album.imageCount    = obj[QLatin1String("ImageCount")].toInt();
0987 
0988         albumList.append(album);
0989 
0990         QStringList albumParams;
0991         albumParams << album.nodeID
0992                     << album.name
0993                     << album.key
0994                     << album.title
0995                     << album.description
0996                     << album.keywords
0997                     << QString::number(album.canShare)
0998                     << album.passwordHint
0999                     << QString::number(album.imageCount);
1000         qCDebug(DIGIKAM_WEBSERVICES_LOG) << "album " << albumParams.join(QLatin1String(","));
1001     }
1002 
1003     std::sort(albumList.begin(), albumList.end(), SmugAlbum::lessThan);
1004 
1005     Q_EMIT signalBusy(false);
1006     Q_EMIT signalListAlbumsDone(err.error, errorToText(err.error, err.errorString()), albumList);
1007 }
1008 
1009 void SmugTalker::parseResponseListPhotos(const QByteArray& data)
1010 {
1011     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "parseResponseListPhotos";
1012 
1013     QJsonParseError err;
1014     QJsonDocument doc = QJsonDocument::fromJson(data, &err);
1015 
1016     if (err.error != QJsonParseError::NoError)
1017     {
1018         Q_EMIT signalBusy(false);
1019         Q_EMIT signalListPhotosDone(err.error, errorToText(err.error, err.errorString()), QList<SmugPhoto>());
1020         return;
1021     }
1022 
1023     QJsonObject jsonObject  = doc.object();
1024     QJsonObject response    = jsonObject[QLatin1String("Response")].toObject();
1025     QJsonArray jsonArray    = response[QLatin1String("AlbumImage")].toArray();
1026 
1027     QList<SmugPhoto> photosList;
1028 
1029     Q_FOREACH (const QJsonValue& value, jsonArray)
1030     {
1031         QJsonObject obj = value.toObject();
1032 
1033         SmugPhoto photo;
1034         photo.key           = obj[QLatin1String("ImageKey")].toString();
1035         photo.caption       = obj[QLatin1String("Caption")].toString();
1036         photo.keywords      = obj[QLatin1String("Keywords")].toString();
1037         photo.thumbURL      = obj[QLatin1String("ThumbnailUrl")].toString();
1038         photo.originalURL   = obj[QLatin1String("ArchivedUri")].toString();
1039 
1040         photosList.append(photo);
1041 
1042         qCDebug(DIGIKAM_WEBSERVICES_LOG) << "photo key: "   << photo.key
1043                                          << ", captions: "  << photo.caption
1044                                          << ", keywords: "  << photo.keywords
1045                                          << ", ThumbnailUrl " << photo.thumbURL
1046                                          << ", originalURL "  << photo.originalURL;
1047 
1048     }
1049 
1050     Q_EMIT signalBusy(false);
1051     Q_EMIT signalListPhotosDone(0, QString(), photosList);
1052 }
1053 
1054 void SmugTalker::parseResponseListAlbumTmpl(const QByteArray& data)
1055 {
1056     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "ParseResponseListAlbumTmpl";
1057 
1058     QJsonParseError err;
1059     QJsonDocument doc = QJsonDocument::fromJson(data, &err);
1060 
1061     if (err.error != QJsonParseError::NoError)
1062     {
1063         Q_EMIT signalBusy(false);
1064         Q_EMIT signalListAlbumTmplDone(err.error,i18n("Failed to list album template"), QList<SmugAlbumTmpl>());
1065         return;
1066     }
1067 
1068     QJsonObject jsonObject  = doc.object();
1069     QJsonObject response    = jsonObject[QLatin1String("Response")].toObject();
1070     QJsonArray jsonArray    = response[QLatin1String("AlbumTemplate")].toArray();
1071     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "listAlbumTmpl = " << jsonArray;
1072 
1073     QList<SmugAlbumTmpl> albumTmplList;
1074 
1075     Q_FOREACH (const QJsonValue &value, jsonArray)
1076     {
1077         QJsonObject obj = value.toObject();
1078 
1079         SmugAlbumTmpl albumTmpl;
1080         albumTmpl.name          = obj[QLatin1String("Name")].toString();
1081         albumTmpl.uri           = obj[QLatin1String("Uri")].toString();
1082         albumTmpl.isPublic      = obj[QLatin1String("Public")].toBool();
1083         albumTmpl.password      = obj[QLatin1String("Password")].toString();
1084         albumTmpl.passwordHint  = obj[QLatin1String("PasswordHint")].toString();
1085 
1086         albumTmplList.append(albumTmpl);
1087 
1088         qCDebug(DIGIKAM_WEBSERVICES_LOG) << "albumTmpl : name " << albumTmpl.name
1089                                             << ", uri : " << albumTmpl.uri
1090                                             << ", isPublic " << albumTmpl.isPublic
1091                                             << ", password " << albumTmpl.password
1092                                             << ", passwordHint " << albumTmpl.passwordHint;
1093     }
1094 
1095     Q_EMIT signalBusy(false);
1096     Q_EMIT signalListAlbumTmplDone(0, errorToText(0, QString()), albumTmplList);
1097 }
1098 
1099 /* Categories are deprecated in API v2
1100 
1101 void SmugTalker::parseResponseListCategories(const QByteArray& data)
1102 {
1103     int errCode = -1;
1104     QString errMsg;
1105     QDomDocument doc(QLatin1String("categories.get"));
1106 
1107     if (!doc.setContent(data))
1108     {
1109         return;
1110     }
1111 
1112     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Parse Categories response:" << QT_ENDL << data;
1113 
1114     QList <SmugCategory> categoriesList;
1115     QDomElement e = doc.documentElement();
1116 
1117     for (QDomNode node = e.firstChild(); !node.isNull(); node = node.nextSibling())
1118     {
1119         if (!node.isElement())
1120         {
1121             continue;
1122         }
1123 
1124         e = node.toElement();
1125 
1126         if      (e.tagName() == QLatin1String("Categories"))
1127         {
1128             for (QDomNode nodeC = e.firstChild(); !nodeC.isNull(); nodeC = nodeC.nextSibling())
1129             {
1130                 if (!nodeC.isElement())
1131                 {
1132                     continue;
1133                 }
1134 
1135                 QDomElement e = nodeC.toElement();
1136 
1137                 if (e.tagName() == QLatin1String("Category"))
1138                 {
1139                     SmugCategory category;
1140                     category.id   = e.attribute(QLatin1String("id")).toLongLong();
1141                     category.name = htmlToText(e.attribute(QLatin1String("Name")));
1142                     categoriesList.append(category);
1143                 }
1144             }
1145 
1146             errCode = 0;
1147         }
1148         else if (e.tagName() == QLatin1String("err"))
1149         {
1150             errCode = e.attribute(QLatin1String("code")).toInt();
1151             errMsg  = e.attribute(QLatin1String("msg"));
1152             qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Error:" << errCode << errMsg;
1153         }
1154     }
1155 
1156     if (errCode == 15)  // 15: empty list
1157     {
1158         errCode = 0;
1159     }
1160 
1161     Q_EMIT signalBusy(false);
1162     Q_EMIT signalListCategoriesDone(errCode, errorToText(errCode, errMsg), categoriesList);
1163 }
1164 
1165 void SmugTalker::parseResponseListSubCategories(const QByteArray& data)
1166 {
1167     int errCode = -1;
1168     QString errMsg;
1169     QDomDocument doc(QLatin1String("subcategories.get"));
1170 
1171     if (!doc.setContent(data))
1172     {
1173         return;
1174     }
1175 
1176     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Parse SubCategories response:" << QT_ENDL << data;
1177 
1178     QList <SmugCategory> categoriesList;
1179     QDomElement e = doc.documentElement();
1180 
1181     for (QDomNode node = e.firstChild(); !node.isNull(); node = node.nextSibling())
1182     {
1183         if (!node.isElement())
1184         {
1185             continue;
1186         }
1187 
1188         e = node.toElement();
1189 
1190         if      (e.tagName() == QLatin1String("SubCategories"))
1191         {
1192             for (QDomNode nodeC = e.firstChild(); !nodeC.isNull(); nodeC = nodeC.nextSibling())
1193             {
1194                 if (!nodeC.isElement())
1195                 {
1196                     continue;
1197                 }
1198 
1199                 e = nodeC.toElement();
1200 
1201                 if (e.tagName() == QLatin1String("SubCategory"))
1202                 {
1203                     SmugCategory category;
1204                     category.id   = e.attribute(QLatin1String("id")).toLongLong();
1205                     category.name = htmlToText(e.attribute(QLatin1String("Name")));
1206                     categoriesList.append(category);
1207                 }
1208             }
1209 
1210             errCode = 0;
1211         }
1212         else if (e.tagName() == QLatin1String("err"))
1213         {
1214             errCode = e.attribute(QLatin1String("code")).toInt();
1215             errMsg  = e.attribute(QLatin1String("msg"));
1216             qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Error:" << errCode << errMsg;
1217         }
1218     }
1219 
1220     if (errCode == 15)  // 15: empty list
1221     {
1222         errCode = 0;
1223     }
1224 
1225     Q_EMIT signalBusy(false);
1226     Q_EMIT signalListSubCategoriesDone(errCode, errorToText(errCode, errMsg), categoriesList);
1227 }
1228 */
1229 
1230 QString SmugTalker::htmlToText(const QString& htmlText) const
1231 {
1232     QTextDocument txtDoc;
1233     txtDoc.setHtml(htmlText);
1234 
1235     return txtDoc.toPlainText();
1236 }
1237 
1238 } // namespace DigikamGenericSmugPlugin
1239 
1240 #include "moc_smugtalker.cpp"