Changeset View
Changeset View
Standalone View
Standalone View
src/bugzillaintegration/libbugzilla/connection.cpp
- This file was added.
1 | /* | ||||
---|---|---|---|---|---|
2 | Copyright 2019 Harald Sitter <sitter@kde.org> | ||||
3 | | ||||
4 | This library is free software; you can redistribute it and/or | ||||
5 | modify it under the terms of the GNU Lesser General Public | ||||
6 | License as published by the Free Software Foundation; either | ||||
7 | version 2.1 of the License, or (at your option) version 3, or any | ||||
8 | later version accepted by the membership of KDE e.V. (or its | ||||
9 | successor approved by the membership of KDE e.V.), which shall | ||||
10 | act as a proxy defined in Section 6 of version 3 of the license. | ||||
11 | | ||||
12 | This library is distributed in the hope that it will be useful, | ||||
13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | ||||
15 | Lesser General Public License for more details. | ||||
16 | | ||||
17 | You should have received a copy of the GNU Lesser General Public | ||||
18 | License along with this library. If not, see <http://www.gnu.org/licenses/>. | ||||
19 | */ | ||||
20 | | ||||
21 | #include "connection.h" | ||||
22 | | ||||
23 | #include <QDebug> | ||||
24 | #include <QMetaMethod> | ||||
25 | #include <QUrlQuery> | ||||
26 | | ||||
27 | #include <KIOCore/KIO/TransferJob> | ||||
28 | | ||||
29 | namespace Bugzilla { | ||||
30 | | ||||
31 | APIException::APIException(const QJsonDocument &document) | ||||
32 | { | ||||
33 | QJsonObject object = document.object(); | ||||
34 | if (object.isEmpty()) { | ||||
35 | return; | ||||
36 | } | ||||
37 | m_isError = object.value("error").toBool(m_isError); | ||||
38 | m_message = object.value("message").toString(m_message); | ||||
39 | m_code = object.value("code").toInt(m_code); | ||||
40 | } | ||||
41 | | ||||
42 | APIException::APIException(const APIException &other) | ||||
43 | : m_isError(other.m_isError) | ||||
44 | , m_message(other.m_message) | ||||
45 | , m_code(other.m_code) | ||||
46 | { | ||||
47 | } | ||||
48 | | ||||
49 | QString APIException::whatString() const | ||||
50 | { | ||||
51 | return QStringLiteral("[%1] %2").arg(m_code).arg(m_message); | ||||
52 | } | ||||
53 | | ||||
54 | void APIException::maybeThrow(const QJsonDocument &document) | ||||
55 | { | ||||
56 | APIException ex(document); | ||||
57 | | ||||
58 | if (ex.isError()) { | ||||
59 | ex.raise(); | ||||
60 | } | ||||
61 | } | ||||
62 | | ||||
63 | HTTPConnection::HTTPConnection(const QUrl &root, QObject *parent) | ||||
64 | : Connection(parent) | ||||
65 | , m_root(QStringLiteral("http://bugstest.kde.org/rest")) | ||||
66 | { | ||||
67 | if (!root.isEmpty()) { | ||||
68 | m_root = QUrl(root); | ||||
69 | } | ||||
70 | const QString override = qEnvironmentVariable("DRKONQI_KDE_BUGZILLA_URL"); | ||||
71 | if (!override.isEmpty()) { | ||||
72 | m_root = QUrl(override + QStringLiteral("/rest")); | ||||
73 | } | ||||
74 | } | ||||
75 | | ||||
76 | HTTPConnection::~HTTPConnection() | ||||
77 | { | ||||
78 | qDebug() << "-----------------------------------"; | ||||
79 | } | ||||
80 | | ||||
81 | void HTTPConnection::setToken(const QString &authToken) | ||||
82 | { | ||||
83 | m_token = authToken; | ||||
84 | } | ||||
85 | | ||||
86 | APIJob *HTTPConnection::get(const QString &path, const QUrlQuery &query) const | ||||
87 | { | ||||
88 | auto job = new TransferAPIJob(KIO::get(url(path, query), KIO::Reload, KIO::HideProgressInfo)); | ||||
89 | return job; | ||||
90 | } | ||||
91 | | ||||
92 | APIJob *HTTPConnection::post(const QString &path, const QByteArray &data, const QUrlQuery &query) const | ||||
93 | { | ||||
94 | auto job = new TransferAPIJob(KIO::http_post(url(path, query), data, KIO::HideProgressInfo)); | ||||
95 | return job; | ||||
96 | } | ||||
97 | | ||||
98 | APIJob *HTTPConnection::put(const QString &path, const QByteArray &data, const QUrlQuery &query) const | ||||
99 | { | ||||
100 | auto job = new TransferAPIJob(KIO::put(url(path, query), KIO::HideProgressInfo)); | ||||
101 | job->setPutData(data); | ||||
102 | return job; | ||||
103 | } | ||||
104 | | ||||
105 | QUrl HTTPConnection::url(const QString &appendix, QUrlQuery query) const | ||||
106 | { | ||||
107 | QUrl url(m_root); | ||||
108 | url.setPath(m_root.path() + appendix); | ||||
109 | | ||||
110 | qDebug() << url; | ||||
111 | if (!m_token.isEmpty()) { | ||||
112 | query.addQueryItem(QStringLiteral("token"), m_token); | ||||
113 | } | ||||
114 | | ||||
115 | url.setQuery(query); | ||||
116 | qDebug() << url; | ||||
117 | return url; | ||||
118 | } | ||||
119 | | ||||
120 | TransferAPIJob::TransferAPIJob(KIO::TransferJob *transferJob, QObject *parent) | ||||
121 | : APIJob(parent) | ||||
122 | , m_transferJob(transferJob) | ||||
123 | { | ||||
124 | // Required for every request type. | ||||
125 | addMetaData(QStringLiteral("content-type"), QStringLiteral("application/json")); | ||||
126 | addMetaData(QStringLiteral("accept"), QStringLiteral("application/json")); | ||||
127 | addMetaData(QStringLiteral("UserAgent"), QStringLiteral("DrKonqi")); | ||||
128 | | ||||
129 | connect(m_transferJob, &KIO::TransferJob::data, | ||||
130 | this, [this](KIO::Job *, const QByteArray &data) { | ||||
131 | m_data += data; | ||||
132 | }); | ||||
133 | | ||||
134 | connect(m_transferJob, &KIO::TransferJob::finished, | ||||
135 | this, [this](KJob *job) { | ||||
136 | setError(job->error()); | ||||
137 | setErrorText(job->errorText()); | ||||
138 | | ||||
139 | #warning this is kinda replaced by APIException | ||||
140 | if (error() == KJob::NoError) { | ||||
141 | // If we have an API error the HTTP code will be fine even though | ||||
142 | // there was an error on the API level. | ||||
143 | // This only applies if there was not a higher level http error! | ||||
144 | auto object = QJsonDocument::fromJson(data()).object(); | ||||
145 | bool error = object.value("error").toBool(false); | ||||
146 | if (error) { | ||||
147 | #warning depth is a bit intense here | ||||
148 | #warning should we maybe make an error object that manages deserialization? | ||||
149 | const QString message = object.value("message").toString(); | ||||
150 | const int code = object.value("code").toInt(); | ||||
151 | const QString errorText = | ||||
152 | QStringLiteral("<b>[%1]</b> %2").arg(code).arg(message); | ||||
153 | setErrorText(errorText); | ||||
154 | // NB: the code technicallly can conflict with KIO's errors, since | ||||
155 | // we don't attach meaning to the codes beyond KJob's that | ||||
156 | // should not make a difference though. | ||||
157 | setError(KJob::UserDefinedError + object.value("code").toInt()); | ||||
158 | } | ||||
159 | } | ||||
160 | #warning the error managing is a bit garbage... errorText is not localized, but at the same time bugzilla has no well defined codes we could translate | ||||
161 | | ||||
162 | emitResult(); | ||||
163 | }); | ||||
164 | } | ||||
165 | | ||||
166 | void TransferAPIJob::addMetaData(const QString &key, const QString &value) | ||||
167 | { | ||||
168 | m_transferJob->addMetaData(key, value); | ||||
169 | } | ||||
170 | | ||||
171 | void TransferAPIJob::setPutData(const QByteArray &data) | ||||
172 | { | ||||
173 | m_putData = data; | ||||
174 | | ||||
175 | // This is rally awkward, does it need to be this way? Why can't we just | ||||
176 | // push the entire array in? | ||||
177 | | ||||
178 | // dataReq says we shouldn't send data >1mb, so segment the incoming data | ||||
179 | // accordingly and generate QBAs wrapping the raw data (zero-copy). | ||||
180 | int segmentSize = 1024 * 1024; // 1 mb per segment maximum | ||||
181 | int segments = qMax(data.size() / segmentSize, 1); | ||||
182 | m_dataSegments.reserve(segments); | ||||
183 | for (int i = 0; i < segments; ++i) { | ||||
184 | int offset = i * segmentSize; | ||||
185 | const char *buf = data.constData() + offset; | ||||
186 | int segmentLength = qMin(offset + segmentSize, data.size()); | ||||
187 | m_dataSegments.append(QByteArray::fromRawData(buf, segmentLength)); | ||||
188 | } | ||||
189 | | ||||
190 | // TODO: throw away, only here to make sure I don't mess up the | ||||
191 | // segmentation. | ||||
192 | int allLengths = 0; | ||||
193 | for (auto a : m_dataSegments) { | ||||
194 | allLengths += a.size(); | ||||
195 | } | ||||
196 | Q_ASSERT(allLengths == data.size()); | ||||
197 | | ||||
198 | connect(m_transferJob, &KIO::TransferJob::dataReq, | ||||
199 | this, [this](KIO::Job *, QByteArray &dataForSending) { | ||||
200 | if (m_dataSegments.isEmpty()) { | ||||
201 | return; | ||||
202 | } | ||||
203 | dataForSending = m_dataSegments.takeFirst(); | ||||
204 | }); | ||||
205 | } | ||||
206 | | ||||
207 | QJsonDocument APIJob::document() const | ||||
208 | { | ||||
209 | ProtocolException::maybeThrow(this); | ||||
210 | Q_ASSERT(error() == KJob::NoError); | ||||
211 | | ||||
212 | auto document = QJsonDocument::fromJson(data()); | ||||
213 | APIException::maybeThrow(document); | ||||
214 | return document; | ||||
215 | } | ||||
216 | | ||||
217 | void APIJob::connectNotify(const QMetaMethod &signal) | ||||
218 | { | ||||
219 | if (signal == QMetaMethod::fromSignal(&KJob::finished)) { | ||||
220 | qDebug() << "auto starting"; | ||||
221 | start(); | ||||
222 | } | ||||
223 | KJob::connectNotify(signal); | ||||
224 | } | ||||
225 | | ||||
226 | class GlobalConnection | ||||
227 | { | ||||
228 | public: | ||||
229 | ~GlobalConnection() | ||||
230 | { | ||||
231 | delete m_connection; | ||||
232 | } | ||||
233 | | ||||
234 | Connection *m_connection = new HTTPConnection; | ||||
235 | }; | ||||
236 | | ||||
237 | Q_GLOBAL_STATIC(GlobalConnection, s_connection) | ||||
238 | | ||||
239 | Connection &connection() | ||||
240 | { | ||||
241 | return *(s_connection->m_connection); | ||||
242 | } | ||||
243 | | ||||
244 | void setConnection(Connection *newConnection) | ||||
245 | { | ||||
246 | delete s_connection->m_connection; | ||||
247 | s_connection->m_connection = newConnection; | ||||
248 | } | ||||
249 | | ||||
250 | ProtocolException::ProtocolException(const APIJob *job) | ||||
251 | : Exception() | ||||
252 | , m_job(job) | ||||
253 | { | ||||
254 | } | ||||
255 | | ||||
256 | ProtocolException::ProtocolException(const ProtocolException &other) | ||||
257 | : m_job(other.m_job) | ||||
258 | { | ||||
259 | } | ||||
260 | | ||||
261 | QString ProtocolException::whatString() const | ||||
262 | { | ||||
263 | // String generally includes the error code, so no extra logic needed. | ||||
264 | return m_job->errorString(); | ||||
265 | } | ||||
266 | | ||||
267 | void ProtocolException::maybeThrow(const APIJob *job) | ||||
268 | { | ||||
269 | if (job->error() == KJob::NoError) { | ||||
270 | return; | ||||
271 | } | ||||
272 | throw ProtocolException(job); | ||||
273 | } | ||||
274 | | ||||
275 | const char *Exception::what() const noexcept | ||||
276 | { | ||||
277 | return qUtf8Printable(whatString()); | ||||
278 | } | ||||
279 | | ||||
280 | } // namespace Bugzilla | ||||
281 | |