File indexing completed on 2024-05-05 04:39:55

0001 /*
0002     SPDX-FileCopyrightText: 2012-2013 Miquel Sabaté <mikisabate@gmail.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include <ghresource.h>
0008 
0009 #include <debug.h>
0010 #include <ghprovidermodel.h>
0011 
0012 #include <KIO/TransferJob>
0013 #include <KIO/StoredTransferJob>
0014 
0015 #include <QUrl>
0016 #include <QJsonDocument>
0017 #include <QHostInfo>
0018 #include <QDateTime>
0019 
0020 namespace gh
0021 {
0022 /// Base url for the Github API v3.
0023 const static QUrl baseUrl(QStringLiteral("https://api.github.com"));
0024 
0025 KIO::StoredTransferJob* createHttpAuthJob(const QString &httpHeader)
0026 {
0027     QUrl url = baseUrl;
0028     url = url.adjusted(QUrl::StripTrailingSlash);
0029     url.setPath(url.path() + QLatin1String("/authorizations"));
0030 
0031     // generate a unique token, see bug 372144
0032     const QString tokenName = QLatin1String("KDevelop Github Provider : ")
0033         + QHostInfo::localHostName() + QLatin1String(" - ")
0034         + QDateTime::currentDateTimeUtc().toString();
0035     const QByteArray data = QByteArrayLiteral("{ \"scopes\": [\"repo\"], \"note\": \"") + tokenName.toUtf8() + QByteArrayLiteral("\" }");
0036 
0037     KIO::StoredTransferJob *job = KIO::storedHttpPost(data, url, KIO::HideProgressInfo);
0038     job->setProperty("requestedTokenName", tokenName);
0039     job->addMetaData(QStringLiteral("customHTTPHeader"), httpHeader);
0040 
0041     return job;
0042 }
0043 
0044 Resource::Resource(QObject *parent, ProviderModel *model)
0045     : QObject(parent), m_model(model)
0046 {
0047     /* There's nothing to do here */
0048 }
0049 
0050 void Resource::searchRepos(const QString &uri, const QString &token)
0051 {
0052     KIO::TransferJob *job = getTransferJob(uri, token);
0053     connect(job, &KIO::TransferJob::data,
0054             this, &Resource::slotRepos);
0055 }
0056 
0057 void Resource::getOrgs(const QString &token)
0058 {
0059     KIO::TransferJob *job = getTransferJob(QStringLiteral("/user/orgs"), token);
0060     connect(job, &KIO::TransferJob::data,
0061             this, &Resource::slotOrgs);
0062 }
0063 
0064 void Resource::authenticate(const QString &name, const QString &password)
0065 {
0066     auto job = createHttpAuthJob(QLatin1String("Authorization: Basic ") + QString::fromUtf8(QByteArray(name.toUtf8() + ':' + password.toUtf8()).toBase64()));
0067     job->addMetaData(QStringLiteral("PropagateHttpHeader"), QStringLiteral("true"));
0068     connect(job, &KIO::StoredTransferJob::result,
0069             this, &Resource::slotAuthenticate);
0070     job->start();
0071 }
0072 
0073 void Resource::twoFactorAuthenticate(const QString &transferHeader, const QString &code)
0074 {
0075     auto job = createHttpAuthJob(transferHeader + QLatin1String("\nX-GitHub-OTP: ") + code);
0076     connect(job, &KIO::StoredTransferJob::result,
0077             this, &Resource::slotAuthenticate);
0078     job->start();
0079 }
0080 
0081 void Resource::revokeAccess(const QString &id, const QString &name, const QString &password)
0082 {
0083     QUrl url = baseUrl;
0084     url.setPath(url.path() + QLatin1String("/authorizations/") + id);
0085     KIO::TransferJob *job = KIO::http_delete(url, KIO::HideProgressInfo);
0086     const auto passwordBase64 = QString::fromLatin1(QString(name + QLatin1Char(':') + password).toUtf8().toBase64());
0087     job->addMetaData(QStringLiteral("customHTTPHeader"), QLatin1String("Authorization: Basic ") + passwordBase64);
0088     /* And we don't care if it's successful ;) */
0089     job->start();
0090 }
0091 
0092 KIO::TransferJob * Resource::getTransferJob(const QString &path, const QString &token) const
0093 {
0094     QUrl url = baseUrl;
0095     url = url.adjusted(QUrl::StripTrailingSlash);
0096     url.setPath(url.path() + path);
0097     KIO::TransferJob *job = KIO::get(url, KIO::Reload, KIO::HideProgressInfo);
0098     if (!token.isEmpty())
0099         job->addMetaData(QStringLiteral("customHTTPHeader"), QLatin1String("Authorization: token ") + token);
0100     return job;
0101 }
0102 
0103 void Resource::retrieveRepos(const QByteArray &data)
0104 {
0105     QJsonParseError error;
0106     QJsonDocument doc = QJsonDocument::fromJson(data, &error);
0107 
0108     if (error.error == 0) {
0109         const QVariantList list = doc.toVariant().toList();
0110         m_model->clear();
0111         for (const QVariant& it : list) {
0112             const QVariantMap &map = it.toMap();
0113             Response res;
0114             res.name = map.value(QStringLiteral("name")).toString();
0115             res.url = map.value(QStringLiteral("clone_url")).toUrl();
0116             if (map.value(QStringLiteral("fork")).toBool())
0117                 res.kind = Fork;
0118             else if (map.value(QStringLiteral("private")).toBool())
0119                 res.kind = Private;
0120             else
0121                 res.kind = Public;
0122             auto *item = new ProviderItem(res);
0123             m_model->appendRow(item);
0124         }
0125     }
0126     emit reposUpdated();
0127 }
0128 
0129 void Resource::retrieveOrgs(const QByteArray &data)
0130 {
0131     QStringList res;
0132     QJsonParseError error;
0133     QJsonDocument doc = QJsonDocument::fromJson(data, &error);
0134 
0135     if (error.error == 0) {
0136         const QVariantList json = doc.toVariant().toList();
0137         res.reserve(json.size());
0138         for (const QVariant& it : json) {
0139             QVariantMap map = it.toMap();
0140             res << map.value(QStringLiteral("login")).toString();
0141         }
0142     }
0143     emit orgsUpdated(res);
0144 }
0145 
0146 void Resource::slotAuthenticate(KJob *job)
0147 {
0148     const QString tokenName = job->property("requestedTokenName").toString();
0149     Q_ASSERT(!tokenName.isEmpty());
0150 
0151     if (job->error()) {
0152         emit authenticated("", "", tokenName);
0153         return;
0154     }
0155 
0156     const auto metaData = qobject_cast<KIO::StoredTransferJob*>(job)->metaData();
0157     if (metaData[QStringLiteral("responsecode")] == QLatin1String("401")) {
0158         const auto& header = metaData[QStringLiteral("HTTP-Headers")];
0159         if (header.contains(QLatin1String("X-GitHub-OTP: required;"), Qt::CaseInsensitive)) {
0160           emit twoFactorAuthRequested(qobject_cast<KIO::StoredTransferJob*>(job)->outgoingMetaData()[QStringLiteral("customHTTPHeader")]);
0161           return;
0162         }
0163     }
0164 
0165     QJsonParseError error;
0166     QJsonDocument doc = QJsonDocument::fromJson(qobject_cast<KIO::StoredTransferJob *>(job)->data(), &error);
0167 
0168     qCDebug(GHPROVIDER) << "Response:" << doc;
0169 
0170     if (error.error == 0) {
0171         QVariantMap map = doc.toVariant().toMap();
0172         emit authenticated(map.value(QStringLiteral("id")).toByteArray(),
0173                            map.value(QStringLiteral("token")).toByteArray(), tokenName);
0174     } else
0175         emit authenticated("", "", tokenName);
0176 }
0177 
0178 void Resource::slotRepos(KIO::Job *job, const QByteArray &data)
0179 {
0180     if (!job) {
0181         qCWarning(GHPROVIDER) << "NULL job returned!";
0182         return;
0183     }
0184     if (job->error()) {
0185         qCWarning(GHPROVIDER) << "Job error: " << job->errorString();
0186         return;
0187     }
0188 
0189     m_temp.append(data);
0190     if (data.isEmpty()) {
0191         retrieveRepos(m_temp);
0192         m_temp = "";
0193     }
0194 }
0195 
0196 void Resource::slotOrgs(KIO::Job *job, const QByteArray &data)
0197 {
0198     QList<QString> res;
0199 
0200     if (!job) {
0201         qCWarning(GHPROVIDER) << "NULL job returned!";
0202         emit orgsUpdated(res);
0203         return;
0204     }
0205     if (job->error()) {
0206         qCWarning(GHPROVIDER) << "Job error: " << job->errorString();
0207         emit orgsUpdated(res);
0208         return;
0209     }
0210 
0211     m_orgTemp.append(data);
0212     if (data.isEmpty()) {
0213         retrieveOrgs(m_orgTemp);
0214         m_orgTemp = "";
0215     }
0216 }
0217 
0218 } // End of namespace gh
0219 
0220 #include "moc_ghresource.cpp"