File indexing completed on 2025-01-05 03:53:32
0001 /* ============================================================ 0002 * 0003 * This file is a part of digiKam project 0004 * https://www.digikam.org 0005 * 0006 * Date : 2016-05-27 0007 * Description : Implementation of v3 of the Imgur API 0008 * 0009 * SPDX-FileCopyrightText: 2016 by Fabian Vogt <fabian at ritter dash vogt dot de> 0010 * SPDX-FileCopyrightText: 2016-2020 by Caulier Gilles <caulier dot gilles at gmail dot com> 0011 * 0012 * SPDX-License-Identifier: GPL-2.0-or-later 0013 * 0014 * ============================================================ */ 0015 0016 #include "imgurtalker.h" 0017 0018 // Qt includes 0019 0020 #include <QHttpMultiPart> 0021 #include <QJsonDocument> 0022 #include <QJsonObject> 0023 #include <QTimerEvent> 0024 #include <QUrlQuery> 0025 #include <QFileInfo> 0026 #include <QQueue> 0027 0028 // KDE includes 0029 0030 #include <klocalizedstring.h> 0031 0032 // Local includes 0033 0034 #include "dinfointerface.h" 0035 #include "digikam_debug.h" 0036 #include "wstoolutils.h" 0037 #include "networkmanager.h" 0038 #include "o0settingsstore.h" 0039 #include "o0globals.h" 0040 0041 0042 using namespace Digikam; 0043 0044 namespace DigikamGenericImgUrPlugin 0045 { 0046 0047 static const QString imgur_auth_url = QLatin1String("https://api.imgur.com/oauth2/authorize"), 0048 imgur_token_url = QLatin1String("https://api.imgur.com/oauth2/token"); 0049 static const uint16_t imgur_redirect_port = 8000; // Redirect URI is http://127.0.0.1:8000 // krazy:exclude=insecurenet 0050 0051 class Q_DECL_HIDDEN ImgurTalker::Private 0052 { 0053 public: 0054 0055 explicit Private() 0056 : client_id (QLatin1String("bd2572bce74b73d")), 0057 client_secret (QLatin1String("300988683e99cb7b203a5889cf71de9ac891c1c1")), 0058 workTimer (0), 0059 reply (nullptr), 0060 image (nullptr), 0061 netMngr (nullptr) 0062 { 0063 } 0064 0065 /// API key and secret 0066 QString client_id; 0067 QString client_secret; 0068 0069 /// Handler for OAuth 2 related requests. 0070 O2 auth; 0071 0072 /// Work queue. 0073 QQueue<ImgurTalkerAction> workQueue; 0074 0075 /// ID of timer triggering on idle (0ms). 0076 int workTimer; 0077 0078 /// Current QNetworkReply instance. 0079 QNetworkReply* reply; 0080 0081 /// Current image being uploaded. 0082 QFile* image; 0083 0084 /// The QNetworkAccessManager instance used for connections. 0085 QNetworkAccessManager* netMngr; 0086 }; 0087 0088 ImgurTalker::ImgurTalker(QObject* const parent) 0089 : QObject(parent), 0090 d (new Private) 0091 { 0092 d->netMngr = NetworkManager::instance()->getNetworkManager(this); 0093 0094 d->auth.setClientId(d->client_id); 0095 d->auth.setClientSecret(d->client_secret); 0096 d->auth.setRequestUrl(imgur_auth_url); 0097 d->auth.setTokenUrl(imgur_token_url); 0098 d->auth.setRefreshTokenUrl(imgur_token_url); 0099 d->auth.setLocalPort(imgur_redirect_port); 0100 d->auth.setLocalhostPolicy(QString()); 0101 0102 QSettings* const settings = WSToolUtils::getOauthSettings(this); 0103 O0SettingsStore* const store = new O0SettingsStore(settings, 0104 QLatin1String(O2_ENCRYPTION_KEY), this); 0105 store->setGroupKey(QLatin1String("Imgur")); 0106 d->auth.setStore(store); 0107 0108 connect(&d->auth, &O2::linkedChanged, 0109 this, &ImgurTalker::slotOauthAuthorized); 0110 0111 connect(&d->auth, &O2::openBrowser, 0112 this, &ImgurTalker::slotOauthRequestPin); 0113 0114 connect(&d->auth, &O2::linkingFailed, 0115 this, &ImgurTalker::slotOauthFailed); 0116 } 0117 0118 ImgurTalker::~ImgurTalker() 0119 { 0120 // Disconnect all signals as cancelAllWork may emit. 0121 0122 disconnect(this, nullptr, nullptr, nullptr); 0123 cancelAllWork(); 0124 0125 delete d; 0126 } 0127 0128 O2& ImgurTalker::getAuth() 0129 { 0130 return d->auth; 0131 } 0132 0133 unsigned int ImgurTalker::workQueueLength() 0134 { 0135 return d->workQueue.size(); 0136 } 0137 0138 void ImgurTalker::queueWork(const ImgurTalkerAction& action) 0139 { 0140 d->workQueue.enqueue(action); 0141 startWorkTimer(); 0142 } 0143 0144 void ImgurTalker::cancelAllWork() 0145 { 0146 stopWorkTimer(); 0147 0148 if (d->reply) 0149 { 0150 d->reply->abort(); 0151 } 0152 0153 // Should signalError be emitted for those actions? 0154 0155 while (!d->workQueue.isEmpty()) 0156 { 0157 d->workQueue.dequeue(); 0158 } 0159 } 0160 0161 QUrl ImgurTalker::urlForDeletehash(const QString& deletehash) 0162 { 0163 return QUrl{QLatin1String("https://imgur.com/delete/") + deletehash}; 0164 } 0165 0166 void ImgurTalker::slotOauthAuthorized() 0167 { 0168 bool success = d->auth.linked(); 0169 0170 if (success) 0171 { 0172 startWorkTimer(); 0173 } 0174 else 0175 { 0176 Q_EMIT signalBusy(false); 0177 } 0178 0179 Q_EMIT signalAuthorized(success, 0180 d->auth.extraTokens()[QLatin1String("account_username")].toString()); 0181 } 0182 0183 void ImgurTalker::slotOauthRequestPin(const QUrl& url) 0184 { 0185 Q_EMIT signalBusy(false); 0186 Q_EMIT signalRequestPin(url); 0187 } 0188 0189 void ImgurTalker::slotOauthFailed() 0190 { 0191 cancelAllWork(); 0192 Q_EMIT signalAuthError(i18n("Could not authorize")); 0193 } 0194 0195 void ImgurTalker::slotUploadProgress(qint64 sent, qint64 total) 0196 { 0197 // Don't divide by 0 0198 0199 if (total > 0) 0200 { 0201 Q_EMIT signalProgress((sent * 100) / total, d->workQueue.first()); 0202 } 0203 } 0204 0205 void ImgurTalker::slotReplyFinished() 0206 { 0207 auto* const reply = d->reply; 0208 reply->deleteLater(); 0209 d->reply = nullptr; 0210 0211 if (d->image) 0212 { 0213 delete d->image; 0214 d->image = nullptr; 0215 } 0216 0217 if (d->workQueue.isEmpty()) 0218 { 0219 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Received result without request"; 0220 return; 0221 } 0222 0223 // NOTE: toInt() returns 0 if conversion fails. That fits nicely already. 0224 0225 int netcode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); 0226 auto response = QJsonDocument::fromJson(reply->readAll()); 0227 0228 if ((netcode == 200) && !response.isEmpty()) 0229 { 0230 // Success! 0231 0232 ImgurTalkerResult result; 0233 result.action = &d->workQueue.first(); 0234 auto data = response.object()[QLatin1String("data")].toObject(); 0235 0236 switch (result.action->type) 0237 { 0238 case ImgurTalkerActionType::IMG_UPLOAD: 0239 case ImgurTalkerActionType::ANON_IMG_UPLOAD: 0240 result.image.animated = data[QLatin1String("animated")].toBool(); 0241 result.image.bandwidth = data[QLatin1String("bandwidth")].toInt(); 0242 result.image.datetime = data[QLatin1String("datetime")].toInt(); 0243 result.image.deletehash = data[QLatin1String("deletehash")].toString(); 0244 result.image.description = data[QLatin1String("description")].toString(); 0245 result.image.height = data[QLatin1String("height")].toInt(); 0246 result.image.hash = data[QLatin1String("id")].toString(); 0247 result.image.name = data[QLatin1String("name")].toString(); 0248 result.image.size = data[QLatin1String("size")].toInt(); 0249 result.image.title = data[QLatin1String("title")].toString(); 0250 result.image.type = data[QLatin1String("type")].toString(); 0251 result.image.url = data[QLatin1String("link")].toString(); 0252 result.image.views = data[QLatin1String("views")].toInt(); 0253 result.image.width = data[QLatin1String("width")].toInt(); 0254 break; 0255 0256 case ImgurTalkerActionType::ACCT_INFO: 0257 result.account.username = data[QLatin1String("url")].toString(); 0258 // TODO: Other fields. 0259 break; 0260 0261 default: 0262 qCWarning(DIGIKAM_WEBSERVICES_LOG) << "Unexpected action"; 0263 qCDebug(DIGIKAM_WEBSERVICES_LOG) << response.toJson(); 0264 break; 0265 } 0266 0267 Q_EMIT signalSuccess(result); 0268 } 0269 else 0270 { 0271 if (netcode == 403) 0272 { 0273 /** 0274 * HTTP 403 Forbidden -> Invalid token? 0275 * That needs to be handled internally, so don't Q_EMIT signalProgress 0276 * and keep the action in the queue for later retries. 0277 */ 0278 d->auth.refresh(); 0279 return; 0280 } 0281 else 0282 { 0283 // Failed. 0284 0285 auto msg = response.object()[QLatin1String("data")] 0286 .toObject()[QLatin1String("error")] 0287 .toString(QLatin1String("Could not read response.")); 0288 0289 Q_EMIT signalError(msg, d->workQueue.first()); 0290 } 0291 } 0292 0293 // Next work item. 0294 0295 d->workQueue.dequeue(); 0296 startWorkTimer(); 0297 } 0298 0299 void ImgurTalker::timerEvent(QTimerEvent* event) 0300 { 0301 if (event->timerId() != d->workTimer) 0302 { 0303 QObject::timerEvent(event); 0304 return; 0305 } 0306 0307 event->accept(); 0308 0309 // One-shot only. 0310 0311 QObject::killTimer(event->timerId()); 0312 d->workTimer = 0; 0313 0314 doWork(); 0315 } 0316 0317 void ImgurTalker::startWorkTimer() 0318 { 0319 if (!d->workQueue.isEmpty() && d->workTimer == 0) 0320 { 0321 d->workTimer = QObject::startTimer(0); 0322 Q_EMIT signalBusy(true); 0323 } 0324 else 0325 { 0326 Q_EMIT signalBusy(false); 0327 } 0328 } 0329 0330 void ImgurTalker::stopWorkTimer() 0331 { 0332 if (d->workTimer != 0) 0333 { 0334 QObject::killTimer(d->workTimer); 0335 d->workTimer = 0; 0336 } 0337 } 0338 0339 void ImgurTalker::addAuthToken(QNetworkRequest* request) 0340 { 0341 request->setRawHeader(QByteArray("Authorization"), 0342 QString::fromLatin1("Bearer %1").arg(d->auth.token()).toUtf8()); 0343 } 0344 0345 void ImgurTalker::addAnonToken(QNetworkRequest* request) 0346 { 0347 request->setRawHeader(QByteArray("Authorization"), 0348 QString::fromLatin1("Client-ID %1").arg(d->auth.clientId()).toUtf8()); 0349 } 0350 0351 void ImgurTalker::doWork() 0352 { 0353 if (d->workQueue.isEmpty() || (d->reply != nullptr)) 0354 { 0355 return; 0356 } 0357 0358 auto &work = d->workQueue.first(); 0359 0360 if ((work.type != ImgurTalkerActionType::ANON_IMG_UPLOAD) && !d->auth.linked()) 0361 { 0362 d->auth.link(); 0363 return; // Wait for the signalAuthorized() signal. 0364 } 0365 0366 switch (work.type) 0367 { 0368 case ImgurTalkerActionType::ACCT_INFO: 0369 { 0370 QNetworkRequest request(QUrl(QString::fromLatin1("https://api.imgur.com/3/account/%1") 0371 .arg(QLatin1String(work.account.username.toUtf8().toPercentEncoding())))); 0372 addAuthToken(&request); 0373 0374 d->reply = d->netMngr->get(request); 0375 break; 0376 } 0377 0378 case ImgurTalkerActionType::ANON_IMG_UPLOAD: 0379 case ImgurTalkerActionType::IMG_UPLOAD: 0380 { 0381 d->image = new QFile(work.upload.imgpath); 0382 0383 if (!d->image->open(QIODevice::ReadOnly)) 0384 { 0385 delete d->image; 0386 d->image = nullptr; 0387 0388 // Failed. 0389 Q_EMIT signalError(i18n("Could not open file"), d->workQueue.first()); 0390 0391 d->workQueue.dequeue(); 0392 doWork(); 0393 return; 0394 } 0395 0396 // Set ownership to d->image to delete that as well. 0397 0398 auto* const multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType, d->image); 0399 QHttpPart title; 0400 title.setHeader(QNetworkRequest::ContentDispositionHeader, 0401 QLatin1String("form-data; name=\"title\"")); 0402 title.setBody(work.upload.title.toUtf8().toPercentEncoding()); 0403 multiPart->append(title); 0404 0405 QHttpPart description; 0406 description.setHeader(QNetworkRequest::ContentDispositionHeader, 0407 QLatin1String("form-data; name=\"description\"")); 0408 description.setBody(work.upload.description.toUtf8().toPercentEncoding()); 0409 multiPart->append(description); 0410 0411 QHttpPart imagePart; 0412 imagePart.setHeader(QNetworkRequest::ContentDispositionHeader, 0413 QVariant(QString::fromLatin1("form-data; name=\"image\"; filename=\"%1\"") 0414 .arg(QLatin1String(QFileInfo(work.upload.imgpath).fileName().toUtf8().toPercentEncoding())))); 0415 imagePart.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String("application/octet-stream")); 0416 imagePart.setBodyDevice(d->image); 0417 multiPart->append(imagePart); 0418 0419 QNetworkRequest request(QUrl(QLatin1String("https://api.imgur.com/3/image"))); 0420 0421 if (work.type == ImgurTalkerActionType::IMG_UPLOAD) 0422 { 0423 addAuthToken(&request); 0424 } 0425 else 0426 { 0427 addAnonToken(&request); 0428 } 0429 0430 d->reply = d->netMngr->post(request, multiPart); 0431 0432 // delete the multiPart with the reply 0433 0434 multiPart->setParent(d->reply); 0435 0436 break; 0437 } 0438 } 0439 0440 if (d->reply) 0441 { 0442 connect(d->reply, &QNetworkReply::uploadProgress, 0443 this, &ImgurTalker::slotUploadProgress); 0444 0445 connect(d->reply, &QNetworkReply::finished, 0446 this, &ImgurTalker::slotReplyFinished); 0447 } 0448 } 0449 0450 } // namespace DigikamGenericImgUrPlugin 0451 0452 #include "moc_imgurtalker.cpp"