1 /*
2 * Copyright (C) by Klaas Freitag <freitag@owncloud.com>
3 * Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful, but
11 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
12 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
13 * for more details.
14 */
15
16 #include <QLoggingCategory>
17 #include <QNetworkRequest>
18 #include <QNetworkAccessManager>
19 #include <QNetworkReply>
20 #include <QNetworkRequest>
21 #include <QSslConfiguration>
22 #include <QSslCipher>
23 #include <QBuffer>
24 #include <QXmlStreamReader>
25 #include <QStringList>
26 #include <QStack>
27 #include <QTimer>
28 #include <QMutex>
29 #include <QCoreApplication>
30 #include <QJsonDocument>
31 #include <QJsonObject>
32 #ifndef TOKEN_AUTH_ONLY
33 #include <QPainter>
34 #include <QPainterPath>
35 #endif
36
37 #include "networkjobs.h"
38 #include "account.h"
39 #include "owncloudpropagator.h"
40
41 #include "creds/abstractcredentials.h"
42 #include "creds/httpcredentials.h"
43
44 namespace OCC {
45
46 Q_LOGGING_CATEGORY(lcEtagJob, "sync.networkjob.etag", QtInfoMsg)
47 Q_LOGGING_CATEGORY(lcLsColJob, "sync.networkjob.lscol", QtInfoMsg)
48 Q_LOGGING_CATEGORY(lcCheckServerJob, "sync.networkjob.checkserver", QtInfoMsg)
49 Q_LOGGING_CATEGORY(lcPropfindJob, "sync.networkjob.propfind", QtInfoMsg)
50 Q_LOGGING_CATEGORY(lcAvatarJob, "sync.networkjob.avatar", QtInfoMsg)
51 Q_LOGGING_CATEGORY(lcMkColJob, "sync.networkjob.mkcol", QtInfoMsg)
52 Q_LOGGING_CATEGORY(lcProppatchJob, "sync.networkjob.proppatch", QtInfoMsg)
53 Q_LOGGING_CATEGORY(lcJsonApiJob, "sync.networkjob.jsonapi", QtInfoMsg)
54 Q_LOGGING_CATEGORY(lcDetermineAuthTypeJob, "sync.networkjob.determineauthtype", QtInfoMsg)
55
parseEtag(const QByteArray & header)56 QByteArray parseEtag(const QByteArray &header)
57 {
58 if (header.isEmpty())
59 return QByteArray();
60 QByteArray arr = header;
61
62 // Weak E-Tags can appear when gzip compression is on, see #3946
63 if (arr.startsWith("W/"))
64 arr = arr.mid(2);
65
66 // https://github.com/owncloud/client/issues/1195
67 arr.replace("-gzip", "");
68
69 if (arr.length() >= 2 && arr.startsWith('"') && arr.endsWith('"')) {
70 arr = arr.mid(1, arr.length() - 2);
71 }
72 return arr;
73 }
74
RequestEtagJob(AccountPtr account,const QString & path,QObject * parent)75 RequestEtagJob::RequestEtagJob(AccountPtr account, const QString &path, QObject *parent)
76 : AbstractNetworkJob(account, path, parent)
77 {
78 }
79
start()80 void RequestEtagJob::start()
81 {
82 QNetworkRequest req;
83 req.setRawHeader("Depth", "0");
84
85 QByteArray xml("<?xml version=\"1.0\" ?>\n"
86 "<d:propfind xmlns:d=\"DAV:\">\n"
87 " <d:prop>\n"
88 " <d:getetag/>\n"
89 " </d:prop>\n"
90 "</d:propfind>\n");
91 QBuffer *buf = new QBuffer(this);
92 buf->setData(xml);
93 buf->open(QIODevice::ReadOnly);
94 // assumes ownership
95 sendRequest("PROPFIND", makeDavUrl(path()), req, buf);
96
97 if (reply()->error() != QNetworkReply::NoError) {
98 qCWarning(lcEtagJob) << "request network error: " << reply()->errorString();
99 }
100 AbstractNetworkJob::start();
101 }
102
finished()103 bool RequestEtagJob::finished()
104 {
105 qCInfo(lcEtagJob) << "Request Etag of" << reply()->request().url() << "FINISHED WITH STATUS"
106 << replyStatusString();
107
108 auto httpCode = reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
109 if (httpCode == 207) {
110 // Parse DAV response
111 QXmlStreamReader reader(reply());
112 reader.addExtraNamespaceDeclaration(QXmlStreamNamespaceDeclaration(QStringLiteral("d"), QStringLiteral("DAV:")));
113 QString etag;
114 while (!reader.atEnd()) {
115 QXmlStreamReader::TokenType type = reader.readNext();
116 if (type == QXmlStreamReader::StartElement && reader.namespaceUri() == QLatin1String("DAV:")) {
117 QString name = reader.name().toString();
118 if (name == QLatin1String("getetag")) {
119 auto etagText = reader.readElementText();
120 auto parsedTag = parseEtag(etagText.toUtf8());
121 if (!parsedTag.isEmpty()) {
122 etag += QString::fromUtf8(parsedTag);
123 } else {
124 etag += etagText;
125 }
126 }
127 }
128 }
129 emit etagRetreived(etag);
130 emit finishedWithResult(etag);
131 } else {
132 emit finishedWithResult(HttpError{ httpCode, errorString() });
133 }
134 return true;
135 }
136
137 /*********************************************************************************************/
138
MkColJob(AccountPtr account,const QString & path,QObject * parent)139 MkColJob::MkColJob(AccountPtr account, const QString &path, QObject *parent)
140 : AbstractNetworkJob(account, path, parent)
141 {
142 }
143
MkColJob(AccountPtr account,const QUrl & url,const QMap<QByteArray,QByteArray> & extraHeaders,QObject * parent)144 MkColJob::MkColJob(AccountPtr account, const QUrl &url,
145 const QMap<QByteArray, QByteArray> &extraHeaders, QObject *parent)
146 : AbstractNetworkJob(account, QString(), parent)
147 , _url(url)
148 , _extraHeaders(extraHeaders)
149 {
150 }
151
start()152 void MkColJob::start()
153 {
154 // add 'Content-Length: 0' header (see https://github.com/owncloud/client/issues/3256)
155 QNetworkRequest req;
156 req.setRawHeader("Content-Length", "0");
157 for (auto it = _extraHeaders.constBegin(); it != _extraHeaders.constEnd(); ++it) {
158 req.setRawHeader(it.key(), it.value());
159 }
160
161 // assumes ownership
162 if (_url.isValid()) {
163 sendRequest("MKCOL", _url, req);
164 } else {
165 sendRequest("MKCOL", makeDavUrl(path()), req);
166 }
167 AbstractNetworkJob::start();
168 }
169
finished()170 bool MkColJob::finished()
171 {
172 qCInfo(lcMkColJob) << "MKCOL of" << reply()->request().url() << "FINISHED WITH STATUS"
173 << replyStatusString();
174
175 emit finished(reply()->error());
176 return true;
177 }
178
179 /*********************************************************************************************/
180 // supposed to read <D:collection> when pointing to <D:resourcetype><D:collection></D:resourcetype>..
readContentsAsString(QXmlStreamReader & reader)181 static QString readContentsAsString(QXmlStreamReader &reader)
182 {
183 QString result;
184 int level = 0;
185 do {
186 QXmlStreamReader::TokenType type = reader.readNext();
187 if (type == QXmlStreamReader::StartElement) {
188 level++;
189 result += QLatin1Char('<') + reader.name().toString() + QLatin1Char('>');
190 } else if (type == QXmlStreamReader::Characters) {
191 result += reader.text();
192 } else if (type == QXmlStreamReader::EndElement) {
193 level--;
194 if (level < 0) {
195 break;
196 }
197 result += QStringLiteral("</") + reader.name().toString() + QLatin1Char('>');
198 }
199
200 } while (!reader.atEnd());
201 return result;
202 }
203
204
LsColXMLParser()205 LsColXMLParser::LsColXMLParser()
206 {
207 }
208
parse(const QByteArray & xml,QHash<QString,qint64> * sizes,const QString & expectedPath)209 bool LsColXMLParser::parse(const QByteArray &xml, QHash<QString, qint64> *sizes, const QString &expectedPath)
210 {
211 // Parse DAV response
212 QXmlStreamReader reader(xml);
213 reader.addExtraNamespaceDeclaration(QXmlStreamNamespaceDeclaration(QStringLiteral("d"), QStringLiteral("DAV:")));
214
215 QStringList folders;
216 QString currentHref;
217 QMap<QString, QString> currentTmpProperties;
218 QMap<QString, QString> currentHttp200Properties;
219 bool currentPropsHaveHttp200 = false;
220 bool insidePropstat = false;
221 bool insideProp = false;
222 bool insideMultiStatus = false;
223
224 while (!reader.atEnd()) {
225 QXmlStreamReader::TokenType type = reader.readNext();
226 QString name = reader.name().toString();
227 // Start elements with DAV:
228 if (type == QXmlStreamReader::StartElement && reader.namespaceUri() == QLatin1String("DAV:")) {
229 if (name == QLatin1String("href")) {
230 // We don't use URL encoding in our request URL (which is the expected path) (QNAM will do it for us)
231 // but the result will have URL encoding..
232 QString hrefString = QString::fromUtf8(QByteArray::fromPercentEncoding(reader.readElementText().toUtf8()));
233 if (!hrefString.startsWith(expectedPath)) {
234 qCWarning(lcLsColJob) << "Invalid href" << hrefString << "expected starting with" << expectedPath;
235 return false;
236 }
237 currentHref = hrefString;
238 } else if (name == QLatin1String("response")) {
239 } else if (name == QLatin1String("propstat")) {
240 insidePropstat = true;
241 } else if (name == QLatin1String("status") && insidePropstat) {
242 QString httpStatus = reader.readElementText();
243 if (httpStatus.startsWith(QLatin1String("HTTP/1.1 200"))) {
244 currentPropsHaveHttp200 = true;
245 } else {
246 currentPropsHaveHttp200 = false;
247 }
248 } else if (name == QLatin1String("prop")) {
249 insideProp = true;
250 continue;
251 } else if (name == QLatin1String("multistatus")) {
252 insideMultiStatus = true;
253 continue;
254 }
255 }
256
257 if (type == QXmlStreamReader::StartElement && insidePropstat && insideProp) {
258 // All those elements are properties
259 QString propertyContent = readContentsAsString(reader);
260 if (name == QLatin1String("resourcetype") && propertyContent.contains(QLatin1String("collection"))) {
261 folders.append(currentHref);
262 } else if (name == QLatin1String("size")) {
263 bool ok = false;
264 auto s = propertyContent.toLongLong(&ok);
265 if (ok && sizes) {
266 sizes->insert(currentHref, s);
267 }
268 }
269 currentTmpProperties.insert(reader.name().toString(), propertyContent);
270 }
271
272 // End elements with DAV:
273 if (type == QXmlStreamReader::EndElement) {
274 if (reader.namespaceUri() == QLatin1String("DAV:")) {
275 if (reader.name() == QLatin1String("response")) {
276 if (currentHref.endsWith(QLatin1Char('/'))) {
277 currentHref.chop(1);
278 }
279 emit directoryListingIterated(currentHref, currentHttp200Properties);
280 currentHref.clear();
281 currentHttp200Properties.clear();
282 } else if (reader.name() == QLatin1String("propstat")) {
283 insidePropstat = false;
284 if (currentPropsHaveHttp200) {
285 currentHttp200Properties = QMap<QString, QString>(currentTmpProperties);
286 }
287 currentTmpProperties.clear();
288 currentPropsHaveHttp200 = false;
289 } else if (reader.name() == QLatin1String("prop")) {
290 insideProp = false;
291 }
292 }
293 }
294 }
295
296 if (reader.hasError()) {
297 // XML Parser error? Whatever had been emitted before will come as directoryListingIterated
298 qCWarning(lcLsColJob) << "ERROR" << reader.errorString() << xml;
299 return false;
300 } else if (!insideMultiStatus) {
301 qCWarning(lcLsColJob) << "ERROR no WebDAV response?" << xml;
302 return false;
303 } else {
304 emit directoryListingSubfolders(folders);
305 emit finishedWithoutError();
306 }
307 return true;
308 }
309
310 /*********************************************************************************************/
311
LsColJob(AccountPtr account,const QString & path,QObject * parent)312 LsColJob::LsColJob(AccountPtr account, const QString &path, QObject *parent)
313 : AbstractNetworkJob(account, path, parent)
314 {
315 }
316
LsColJob(AccountPtr account,const QUrl & url,QObject * parent)317 LsColJob::LsColJob(AccountPtr account, const QUrl &url, QObject *parent)
318 : AbstractNetworkJob(account, QString(), parent)
319 , _url(url)
320 {
321 }
322
setProperties(QList<QByteArray> properties)323 void LsColJob::setProperties(QList<QByteArray> properties)
324 {
325 _properties = properties;
326 }
327
properties() const328 QList<QByteArray> LsColJob::properties() const
329 {
330 return _properties;
331 }
332
start()333 void LsColJob::start()
334 {
335 QList<QByteArray> properties = _properties;
336
337 if (properties.isEmpty()) {
338 qCWarning(lcLsColJob) << "Propfind with no properties!";
339 }
340 QByteArray propStr;
341 foreach (const QByteArray &prop, properties) {
342 if (prop.contains(':')) {
343 int colIdx = prop.lastIndexOf(":");
344 auto ns = prop.left(colIdx);
345 if (ns == "http://owncloud.org/ns") {
346 propStr += " <oc:" + prop.mid(colIdx + 1) + " />\n";
347 } else {
348 propStr += " <" + prop.mid(colIdx + 1) + " xmlns=\"" + ns + "\" />\n";
349 }
350 } else {
351 propStr += " <d:" + prop + " />\n";
352 }
353 }
354
355 QNetworkRequest req;
356 req.setRawHeader("Depth", "1");
357 QByteArray xml("<?xml version=\"1.0\" ?>\n"
358 "<d:propfind xmlns:d=\"DAV:\" xmlns:oc=\"http://owncloud.org/ns\">\n"
359 " <d:prop>\n"
360 + propStr + " </d:prop>\n"
361 "</d:propfind>\n");
362 QBuffer *buf = new QBuffer(this);
363 buf->setData(xml);
364 buf->open(QIODevice::ReadOnly);
365 if (_url.isValid()) {
366 sendRequest("PROPFIND", _url, req, buf);
367 } else {
368 sendRequest("PROPFIND", makeDavUrl(path()), req, buf);
369 }
370 AbstractNetworkJob::start();
371 }
372
373 // TODO: Instead of doing all in this slot, we should iteratively parse in readyRead(). This
374 // would allow us to be more asynchronous in processing while data is coming from the network,
375 // not all in one big blob at the end.
finished()376 bool LsColJob::finished()
377 {
378 qCInfo(lcLsColJob) << "LSCOL of" << reply()->request().url() << "FINISHED WITH STATUS"
379 << replyStatusString();
380
381 QString contentType = reply()->header(QNetworkRequest::ContentTypeHeader).toString();
382 int httpCode = reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
383 if (httpCode == 207 && contentType.contains(QLatin1String("application/xml; charset=utf-8"))) {
384 LsColXMLParser parser;
385 connect(&parser, &LsColXMLParser::directoryListingSubfolders,
386 this, &LsColJob::directoryListingSubfolders);
387 connect(&parser, &LsColXMLParser::directoryListingIterated,
388 this, &LsColJob::directoryListingIterated);
389 connect(&parser, &LsColXMLParser::finishedWithError,
390 this, &LsColJob::finishedWithError);
391 connect(&parser, &LsColXMLParser::finishedWithoutError,
392 this, &LsColJob::finishedWithoutError);
393
394 QString expectedPath = reply()->request().url().path(); // something like "/owncloud/remote.php/webdav/folder"
395 if (!parser.parse(reply()->readAll(), &_sizes, expectedPath)) {
396 // XML parse error
397 emit finishedWithError(reply());
398 }
399 } else if (httpCode == 207) {
400 // wrong content type
401 emit finishedWithError(reply());
402 } else {
403 // wrong HTTP code or any other network error
404 emit finishedWithError(reply());
405 }
406
407 return true;
408 }
409
410 /*********************************************************************************************/
411
412 namespace {
statusphpC()413 const QString statusphpC() { return QStringLiteral("status.php"); }
owncloudDirC()414 const QString owncloudDirC() { return QStringLiteral("owncloud/"); }
415 }
416
CheckServerJob(AccountPtr account,QObject * parent)417 CheckServerJob::CheckServerJob(AccountPtr account, QObject *parent)
418 : AbstractNetworkJob(account, statusphpC(), parent)
419 , _subdirFallback(false)
420 , _permanentRedirects(0)
421 {
422 setIgnoreCredentialFailure(true);
423 connect(this, &AbstractNetworkJob::redirected,
424 this, &CheckServerJob::slotRedirected);
425 }
426
start()427 void CheckServerJob::start()
428 {
429 _serverUrl = account()->url();
430 sendRequest("GET", Utility::concatUrlPath(_serverUrl, path()));
431 connect(reply(), &QNetworkReply::metaDataChanged, this, &CheckServerJob::metaDataChangedSlot);
432 connect(reply(), &QNetworkReply::encrypted, this, &CheckServerJob::encryptedSlot);
433 AbstractNetworkJob::start();
434 }
435
onTimedOut()436 void CheckServerJob::onTimedOut()
437 {
438 qCWarning(lcCheckServerJob) << "TIMEOUT";
439 if (reply() && reply()->isRunning()) {
440 emit timeout(reply()->url());
441 } else if (!reply()) {
442 qCWarning(lcCheckServerJob) << "Timeout even there was no reply?";
443 }
444 deleteLater();
445 }
446
version(const QJsonObject & info)447 QString CheckServerJob::version(const QJsonObject &info)
448 {
449 return info.value(QLatin1String("version")).toString() + QLatin1Char('-') + info.value(QLatin1String("productname")).toString();
450 }
451
versionString(const QJsonObject & info)452 QString CheckServerJob::versionString(const QJsonObject &info)
453 {
454 return info.value(QLatin1String("versionstring")).toString();
455 }
456
installed(const QJsonObject & info)457 bool CheckServerJob::installed(const QJsonObject &info)
458 {
459 return info.value(QLatin1String("installed")).toBool();
460 }
461
mergeSslConfigurationForSslButton(const QSslConfiguration & config,AccountPtr account)462 static void mergeSslConfigurationForSslButton(const QSslConfiguration &config, AccountPtr account)
463 {
464 if (config.peerCertificateChain().length() > 0) {
465 account->_peerCertificateChain = config.peerCertificateChain();
466 }
467 if (!config.sessionCipher().isNull()) {
468 account->_sessionCipher = config.sessionCipher();
469 }
470 if (config.sessionTicket().length() > 0) {
471 account->_sessionTicket = config.sessionTicket();
472 }
473 }
474
encryptedSlot()475 void CheckServerJob::encryptedSlot()
476 {
477 mergeSslConfigurationForSslButton(reply()->sslConfiguration(), account());
478 }
479
slotRedirected(QNetworkReply * reply,const QUrl & targetUrl,int redirectCount)480 void CheckServerJob::slotRedirected(QNetworkReply *reply, const QUrl &targetUrl, int redirectCount)
481 {
482 const auto slashStatusPhp = QStringLiteral("/%1").arg(statusphpC());
483
484 int httpCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
485 QString path = targetUrl.path();
486 if ((httpCode == 301 || httpCode == 308) // permanent redirection
487 && redirectCount == _permanentRedirects // don't apply permanent redirects after a temporary one
488 && path.endsWith(slashStatusPhp)) {
489 _serverUrl = targetUrl;
490 _serverUrl.setPath(path.left(path.size() - slashStatusPhp.size()));
491 qCInfo(lcCheckServerJob) << "status.php was permanently redirected to"
492 << targetUrl << "new server url is" << _serverUrl;
493 ++_permanentRedirects;
494 }
495 }
496
metaDataChangedSlot()497 void CheckServerJob::metaDataChangedSlot()
498 {
499 account()->setSslConfiguration(reply()->sslConfiguration());
500 mergeSslConfigurationForSslButton(reply()->sslConfiguration(), account());
501 }
502
503
finished()504 bool CheckServerJob::finished()
505 {
506 if (reply()->request().url().scheme() == QLatin1String("https")
507 && reply()->sslConfiguration().sessionTicket().isEmpty()
508 && reply()->error() == QNetworkReply::NoError) {
509 qCWarning(lcCheckServerJob) << "No SSL session identifier / session ticket is used, this might impact sync performance negatively.";
510 }
511
512 mergeSslConfigurationForSslButton(reply()->sslConfiguration(), account());
513
514 // The server installs to /owncloud. Let's try that if the file wasn't found
515 // at the original location
516 if ((reply()->error() == QNetworkReply::ContentNotFoundError) && (!_subdirFallback)) {
517 _subdirFallback = true;
518 setPath(owncloudDirC() + statusphpC());
519 start();
520 qCInfo(lcCheckServerJob) << "Retrying with" << reply()->url();
521 return false;
522 }
523
524 QByteArray body = reply()->peek(4 * 1024);
525 int httpStatus = reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
526 if (body.isEmpty() || httpStatus != 200) {
527 qCWarning(lcCheckServerJob) << "error: status.php replied " << httpStatus << body;
528 emit instanceNotFound(reply());
529 } else {
530 QJsonParseError error;
531 auto status = QJsonDocument::fromJson(body, &error);
532 // empty or invalid response
533 if (error.error != QJsonParseError::NoError || status.isNull()) {
534 qCWarning(lcCheckServerJob) << "status.php from server is not valid JSON!" << body << reply()->request().url() << error.errorString();
535 }
536
537 qCInfo(lcCheckServerJob) << "status.php returns: " << status << " " << reply()->error() << " Reply: " << reply();
538 if (status.object().contains(QStringLiteral("installed"))) {
539 emit instanceFound(_serverUrl, status.object());
540 } else {
541 qCWarning(lcCheckServerJob) << "No proper answer on " << reply()->url();
542 emit instanceNotFound(reply());
543 }
544 }
545 return true;
546 }
547
548 /*********************************************************************************************/
549
PropfindJob(AccountPtr account,const QString & path,QObject * parent)550 PropfindJob::PropfindJob(AccountPtr account, const QString &path, QObject *parent)
551 : AbstractNetworkJob(account, path, parent)
552 {
553 }
554
start()555 void PropfindJob::start()
556 {
557 QList<QByteArray> properties = _properties;
558
559 if (properties.isEmpty()) {
560 qCWarning(lcLsColJob) << "Propfind with no properties!";
561 }
562 QNetworkRequest req;
563 // Always have a higher priority than the propagator because we use this from the UI
564 // and really want this to be done first (no matter what internal scheduling QNAM uses).
565 // Also possibly useful for avoiding false timeouts.
566 req.setPriority(QNetworkRequest::HighPriority);
567 req.setRawHeader("Depth", "0");
568 QByteArray propStr;
569 foreach (const QByteArray &prop, properties) {
570 if (prop.contains(':')) {
571 int colIdx = prop.lastIndexOf(":");
572 propStr += " <" + prop.mid(colIdx + 1) + " xmlns=\"" + prop.left(colIdx) + "\" />\n";
573 } else {
574 propStr += " <d:" + prop + " />\n";
575 }
576 }
577 QByteArray xml = "<?xml version=\"1.0\" ?>\n"
578 "<d:propfind xmlns:d=\"DAV:\">\n"
579 " <d:prop>\n"
580 + propStr + " </d:prop>\n"
581 "</d:propfind>\n";
582
583 QBuffer *buf = new QBuffer(this);
584 buf->setData(xml);
585 buf->open(QIODevice::ReadOnly);
586 sendRequest("PROPFIND", makeDavUrl(path()), req, buf);
587 AbstractNetworkJob::start();
588 }
589
setProperties(QList<QByteArray> properties)590 void PropfindJob::setProperties(QList<QByteArray> properties)
591 {
592 _properties = properties;
593 }
594
properties() const595 QList<QByteArray> PropfindJob::properties() const
596 {
597 return _properties;
598 }
599
finished()600 bool PropfindJob::finished()
601 {
602 qCInfo(lcPropfindJob) << "PROPFIND of" << reply()->request().url() << "FINISHED WITH STATUS"
603 << replyStatusString();
604
605 int http_result_code = reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
606
607 if (http_result_code == 207) {
608 // Parse DAV response
609 QXmlStreamReader reader(reply());
610 reader.addExtraNamespaceDeclaration(QXmlStreamNamespaceDeclaration(QStringLiteral("d"), QStringLiteral("DAV:")));
611
612 QVariantMap items;
613 // introduced to nesting is ignored
614 QStack<QString> curElement;
615
616 while (!reader.atEnd()) {
617 QXmlStreamReader::TokenType type = reader.readNext();
618 if (type == QXmlStreamReader::StartElement) {
619 if (!curElement.isEmpty() && curElement.top() == QLatin1String("prop")) {
620 items.insert(reader.name().toString(), reader.readElementText(QXmlStreamReader::SkipChildElements));
621 } else {
622 curElement.push(reader.name().toString());
623 }
624 }
625 if (type == QXmlStreamReader::EndElement) {
626 if (curElement.top() == reader.name()) {
627 curElement.pop();
628 }
629 }
630 }
631 if (reader.hasError()) {
632 qCWarning(lcPropfindJob) << "XML parser error: " << reader.errorString();
633 emit finishedWithError(reply());
634 } else {
635 emit result(items);
636 }
637 } else {
638 qCWarning(lcPropfindJob) << "*not* successful, http result code is" << http_result_code
639 << (http_result_code == 302 ? reply()->header(QNetworkRequest::LocationHeader).toString() : QLatin1String(""));
640 emit finishedWithError(reply());
641 }
642 return true;
643 }
644
645 /*********************************************************************************************/
646
647 #ifndef TOKEN_AUTH_ONLY
AvatarJob(AccountPtr account,const QString & userId,int size,QObject * parent)648 AvatarJob::AvatarJob(AccountPtr account, const QString &userId, int size, QObject *parent)
649 : AbstractNetworkJob(account, QString(), parent)
650 {
651 if (account->serverVersionInt() >= Account::makeServerVersion(10, 0, 0)) {
652 _avatarUrl = Utility::concatUrlPath(account->url(), QStringLiteral("remote.php/dav/avatars/%1/%2.png").arg(userId, QString::number(size)));
653 } else {
654 _avatarUrl = Utility::concatUrlPath(account->url(), QStringLiteral("index.php/avatar/%1/%2").arg(userId, QString::number(size)));
655 }
656 }
657
start()658 void AvatarJob::start()
659 {
660 QNetworkRequest req;
661 sendRequest("GET", _avatarUrl, req);
662 AbstractNetworkJob::start();
663 }
664
makeCircularAvatar(const QPixmap & baseAvatar)665 QPixmap AvatarJob::makeCircularAvatar(const QPixmap &baseAvatar)
666 {
667 int dim = baseAvatar.width();
668
669 QPixmap avatar(dim, dim);
670 avatar.fill(Qt::transparent);
671
672 QPainter painter(&avatar);
673 painter.setRenderHint(QPainter::Antialiasing);
674
675 QPainterPath path;
676 path.addEllipse(0, 0, dim, dim);
677 painter.setClipPath(path);
678
679 painter.drawPixmap(0, 0, baseAvatar);
680 painter.end();
681
682 return avatar;
683 }
684
finished()685 bool AvatarJob::finished()
686 {
687 int http_result_code = reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
688
689 QPixmap avImage;
690
691 if (http_result_code == 200) {
692 QByteArray pngData = reply()->readAll();
693 if (pngData.size()) {
694 if (avImage.loadFromData(pngData)) {
695 qCDebug(lcAvatarJob) << "Retrieved Avatar pixmap!";
696 }
697 }
698 }
699 emit avatarPixmap(avImage);
700 return true;
701 }
702 #endif
703
704 /*********************************************************************************************/
705
ProppatchJob(AccountPtr account,const QString & path,QObject * parent)706 ProppatchJob::ProppatchJob(AccountPtr account, const QString &path, QObject *parent)
707 : AbstractNetworkJob(account, path, parent)
708 {
709 }
710
start()711 void ProppatchJob::start()
712 {
713 if (_properties.isEmpty()) {
714 qCWarning(lcProppatchJob) << "Proppatch with no properties!";
715 }
716 QNetworkRequest req;
717
718 QByteArray propStr;
719 QMapIterator<QByteArray, QByteArray> it(_properties);
720 while (it.hasNext()) {
721 it.next();
722 QByteArray keyName = it.key();
723 QByteArray keyNs;
724 if (keyName.contains(':')) {
725 int colIdx = keyName.lastIndexOf(":");
726 keyNs = keyName.left(colIdx);
727 keyName = keyName.mid(colIdx + 1);
728 }
729
730 propStr += " <" + keyName;
731 if (!keyNs.isEmpty()) {
732 propStr += " xmlns=\"" + keyNs + "\" ";
733 }
734 propStr += ">";
735 propStr += it.value();
736 propStr += "</" + keyName + ">\n";
737 }
738 QByteArray xml = "<?xml version=\"1.0\" ?>\n"
739 "<d:propertyupdate xmlns:d=\"DAV:\">\n"
740 " <d:set><d:prop>\n"
741 + propStr + " </d:prop></d:set>\n"
742 "</d:propertyupdate>\n";
743
744 QBuffer *buf = new QBuffer(this);
745 buf->setData(xml);
746 buf->open(QIODevice::ReadOnly);
747 sendRequest("PROPPATCH", makeDavUrl(path()), req, buf);
748 AbstractNetworkJob::start();
749 }
750
setProperties(QMap<QByteArray,QByteArray> properties)751 void ProppatchJob::setProperties(QMap<QByteArray, QByteArray> properties)
752 {
753 _properties = properties;
754 }
755
properties() const756 QMap<QByteArray, QByteArray> ProppatchJob::properties() const
757 {
758 return _properties;
759 }
760
finished()761 bool ProppatchJob::finished()
762 {
763 qCInfo(lcProppatchJob) << "PROPPATCH of" << reply()->request().url() << "FINISHED WITH STATUS"
764 << replyStatusString();
765
766 int http_result_code = reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
767
768 if (http_result_code == 207) {
769 emit success();
770 } else {
771 qCWarning(lcProppatchJob) << "*not* successful, http result code is" << http_result_code
772 << (http_result_code == 302 ? reply()->header(QNetworkRequest::LocationHeader).toString() : QLatin1String(""));
773 emit finishedWithError();
774 }
775 return true;
776 }
777
778 /*********************************************************************************************/
779
EntityExistsJob(AccountPtr account,const QString & path,QObject * parent)780 EntityExistsJob::EntityExistsJob(AccountPtr account, const QString &path, QObject *parent)
781 : AbstractNetworkJob(account, path, parent)
782 {
783 }
784
start()785 void EntityExistsJob::start()
786 {
787 sendRequest("HEAD", makeAccountUrl(path()));
788 AbstractNetworkJob::start();
789 }
790
finished()791 bool EntityExistsJob::finished()
792 {
793 emit exists(reply());
794 return true;
795 }
796
797 /*********************************************************************************************/
798
JsonApiJob(const AccountPtr & account,const QString & path,QObject * parent)799 JsonApiJob::JsonApiJob(const AccountPtr &account, const QString &path, QObject *parent)
800 : AbstractNetworkJob(account, path, parent)
801 {
802 }
803
addQueryParams(const QUrlQuery & params)804 void JsonApiJob::addQueryParams(const QUrlQuery ¶ms)
805 {
806 _additionalParams = params;
807 }
808
start()809 void JsonApiJob::start()
810 {
811 startWithRequest(QNetworkRequest());
812 }
813
startWithRequest(QNetworkRequest req)814 void OCC::JsonApiJob::startWithRequest(QNetworkRequest req)
815 {
816 req.setRawHeader("OCS-APIREQUEST", "true");
817 auto query = _additionalParams;
818 query.addQueryItem(QStringLiteral("format"), QStringLiteral("json"));
819 QUrl url = Utility::concatUrlPath(account()->url(), path(), query);
820 sendRequest("GET", url, req);
821 AbstractNetworkJob::start();
822 }
823
finished()824 bool JsonApiJob::finished()
825 {
826 qCInfo(lcJsonApiJob) << "JsonApiJob of" << reply()->request().url() << "FINISHED WITH STATUS"
827 << replyStatusString();
828
829 int statusCode = 0;
830
831 if (reply()->error() != QNetworkReply::NoError) {
832 qCWarning(lcJsonApiJob) << "Network error: " << path() << errorString() << reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute);
833 emit jsonReceived(QJsonDocument(), statusCode);
834 return true;
835 }
836
837 QString jsonStr = QString::fromUtf8(reply()->readAll());
838 if (jsonStr.contains(QLatin1String("<?xml version=\"1.0\"?>"))) {
839 QRegExp rex(QStringLiteral("<statuscode>(\\d+)</statuscode>"));
840 if (jsonStr.contains(rex)) {
841 // this is a error message coming back from ocs.
842 statusCode = rex.cap(1).toInt();
843 }
844
845 } else {
846 QRegExp rex(QStringLiteral("\"statuscode\":(\\d+),"));
847 // example: "{"ocs":{"meta":{"status":"ok","statuscode":100,"message":null},"data":{"version":{"major":8,"minor":"... (504)
848 if (jsonStr.contains(rex)) {
849 statusCode = rex.cap(1).toInt();
850 }
851 }
852
853 QJsonParseError error;
854 auto json = QJsonDocument::fromJson(jsonStr.toUtf8(), &error);
855 // empty or invalid response
856 if (error.error != QJsonParseError::NoError || json.isNull()) {
857 qCWarning(lcJsonApiJob) << "invalid JSON!" << jsonStr << error.errorString();
858 emit jsonReceived(json, statusCode);
859 return true;
860 }
861
862 emit jsonReceived(json, statusCode);
863 return true;
864 }
865
DetermineAuthTypeJob(AccountPtr account,QObject * parent)866 DetermineAuthTypeJob::DetermineAuthTypeJob(AccountPtr account, QObject *parent)
867 : QObject(parent)
868 , _account(account)
869 {
870 }
871
start()872 void DetermineAuthTypeJob::start()
873 {
874 qCInfo(lcDetermineAuthTypeJob) << "Determining auth type for" << _account->davUrl();
875
876 QNetworkRequest req;
877 // Prevent HttpCredentialsAccessManager from setting an Authorization header.
878 req.setAttribute(HttpCredentials::DontAddCredentialsAttribute, true);
879 // Don't reuse previous auth credentials
880 req.setAttribute(QNetworkRequest::AuthenticationReuseAttribute, QNetworkRequest::Manual);
881
882 auto propfind = _account->sendRequest("PROPFIND", _account->davUrl(), req);
883 propfind->setTimeout(30 * 1000);
884 propfind->setIgnoreCredentialFailure(true);
885 connect(propfind, &SimpleNetworkJob::finishedSignal, this, [this](QNetworkReply *reply) {
886 auto authChallenge = reply->rawHeader("WWW-Authenticate").toLower();
887 auto result = AuthType::Basic;
888 if (authChallenge.contains("bearer ")) {
889 result = AuthType::OAuth;
890 } else if (authChallenge.isEmpty()) {
891 qCWarning(lcDetermineAuthTypeJob) << "Did not receive WWW-Authenticate reply to auth-test PROPFIND";
892 }
893 qCInfo(lcDetermineAuthTypeJob) << "Auth type for" << _account->davUrl() << "is" << result;
894 emit this->authType(result);
895 this->deleteLater();
896 });
897 }
898
SimpleNetworkJob(AccountPtr account,QObject * parent)899 SimpleNetworkJob::SimpleNetworkJob(AccountPtr account, QObject *parent)
900 : AbstractNetworkJob(account, QString(), parent)
901 {
902 }
903
startRequest(const QByteArray & verb,const QUrl & url,QNetworkRequest req,QIODevice * requestBody)904 QNetworkReply *SimpleNetworkJob::startRequest(const QByteArray &verb, const QUrl &url,
905 QNetworkRequest req, QIODevice *requestBody)
906 {
907 auto reply = sendRequest(verb, url, req, requestBody);
908 start();
909 return reply;
910 }
911
finished()912 bool SimpleNetworkJob::finished()
913 {
914 emit finishedSignal(reply());
915 return true;
916 }
917
fetchPrivateLinkUrl(AccountPtr account,const QString & remotePath,const QByteArray & numericFileId,QObject * target,std::function<void (const QString & url)> targetFun)918 void fetchPrivateLinkUrl(AccountPtr account, const QString &remotePath,
919 const QByteArray &numericFileId, QObject *target,
920 std::function<void(const QString &url)> targetFun)
921 {
922 QString oldUrl;
923 if (!numericFileId.isEmpty())
924 oldUrl = account->deprecatedPrivateLinkUrl(numericFileId).toString(QUrl::FullyEncoded);
925
926 // Retrieve the new link by PROPFIND
927 PropfindJob *job = new PropfindJob(account, remotePath, target);
928 job->setProperties(
929 QList<QByteArray>()
930 << "http://owncloud.org/ns:fileid" // numeric file id for fallback private link generation
931 << "http://owncloud.org/ns:privatelink");
932 job->setTimeout(10 * 1000);
933 QObject::connect(job, &PropfindJob::result, target, [=](const QVariantMap &result) {
934 auto privateLinkUrl = result[QStringLiteral("privatelink")].toString();
935 auto numericFileId = result[QStringLiteral("fileid")].toByteArray();
936 if (!privateLinkUrl.isEmpty()) {
937 targetFun(privateLinkUrl);
938 } else if (!numericFileId.isEmpty()) {
939 targetFun(account->deprecatedPrivateLinkUrl(numericFileId).toString(QUrl::FullyEncoded));
940 } else {
941 targetFun(oldUrl);
942 }
943 });
944 QObject::connect(job, &PropfindJob::finishedWithError, target, [=](QNetworkReply *) {
945 targetFun(oldUrl);
946 });
947 job->start();
948 }
949
950 } // namespace OCC
951