Purpose

reviewboardjobs.cpp
1/*
2 SPDX-FileCopyrightText: 2010 Aleix Pol Gonzalez <aleixpol@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6
7#include "reviewboardjobs.h"
8#include "debug.h"
9
10#include <QFile>
11#include <QJsonDocument>
12#include <QMimeDatabase>
13#include <QMimeType>
14#include <QNetworkReply>
15#include <QNetworkRequest>
16#include <QUrlQuery>
17
18#include <KLocalizedString>
19#include <KRandom>
20
21using namespace ReviewBoard;
22
23QByteArray ReviewBoard::urlToData(const QUrl &url)
24{
25 QByteArray ret;
26 if (url.isLocalFile()) {
27 QFile f(url.toLocalFile());
28 Q_ASSERT(f.exists());
29 bool corr = f.open(QFile::ReadOnly | QFile::Text);
30 Q_ASSERT(corr);
31 Q_UNUSED(corr);
32
33 ret = f.readAll();
34
35 } else {
36 // TODO: add downloading the data
37 }
38 return ret;
39}
40namespace
41{
42static const QByteArray m_boundary = "----------" + KRandom::randomString(42 + 13).toLatin1();
43
44QByteArray multipartFormData(const QList<QPair<QString, QVariant>> &values)
45{
46 QByteArray form_data;
47 for (const auto &val : values) {
48 QByteArray hstr("--");
49 hstr += m_boundary;
50 hstr += "\r\n";
51 hstr += "Content-Disposition: form-data; name=\"";
52 hstr += val.first.toLatin1();
53 hstr += "\"";
54
55 // File
56 if (val.second.userType() == QMetaType::QUrl) {
57 QUrl path = val.second.toUrl();
58 hstr += "; filename=\"" + path.fileName().toLatin1() + "\"";
59 const QMimeType mime = QMimeDatabase().mimeTypeForUrl(path);
60 if (!mime.name().isEmpty()) {
61 hstr += "\r\nContent-Type: ";
62 hstr += mime.name().toLatin1();
63 }
64 }
65 //
66
67 hstr += "\r\n\r\n";
68
69 // append body
70 form_data.append(hstr);
71 if (val.second.userType() == QMetaType::QUrl) {
72 form_data += urlToData(val.second.toUrl());
73 } else {
74 form_data += val.second.toByteArray();
75 }
76 form_data.append("\r\n");
77 // EOFILE
78 }
79
80 form_data += QByteArray("--" + m_boundary + "--\r\n");
81
82 return form_data;
83}
84
85QByteArray multipartFormData(const QVariantMap &values)
86{
88 for (QVariantMap::const_iterator it = values.constBegin(), itEnd = values.constEnd(); it != itEnd; ++it) {
89 vals += qMakePair<QString, QVariant>(QString(it.key()), QVariant(it.value()));
90 }
91 return multipartFormData(vals);
92}
93
94}
95
96HttpCall::HttpCall(const QUrl &s,
97 const QString &apiPath,
98 const QList<QPair<QString, QString>> &queryParameters,
99 Method method,
100 const QByteArray &post,
101 bool multipart,
102 QObject *parent)
103 : KJob(parent)
104 , m_reply(nullptr)
105 , m_post(post)
106 , m_multipart(multipart)
107 , m_method(method)
108{
109 m_requrl = s;
110 m_requrl.setPath(m_requrl.path() + QLatin1Char('/') + apiPath);
112 for (QList<QPair<QString, QString>>::const_iterator i = queryParameters.begin(); i < queryParameters.end(); i++) {
113 query.addQueryItem(i->first, i->second);
114 }
115 m_requrl.setQuery(query);
116}
117
118void HttpCall::start()
119{
120 QNetworkRequest r(m_requrl);
121
122 if (!m_requrl.userName().isEmpty()) {
123 QByteArray head = "Basic " + m_requrl.userInfo().toLatin1().toBase64();
124 r.setRawHeader("Authorization", head);
125 }
126
127 if (m_multipart) {
128 r.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("multipart/form-data"));
130 r.setRawHeader("Content-Type", "multipart/form-data; boundary=" + m_boundary);
131 }
132
133 switch (m_method) {
134 case Get:
135 m_reply = m_manager.get(r);
136 break;
137 case Post:
138 m_reply = m_manager.post(r, m_post);
139 break;
140 case Put:
141 m_reply = m_manager.put(r, m_post);
142 break;
143 }
144 connect(m_reply, &QNetworkReply::finished, this, &HttpCall::onFinished);
145
146 // qCDebug(PLUGIN_REVIEWBOARD) << "starting... requrl=" << m_requrl << "post=" << m_post;
147}
148
149QVariant HttpCall::result() const
150{
151 Q_ASSERT(m_reply->isFinished());
152 return m_result;
153}
154
155void HttpCall::onFinished()
156{
157 const QByteArray receivedData = m_reply->readAll();
159 QJsonDocument parser = QJsonDocument::fromJson(receivedData, &error);
160 const QVariant output = parser.toVariant();
161
162 if (error.error == 0) {
163 m_result = output;
164 } else {
165 setError(1);
166 setErrorText(i18n("JSON error: %1", error.errorString()));
167 }
168
169 if (output.toMap().value(QStringLiteral("stat")).toString() != QLatin1String("ok")) {
170 setError(2);
171 setErrorText(i18n("Request Error: %1", output.toMap().value(QStringLiteral("err")).toMap().value(QStringLiteral("msg")).toString()));
172 }
173
174 if (receivedData.size() > 10000) {
175 qCDebug(PLUGIN_REVIEWBOARD) << "parsing..." << receivedData.size();
176 } else {
177 qCDebug(PLUGIN_REVIEWBOARD) << "parsing..." << receivedData;
178 }
179 emitResult();
180}
181
182NewRequest::NewRequest(const QUrl &server, const QString &projectPath, QObject *parent)
183 : ReviewRequest(server, QString(), parent)
184 , m_project(projectPath)
185{
186 m_newreq = new HttpCall(this->server(), QStringLiteral("/api/review-requests/"), {}, HttpCall::Post, "repository=" + projectPath.toLatin1(), false, this);
187 connect(m_newreq, &HttpCall::finished, this, &NewRequest::done);
188}
189
190void NewRequest::start()
191{
192 m_newreq->start();
193}
194
195void NewRequest::done()
196{
197 if (m_newreq->error()) {
198 qCDebug(PLUGIN_REVIEWBOARD) << "Could not create the new request" << m_newreq->errorString();
199 setError(2);
200 setErrorText(i18n("Could not create the new request:\n%1", m_newreq->errorString()));
201 } else {
202 QVariant res = m_newreq->result();
203 setRequestId(res.toMap()[QStringLiteral("review_request")].toMap()[QStringLiteral("id")].toString());
204 Q_ASSERT(!requestId().isEmpty());
205 }
206
207 emitResult();
208}
209
210SubmitPatchRequest::SubmitPatchRequest(const QUrl &server, const QUrl &patch, const QString &basedir, const QString &id, QObject *parent)
211 : ReviewRequest(server, id, parent)
212 , m_patch(patch)
213 , m_basedir(basedir)
214{
216 vals += QPair<QString, QVariant>(QStringLiteral("basedir"), m_basedir);
217 vals += QPair<QString, QVariant>(QStringLiteral("path"), QVariant::fromValue<QUrl>(m_patch));
218
219 m_uploadpatch = new HttpCall(this->server(),
220 QStringLiteral("/api/review-requests/") + requestId() + QStringLiteral("/diffs/"),
221 {},
222 HttpCall::Post,
223 multipartFormData(vals),
224 true,
225 this);
226 connect(m_uploadpatch, &HttpCall::finished, this, &SubmitPatchRequest::done);
227}
228
229void SubmitPatchRequest::start()
230{
231 m_uploadpatch->start();
232}
233
234void SubmitPatchRequest::done()
235{
236 if (m_uploadpatch->error()) {
237 qCWarning(PLUGIN_REVIEWBOARD) << "Could not upload the patch" << m_uploadpatch->errorString();
238 setError(3);
239 setErrorText(i18n("Could not upload the patch"));
240 }
241
242 emitResult();
243}
244
245ProjectsListRequest::ProjectsListRequest(const QUrl &server, QObject *parent)
246 : KJob(parent)
247 , m_server(server)
248{
249}
250
251void ProjectsListRequest::start()
252{
253 requestRepositoryList(0);
254}
255
256QVariantList ProjectsListRequest::repositories() const
257{
258 return m_repositories;
259}
260
261void ProjectsListRequest::requestRepositoryList(int startIndex)
262{
263 QList<QPair<QString, QString>> repositoriesParameters;
264
265 // In practice, the web API will return at most 200 repos per call, so just hardcode that value here
266 repositoriesParameters << qMakePair(QStringLiteral("max-results"), QStringLiteral("200"));
267 repositoriesParameters << qMakePair(QStringLiteral("start"), QString::number(startIndex));
268
269 HttpCall *repositoriesCall = new HttpCall(m_server, QStringLiteral("/api/repositories/"), repositoriesParameters, HttpCall::Get, QByteArray(), false, this);
270 connect(repositoriesCall, &HttpCall::finished, this, &ProjectsListRequest::done);
271
272 repositoriesCall->start();
273}
274
275void ProjectsListRequest::done(KJob *job)
276{
277 // TODO error
278 // TODO max iterations
279 HttpCall *repositoriesCall = qobject_cast<HttpCall *>(job);
280 const QMap<QString, QVariant> resultMap = repositoriesCall->result().toMap();
281 const int totalResults = resultMap[QStringLiteral("total_results")].toInt();
282 m_repositories << resultMap[QStringLiteral("repositories")].toList();
283
284 if (m_repositories.count() < totalResults) {
285 requestRepositoryList(m_repositories.count());
286 } else {
287 emitResult();
288 }
289}
290
291ReviewListRequest::ReviewListRequest(const QUrl &server, const QString &user, const QString &reviewStatus, QObject *parent)
292 : KJob(parent)
293 , m_server(server)
294 , m_user(user)
295 , m_reviewStatus(reviewStatus)
296{
297}
298
299void ReviewListRequest::start()
300{
301 requestReviewList(0);
302}
303
304QVariantList ReviewListRequest::reviews() const
305{
306 return m_reviews;
307}
308
309void ReviewListRequest::requestReviewList(int startIndex)
310{
311 QList<QPair<QString, QString>> reviewParameters;
312
313 // In practice, the web API will return at most 200 repos per call, so just hardcode that value here
314 reviewParameters << qMakePair(QStringLiteral("max-results"), QStringLiteral("200"));
315 reviewParameters << qMakePair(QStringLiteral("start"), QString::number(startIndex));
316 reviewParameters << qMakePair(QStringLiteral("from-user"), m_user);
317 reviewParameters << qMakePair(QStringLiteral("status"), m_reviewStatus);
318
319 HttpCall *reviewsCall = new HttpCall(m_server, QStringLiteral("/api/review-requests/"), reviewParameters, HttpCall::Get, QByteArray(), false, this);
320 connect(reviewsCall, &HttpCall::finished, this, &ReviewListRequest::done);
321
322 reviewsCall->start();
323}
324
325void ReviewListRequest::done(KJob *job)
326{
327 // TODO error
328 // TODO max iterations
329 if (job->error()) {
330 qCDebug(PLUGIN_REVIEWBOARD) << "Could not get reviews list" << job->errorString();
331 setError(3);
332 setErrorText(i18n("Could not get reviews list"));
333 emitResult();
334 }
335
336 HttpCall *reviewsCall = qobject_cast<HttpCall *>(job);
337 QMap<QString, QVariant> resultMap = reviewsCall->result().toMap();
338 const int totalResults = resultMap[QStringLiteral("total_results")].toInt();
339
340 m_reviews << resultMap[QStringLiteral("review_requests")].toList();
341
342 if (m_reviews.count() < totalResults) {
343 requestReviewList(m_reviews.count());
344 } else {
345 emitResult();
346 }
347}
348
349UpdateRequest::UpdateRequest(const QUrl &server, const QString &id, const QVariantMap &newValues, QObject *parent)
350 : ReviewRequest(server, id, parent)
351{
352 m_req = new HttpCall(this->server(),
353 QStringLiteral("/api/review-requests/") + id + QStringLiteral("/draft/"),
354 {},
355 HttpCall::Put,
356 multipartFormData(newValues),
357 true,
358 this);
359 connect(m_req, &HttpCall::finished, this, &UpdateRequest::done);
360}
361
362void UpdateRequest::start()
363{
364 m_req->start();
365}
366
367void UpdateRequest::done()
368{
369 if (m_req->error()) {
370 qCWarning(PLUGIN_REVIEWBOARD) << "Could not set all metadata to the review" << m_req->errorString() << m_req->property("result");
371 setError(3);
372 setErrorText(i18n("Could not set metadata"));
373 }
374
375 emitResult();
376}
377
378#include "moc_reviewboardjobs.cpp"
void setErrorText(const QString &errorText)
virtual QString errorString() const
void emitResult()
int error() const
void finished(KJob *job)
void setError(int errorCode)
Http call to the specified service.
QString i18n(const char *text, const TYPE &arg...)
std::optional< QSqlQuery > query(const QString &queryStatement)
char * toString(const EngineQuery &query)
const QVariantMap toMap(const MODEL &model)
QString path(const QString &relativePath)
KCOREADDONS_EXPORT QString randomString(int length)
QByteArray & append(QByteArrayView data)
QByteArray first(qsizetype n) const const
qsizetype size() const const
QByteArray toBase64(Base64Options options) const const
QByteArray readAll()
QJsonDocument fromJson(const QByteArray &json, QJsonParseError *error)
QVariant toVariant() const const
iterator begin()
QMimeType mimeTypeForUrl(const QUrl &url) const const
QNetworkReply * get(const QNetworkRequest &request)
QNetworkReply * post(const QNetworkRequest &request, QHttpMultiPart *multiPart)
QNetworkReply * put(const QNetworkRequest &request, QHttpMultiPart *multiPart)
bool isFinished() const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
QVariant property(const char *name) const const
T qobject_cast(QObject *object)
bool isEmpty() const const
QString number(double n, char format, int precision)
QByteArray toLatin1() const const
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
bool isLocalFile() const const
void setPath(const QString &path, ParsingMode mode)
QString toLocalFile() const const
QString userInfo(ComponentFormattingOptions options) const const
QString userName(ComponentFormattingOptions options) const const
QVariant fromValue(T &&value)
QMap< QString, QVariant > toMap() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Sat Dec 21 2024 17:03:06 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.