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(flags: 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(length: 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(url: 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(a: hstr);
71 if (val.second.userType() == QMetaType::QUrl)
72 form_data += urlToData(url: val.second.toUrl());
73 else
74 form_data += val.second.toByteArray();
75 form_data.append(s: "\r\n");
76 // EOFILE
77 }
78
79 form_data += QByteArray("--" + m_boundary + "--\r\n");
80
81 return form_data;
82}
83
84QByteArray multipartFormData(const QVariantMap &values)
85{
86 QList<QPair<QString, QVariant>> vals;
87 for (QVariantMap::const_iterator it = values.constBegin(), itEnd = values.constEnd(); it != itEnd; ++it) {
88 vals += qMakePair<QString, QVariant>(value1: QString(it.key()), value2: QVariant(it.value()));
89 }
90 return multipartFormData(values: vals);
91}
92
93}
94
95HttpCall::HttpCall(const QUrl &s,
96 const QString &apiPath,
97 const QList<QPair<QString, QString>> &queryParameters,
98 Method method,
99 const QByteArray &post,
100 bool multipart,
101 QObject *parent)
102 : KJob(parent)
103 , m_reply(nullptr)
104 , m_post(post)
105 , m_multipart(multipart)
106 , m_method(method)
107{
108 m_requrl = s;
109 m_requrl.setPath(path: m_requrl.path() + QLatin1Char('/') + apiPath);
110 QUrlQuery query;
111 for (QList<QPair<QString, QString>>::const_iterator i = queryParameters.begin(); i < queryParameters.end(); i++) {
112 query.addQueryItem(key: i->first, value: i->second);
113 }
114 m_requrl.setQuery(query);
115}
116
117void HttpCall::start()
118{
119 QNetworkRequest r(m_requrl);
120
121 if (!m_requrl.userName().isEmpty()) {
122 QByteArray head = "Basic " + m_requrl.userInfo().toLatin1().toBase64();
123 r.setRawHeader(headerName: "Authorization", value: head);
124 }
125
126 if (m_multipart) {
127 r.setHeader(header: QNetworkRequest::ContentTypeHeader, QStringLiteral("multipart/form-data"));
128 r.setHeader(header: QNetworkRequest::ContentLengthHeader, value: QString::number(m_post.size()));
129 r.setRawHeader(headerName: "Content-Type", value: "multipart/form-data; boundary=" + m_boundary);
130 }
131
132 switch (m_method) {
133 case Get:
134 m_reply = m_manager.get(request: r);
135 break;
136 case Post:
137 m_reply = m_manager.post(request: r, data: m_post);
138 break;
139 case Put:
140 m_reply = m_manager.put(request: r, data: m_post);
141 break;
142 }
143 connect(sender: m_reply, signal: &QNetworkReply::finished, context: this, slot: &HttpCall::onFinished);
144
145 // qCDebug(PLUGIN_REVIEWBOARD) << "starting... requrl=" << m_requrl << "post=" << m_post;
146}
147
148QVariant HttpCall::result() const
149{
150 Q_ASSERT(m_reply->isFinished());
151 return m_result;
152}
153
154void HttpCall::onFinished()
155{
156 const QByteArray receivedData = m_reply->readAll();
157 QJsonParseError error;
158 QJsonDocument parser = QJsonDocument::fromJson(json: receivedData, error: &error);
159 const QVariant output = parser.toVariant();
160
161 if (error.error == 0) {
162 m_result = output;
163 } else {
164 setError(1);
165 setErrorText(i18n("JSON error: %1", error.errorString()));
166 }
167
168 if (output.toMap().value(QStringLiteral("stat")).toString() != QLatin1String("ok")) {
169 setError(2);
170 setErrorText(i18n("Request Error: %1", output.toMap().value(QStringLiteral("err")).toMap().value(QStringLiteral("msg")).toString()));
171 }
172
173 if (receivedData.size() > 10000)
174 qCDebug(PLUGIN_REVIEWBOARD) << "parsing..." << receivedData.size();
175 else
176 qCDebug(PLUGIN_REVIEWBOARD) << "parsing..." << receivedData;
177 emitResult();
178}
179
180NewRequest::NewRequest(const QUrl &server, const QString &projectPath, QObject *parent)
181 : ReviewRequest(server, QString(), parent)
182 , m_project(projectPath)
183{
184 m_newreq = new HttpCall(this->server(), QStringLiteral("/api/review-requests/"), {}, HttpCall::Post, "repository=" + projectPath.toLatin1(), false, this);
185 connect(sender: m_newreq, signal: &HttpCall::finished, context: this, slot: &NewRequest::done);
186}
187
188void NewRequest::start()
189{
190 m_newreq->start();
191}
192
193void NewRequest::done()
194{
195 if (m_newreq->error()) {
196 qCDebug(PLUGIN_REVIEWBOARD) << "Could not create the new request" << m_newreq->errorString();
197 setError(2);
198 setErrorText(i18n("Could not create the new request:\n%1", m_newreq->errorString()));
199 } else {
200 QVariant res = m_newreq->result();
201 setRequestId(res.toMap()[QStringLiteral("review_request")].toMap()[QStringLiteral("id")].toString());
202 Q_ASSERT(!requestId().isEmpty());
203 }
204
205 emitResult();
206}
207
208SubmitPatchRequest::SubmitPatchRequest(const QUrl &server, const QUrl &patch, const QString &basedir, const QString &id, QObject *parent)
209 : ReviewRequest(server, id, parent)
210 , m_patch(patch)
211 , m_basedir(basedir)
212{
213 QList<QPair<QString, QVariant>> vals;
214 vals += QPair<QString, QVariant>(QStringLiteral("basedir"), m_basedir);
215 vals += QPair<QString, QVariant>(QStringLiteral("path"), QVariant::fromValue<QUrl>(value: m_patch));
216
217 m_uploadpatch = new HttpCall(this->server(),
218 QStringLiteral("/api/review-requests/") + requestId() + QStringLiteral("/diffs/"),
219 {},
220 HttpCall::Post,
221 multipartFormData(values: vals),
222 true,
223 this);
224 connect(sender: m_uploadpatch, signal: &HttpCall::finished, context: this, slot: &SubmitPatchRequest::done);
225}
226
227void SubmitPatchRequest::start()
228{
229 m_uploadpatch->start();
230}
231
232void SubmitPatchRequest::done()
233{
234 if (m_uploadpatch->error()) {
235 qCWarning(PLUGIN_REVIEWBOARD) << "Could not upload the patch" << m_uploadpatch->errorString();
236 setError(3);
237 setErrorText(i18n("Could not upload the patch"));
238 }
239
240 emitResult();
241}
242
243ProjectsListRequest::ProjectsListRequest(const QUrl &server, QObject *parent)
244 : KJob(parent)
245 , m_server(server)
246{
247}
248
249void ProjectsListRequest::start()
250{
251 requestRepositoryList(startIndex: 0);
252}
253
254QVariantList ProjectsListRequest::repositories() const
255{
256 return m_repositories;
257}
258
259void ProjectsListRequest::requestRepositoryList(int startIndex)
260{
261 QList<QPair<QString, QString>> repositoriesParameters;
262
263 // In practice, the web API will return at most 200 repos per call, so just hardcode that value here
264 repositoriesParameters << qMakePair(QStringLiteral("max-results"), QStringLiteral("200"));
265 repositoriesParameters << qMakePair(QStringLiteral("start"), value2: QString::number(startIndex));
266
267 HttpCall *repositoriesCall = new HttpCall(m_server, QStringLiteral("/api/repositories/"), repositoriesParameters, HttpCall::Get, QByteArray(), false, this);
268 connect(sender: repositoriesCall, signal: &HttpCall::finished, context: this, slot: &ProjectsListRequest::done);
269
270 repositoriesCall->start();
271}
272
273void ProjectsListRequest::done(KJob *job)
274{
275 // TODO error
276 // TODO max iterations
277 HttpCall *repositoriesCall = qobject_cast<HttpCall *>(object: job);
278 const QMap<QString, QVariant> resultMap = repositoriesCall->result().toMap();
279 const int totalResults = resultMap[QStringLiteral("total_results")].toInt();
280 m_repositories << resultMap[QStringLiteral("repositories")].toList();
281
282 if (m_repositories.count() < totalResults) {
283 requestRepositoryList(startIndex: m_repositories.count());
284 } else {
285 emitResult();
286 }
287}
288
289ReviewListRequest::ReviewListRequest(const QUrl &server, const QString &user, const QString &reviewStatus, QObject *parent)
290 : KJob(parent)
291 , m_server(server)
292 , m_user(user)
293 , m_reviewStatus(reviewStatus)
294{
295}
296
297void ReviewListRequest::start()
298{
299 requestReviewList(startIndex: 0);
300}
301
302QVariantList ReviewListRequest::reviews() const
303{
304 return m_reviews;
305}
306
307void ReviewListRequest::requestReviewList(int startIndex)
308{
309 QList<QPair<QString, QString>> reviewParameters;
310
311 // In practice, the web API will return at most 200 repos per call, so just hardcode that value here
312 reviewParameters << qMakePair(QStringLiteral("max-results"), QStringLiteral("200"));
313 reviewParameters << qMakePair(QStringLiteral("start"), value2: QString::number(startIndex));
314 reviewParameters << qMakePair(QStringLiteral("from-user"), value2&: m_user);
315 reviewParameters << qMakePair(QStringLiteral("status"), value2&: m_reviewStatus);
316
317 HttpCall *reviewsCall = new HttpCall(m_server, QStringLiteral("/api/review-requests/"), reviewParameters, HttpCall::Get, QByteArray(), false, this);
318 connect(sender: reviewsCall, signal: &HttpCall::finished, context: this, slot: &ReviewListRequest::done);
319
320 reviewsCall->start();
321}
322
323void ReviewListRequest::done(KJob *job)
324{
325 // TODO error
326 // TODO max iterations
327 if (job->error()) {
328 qCDebug(PLUGIN_REVIEWBOARD) << "Could not get reviews list" << job->errorString();
329 setError(3);
330 setErrorText(i18n("Could not get reviews list"));
331 emitResult();
332 }
333
334 HttpCall *reviewsCall = qobject_cast<HttpCall *>(object: job);
335 QMap<QString, QVariant> resultMap = reviewsCall->result().toMap();
336 const int totalResults = resultMap[QStringLiteral("total_results")].toInt();
337
338 m_reviews << resultMap[QStringLiteral("review_requests")].toList();
339
340 if (m_reviews.count() < totalResults) {
341 requestReviewList(startIndex: m_reviews.count());
342 } else {
343 emitResult();
344 }
345}
346
347UpdateRequest::UpdateRequest(const QUrl &server, const QString &id, const QVariantMap &newValues, QObject *parent)
348 : ReviewRequest(server, id, parent)
349{
350 m_req = new HttpCall(this->server(),
351 QStringLiteral("/api/review-requests/") + id + QStringLiteral("/draft/"),
352 {},
353 HttpCall::Put,
354 multipartFormData(values: newValues),
355 true,
356 this);
357 connect(sender: m_req, signal: &HttpCall::finished, context: this, slot: &UpdateRequest::done);
358}
359
360void UpdateRequest::start()
361{
362 m_req->start();
363}
364
365void UpdateRequest::done()
366{
367 if (m_req->error()) {
368 qCWarning(PLUGIN_REVIEWBOARD) << "Could not set all metadata to the review" << m_req->errorString() << m_req->property(name: "result");
369 setError(3);
370 setErrorText(i18n("Could not set metadata"));
371 }
372
373 emitResult();
374}
375
376#include "moc_reviewboardjobs.cpp"
377

source code of purpose/src/plugins/reviewboard/reviewboardjobs.cpp