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 <QJsonDocument>
17 #include <QLoggingCategory>
18 #include <QNetworkRequest>
19 #include <QNetworkAccessManager>
20 #include <QNetworkReply>
21 #include <QNetworkRequest>
22 #include <QSslConfiguration>
23 #include <QSslCipher>
24 #include <QBuffer>
25 #include <QXmlStreamReader>
26 #include <QStringList>
27 #include <QStack>
28 #include <QTimer>
29 #include <QMutex>
30 #include <QCoreApplication>
31 #include <QJsonDocument>
32 #include <QJsonObject>
33 #include <qloggingcategory.h>
34 #ifndef TOKEN_AUTH_ONLY
35 #include <QPainter>
36 #include <QPainterPath>
37 #endif
38
39 #include "networkjobs.h"
40 #include "account.h"
41 #include "owncloudpropagator.h"
42 #include "clientsideencryption.h"
43
44 #include "creds/abstractcredentials.h"
45 #include "creds/httpcredentials.h"
46
47 namespace OCC {
48
49 Q_LOGGING_CATEGORY(lcEtagJob, "nextcloud.sync.networkjob.etag", QtInfoMsg)
50 Q_LOGGING_CATEGORY(lcLsColJob, "nextcloud.sync.networkjob.lscol", QtInfoMsg)
51 Q_LOGGING_CATEGORY(lcCheckServerJob, "nextcloud.sync.networkjob.checkserver", QtInfoMsg)
52 Q_LOGGING_CATEGORY(lcPropfindJob, "nextcloud.sync.networkjob.propfind", QtInfoMsg)
53 Q_LOGGING_CATEGORY(lcAvatarJob, "nextcloud.sync.networkjob.avatar", QtInfoMsg)
54 Q_LOGGING_CATEGORY(lcMkColJob, "nextcloud.sync.networkjob.mkcol", QtInfoMsg)
55 Q_LOGGING_CATEGORY(lcProppatchJob, "nextcloud.sync.networkjob.proppatch", QtInfoMsg)
56 Q_LOGGING_CATEGORY(lcJsonApiJob, "nextcloud.sync.networkjob.jsonapi", QtInfoMsg)
57 Q_LOGGING_CATEGORY(lcDetermineAuthTypeJob, "nextcloud.sync.networkjob.determineauthtype", QtInfoMsg)
58 const int notModifiedStatusCode = 304;
59
parseEtag(const char * header)60 QByteArray parseEtag(const char *header)
61 {
62 if (!header)
63 return QByteArray();
64 QByteArray arr = header;
65
66 // Weak E-Tags can appear when gzip compression is on, see #3946
67 if (arr.startsWith("W/"))
68 arr = arr.mid(2);
69
70 // https://github.com/owncloud/client/issues/1195
71 arr.replace("-gzip", "");
72
73 if (arr.length() >= 2 && arr.startsWith('"') && arr.endsWith('"')) {
74 arr = arr.mid(1, arr.length() - 2);
75 }
76 return arr;
77 }
78
RequestEtagJob(AccountPtr account,const QString & path,QObject * parent)79 RequestEtagJob::RequestEtagJob(AccountPtr account, const QString &path, QObject *parent)
80 : AbstractNetworkJob(account, path, parent)
81 {
82 }
83
start()84 void RequestEtagJob::start()
85 {
86 QNetworkRequest req;
87 req.setRawHeader("Depth", "0");
88
89 QByteArray xml("<?xml version=\"1.0\" ?>\n"
90 "<d:propfind xmlns:d=\"DAV:\">\n"
91 " <d:prop>\n"
92 " <d:getetag/>\n"
93 " </d:prop>\n"
94 "</d:propfind>\n");
95 auto *buf = new QBuffer(this);
96 buf->setData(xml);
97 buf->open(QIODevice::ReadOnly);
98 // assumes ownership
99 sendRequest("PROPFIND", makeDavUrl(path()), req, buf);
100
101 if (reply()->error() != QNetworkReply::NoError) {
102 qCWarning(lcEtagJob) << "request network error: " << reply()->errorString();
103 }
104 AbstractNetworkJob::start();
105 }
106
finished()107 bool RequestEtagJob::finished()
108 {
109 qCInfo(lcEtagJob) << "Request Etag of" << reply()->request().url() << "FINISHED WITH STATUS"
110 << replyStatusString();
111
112 auto httpCode = reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
113 if (httpCode == 207) {
114 // Parse DAV response
115 QXmlStreamReader reader(reply());
116 reader.addExtraNamespaceDeclaration(QXmlStreamNamespaceDeclaration(QStringLiteral("d"), QStringLiteral("DAV:")));
117 QByteArray etag;
118 while (!reader.atEnd()) {
119 QXmlStreamReader::TokenType type = reader.readNext();
120 if (type == QXmlStreamReader::StartElement && reader.namespaceUri() == QLatin1String("DAV:")) {
121 QString name = reader.name().toString();
122 if (name == QLatin1String("getetag")) {
123 auto etagText = reader.readElementText();
124 auto parsedTag = parseEtag(etagText.toUtf8());
125 if (!parsedTag.isEmpty()) {
126 etag += parsedTag;
127 } else {
128 etag += etagText.toUtf8();
129 }
130 }
131 }
132 }
133 emit etagRetrieved(etag, QDateTime::fromString(QString::fromUtf8(_responseTimestamp), Qt::RFC2822Date));
134 emit finishedWithResult(etag);
135 } else {
136 emit finishedWithResult(HttpError{ httpCode, errorString() });
137 }
138 return true;
139 }
140
141 /*********************************************************************************************/
142
MkColJob(AccountPtr account,const QString & path,QObject * parent)143 MkColJob::MkColJob(AccountPtr account, const QString &path, QObject *parent)
144 : AbstractNetworkJob(account, path, parent)
145 {
146 }
147
MkColJob(AccountPtr account,const QString & path,const QMap<QByteArray,QByteArray> & extraHeaders,QObject * parent)148 MkColJob::MkColJob(AccountPtr account, const QString &path, const QMap<QByteArray, QByteArray> &extraHeaders, QObject *parent)
149 : AbstractNetworkJob(account, path, parent)
150 , _extraHeaders(extraHeaders)
151 {
152 }
153
MkColJob(AccountPtr account,const QUrl & url,const QMap<QByteArray,QByteArray> & extraHeaders,QObject * parent)154 MkColJob::MkColJob(AccountPtr account, const QUrl &url,
155 const QMap<QByteArray, QByteArray> &extraHeaders, QObject *parent)
156 : AbstractNetworkJob(account, QString(), parent)
157 , _url(url)
158 , _extraHeaders(extraHeaders)
159 {
160 }
161
start()162 void MkColJob::start()
163 {
164 // add 'Content-Length: 0' header (see https://github.com/owncloud/client/issues/3256)
165 QNetworkRequest req;
166 req.setRawHeader("Content-Length", "0");
167 for (auto it = _extraHeaders.constBegin(); it != _extraHeaders.constEnd(); ++it) {
168 req.setRawHeader(it.key(), it.value());
169 }
170
171 // assumes ownership
172 if (_url.isValid()) {
173 sendRequest("MKCOL", _url, req);
174 } else {
175 sendRequest("MKCOL", makeDavUrl(path()), req);
176 }
177 AbstractNetworkJob::start();
178 }
179
finished()180 bool MkColJob::finished()
181 {
182 qCInfo(lcMkColJob) << "MKCOL of" << reply()->request().url() << "FINISHED WITH STATUS"
183 << replyStatusString();
184
185 if (reply()->error() != QNetworkReply::NoError) {
186 Q_EMIT finishedWithError(reply());
187 } else {
188 Q_EMIT finishedWithoutError();
189 }
190 return true;
191 }
192
193 /*********************************************************************************************/
194 // supposed to read <D:collection> when pointing to <D:resourcetype><D:collection></D:resourcetype>..
readContentsAsString(QXmlStreamReader & reader)195 static QString readContentsAsString(QXmlStreamReader &reader)
196 {
197 QString result;
198 int level = 0;
199 do {
200 QXmlStreamReader::TokenType type = reader.readNext();
201 if (type == QXmlStreamReader::StartElement) {
202 level++;
203 result += "<" + reader.name().toString() + ">";
204 } else if (type == QXmlStreamReader::Characters) {
205 result += reader.text();
206 } else if (type == QXmlStreamReader::EndElement) {
207 level--;
208 if (level < 0) {
209 break;
210 }
211 result += "</" + reader.name().toString() + ">";
212 }
213
214 } while (!reader.atEnd());
215 return result;
216 }
217
218
219 LsColXMLParser::LsColXMLParser() = default;
220
parse(const QByteArray & xml,QHash<QString,ExtraFolderInfo> * fileInfo,const QString & expectedPath)221 bool LsColXMLParser::parse(const QByteArray &xml, QHash<QString, ExtraFolderInfo> *fileInfo, const QString &expectedPath)
222 {
223 // Parse DAV response
224 QXmlStreamReader reader(xml);
225 reader.addExtraNamespaceDeclaration(QXmlStreamNamespaceDeclaration("d", "DAV:"));
226
227 QStringList folders;
228 QString currentHref;
229 QMap<QString, QString> currentTmpProperties;
230 QMap<QString, QString> currentHttp200Properties;
231 bool currentPropsHaveHttp200 = false;
232 bool insidePropstat = false;
233 bool insideProp = false;
234 bool insideMultiStatus = false;
235
236 while (!reader.atEnd()) {
237 QXmlStreamReader::TokenType type = reader.readNext();
238 QString name = reader.name().toString();
239 // Start elements with DAV:
240 if (type == QXmlStreamReader::StartElement && reader.namespaceUri() == QLatin1String("DAV:")) {
241 if (name == QLatin1String("href")) {
242 // We don't use URL encoding in our request URL (which is the expected path) (QNAM will do it for us)
243 // but the result will have URL encoding..
244 QString hrefString = QUrl::fromLocalFile(QUrl::fromPercentEncoding(reader.readElementText().toUtf8()))
245 .adjusted(QUrl::NormalizePathSegments)
246 .path();
247 if (!hrefString.startsWith(expectedPath)) {
248 qCWarning(lcLsColJob) << "Invalid href" << hrefString << "expected starting with" << expectedPath;
249 return false;
250 }
251 currentHref = hrefString;
252 } else if (name == QLatin1String("response")) {
253 } else if (name == QLatin1String("propstat")) {
254 insidePropstat = true;
255 } else if (name == QLatin1String("status") && insidePropstat) {
256 QString httpStatus = reader.readElementText();
257 if (httpStatus.startsWith("HTTP/1.1 200")) {
258 currentPropsHaveHttp200 = true;
259 } else {
260 currentPropsHaveHttp200 = false;
261 }
262 } else if (name == QLatin1String("prop")) {
263 insideProp = true;
264 continue;
265 } else if (name == QLatin1String("multistatus")) {
266 insideMultiStatus = true;
267 continue;
268 }
269 }
270
271 if (type == QXmlStreamReader::StartElement && insidePropstat && insideProp) {
272 // All those elements are properties
273 QString propertyContent = readContentsAsString(reader);
274 if (name == QLatin1String("resourcetype") && propertyContent.contains("collection")) {
275 folders.append(currentHref);
276 } else if (name == QLatin1String("size")) {
277 bool ok = false;
278 auto s = propertyContent.toLongLong(&ok);
279 if (ok && fileInfo) {
280 (*fileInfo)[currentHref].size = s;
281 }
282 } else if (name == QLatin1String("fileid")) {
283 (*fileInfo)[currentHref].fileId = propertyContent.toUtf8();
284 }
285 currentTmpProperties.insert(reader.name().toString(), propertyContent);
286 }
287
288 // End elements with DAV:
289 if (type == QXmlStreamReader::EndElement) {
290 if (reader.namespaceUri() == QLatin1String("DAV:")) {
291 if (reader.name() == "response") {
292 if (currentHref.endsWith('/')) {
293 currentHref.chop(1);
294 }
295 emit directoryListingIterated(currentHref, currentHttp200Properties);
296 currentHref.clear();
297 currentHttp200Properties.clear();
298 } else if (reader.name() == "propstat") {
299 insidePropstat = false;
300 if (currentPropsHaveHttp200) {
301 currentHttp200Properties = QMap<QString, QString>(currentTmpProperties);
302 }
303 currentTmpProperties.clear();
304 currentPropsHaveHttp200 = false;
305 } else if (reader.name() == "prop") {
306 insideProp = false;
307 }
308 }
309 }
310 }
311
312 if (reader.hasError()) {
313 // XML Parser error? Whatever had been emitted before will come as directoryListingIterated
314 qCWarning(lcLsColJob) << "ERROR" << reader.errorString() << xml;
315 return false;
316 } else if (!insideMultiStatus) {
317 qCWarning(lcLsColJob) << "ERROR no WebDAV response?" << xml;
318 return false;
319 } else {
320 emit directoryListingSubfolders(folders);
321 emit finishedWithoutError();
322 }
323 return true;
324 }
325
326 /*********************************************************************************************/
327
LsColJob(AccountPtr account,const QString & path,QObject * parent)328 LsColJob::LsColJob(AccountPtr account, const QString &path, QObject *parent)
329 : AbstractNetworkJob(account, path, parent)
330 {
331 }
332
LsColJob(AccountPtr account,const QUrl & url,QObject * parent)333 LsColJob::LsColJob(AccountPtr account, const QUrl &url, QObject *parent)
334 : AbstractNetworkJob(account, QString(), parent)
335 , _url(url)
336 {
337 }
338
setProperties(QList<QByteArray> properties)339 void LsColJob::setProperties(QList<QByteArray> properties)
340 {
341 _properties = properties;
342 }
343
properties() const344 QList<QByteArray> LsColJob::properties() const
345 {
346 return _properties;
347 }
348
start()349 void LsColJob::start()
350 {
351 QList<QByteArray> properties = _properties;
352
353 if (properties.isEmpty()) {
354 qCWarning(lcLsColJob) << "Propfind with no properties!";
355 }
356 QByteArray propStr;
357 foreach (const QByteArray &prop, properties) {
358 if (prop.contains(':')) {
359 int colIdx = prop.lastIndexOf(":");
360 auto ns = prop.left(colIdx);
361 if (ns == "http://owncloud.org/ns") {
362 propStr += " <oc:" + prop.mid(colIdx + 1) + " />\n";
363 } else {
364 propStr += " <" + prop.mid(colIdx + 1) + " xmlns=\"" + ns + "\" />\n";
365 }
366 } else {
367 propStr += " <d:" + prop + " />\n";
368 }
369 }
370
371 QNetworkRequest req;
372 req.setRawHeader("Depth", "1");
373 QByteArray xml("<?xml version=\"1.0\" ?>\n"
374 "<d:propfind xmlns:d=\"DAV:\" xmlns:oc=\"http://owncloud.org/ns\">\n"
375 " <d:prop>\n"
376 + propStr + " </d:prop>\n"
377 "</d:propfind>\n");
378 auto *buf = new QBuffer(this);
379 buf->setData(xml);
380 buf->open(QIODevice::ReadOnly);
381 if (_url.isValid()) {
382 sendRequest("PROPFIND", _url, req, buf);
383 } else {
384 sendRequest("PROPFIND", makeDavUrl(path()), req, buf);
385 }
386 AbstractNetworkJob::start();
387 }
388
389 // TODO: Instead of doing all in this slot, we should iteratively parse in readyRead(). This
390 // would allow us to be more asynchronous in processing while data is coming from the network,
391 // not all in one big blob at the end.
finished()392 bool LsColJob::finished()
393 {
394 qCInfo(lcLsColJob) << "LSCOL of" << reply()->request().url() << "FINISHED WITH STATUS"
395 << replyStatusString();
396
397 QString contentType = reply()->header(QNetworkRequest::ContentTypeHeader).toString();
398 int httpCode = reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
399 if (httpCode == 207 && contentType.contains("application/xml; charset=utf-8")) {
400 LsColXMLParser parser;
401 connect(&parser, &LsColXMLParser::directoryListingSubfolders,
402 this, &LsColJob::directoryListingSubfolders);
403 connect(&parser, &LsColXMLParser::directoryListingIterated,
404 this, &LsColJob::directoryListingIterated);
405 connect(&parser, &LsColXMLParser::finishedWithError,
406 this, &LsColJob::finishedWithError);
407 connect(&parser, &LsColXMLParser::finishedWithoutError,
408 this, &LsColJob::finishedWithoutError);
409
410 QString expectedPath = reply()->request().url().path(); // something like "/owncloud/remote.php/dav/folder"
411 if (!parser.parse(reply()->readAll(), &_folderInfos, expectedPath)) {
412 // XML parse error
413 emit finishedWithError(reply());
414 }
415 } else {
416 // wrong content type, wrong HTTP code or any other network error
417 emit finishedWithError(reply());
418 }
419
420 return true;
421 }
422
423 /*********************************************************************************************/
424
425 namespace {
426 const char statusphpC[] = "status.php";
427 const char nextcloudDirC[] = "nextcloud/";
428 }
429
CheckServerJob(AccountPtr account,QObject * parent)430 CheckServerJob::CheckServerJob(AccountPtr account, QObject *parent)
431 : AbstractNetworkJob(account, QLatin1String(statusphpC), parent)
432 , _subdirFallback(false)
433 , _permanentRedirects(0)
434 {
435 setIgnoreCredentialFailure(true);
436 connect(this, &AbstractNetworkJob::redirected,
437 this, &CheckServerJob::slotRedirected);
438 }
439
start()440 void CheckServerJob::start()
441 {
442 _serverUrl = account()->url();
443 sendRequest("GET", Utility::concatUrlPath(_serverUrl, path()));
444 connect(reply(), &QNetworkReply::metaDataChanged, this, &CheckServerJob::metaDataChangedSlot);
445 connect(reply(), &QNetworkReply::encrypted, this, &CheckServerJob::encryptedSlot);
446 AbstractNetworkJob::start();
447 }
448
onTimedOut()449 void CheckServerJob::onTimedOut()
450 {
451 qCWarning(lcCheckServerJob) << "TIMEOUT";
452 if (reply() && reply()->isRunning()) {
453 emit timeout(reply()->url());
454 } else if (!reply()) {
455 qCWarning(lcCheckServerJob) << "Timeout even there was no reply?";
456 }
457 deleteLater();
458 }
459
version(const QJsonObject & info)460 QString CheckServerJob::version(const QJsonObject &info)
461 {
462 return info.value(QLatin1String("version")).toString();
463 }
464
versionString(const QJsonObject & info)465 QString CheckServerJob::versionString(const QJsonObject &info)
466 {
467 return info.value(QLatin1String("versionstring")).toString();
468 }
469
installed(const QJsonObject & info)470 bool CheckServerJob::installed(const QJsonObject &info)
471 {
472 return info.value(QLatin1String("installed")).toBool();
473 }
474
mergeSslConfigurationForSslButton(const QSslConfiguration & config,AccountPtr account)475 static void mergeSslConfigurationForSslButton(const QSslConfiguration &config, AccountPtr account)
476 {
477 if (config.peerCertificateChain().length() > 0) {
478 account->_peerCertificateChain = config.peerCertificateChain();
479 }
480 if (!config.sessionCipher().isNull()) {
481 account->_sessionCipher = config.sessionCipher();
482 }
483 if (config.sessionTicket().length() > 0) {
484 account->_sessionTicket = config.sessionTicket();
485 }
486 }
487
encryptedSlot()488 void CheckServerJob::encryptedSlot()
489 {
490 mergeSslConfigurationForSslButton(reply()->sslConfiguration(), account());
491 }
492
slotRedirected(QNetworkReply * reply,const QUrl & targetUrl,int redirectCount)493 void CheckServerJob::slotRedirected(QNetworkReply *reply, const QUrl &targetUrl, int redirectCount)
494 {
495 QByteArray slashStatusPhp("/");
496 slashStatusPhp.append(statusphpC);
497
498 int httpCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
499 QString path = targetUrl.path();
500 if ((httpCode == 301 || httpCode == 308) // permanent redirection
501 && redirectCount == _permanentRedirects // don't apply permanent redirects after a temporary one
502 && path.endsWith(slashStatusPhp)) {
503 _serverUrl = targetUrl;
504 _serverUrl.setPath(path.left(path.size() - slashStatusPhp.size()));
505 qCInfo(lcCheckServerJob) << "status.php was permanently redirected to"
506 << targetUrl << "new server url is" << _serverUrl;
507 ++_permanentRedirects;
508 }
509 }
510
metaDataChangedSlot()511 void CheckServerJob::metaDataChangedSlot()
512 {
513 account()->setSslConfiguration(reply()->sslConfiguration());
514 mergeSslConfigurationForSslButton(reply()->sslConfiguration(), account());
515 }
516
517
finished()518 bool CheckServerJob::finished()
519 {
520 if (reply()->request().url().scheme() == QLatin1String("https")
521 && reply()->sslConfiguration().sessionTicket().isEmpty()
522 && reply()->error() == QNetworkReply::NoError) {
523 qCWarning(lcCheckServerJob) << "No SSL session identifier / session ticket is used, this might impact sync performance negatively.";
524 }
525
526 mergeSslConfigurationForSslButton(reply()->sslConfiguration(), account());
527
528 // The server installs to /owncloud. Let's try that if the file wasn't found
529 // at the original location
530 if ((reply()->error() == QNetworkReply::ContentNotFoundError) && (!_subdirFallback)) {
531 _subdirFallback = true;
532 setPath(QLatin1String(nextcloudDirC) + QLatin1String(statusphpC));
533 start();
534 qCInfo(lcCheckServerJob) << "Retrying with" << reply()->url();
535 return false;
536 }
537
538 QByteArray body = reply()->peek(4 * 1024);
539 int httpStatus = reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
540 if (body.isEmpty() || httpStatus != 200) {
541 qCWarning(lcCheckServerJob) << "error: status.php replied " << httpStatus << body;
542 emit instanceNotFound(reply());
543 } else {
544 QJsonParseError error;
545 auto status = QJsonDocument::fromJson(body, &error);
546 // empty or invalid response
547 if (error.error != QJsonParseError::NoError || status.isNull()) {
548 qCWarning(lcCheckServerJob) << "status.php from server is not valid JSON!" << body << reply()->request().url() << error.errorString();
549 }
550
551 qCInfo(lcCheckServerJob) << "status.php returns: " << status << " " << reply()->error() << " Reply: " << reply();
552 if (status.object().contains("installed")) {
553 emit instanceFound(_serverUrl, status.object());
554 } else {
555 qCWarning(lcCheckServerJob) << "No proper answer on " << reply()->url();
556 emit instanceNotFound(reply());
557 }
558 }
559 return true;
560 }
561
562 /*********************************************************************************************/
563
PropfindJob(AccountPtr account,const QString & path,QObject * parent)564 PropfindJob::PropfindJob(AccountPtr account, const QString &path, QObject *parent)
565 : AbstractNetworkJob(account, path, parent)
566 {
567 }
568
start()569 void PropfindJob::start()
570 {
571 QList<QByteArray> properties = _properties;
572
573 if (properties.isEmpty()) {
574 qCWarning(lcLsColJob) << "Propfind with no properties!";
575 }
576 QNetworkRequest req;
577 // Always have a higher priority than the propagator because we use this from the UI
578 // and really want this to be done first (no matter what internal scheduling QNAM uses).
579 // Also possibly useful for avoiding false timeouts.
580 req.setPriority(QNetworkRequest::HighPriority);
581 req.setRawHeader("Depth", "0");
582 QByteArray propStr;
583 foreach (const QByteArray &prop, properties) {
584 if (prop.contains(':')) {
585 int colIdx = prop.lastIndexOf(":");
586 propStr += " <" + prop.mid(colIdx + 1) + " xmlns=\"" + prop.left(colIdx) + "\" />\n";
587 } else {
588 propStr += " <d:" + prop + " />\n";
589 }
590 }
591 QByteArray xml = "<?xml version=\"1.0\" ?>\n"
592 "<d:propfind xmlns:d=\"DAV:\">\n"
593 " <d:prop>\n"
594 + propStr + " </d:prop>\n"
595 "</d:propfind>\n";
596
597 auto *buf = new QBuffer(this);
598 buf->setData(xml);
599 buf->open(QIODevice::ReadOnly);
600 sendRequest("PROPFIND", makeDavUrl(path()), req, buf);
601
602 AbstractNetworkJob::start();
603 }
604
setProperties(QList<QByteArray> properties)605 void PropfindJob::setProperties(QList<QByteArray> properties)
606 {
607 _properties = properties;
608 }
609
properties() const610 QList<QByteArray> PropfindJob::properties() const
611 {
612 return _properties;
613 }
614
finished()615 bool PropfindJob::finished()
616 {
617 qCInfo(lcPropfindJob) << "PROPFIND of" << reply()->request().url() << "FINISHED WITH STATUS"
618 << replyStatusString();
619
620 int http_result_code = reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
621
622 if (http_result_code == 207) {
623 // Parse DAV response
624 QXmlStreamReader reader(reply());
625 reader.addExtraNamespaceDeclaration(QXmlStreamNamespaceDeclaration("d", "DAV:"));
626
627 QVariantMap items;
628 // introduced to nesting is ignored
629 QStack<QString> curElement;
630
631 while (!reader.atEnd()) {
632 QXmlStreamReader::TokenType type = reader.readNext();
633 if (type == QXmlStreamReader::StartElement) {
634 if (!curElement.isEmpty() && curElement.top() == QLatin1String("prop")) {
635 items.insert(reader.name().toString(), reader.readElementText(QXmlStreamReader::SkipChildElements));
636 } else {
637 curElement.push(reader.name().toString());
638 }
639 }
640 if (type == QXmlStreamReader::EndElement) {
641 if (curElement.top() == reader.name()) {
642 curElement.pop();
643 }
644 }
645 }
646 if (reader.hasError()) {
647 qCWarning(lcPropfindJob) << "XML parser error: " << reader.errorString();
648 emit finishedWithError(reply());
649 } else {
650 emit result(items);
651 }
652 } else {
653 qCWarning(lcPropfindJob) << "*not* successful, http result code is" << http_result_code
654 << (http_result_code == 302 ? reply()->header(QNetworkRequest::LocationHeader).toString() : QLatin1String(""));
655 emit finishedWithError(reply());
656 }
657 return true;
658 }
659
660 /*********************************************************************************************/
661
662 #ifndef TOKEN_AUTH_ONLY
AvatarJob(AccountPtr account,const QString & userId,int size,QObject * parent)663 AvatarJob::AvatarJob(AccountPtr account, const QString &userId, int size, QObject *parent)
664 : AbstractNetworkJob(account, QString(), parent)
665 {
666 if (account->serverVersionInt() >= Account::makeServerVersion(10, 0, 0)) {
667 _avatarUrl = Utility::concatUrlPath(account->url(), QString("remote.php/dav/avatars/%1/%2.png").arg(userId, QString::number(size)));
668 } else {
669 _avatarUrl = Utility::concatUrlPath(account->url(), QString("index.php/avatar/%1/%2").arg(userId, QString::number(size)));
670 }
671 }
672
start()673 void AvatarJob::start()
674 {
675 QNetworkRequest req;
676 sendRequest("GET", _avatarUrl, req);
677 AbstractNetworkJob::start();
678 }
679
makeCircularAvatar(const QImage & baseAvatar)680 QImage AvatarJob::makeCircularAvatar(const QImage &baseAvatar)
681 {
682 if (baseAvatar.isNull()) {
683 return {};
684 }
685
686 int dim = baseAvatar.width();
687
688 QImage avatar(dim, dim, QImage::Format_ARGB32);
689 avatar.fill(Qt::transparent);
690
691 QPainter painter(&avatar);
692 painter.setRenderHint(QPainter::Antialiasing);
693
694 QPainterPath path;
695 path.addEllipse(0, 0, dim, dim);
696 painter.setClipPath(path);
697
698 painter.drawImage(0, 0, baseAvatar);
699 painter.end();
700
701 return avatar;
702 }
703
finished()704 bool AvatarJob::finished()
705 {
706 int http_result_code = reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
707
708 QImage avImage;
709
710 if (http_result_code == 200) {
711 QByteArray pngData = reply()->readAll();
712 if (pngData.size()) {
713 if (avImage.loadFromData(pngData)) {
714 qCDebug(lcAvatarJob) << "Retrieved Avatar pixmap!";
715 }
716 }
717 }
718 emit(avatarPixmap(avImage));
719 return true;
720 }
721 #endif
722
723 /*********************************************************************************************/
724
ProppatchJob(AccountPtr account,const QString & path,QObject * parent)725 ProppatchJob::ProppatchJob(AccountPtr account, const QString &path, QObject *parent)
726 : AbstractNetworkJob(account, path, parent)
727 {
728 }
729
start()730 void ProppatchJob::start()
731 {
732 if (_properties.isEmpty()) {
733 qCWarning(lcProppatchJob) << "Proppatch with no properties!";
734 }
735 QNetworkRequest req;
736
737 QByteArray propStr;
738 QMapIterator<QByteArray, QByteArray> it(_properties);
739 while (it.hasNext()) {
740 it.next();
741 QByteArray keyName = it.key();
742 QByteArray keyNs;
743 if (keyName.contains(':')) {
744 int colIdx = keyName.lastIndexOf(":");
745 keyNs = keyName.left(colIdx);
746 keyName = keyName.mid(colIdx + 1);
747 }
748
749 propStr += " <" + keyName;
750 if (!keyNs.isEmpty()) {
751 propStr += " xmlns=\"" + keyNs + "\" ";
752 }
753 propStr += ">";
754 propStr += it.value();
755 propStr += "</" + keyName + ">\n";
756 }
757 QByteArray xml = "<?xml version=\"1.0\" ?>\n"
758 "<d:propertyupdate xmlns:d=\"DAV:\">\n"
759 " <d:set><d:prop>\n"
760 + propStr + " </d:prop></d:set>\n"
761 "</d:propertyupdate>\n";
762
763 auto *buf = new QBuffer(this);
764 buf->setData(xml);
765 buf->open(QIODevice::ReadOnly);
766 sendRequest("PROPPATCH", makeDavUrl(path()), req, buf);
767 AbstractNetworkJob::start();
768 }
769
setProperties(QMap<QByteArray,QByteArray> properties)770 void ProppatchJob::setProperties(QMap<QByteArray, QByteArray> properties)
771 {
772 _properties = properties;
773 }
774
properties() const775 QMap<QByteArray, QByteArray> ProppatchJob::properties() const
776 {
777 return _properties;
778 }
779
finished()780 bool ProppatchJob::finished()
781 {
782 qCInfo(lcProppatchJob) << "PROPPATCH of" << reply()->request().url() << "FINISHED WITH STATUS"
783 << replyStatusString();
784
785 int http_result_code = reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
786
787 if (http_result_code == 207) {
788 emit success();
789 } else {
790 qCWarning(lcProppatchJob) << "*not* successful, http result code is" << http_result_code
791 << (http_result_code == 302 ? reply()->header(QNetworkRequest::LocationHeader).toString() : QLatin1String(""));
792 emit finishedWithError();
793 }
794 return true;
795 }
796
797 /*********************************************************************************************/
798
EntityExistsJob(AccountPtr account,const QString & path,QObject * parent)799 EntityExistsJob::EntityExistsJob(AccountPtr account, const QString &path, QObject *parent)
800 : AbstractNetworkJob(account, path, parent)
801 {
802 }
803
start()804 void EntityExistsJob::start()
805 {
806 sendRequest("HEAD", makeAccountUrl(path()));
807 AbstractNetworkJob::start();
808 }
809
finished()810 bool EntityExistsJob::finished()
811 {
812 emit exists(reply());
813 return true;
814 }
815
816 /*********************************************************************************************/
817
JsonApiJob(const AccountPtr & account,const QString & path,QObject * parent)818 JsonApiJob::JsonApiJob(const AccountPtr &account, const QString &path, QObject *parent)
819 : AbstractNetworkJob(account, path, parent)
820 {
821 }
822
addQueryParams(const QUrlQuery & params)823 void JsonApiJob::addQueryParams(const QUrlQuery ¶ms)
824 {
825 _additionalParams = params;
826 }
827
addRawHeader(const QByteArray & headerName,const QByteArray & value)828 void JsonApiJob::addRawHeader(const QByteArray &headerName, const QByteArray &value)
829 {
830 _request.setRawHeader(headerName, value);
831 }
832
setBody(const QJsonDocument & body)833 void JsonApiJob::setBody(const QJsonDocument &body)
834 {
835 _body = body.toJson();
836 qCDebug(lcJsonApiJob) << "Set body for request:" << _body;
837 if (!_body.isEmpty()) {
838 _request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
839 }
840 }
841
842
setVerb(Verb value)843 void JsonApiJob::setVerb(Verb value)
844 {
845 _verb = value;
846 }
847
848
verbToString() const849 QByteArray JsonApiJob::verbToString() const
850 {
851 switch (_verb) {
852 case Verb::Get:
853 return "GET";
854 case Verb::Post:
855 return "POST";
856 case Verb::Put:
857 return "PUT";
858 case Verb::Delete:
859 return "DELETE";
860 }
861 return "GET";
862 }
863
start()864 void JsonApiJob::start()
865 {
866 addRawHeader("OCS-APIREQUEST", "true");
867 auto query = _additionalParams;
868 query.addQueryItem(QLatin1String("format"), QLatin1String("json"));
869 QUrl url = Utility::concatUrlPath(account()->url(), path(), query);
870 const auto httpVerb = verbToString();
871 if (!_body.isEmpty()) {
872 sendRequest(httpVerb, url, _request, _body);
873 } else {
874 sendRequest(httpVerb, url, _request);
875 }
876 AbstractNetworkJob::start();
877 }
878
finished()879 bool JsonApiJob::finished()
880 {
881 qCInfo(lcJsonApiJob) << "JsonApiJob of" << reply()->request().url() << "FINISHED WITH STATUS"
882 << replyStatusString();
883
884 int statusCode = 0;
885 int httpStatusCode = reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
886 if (reply()->error() != QNetworkReply::NoError) {
887 qCWarning(lcJsonApiJob) << "Network error: " << path() << errorString() << reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute);
888 statusCode = reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
889 emit jsonReceived(QJsonDocument(), statusCode);
890 return true;
891 }
892
893 QString jsonStr = QString::fromUtf8(reply()->readAll());
894 if (jsonStr.contains("<?xml version=\"1.0\"?>")) {
895 const QRegularExpression rex("<statuscode>(\\d+)</statuscode>");
896 const auto rexMatch = rex.match(jsonStr);
897 if (rexMatch.hasMatch()) {
898 // this is a error message coming back from ocs.
899 statusCode = rexMatch.captured(1).toInt();
900 }
901 } else if(jsonStr.isEmpty() && httpStatusCode == notModifiedStatusCode){
902 qCWarning(lcJsonApiJob) << "Nothing changed so nothing to retrieve - status code: " << httpStatusCode;
903 statusCode = httpStatusCode;
904 } else {
905 const QRegularExpression rex(R"("statuscode":(\d+))");
906 // example: "{"ocs":{"meta":{"status":"ok","statuscode":100,"message":null},"data":{"version":{"major":8,"minor":"... (504)
907 const auto rxMatch = rex.match(jsonStr);
908 if (rxMatch.hasMatch()) {
909 statusCode = rxMatch.captured(1).toInt();
910 }
911 }
912
913 // save new ETag value
914 if(reply()->rawHeaderList().contains("ETag"))
915 emit etagResponseHeaderReceived(reply()->rawHeader("ETag"), statusCode);
916
917 const auto desktopNotificationsAllowed = reply()->rawHeader(QByteArray("X-Nextcloud-User-Status"));
918 if(!desktopNotificationsAllowed.isEmpty()) {
919 emit allowDesktopNotificationsChanged(desktopNotificationsAllowed == "online");
920 }
921
922 QJsonParseError error;
923 auto json = QJsonDocument::fromJson(jsonStr.toUtf8(), &error);
924 // empty or invalid response and status code is != 304 because jsonStr is expected to be empty
925 if ((error.error != QJsonParseError::NoError || json.isNull()) && httpStatusCode != notModifiedStatusCode) {
926 qCWarning(lcJsonApiJob) << "invalid JSON!" << jsonStr << error.errorString();
927 emit jsonReceived(json, statusCode);
928 return true;
929 }
930
931 emit jsonReceived(json, statusCode);
932 return true;
933 }
934
935
DetermineAuthTypeJob(AccountPtr account,QObject * parent)936 DetermineAuthTypeJob::DetermineAuthTypeJob(AccountPtr account, QObject *parent)
937 : QObject(parent)
938 , _account(account)
939 {
940 }
941
start()942 void DetermineAuthTypeJob::start()
943 {
944 qCInfo(lcDetermineAuthTypeJob) << "Determining auth type for" << _account->davUrl();
945
946 QNetworkRequest req;
947 // Prevent HttpCredentialsAccessManager from setting an Authorization header.
948 req.setAttribute(HttpCredentials::DontAddCredentialsAttribute, true);
949 // Don't reuse previous auth credentials
950 req.setAttribute(QNetworkRequest::AuthenticationReuseAttribute, QNetworkRequest::Manual);
951
952 // Start three parallel requests
953
954 // 1. determines whether it's a basic auth server
955 auto get = _account->sendRequest("GET", _account->url(), req);
956
957 // 2. checks the HTTP auth method.
958 auto propfind = _account->sendRequest("PROPFIND", _account->davUrl(), req);
959
960 // 3. Determines if the old flow has to be used (GS for now)
961 auto oldFlowRequired = new JsonApiJob(_account, "/ocs/v2.php/cloud/capabilities", this);
962
963 get->setTimeout(30 * 1000);
964 propfind->setTimeout(30 * 1000);
965 oldFlowRequired->setTimeout(30 * 1000);
966 get->setIgnoreCredentialFailure(true);
967 propfind->setIgnoreCredentialFailure(true);
968 oldFlowRequired->setIgnoreCredentialFailure(true);
969
970 connect(get, &SimpleNetworkJob::finishedSignal, this, [this, get]() {
971 const auto reply = get->reply();
972 const auto wwwAuthenticateHeader = reply->rawHeader("WWW-Authenticate");
973 if (reply->error() == QNetworkReply::AuthenticationRequiredError
974 && (wwwAuthenticateHeader.startsWith("Basic") || wwwAuthenticateHeader.startsWith("Bearer"))) {
975 _resultGet = Basic;
976 } else {
977 _resultGet = LoginFlowV2;
978 }
979 _getDone = true;
980 checkAllDone();
981 });
982 connect(propfind, &SimpleNetworkJob::finishedSignal, this, [this](QNetworkReply *reply) {
983 auto authChallenge = reply->rawHeader("WWW-Authenticate").toLower();
984 if (authChallenge.contains("bearer ")) {
985 _resultPropfind = OAuth;
986 } else {
987 if (authChallenge.isEmpty()) {
988 qCWarning(lcDetermineAuthTypeJob) << "Did not receive WWW-Authenticate reply to auth-test PROPFIND";
989 } else {
990 qCWarning(lcDetermineAuthTypeJob) << "Unknown WWW-Authenticate reply to auth-test PROPFIND:" << authChallenge;
991 }
992 _resultPropfind = Basic;
993 }
994 _propfindDone = true;
995 checkAllDone();
996 });
997 connect(oldFlowRequired, &JsonApiJob::jsonReceived, this, [this](const QJsonDocument &json, int statusCode) {
998 if (statusCode == 200) {
999 _resultOldFlow = LoginFlowV2;
1000
1001 auto data = json.object().value("ocs").toObject().value("data").toObject().value("capabilities").toObject();
1002 auto gs = data.value("globalscale");
1003 if (gs != QJsonValue::Undefined) {
1004 auto flow = gs.toObject().value("desktoplogin");
1005 if (flow != QJsonValue::Undefined) {
1006 if (flow.toInt() == 1) {
1007 #ifdef WITH_WEBENGINE
1008 _resultOldFlow = WebViewFlow;
1009 #else // WITH_WEBENGINE
1010 qCWarning(lcDetermineAuthTypeJob) << "Server does only support flow1, but this client was compiled without support for flow1";
1011 #endif // WITH_WEBENGINE
1012 }
1013 }
1014 }
1015 } else {
1016 _resultOldFlow = Basic;
1017 }
1018 _oldFlowDone = true;
1019 checkAllDone();
1020 });
1021
1022 oldFlowRequired->start();
1023 }
1024
checkAllDone()1025 void DetermineAuthTypeJob::checkAllDone()
1026 {
1027 // Do not conitunue until eve
1028 if (!_getDone || !_propfindDone || !_oldFlowDone) {
1029 return;
1030 }
1031
1032 Q_ASSERT(_resultGet != NoAuthType);
1033 Q_ASSERT(_resultPropfind != NoAuthType);
1034 Q_ASSERT(_resultOldFlow != NoAuthType);
1035
1036 auto result = _resultPropfind;
1037
1038 #ifdef WITH_WEBENGINE
1039 // WebViewFlow > OAuth > Basic
1040 if (_account->serverVersionInt() >= Account::makeServerVersion(12, 0, 0)) {
1041 result = WebViewFlow;
1042 }
1043 #endif // WITH_WEBENGINE
1044
1045 // LoginFlowV2 > WebViewFlow > OAuth > Basic
1046 if (_account->serverVersionInt() >= Account::makeServerVersion(16, 0, 0)) {
1047 result = LoginFlowV2;
1048 }
1049
1050 #ifdef WITH_WEBENGINE
1051 // If we determined that we need the webview flow (GS for example) then we switch to that
1052 if (_resultOldFlow == WebViewFlow) {
1053 result = WebViewFlow;
1054 }
1055 #endif // WITH_WEBENGINE
1056
1057 // If we determined that a simple get gave us an authentication required error
1058 // then the server enforces basic auth and we got no choice but to use this
1059 if (_resultGet == Basic) {
1060 result = Basic;
1061 }
1062
1063 qCInfo(lcDetermineAuthTypeJob) << "Auth type for" << _account->davUrl() << "is" << result;
1064 emit authType(result);
1065 deleteLater();
1066 }
1067
SimpleNetworkJob(AccountPtr account,QObject * parent)1068 SimpleNetworkJob::SimpleNetworkJob(AccountPtr account, QObject *parent)
1069 : AbstractNetworkJob(account, QString(), parent)
1070 {
1071 }
1072
startRequest(const QByteArray & verb,const QUrl & url,QNetworkRequest req,QIODevice * requestBody)1073 QNetworkReply *SimpleNetworkJob::startRequest(const QByteArray &verb, const QUrl &url,
1074 QNetworkRequest req, QIODevice *requestBody)
1075 {
1076 auto reply = sendRequest(verb, url, req, requestBody);
1077 start();
1078 return reply;
1079 }
1080
finished()1081 bool SimpleNetworkJob::finished()
1082 {
1083 emit finishedSignal(reply());
1084 return true;
1085 }
1086
1087
DeleteApiJob(AccountPtr account,const QString & path,QObject * parent)1088 DeleteApiJob::DeleteApiJob(AccountPtr account, const QString &path, QObject *parent)
1089 : AbstractNetworkJob(account, path, parent)
1090 {
1091
1092 }
1093
start()1094 void DeleteApiJob::start()
1095 {
1096 QNetworkRequest req;
1097 req.setRawHeader("OCS-APIREQUEST", "true");
1098 QUrl url = Utility::concatUrlPath(account()->url(), path());
1099 sendRequest("DELETE", url, req);
1100 AbstractNetworkJob::start();
1101 }
1102
finished()1103 bool DeleteApiJob::finished()
1104 {
1105 qCInfo(lcJsonApiJob) << "JsonApiJob of" << reply()->request().url() << "FINISHED WITH STATUS"
1106 << reply()->error()
1107 << (reply()->error() == QNetworkReply::NoError ? QLatin1String("") : errorString());
1108
1109 int httpStatus = reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
1110
1111
1112 if (reply()->error() != QNetworkReply::NoError) {
1113 qCWarning(lcJsonApiJob) << "Network error: " << path() << errorString() << httpStatus;
1114 emit result(httpStatus);
1115 return true;
1116 }
1117
1118 const auto replyData = QString::fromUtf8(reply()->readAll());
1119 qCInfo(lcJsonApiJob()) << "TMX Delete Job" << replyData;
1120 emit result(httpStatus);
1121 return true;
1122 }
1123
fetchPrivateLinkUrl(AccountPtr account,const QString & remotePath,const QByteArray & numericFileId,QObject * target,std::function<void (const QString & url)> targetFun)1124 void fetchPrivateLinkUrl(AccountPtr account, const QString &remotePath,
1125 const QByteArray &numericFileId, QObject *target,
1126 std::function<void(const QString &url)> targetFun)
1127 {
1128 QString oldUrl;
1129 if (!numericFileId.isEmpty())
1130 oldUrl = account->deprecatedPrivateLinkUrl(numericFileId).toString(QUrl::FullyEncoded);
1131
1132 // Retrieve the new link by PROPFIND
1133 auto *job = new PropfindJob(account, remotePath, target);
1134 job->setProperties(
1135 QList<QByteArray>()
1136 << "http://owncloud.org/ns:fileid" // numeric file id for fallback private link generation
1137 << "http://owncloud.org/ns:privatelink");
1138 job->setTimeout(10 * 1000);
1139 QObject::connect(job, &PropfindJob::result, target, [=](const QVariantMap &result) {
1140 auto privateLinkUrl = result["privatelink"].toString();
1141 auto numericFileId = result["fileid"].toByteArray();
1142 if (!privateLinkUrl.isEmpty()) {
1143 targetFun(privateLinkUrl);
1144 } else if (!numericFileId.isEmpty()) {
1145 targetFun(account->deprecatedPrivateLinkUrl(numericFileId).toString(QUrl::FullyEncoded));
1146 } else {
1147 targetFun(oldUrl);
1148 }
1149 });
1150 QObject::connect(job, &PropfindJob::finishedWithError, target, [=](QNetworkReply *) {
1151 targetFun(oldUrl);
1152 });
1153 job->start();
1154 }
1155
1156 } // namespace OCC
1157