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 &params)
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