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"