File indexing completed on 2024-05-19 04:03:04

0001 /*
0002     SPDX-FileCopyrightText: 2010 Aleix Pol Gonzalez <aleixpol@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "reviewboardjobs.h"
0008 #include "debug.h"
0009 
0010 #include <QFile>
0011 #include <QJsonDocument>
0012 #include <QMimeDatabase>
0013 #include <QMimeType>
0014 #include <QNetworkReply>
0015 #include <QNetworkRequest>
0016 #include <QUrlQuery>
0017 
0018 #include <KLocalizedString>
0019 #include <KRandom>
0020 
0021 using namespace ReviewBoard;
0022 
0023 QByteArray ReviewBoard::urlToData(const QUrl &url)
0024 {
0025     QByteArray ret;
0026     if (url.isLocalFile()) {
0027         QFile f(url.toLocalFile());
0028         Q_ASSERT(f.exists());
0029         bool corr = f.open(QFile::ReadOnly | QFile::Text);
0030         Q_ASSERT(corr);
0031         Q_UNUSED(corr);
0032 
0033         ret = f.readAll();
0034 
0035     } else {
0036         // TODO: add downloading the data
0037     }
0038     return ret;
0039 }
0040 namespace
0041 {
0042 static const QByteArray m_boundary = "----------" + KRandom::randomString(42 + 13).toLatin1();
0043 
0044 QByteArray multipartFormData(const QList<QPair<QString, QVariant>> &values)
0045 {
0046     QByteArray form_data;
0047     for (const auto &val : values) {
0048         QByteArray hstr("--");
0049         hstr += m_boundary;
0050         hstr += "\r\n";
0051         hstr += "Content-Disposition: form-data; name=\"";
0052         hstr += val.first.toLatin1();
0053         hstr += "\"";
0054 
0055         // File
0056         if (val.second.userType() == QMetaType::QUrl) {
0057             QUrl path = val.second.toUrl();
0058             hstr += "; filename=\"" + path.fileName().toLatin1() + "\"";
0059             const QMimeType mime = QMimeDatabase().mimeTypeForUrl(path);
0060             if (!mime.name().isEmpty()) {
0061                 hstr += "\r\nContent-Type: ";
0062                 hstr += mime.name().toLatin1();
0063             }
0064         }
0065         //
0066 
0067         hstr += "\r\n\r\n";
0068 
0069         // append body
0070         form_data.append(hstr);
0071         if (val.second.userType() == QMetaType::QUrl)
0072             form_data += urlToData(val.second.toUrl());
0073         else
0074             form_data += val.second.toByteArray();
0075         form_data.append("\r\n");
0076         // EOFILE
0077     }
0078 
0079     form_data += QByteArray("--" + m_boundary + "--\r\n");
0080 
0081     return form_data;
0082 }
0083 
0084 QByteArray multipartFormData(const QVariantMap &values)
0085 {
0086     QList<QPair<QString, QVariant>> vals;
0087     for (QVariantMap::const_iterator it = values.constBegin(), itEnd = values.constEnd(); it != itEnd; ++it) {
0088         vals += qMakePair<QString, QVariant>(QString(it.key()), QVariant(it.value()));
0089     }
0090     return multipartFormData(vals);
0091 }
0092 
0093 }
0094 
0095 HttpCall::HttpCall(const QUrl &s,
0096                    const QString &apiPath,
0097                    const QList<QPair<QString, QString>> &queryParameters,
0098                    Method method,
0099                    const QByteArray &post,
0100                    bool multipart,
0101                    QObject *parent)
0102     : KJob(parent)
0103     , m_reply(nullptr)
0104     , m_post(post)
0105     , m_multipart(multipart)
0106     , m_method(method)
0107 {
0108     m_requrl = s;
0109     m_requrl.setPath(m_requrl.path() + QLatin1Char('/') + apiPath);
0110     QUrlQuery query;
0111     for (QList<QPair<QString, QString>>::const_iterator i = queryParameters.begin(); i < queryParameters.end(); i++) {
0112         query.addQueryItem(i->first, i->second);
0113     }
0114     m_requrl.setQuery(query);
0115 }
0116 
0117 void HttpCall::start()
0118 {
0119     QNetworkRequest r(m_requrl);
0120 
0121     if (!m_requrl.userName().isEmpty()) {
0122         QByteArray head = "Basic " + m_requrl.userInfo().toLatin1().toBase64();
0123         r.setRawHeader("Authorization", head);
0124     }
0125 
0126     if (m_multipart) {
0127         r.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("multipart/form-data"));
0128         r.setHeader(QNetworkRequest::ContentLengthHeader, QString::number(m_post.size()));
0129         r.setRawHeader("Content-Type", "multipart/form-data; boundary=" + m_boundary);
0130     }
0131 
0132     switch (m_method) {
0133     case Get:
0134         m_reply = m_manager.get(r);
0135         break;
0136     case Post:
0137         m_reply = m_manager.post(r, m_post);
0138         break;
0139     case Put:
0140         m_reply = m_manager.put(r, m_post);
0141         break;
0142     }
0143     connect(m_reply, &QNetworkReply::finished, this, &HttpCall::onFinished);
0144 
0145     //     qCDebug(PLUGIN_REVIEWBOARD) << "starting... requrl=" << m_requrl << "post=" << m_post;
0146 }
0147 
0148 QVariant HttpCall::result() const
0149 {
0150     Q_ASSERT(m_reply->isFinished());
0151     return m_result;
0152 }
0153 
0154 void HttpCall::onFinished()
0155 {
0156     const QByteArray receivedData = m_reply->readAll();
0157     QJsonParseError error;
0158     QJsonDocument parser = QJsonDocument::fromJson(receivedData, &error);
0159     const QVariant output = parser.toVariant();
0160 
0161     if (error.error == 0) {
0162         m_result = output;
0163     } else {
0164         setError(1);
0165         setErrorText(i18n("JSON error: %1", error.errorString()));
0166     }
0167 
0168     if (output.toMap().value(QStringLiteral("stat")).toString() != QLatin1String("ok")) {
0169         setError(2);
0170         setErrorText(i18n("Request Error: %1", output.toMap().value(QStringLiteral("err")).toMap().value(QStringLiteral("msg")).toString()));
0171     }
0172 
0173     if (receivedData.size() > 10000)
0174         qCDebug(PLUGIN_REVIEWBOARD) << "parsing..." << receivedData.size();
0175     else
0176         qCDebug(PLUGIN_REVIEWBOARD) << "parsing..." << receivedData;
0177     emitResult();
0178 }
0179 
0180 NewRequest::NewRequest(const QUrl &server, const QString &projectPath, QObject *parent)
0181     : ReviewRequest(server, QString(), parent)
0182     , m_project(projectPath)
0183 {
0184     m_newreq = new HttpCall(this->server(), QStringLiteral("/api/review-requests/"), {}, HttpCall::Post, "repository=" + projectPath.toLatin1(), false, this);
0185     connect(m_newreq, &HttpCall::finished, this, &NewRequest::done);
0186 }
0187 
0188 void NewRequest::start()
0189 {
0190     m_newreq->start();
0191 }
0192 
0193 void NewRequest::done()
0194 {
0195     if (m_newreq->error()) {
0196         qCDebug(PLUGIN_REVIEWBOARD) << "Could not create the new request" << m_newreq->errorString();
0197         setError(2);
0198         setErrorText(i18n("Could not create the new request:\n%1", m_newreq->errorString()));
0199     } else {
0200         QVariant res = m_newreq->result();
0201         setRequestId(res.toMap()[QStringLiteral("review_request")].toMap()[QStringLiteral("id")].toString());
0202         Q_ASSERT(!requestId().isEmpty());
0203     }
0204 
0205     emitResult();
0206 }
0207 
0208 SubmitPatchRequest::SubmitPatchRequest(const QUrl &server, const QUrl &patch, const QString &basedir, const QString &id, QObject *parent)
0209     : ReviewRequest(server, id, parent)
0210     , m_patch(patch)
0211     , m_basedir(basedir)
0212 {
0213     QList<QPair<QString, QVariant>> vals;
0214     vals += QPair<QString, QVariant>(QStringLiteral("basedir"), m_basedir);
0215     vals += QPair<QString, QVariant>(QStringLiteral("path"), QVariant::fromValue<QUrl>(m_patch));
0216 
0217     m_uploadpatch = new HttpCall(this->server(),
0218                                  QStringLiteral("/api/review-requests/") + requestId() + QStringLiteral("/diffs/"),
0219                                  {},
0220                                  HttpCall::Post,
0221                                  multipartFormData(vals),
0222                                  true,
0223                                  this);
0224     connect(m_uploadpatch, &HttpCall::finished, this, &SubmitPatchRequest::done);
0225 }
0226 
0227 void SubmitPatchRequest::start()
0228 {
0229     m_uploadpatch->start();
0230 }
0231 
0232 void SubmitPatchRequest::done()
0233 {
0234     if (m_uploadpatch->error()) {
0235         qCWarning(PLUGIN_REVIEWBOARD) << "Could not upload the patch" << m_uploadpatch->errorString();
0236         setError(3);
0237         setErrorText(i18n("Could not upload the patch"));
0238     }
0239 
0240     emitResult();
0241 }
0242 
0243 ProjectsListRequest::ProjectsListRequest(const QUrl &server, QObject *parent)
0244     : KJob(parent)
0245     , m_server(server)
0246 {
0247 }
0248 
0249 void ProjectsListRequest::start()
0250 {
0251     requestRepositoryList(0);
0252 }
0253 
0254 QVariantList ProjectsListRequest::repositories() const
0255 {
0256     return m_repositories;
0257 }
0258 
0259 void ProjectsListRequest::requestRepositoryList(int startIndex)
0260 {
0261     QList<QPair<QString, QString>> repositoriesParameters;
0262 
0263     // In practice, the web API will return at most 200 repos per call, so just hardcode that value here
0264     repositoriesParameters << qMakePair(QStringLiteral("max-results"), QStringLiteral("200"));
0265     repositoriesParameters << qMakePair(QStringLiteral("start"), QString::number(startIndex));
0266 
0267     HttpCall *repositoriesCall = new HttpCall(m_server, QStringLiteral("/api/repositories/"), repositoriesParameters, HttpCall::Get, QByteArray(), false, this);
0268     connect(repositoriesCall, &HttpCall::finished, this, &ProjectsListRequest::done);
0269 
0270     repositoriesCall->start();
0271 }
0272 
0273 void ProjectsListRequest::done(KJob *job)
0274 {
0275     // TODO error
0276     // TODO max iterations
0277     HttpCall *repositoriesCall = qobject_cast<HttpCall *>(job);
0278     const QMap<QString, QVariant> resultMap = repositoriesCall->result().toMap();
0279     const int totalResults = resultMap[QStringLiteral("total_results")].toInt();
0280     m_repositories << resultMap[QStringLiteral("repositories")].toList();
0281 
0282     if (m_repositories.count() < totalResults) {
0283         requestRepositoryList(m_repositories.count());
0284     } else {
0285         emitResult();
0286     }
0287 }
0288 
0289 ReviewListRequest::ReviewListRequest(const QUrl &server, const QString &user, const QString &reviewStatus, QObject *parent)
0290     : KJob(parent)
0291     , m_server(server)
0292     , m_user(user)
0293     , m_reviewStatus(reviewStatus)
0294 {
0295 }
0296 
0297 void ReviewListRequest::start()
0298 {
0299     requestReviewList(0);
0300 }
0301 
0302 QVariantList ReviewListRequest::reviews() const
0303 {
0304     return m_reviews;
0305 }
0306 
0307 void ReviewListRequest::requestReviewList(int startIndex)
0308 {
0309     QList<QPair<QString, QString>> reviewParameters;
0310 
0311     // In practice, the web API will return at most 200 repos per call, so just hardcode that value here
0312     reviewParameters << qMakePair(QStringLiteral("max-results"), QStringLiteral("200"));
0313     reviewParameters << qMakePair(QStringLiteral("start"), QString::number(startIndex));
0314     reviewParameters << qMakePair(QStringLiteral("from-user"), m_user);
0315     reviewParameters << qMakePair(QStringLiteral("status"), m_reviewStatus);
0316 
0317     HttpCall *reviewsCall = new HttpCall(m_server, QStringLiteral("/api/review-requests/"), reviewParameters, HttpCall::Get, QByteArray(), false, this);
0318     connect(reviewsCall, &HttpCall::finished, this, &ReviewListRequest::done);
0319 
0320     reviewsCall->start();
0321 }
0322 
0323 void ReviewListRequest::done(KJob *job)
0324 {
0325     // TODO error
0326     // TODO max iterations
0327     if (job->error()) {
0328         qCDebug(PLUGIN_REVIEWBOARD) << "Could not get reviews list" << job->errorString();
0329         setError(3);
0330         setErrorText(i18n("Could not get reviews list"));
0331         emitResult();
0332     }
0333 
0334     HttpCall *reviewsCall = qobject_cast<HttpCall *>(job);
0335     QMap<QString, QVariant> resultMap = reviewsCall->result().toMap();
0336     const int totalResults = resultMap[QStringLiteral("total_results")].toInt();
0337 
0338     m_reviews << resultMap[QStringLiteral("review_requests")].toList();
0339 
0340     if (m_reviews.count() < totalResults) {
0341         requestReviewList(m_reviews.count());
0342     } else {
0343         emitResult();
0344     }
0345 }
0346 
0347 UpdateRequest::UpdateRequest(const QUrl &server, const QString &id, const QVariantMap &newValues, QObject *parent)
0348     : ReviewRequest(server, id, parent)
0349 {
0350     m_req = new HttpCall(this->server(),
0351                          QStringLiteral("/api/review-requests/") + id + QStringLiteral("/draft/"),
0352                          {},
0353                          HttpCall::Put,
0354                          multipartFormData(newValues),
0355                          true,
0356                          this);
0357     connect(m_req, &HttpCall::finished, this, &UpdateRequest::done);
0358 }
0359 
0360 void UpdateRequest::start()
0361 {
0362     m_req->start();
0363 }
0364 
0365 void UpdateRequest::done()
0366 {
0367     if (m_req->error()) {
0368         qCWarning(PLUGIN_REVIEWBOARD) << "Could not set all metadata to the review" << m_req->errorString() << m_req->property("result");
0369         setError(3);
0370         setErrorText(i18n("Could not set metadata"));
0371     }
0372 
0373     emitResult();
0374 }
0375 
0376 #include "moc_reviewboardjobs.cpp"