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 | |
21 | using namespace ReviewBoard; |
22 | |
23 | QByteArray 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 | } |
40 | namespace |
41 | { |
42 | static const QByteArray m_boundary = "----------" + KRandom::randomString(length: 42 + 13).toLatin1(); |
43 | |
44 | QByteArray 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 | |
84 | QByteArray 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 | |
95 | HttpCall::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 | |
117 | void 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 | |
148 | QVariant HttpCall::result() const |
149 | { |
150 | Q_ASSERT(m_reply->isFinished()); |
151 | return m_result; |
152 | } |
153 | |
154 | void 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 | |
180 | NewRequest::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 | |
188 | void NewRequest::start() |
189 | { |
190 | m_newreq->start(); |
191 | } |
192 | |
193 | void 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 | |
208 | SubmitPatchRequest::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 | |
227 | void SubmitPatchRequest::start() |
228 | { |
229 | m_uploadpatch->start(); |
230 | } |
231 | |
232 | void 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 | |
243 | ProjectsListRequest::ProjectsListRequest(const QUrl &server, QObject *parent) |
244 | : KJob(parent) |
245 | , m_server(server) |
246 | { |
247 | } |
248 | |
249 | void ProjectsListRequest::start() |
250 | { |
251 | requestRepositoryList(startIndex: 0); |
252 | } |
253 | |
254 | QVariantList ProjectsListRequest::repositories() const |
255 | { |
256 | return m_repositories; |
257 | } |
258 | |
259 | void 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 | |
273 | void 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 | |
289 | ReviewListRequest::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 | |
297 | void ReviewListRequest::start() |
298 | { |
299 | requestReviewList(startIndex: 0); |
300 | } |
301 | |
302 | QVariantList ReviewListRequest::reviews() const |
303 | { |
304 | return m_reviews; |
305 | } |
306 | |
307 | void 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 | |
323 | void 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 | |
347 | UpdateRequest::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 | |
360 | void UpdateRequest::start() |
361 | { |
362 | m_req->start(); |
363 | } |
364 | |
365 | void 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 | |