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 form_data.append("\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{
87 for (QVariantMap::const_iterator it = values.constBegin(), itEnd = values.constEnd(); it != itEnd; ++it) {
88 vals += qMakePair<QString, QVariant>(QString(it.key()), QVariant(it.value()));
89 }
90 return multipartFormData(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(m_requrl.path() + QLatin1Char('/') + apiPath);
111 for (QList<QPair<QString, QString>>::const_iterator i = queryParameters.begin(); i < queryParameters.end(); i++) {
112 query.addQueryItem(i->first, 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("Authorization", head);
124 }
125
126 if (m_multipart) {
127 r.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("multipart/form-data"));
129 r.setRawHeader("Content-Type", "multipart/form-data; boundary=" + m_boundary);
130 }
131
132 switch (m_method) {
133 case Get:
134 m_reply = m_manager.get(r);
135 break;
136 case Post:
137 m_reply = m_manager.post(r, m_post);
138 break;
139 case Put:
140 m_reply = m_manager.put(r, m_post);
141 break;
142 }
143 connect(m_reply, &QNetworkReply::finished, this, &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();
158 QJsonDocument parser = QJsonDocument::fromJson(receivedData, &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(m_newreq, &HttpCall::finished, this, &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{
214 vals += QPair<QString, QVariant>(QStringLiteral("basedir"), m_basedir);
215 vals += QPair<QString, QVariant>(QStringLiteral("path"), QVariant::fromValue<QUrl>(m_patch));
216
217 m_uploadpatch = new HttpCall(this->server(),
218 QStringLiteral("/api/review-requests/") + requestId() + QStringLiteral("/diffs/"),
219 {},
220 HttpCall::Post,
221 multipartFormData(vals),
222 true,
223 this);
224 connect(m_uploadpatch, &HttpCall::finished, this, &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(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"), QString::number(startIndex));
266
267 HttpCall *repositoriesCall = new HttpCall(m_server, QStringLiteral("/api/repositories/"), repositoriesParameters, HttpCall::Get, QByteArray(), false, this);
268 connect(repositoriesCall, &HttpCall::finished, this, &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 *>(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(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(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"), QString::number(startIndex));
314 reviewParameters << qMakePair(QStringLiteral("from-user"), m_user);
315 reviewParameters << qMakePair(QStringLiteral("status"), m_reviewStatus);
316
317 HttpCall *reviewsCall = new HttpCall(m_server, QStringLiteral("/api/review-requests/"), reviewParameters, HttpCall::Get, QByteArray(), false, this);
318 connect(reviewsCall, &HttpCall::finished, this, &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 *>(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(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(newValues),
355 true,
356 this);
357 connect(m_req, &HttpCall::finished, this, &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("result");
369 setError(3);
370 setErrorText(i18n("Could not set metadata"));
371 }
372
373 emitResult();
374}
375
376#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...)
char * toString(const EngineQuery &query)
const QVariantMap toMap(const MODEL &model)
KSERVICE_EXPORT KService::List query(FilterFunc filterFunc)
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
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
QMap< QString, QVariant > toMap() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:14:05 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.