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 | } |
76 | form_data.append(s: "\r\n" ); |
77 | // EOFILE |
78 | } |
79 | |
80 | form_data += QByteArray("--" + m_boundary + "--\r\n" ); |
81 | |
82 | return form_data; |
83 | } |
84 | |
85 | QByteArray multipartFormData(const QVariantMap &values) |
86 | { |
87 | QList<QPair<QString, QVariant>> vals; |
88 | for (QVariantMap::const_iterator it = values.constBegin(), itEnd = values.constEnd(); it != itEnd; ++it) { |
89 | vals += qMakePair<QString, QVariant>(value1: QString(it.key()), value2: QVariant(it.value())); |
90 | } |
91 | return multipartFormData(values: vals); |
92 | } |
93 | |
94 | } |
95 | |
96 | HttpCall::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(path: m_requrl.path() + QLatin1Char('/') + apiPath); |
111 | QUrlQuery query; |
112 | for (QList<QPair<QString, QString>>::const_iterator i = queryParameters.begin(); i < queryParameters.end(); i++) { |
113 | query.addQueryItem(key: i->first, value: i->second); |
114 | } |
115 | m_requrl.setQuery(query); |
116 | } |
117 | |
118 | void 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(headerName: "Authorization" , value: head); |
125 | } |
126 | |
127 | if (m_multipart) { |
128 | r.setHeader(header: QNetworkRequest::ContentTypeHeader, QStringLiteral("multipart/form-data" )); |
129 | r.setHeader(header: QNetworkRequest::ContentLengthHeader, value: QString::number(m_post.size())); |
130 | r.setRawHeader(headerName: "Content-Type" , value: "multipart/form-data; boundary=" + m_boundary); |
131 | } |
132 | |
133 | switch (m_method) { |
134 | case Get: |
135 | m_reply = m_manager.get(request: r); |
136 | break; |
137 | case Post: |
138 | m_reply = m_manager.post(request: r, data: m_post); |
139 | break; |
140 | case Put: |
141 | m_reply = m_manager.put(request: r, data: m_post); |
142 | break; |
143 | } |
144 | connect(sender: m_reply, signal: &QNetworkReply::finished, context: this, slot: &HttpCall::onFinished); |
145 | |
146 | // qCDebug(PLUGIN_REVIEWBOARD) << "starting... requrl=" << m_requrl << "post=" << m_post; |
147 | } |
148 | |
149 | QVariant HttpCall::result() const |
150 | { |
151 | Q_ASSERT(m_reply->isFinished()); |
152 | return m_result; |
153 | } |
154 | |
155 | void HttpCall::onFinished() |
156 | { |
157 | const QByteArray receivedData = m_reply->readAll(); |
158 | QJsonParseError error; |
159 | QJsonDocument parser = QJsonDocument::fromJson(json: receivedData, error: &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 | |
182 | NewRequest::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(sender: m_newreq, signal: &HttpCall::finished, context: this, slot: &NewRequest::done); |
188 | } |
189 | |
190 | void NewRequest::start() |
191 | { |
192 | m_newreq->start(); |
193 | } |
194 | |
195 | void 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 | |
210 | SubmitPatchRequest::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 | { |
215 | QList<QPair<QString, QVariant>> vals; |
216 | vals += QPair<QString, QVariant>(QStringLiteral("basedir" ), m_basedir); |
217 | vals += QPair<QString, QVariant>(QStringLiteral("path" ), QVariant::fromValue<QUrl>(value: m_patch)); |
218 | |
219 | m_uploadpatch = new HttpCall(this->server(), |
220 | QStringLiteral("/api/review-requests/" ) + requestId() + QStringLiteral("/diffs/" ), |
221 | {}, |
222 | HttpCall::Post, |
223 | multipartFormData(values: vals), |
224 | true, |
225 | this); |
226 | connect(sender: m_uploadpatch, signal: &HttpCall::finished, context: this, slot: &SubmitPatchRequest::done); |
227 | } |
228 | |
229 | void SubmitPatchRequest::start() |
230 | { |
231 | m_uploadpatch->start(); |
232 | } |
233 | |
234 | void 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 | |
245 | ProjectsListRequest::ProjectsListRequest(const QUrl &server, QObject *parent) |
246 | : KJob(parent) |
247 | , m_server(server) |
248 | { |
249 | } |
250 | |
251 | void ProjectsListRequest::start() |
252 | { |
253 | requestRepositoryList(startIndex: 0); |
254 | } |
255 | |
256 | QVariantList ProjectsListRequest::repositories() const |
257 | { |
258 | return m_repositories; |
259 | } |
260 | |
261 | void 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" ), value2: QString::number(startIndex)); |
268 | |
269 | HttpCall *repositoriesCall = new HttpCall(m_server, QStringLiteral("/api/repositories/" ), repositoriesParameters, HttpCall::Get, QByteArray(), false, this); |
270 | connect(sender: repositoriesCall, signal: &HttpCall::finished, context: this, slot: &ProjectsListRequest::done); |
271 | |
272 | repositoriesCall->start(); |
273 | } |
274 | |
275 | void ProjectsListRequest::done(KJob *job) |
276 | { |
277 | // TODO error |
278 | // TODO max iterations |
279 | HttpCall *repositoriesCall = qobject_cast<HttpCall *>(object: 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(startIndex: m_repositories.count()); |
286 | } else { |
287 | emitResult(); |
288 | } |
289 | } |
290 | |
291 | ReviewListRequest::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 | |
299 | void ReviewListRequest::start() |
300 | { |
301 | requestReviewList(startIndex: 0); |
302 | } |
303 | |
304 | QVariantList ReviewListRequest::reviews() const |
305 | { |
306 | return m_reviews; |
307 | } |
308 | |
309 | void 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" ), value2: QString::number(startIndex)); |
316 | reviewParameters << qMakePair(QStringLiteral("from-user" ), value2&: m_user); |
317 | reviewParameters << qMakePair(QStringLiteral("status" ), value2&: m_reviewStatus); |
318 | |
319 | HttpCall *reviewsCall = new HttpCall(m_server, QStringLiteral("/api/review-requests/" ), reviewParameters, HttpCall::Get, QByteArray(), false, this); |
320 | connect(sender: reviewsCall, signal: &HttpCall::finished, context: this, slot: &ReviewListRequest::done); |
321 | |
322 | reviewsCall->start(); |
323 | } |
324 | |
325 | void 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 *>(object: 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(startIndex: m_reviews.count()); |
344 | } else { |
345 | emitResult(); |
346 | } |
347 | } |
348 | |
349 | UpdateRequest::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(values: newValues), |
357 | true, |
358 | this); |
359 | connect(sender: m_req, signal: &HttpCall::finished, context: this, slot: &UpdateRequest::done); |
360 | } |
361 | |
362 | void UpdateRequest::start() |
363 | { |
364 | m_req->start(); |
365 | } |
366 | |
367 | void 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(name: "result" ); |
371 | setError(3); |
372 | setErrorText(i18n("Could not set metadata" )); |
373 | } |
374 | |
375 | emitResult(); |
376 | } |
377 | |
378 | #include "moc_reviewboardjobs.cpp" |
379 | |