#include "yourlsclient.h" #include #include #include #include #include #include #ifndef QT_NO_DEBUG #include #endif /** * @brief Constructs the client. * @param parent QObject parent. */ YourlsClient::YourlsClient(QObject *parent) : QObject(parent), m_networkAccessManager(new QNetworkAccessManager(this)) { } /** * @brief Requests a short URL from the YOURLS API. * @param settings API endpoint and signature. * @param longUrl Source URL to shorten. */ void YourlsClient::shortenUrl(const YourlsSettings &settings, const QUrl &longUrl) { #ifndef QT_NO_DEBUG qDebug() << "[yourls] source URL:" << longUrl << "valid:" << longUrl.isValid() << "scheme:" << longUrl.scheme() << "host:" << longUrl.host(); #endif if (settings.apiUrl.trimmed().isEmpty()) { emit shortenFailed(QStringLiteral("API URL is empty.")); return; } if (settings.signature.trimmed().isEmpty()) { emit shortenFailed(QStringLiteral("API signature is empty.")); return; } if (!longUrl.isValid() || longUrl.scheme().isEmpty() || longUrl.host().isEmpty()) { emit shortenFailed(QStringLiteral("Clipboard does not contain a valid URL.")); return; } QUrl requestUrl(settings.apiUrl); if (!requestUrl.isValid()) { emit shortenFailed(QStringLiteral("Configured API URL is invalid: %1").arg(settings.apiUrl)); return; } QUrlQuery query; query.addQueryItem(QStringLiteral("signature"), settings.signature); query.addQueryItem(QStringLiteral("action"), QStringLiteral("shorturl")); query.addQueryItem(QStringLiteral("format"), QStringLiteral("json")); query.addQueryItem(QStringLiteral("url"), QString::fromLatin1(QUrl::toPercentEncoding(longUrl.toString(QUrl::FullyEncoded)))); requestUrl.setQuery(query); QNetworkRequest request(requestUrl); request.setHeader(QNetworkRequest::UserAgentHeader, QStringLiteral("yourls-ui/1.0")); #ifndef QT_NO_DEBUG // Never log the request query: it contains the API signature. qDebug() << "[yourls] sending GET, endpoint:" << requestUrl.adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment | QUrl::RemoveUserInfo) << "source URL:" << longUrl; #endif QNetworkReply *reply = m_networkAccessManager->get(request); connect(reply, &QNetworkReply::finished, this, [this, reply, longUrl]() { const QByteArray body = reply->readAll(); #ifndef QT_NO_DEBUG qDebug() << "[yourls] response HTTP status:" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute) << "network error code:" << reply->error() << "response bytes:" << body.size(); #endif QJsonParseError parseError; const QJsonDocument jsonDocument = QJsonDocument::fromJson(body, &parseError); const QJsonObject object = jsonDocument.object(); const QUrl existingShortUrl(object.value(QStringLiteral("shorturl")).toString()); const QUrl existingLongUrl(object.value(QStringLiteral("url")).toObject().value(QStringLiteral("url")).toString()); // YOURLS reports duplicate URLs as HTTP 400, but returns the existing link. if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 400 && object.value(QStringLiteral("status")).toString() == QStringLiteral("fail") && object.value(QStringLiteral("code")).toString() == QStringLiteral("error:url") && existingLongUrl == longUrl && existingShortUrl.isValid() && (existingShortUrl.scheme() == QStringLiteral("https") || existingShortUrl.scheme() == QStringLiteral("http")) && !existingShortUrl.host().isEmpty()) { #ifndef QT_NO_DEBUG qDebug() << "[yourls] reusing existing short URL:" << existingShortUrl; #endif reply->deleteLater(); emit shortenSucceeded(existingShortUrl); return; } if (reply->error() != QNetworkReply::NoError) { const QString details = QStringLiteral("Network error: %1\nHTTP status: %2\nResponse body:\n%3") .arg(reply->errorString(), reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toString(), QString::fromUtf8(body)); reply->deleteLater(); emit shortenFailed(details); return; } #ifndef QT_NO_DEBUG qDebug() << "[yourls] JSON parse error:" << parseError.errorString() << "offset:" << parseError.offset << "is object:" << jsonDocument.isObject(); #endif if (parseError.error != QJsonParseError::NoError || !jsonDocument.isObject()) { const QString details = QStringLiteral("Failed to parse YOURLS response as JSON: %1\nResponse body:\n%2") .arg(parseError.errorString(), QString::fromUtf8(body)); reply->deleteLater(); emit shortenFailed(details); return; } const QString status = object.value(QStringLiteral("status")).toString(); const QString shortUrlText = object.value(QStringLiteral("shorturl")).toString(); #ifndef QT_NO_DEBUG qDebug() << "[yourls] API status:" << status << "code:" << object.value(QStringLiteral("code")).toVariant() << "short URL:" << shortUrlText; #endif if (status.compare(QStringLiteral("success"), Qt::CaseInsensitive) != 0 || shortUrlText.isEmpty()) { QStringList details; details << QStringLiteral("YOURLS returned an error."); if (object.contains(QStringLiteral("message"))) details << QStringLiteral("Message: %1").arg(object.value(QStringLiteral("message")).toString()); if (object.contains(QStringLiteral("code"))) details << QStringLiteral("Code: %1").arg(object.value(QStringLiteral("code")).toVariant().toString()); details << QStringLiteral("Raw response:\n%1").arg(QString::fromUtf8(body)); reply->deleteLater(); emit shortenFailed(details.join(QStringLiteral("\n"))); return; } const QUrl shortUrl(shortUrlText); if (!shortUrl.isValid() || shortUrl.host().isEmpty() || (shortUrl.scheme() != QStringLiteral("https") && shortUrl.scheme() != QStringLiteral("http"))) { reply->deleteLater(); emit shortenFailed(QStringLiteral("YOURLS returned an invalid short URL: %1").arg(shortUrlText)); return; } reply->deleteLater(); emit shortenSucceeded(shortUrl); }); }