1 /*
2 * Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful, but
10 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12 * for more details.
13 */
14
15 #include "account.h"
16 #include "accountfwd.h"
17 #include "clientsideencryptionjobs.h"
18 #include "cookiejar.h"
19 #include "networkjobs.h"
20 #include "configfile.h"
21 #include "accessmanager.h"
22 #include "creds/abstractcredentials.h"
23 #include "capabilities.h"
24 #include "theme.h"
25 #include "pushnotifications.h"
26 #include "version.h"
27
28 #include <deletejob.h>
29
30 #include "common/asserts.h"
31 #include "clientsideencryption.h"
32 #include "ocsuserstatusconnector.h"
33
34 #include <QLoggingCategory>
35 #include <QNetworkReply>
36 #include <QNetworkAccessManager>
37 #include <QSslSocket>
38 #include <QNetworkCookieJar>
39 #include <QNetworkProxy>
40
41 #include <QFileInfo>
42 #include <QDir>
43 #include <QSslKey>
44 #include <QAuthenticator>
45 #include <QStandardPaths>
46 #include <QJsonDocument>
47 #include <QJsonObject>
48 #include <QJsonArray>
49 #include <QLoggingCategory>
50 #include <QHttpMultiPart>
51
52 #include <qsslconfiguration.h>
53 #include <qt5keychain/keychain.h>
54 #include "creds/abstractcredentials.h"
55
56 using namespace QKeychain;
57
58 namespace {
59 constexpr int pushNotificationsReconnectInterval = 1000 * 60 * 2;
60 constexpr int usernamePrefillServerVersinMinSupportedMajor = 24;
61 }
62
63 namespace OCC {
64
65 Q_LOGGING_CATEGORY(lcAccount, "nextcloud.sync.account", QtInfoMsg)
66 const char app_password[] = "_app-password";
67
Account(QObject * parent)68 Account::Account(QObject *parent)
69 : QObject(parent)
70 , _capabilities(QVariantMap())
71 {
72 qRegisterMetaType<AccountPtr>("AccountPtr");
73 qRegisterMetaType<Account *>("Account*");
74
75 _pushNotificationsReconnectTimer.setInterval(pushNotificationsReconnectInterval);
76 connect(&_pushNotificationsReconnectTimer, &QTimer::timeout, this, &Account::trySetupPushNotifications);
77 }
78
create()79 AccountPtr Account::create()
80 {
81 AccountPtr acc = AccountPtr(new Account);
82 acc->setSharedThis(acc);
83 return acc;
84 }
85
e2e()86 ClientSideEncryption* Account::e2e()
87 {
88 // Qt expects everything in the connect to be a pointer, so return a pointer.
89 return &_e2e;
90 }
91
92 Account::~Account() = default;
93
davPath() const94 QString Account::davPath() const
95 {
96 return davPathBase() + QLatin1Char('/') + davUser() + QLatin1Char('/');
97 }
98
setSharedThis(AccountPtr sharedThis)99 void Account::setSharedThis(AccountPtr sharedThis)
100 {
101 _sharedThis = sharedThis.toWeakRef();
102 setupUserStatusConnector();
103 }
104
davPathBase()105 QString Account::davPathBase()
106 {
107 return QStringLiteral("/remote.php/dav/files");
108 }
109
sharedFromThis()110 AccountPtr Account::sharedFromThis()
111 {
112 return _sharedThis.toStrongRef();
113 }
114
davUser() const115 QString Account::davUser() const
116 {
117 return _davUser.isEmpty() && _credentials ? _credentials->user() : _davUser;
118 }
119
setDavUser(const QString & newDavUser)120 void Account::setDavUser(const QString &newDavUser)
121 {
122 if (_davUser == newDavUser)
123 return;
124 _davUser = newDavUser;
125 emit wantsAccountSaved(this);
126 }
127
128 #ifndef TOKEN_AUTH_ONLY
avatar() const129 QImage Account::avatar() const
130 {
131 return _avatarImg;
132 }
setAvatar(const QImage & img)133 void Account::setAvatar(const QImage &img)
134 {
135 _avatarImg = img;
136 emit accountChangedAvatar();
137 }
138 #endif
139
displayName() const140 QString Account::displayName() const
141 {
142 QString dn = QString("%1@%2").arg(credentials()->user(), _url.host());
143 int port = url().port();
144 if (port > 0 && port != 80 && port != 443) {
145 dn.append(QLatin1Char(':'));
146 dn.append(QString::number(port));
147 }
148 return dn;
149 }
150
davDisplayName() const151 QString Account::davDisplayName() const
152 {
153 return _displayName;
154 }
155
setDavDisplayName(const QString & newDisplayName)156 void Account::setDavDisplayName(const QString &newDisplayName)
157 {
158 _displayName = newDisplayName;
159 emit accountChangedDisplayName();
160 }
161
id() const162 QString Account::id() const
163 {
164 return _id;
165 }
166
credentials() const167 AbstractCredentials *Account::credentials() const
168 {
169 return _credentials.data();
170 }
171
setCredentials(AbstractCredentials * cred)172 void Account::setCredentials(AbstractCredentials *cred)
173 {
174 // set active credential manager
175 QNetworkCookieJar *jar = nullptr;
176 QNetworkProxy proxy;
177
178 if (_am) {
179 jar = _am->cookieJar();
180 jar->setParent(nullptr);
181
182 // Remember proxy (issue #2108)
183 proxy = _am->proxy();
184
185 _am = QSharedPointer<QNetworkAccessManager>();
186 }
187
188 // The order for these two is important! Reading the credential's
189 // settings accesses the account as well as account->_credentials,
190 _credentials.reset(cred);
191 cred->setAccount(this);
192
193 // Note: This way the QNAM can outlive the Account and Credentials.
194 // This is necessary to avoid issues with the QNAM being deleted while
195 // processing slotHandleSslErrors().
196 _am = QSharedPointer<QNetworkAccessManager>(_credentials->createQNAM(), &QObject::deleteLater);
197
198 if (jar) {
199 _am->setCookieJar(jar);
200 }
201 if (proxy.type() != QNetworkProxy::DefaultProxy) {
202 _am->setProxy(proxy);
203 }
204 connect(_am.data(), SIGNAL(sslErrors(QNetworkReply *, QList<QSslError>)),
205 SLOT(slotHandleSslErrors(QNetworkReply *, QList<QSslError>)));
206 connect(_am.data(), &QNetworkAccessManager::proxyAuthenticationRequired,
207 this, &Account::proxyAuthenticationRequired);
208 connect(_credentials.data(), &AbstractCredentials::fetched,
209 this, &Account::slotCredentialsFetched);
210 connect(_credentials.data(), &AbstractCredentials::asked,
211 this, &Account::slotCredentialsAsked);
212
213 trySetupPushNotifications();
214 }
215
setPushNotificationsReconnectInterval(int interval)216 void Account::setPushNotificationsReconnectInterval(int interval)
217 {
218 _pushNotificationsReconnectTimer.setInterval(interval);
219 }
220
trySetupPushNotifications()221 void Account::trySetupPushNotifications()
222 {
223 // Stop the timer to prevent parallel setup attempts
224 _pushNotificationsReconnectTimer.stop();
225
226 if (_capabilities.availablePushNotifications() != PushNotificationType::None) {
227 qCInfo(lcAccount) << "Try to setup push notifications";
228
229 if (!_pushNotifications) {
230 _pushNotifications = new PushNotifications(this, this);
231
232 connect(_pushNotifications, &PushNotifications::ready, this, [this]() {
233 _pushNotificationsReconnectTimer.stop();
234 emit pushNotificationsReady(this);
235 });
236
237 const auto disablePushNotifications = [this]() {
238 qCInfo(lcAccount) << "Disable push notifications object because authentication failed or connection lost";
239 if (!_pushNotifications) {
240 return;
241 }
242 if (!_pushNotifications->isReady()) {
243 emit pushNotificationsDisabled(this);
244 }
245 if (!_pushNotificationsReconnectTimer.isActive()) {
246 _pushNotificationsReconnectTimer.start();
247 }
248 };
249
250 connect(_pushNotifications, &PushNotifications::connectionLost, this, disablePushNotifications);
251 connect(_pushNotifications, &PushNotifications::authenticationFailed, this, disablePushNotifications);
252 }
253 // If push notifications already running it is no problem to call setup again
254 _pushNotifications->setup();
255 }
256 }
257
davUrl() const258 QUrl Account::davUrl() const
259 {
260 return Utility::concatUrlPath(url(), davPath());
261 }
262
deprecatedPrivateLinkUrl(const QByteArray & numericFileId) const263 QUrl Account::deprecatedPrivateLinkUrl(const QByteArray &numericFileId) const
264 {
265 return Utility::concatUrlPath(_userVisibleUrl,
266 QLatin1String("/index.php/f/") + QUrl::toPercentEncoding(QString::fromLatin1(numericFileId)));
267 }
268
269 /**
270 * clear all cookies. (Session cookies or not)
271 */
clearCookieJar()272 void Account::clearCookieJar()
273 {
274 auto jar = qobject_cast<CookieJar *>(_am->cookieJar());
275 ASSERT(jar);
276 jar->setAllCookies(QList<QNetworkCookie>());
277 emit wantsAccountSaved(this);
278 }
279
280 /*! This shares our official cookie jar (containing all the tasty
281 authentication cookies) with another QNAM while making sure
282 of not losing its ownership. */
lendCookieJarTo(QNetworkAccessManager * guest)283 void Account::lendCookieJarTo(QNetworkAccessManager *guest)
284 {
285 auto jar = _am->cookieJar();
286 auto oldParent = jar->parent();
287 guest->setCookieJar(jar); // takes ownership of our precious cookie jar
288 jar->setParent(oldParent); // takes it back
289 }
290
cookieJarPath()291 QString Account::cookieJarPath()
292 {
293 return QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation) + "/cookies" + id() + ".db";
294 }
295
resetNetworkAccessManager()296 void Account::resetNetworkAccessManager()
297 {
298 if (!_credentials || !_am) {
299 return;
300 }
301
302 qCDebug(lcAccount) << "Resetting QNAM";
303 QNetworkCookieJar *jar = _am->cookieJar();
304 QNetworkProxy proxy = _am->proxy();
305
306 // Use a QSharedPointer to allow locking the life of the QNAM on the stack.
307 // Make it call deleteLater to make sure that we can return to any QNAM stack frames safely.
308 _am = QSharedPointer<QNetworkAccessManager>(_credentials->createQNAM(), &QObject::deleteLater);
309
310 _am->setCookieJar(jar); // takes ownership of the old cookie jar
311 _am->setProxy(proxy); // Remember proxy (issue #2108)
312
313 connect(_am.data(), SIGNAL(sslErrors(QNetworkReply *, QList<QSslError>)),
314 SLOT(slotHandleSslErrors(QNetworkReply *, QList<QSslError>)));
315 connect(_am.data(), &QNetworkAccessManager::proxyAuthenticationRequired,
316 this, &Account::proxyAuthenticationRequired);
317 }
318
networkAccessManager()319 QNetworkAccessManager *Account::networkAccessManager()
320 {
321 return _am.data();
322 }
323
sharedNetworkAccessManager()324 QSharedPointer<QNetworkAccessManager> Account::sharedNetworkAccessManager()
325 {
326 return _am;
327 }
328
sendRawRequest(const QByteArray & verb,const QUrl & url,QNetworkRequest req,QIODevice * data)329 QNetworkReply *Account::sendRawRequest(const QByteArray &verb, const QUrl &url, QNetworkRequest req, QIODevice *data)
330 {
331 req.setUrl(url);
332 req.setSslConfiguration(this->getOrCreateSslConfig());
333 if (verb == "HEAD" && !data) {
334 return _am->head(req);
335 } else if (verb == "GET" && !data) {
336 return _am->get(req);
337 } else if (verb == "POST") {
338 return _am->post(req, data);
339 } else if (verb == "PUT") {
340 return _am->put(req, data);
341 } else if (verb == "DELETE" && !data) {
342 return _am->deleteResource(req);
343 }
344 return _am->sendCustomRequest(req, verb, data);
345 }
346
sendRawRequest(const QByteArray & verb,const QUrl & url,QNetworkRequest req,const QByteArray & data)347 QNetworkReply *Account::sendRawRequest(const QByteArray &verb, const QUrl &url, QNetworkRequest req, const QByteArray &data)
348 {
349 req.setUrl(url);
350 req.setSslConfiguration(this->getOrCreateSslConfig());
351 if (verb == "HEAD" && data.isEmpty()) {
352 return _am->head(req);
353 } else if (verb == "GET" && data.isEmpty()) {
354 return _am->get(req);
355 } else if (verb == "POST") {
356 return _am->post(req, data);
357 } else if (verb == "PUT") {
358 return _am->put(req, data);
359 } else if (verb == "DELETE" && data.isEmpty()) {
360 return _am->deleteResource(req);
361 }
362 return _am->sendCustomRequest(req, verb, data);
363 }
364
sendRawRequest(const QByteArray & verb,const QUrl & url,QNetworkRequest req,QHttpMultiPart * data)365 QNetworkReply *Account::sendRawRequest(const QByteArray &verb, const QUrl &url, QNetworkRequest req, QHttpMultiPart *data)
366 {
367 req.setUrl(url);
368 req.setSslConfiguration(this->getOrCreateSslConfig());
369 if (verb == "PUT") {
370 return _am->put(req, data);
371 } else if (verb == "POST") {
372 return _am->post(req, data);
373 }
374 return _am->sendCustomRequest(req, verb, data);
375 }
376
sendRequest(const QByteArray & verb,const QUrl & url,QNetworkRequest req,QIODevice * data)377 SimpleNetworkJob *Account::sendRequest(const QByteArray &verb, const QUrl &url, QNetworkRequest req, QIODevice *data)
378 {
379 auto job = new SimpleNetworkJob(sharedFromThis());
380 job->startRequest(verb, url, req, data);
381 return job;
382 }
383
setSslConfiguration(const QSslConfiguration & config)384 void Account::setSslConfiguration(const QSslConfiguration &config)
385 {
386 _sslConfiguration = config;
387 }
388
getOrCreateSslConfig()389 QSslConfiguration Account::getOrCreateSslConfig()
390 {
391 if (!_sslConfiguration.isNull()) {
392 // Will be set by CheckServerJob::finished()
393 // We need to use a central shared config to get SSL session tickets
394 return _sslConfiguration;
395 }
396
397 // if setting the client certificate fails, you will probably get an error similar to this:
398 // "An internal error number 1060 happened. SSL handshake failed, client certificate was requested: SSL error: sslv3 alert handshake failure"
399 QSslConfiguration sslConfig = QSslConfiguration::defaultConfiguration();
400
401 // Try hard to re-use session for different requests
402 sslConfig.setSslOption(QSsl::SslOptionDisableSessionTickets, false);
403 sslConfig.setSslOption(QSsl::SslOptionDisableSessionSharing, false);
404 sslConfig.setSslOption(QSsl::SslOptionDisableSessionPersistence, false);
405
406 sslConfig.setOcspStaplingEnabled(Theme::instance()->enableStaplingOCSP());
407
408 return sslConfig;
409 }
410
setApprovedCerts(const QList<QSslCertificate> certs)411 void Account::setApprovedCerts(const QList<QSslCertificate> certs)
412 {
413 _approvedCerts = certs;
414 QSslConfiguration::defaultConfiguration().addCaCertificates(certs);
415 }
416
addApprovedCerts(const QList<QSslCertificate> certs)417 void Account::addApprovedCerts(const QList<QSslCertificate> certs)
418 {
419 _approvedCerts += certs;
420 }
421
resetRejectedCertificates()422 void Account::resetRejectedCertificates()
423 {
424 _rejectedCertificates.clear();
425 }
426
setSslErrorHandler(AbstractSslErrorHandler * handler)427 void Account::setSslErrorHandler(AbstractSslErrorHandler *handler)
428 {
429 _sslErrorHandler.reset(handler);
430 }
431
setUrl(const QUrl & url)432 void Account::setUrl(const QUrl &url)
433 {
434 _url = url;
435 _userVisibleUrl = url;
436 }
437
setUserVisibleHost(const QString & host)438 void Account::setUserVisibleHost(const QString &host)
439 {
440 _userVisibleUrl.setHost(host);
441 }
442
credentialSetting(const QString & key) const443 QVariant Account::credentialSetting(const QString &key) const
444 {
445 if (_credentials) {
446 QString prefix = _credentials->authType();
447 QVariant value = _settingsMap.value(prefix + "_" + key);
448 if (value.isNull()) {
449 value = _settingsMap.value(key);
450 }
451 return value;
452 }
453 return QVariant();
454 }
455
setCredentialSetting(const QString & key,const QVariant & value)456 void Account::setCredentialSetting(const QString &key, const QVariant &value)
457 {
458 if (_credentials) {
459 QString prefix = _credentials->authType();
460 _settingsMap.insert(prefix + "_" + key, value);
461 }
462 }
463
slotHandleSslErrors(QNetworkReply * reply,QList<QSslError> errors)464 void Account::slotHandleSslErrors(QNetworkReply *reply, QList<QSslError> errors)
465 {
466 NetworkJobTimeoutPauser pauser(reply);
467 QString out;
468 QDebug(&out) << "SSL-Errors happened for url " << reply->url().toString();
469 foreach (const QSslError &error, errors) {
470 QDebug(&out) << "\tError in " << error.certificate() << ":"
471 << error.errorString() << "(" << error.error() << ")"
472 << "\n";
473 }
474
475 qCInfo(lcAccount()) << "ssl errors" << out;
476 qCInfo(lcAccount()) << reply->sslConfiguration().peerCertificateChain();
477
478 bool allPreviouslyRejected = true;
479 foreach (const QSslError &error, errors) {
480 if (!_rejectedCertificates.contains(error.certificate())) {
481 allPreviouslyRejected = false;
482 }
483 }
484
485 // If all certs have previously been rejected by the user, don't ask again.
486 if (allPreviouslyRejected) {
487 qCInfo(lcAccount) << out << "Certs not trusted by user decision, returning.";
488 return;
489 }
490
491 QList<QSslCertificate> approvedCerts;
492 if (_sslErrorHandler.isNull()) {
493 qCWarning(lcAccount) << out << "called without valid SSL error handler for account" << url();
494 return;
495 }
496
497 // SslDialogErrorHandler::handleErrors will run an event loop that might execute
498 // the deleteLater() of the QNAM before we have the chance of unwinding our stack.
499 // Keep a ref here on our stackframe to make sure that it doesn't get deleted before
500 // handleErrors returns.
501 QSharedPointer<QNetworkAccessManager> qnamLock = _am;
502 QPointer<QObject> guard = reply;
503
504 if (_sslErrorHandler->handleErrors(errors, reply->sslConfiguration(), &approvedCerts, sharedFromThis())) {
505 if (!guard)
506 return;
507
508 if (!approvedCerts.isEmpty()) {
509 QSslConfiguration::defaultConfiguration().addCaCertificates(approvedCerts);
510 addApprovedCerts(approvedCerts);
511 emit wantsAccountSaved(this);
512
513 // all ssl certs are known and accepted. We can ignore the problems right away.
514 qCInfo(lcAccount) << out << "Certs are known and trusted! This is not an actual error.";
515 }
516
517 // Warning: Do *not* use ignoreSslErrors() (without args) here:
518 // it permanently ignores all SSL errors for this host, even
519 // certificate changes.
520 reply->ignoreSslErrors(errors);
521 } else {
522 if (!guard)
523 return;
524
525 // Mark all involved certificates as rejected, so we don't ask the user again.
526 foreach (const QSslError &error, errors) {
527 if (!_rejectedCertificates.contains(error.certificate())) {
528 _rejectedCertificates.append(error.certificate());
529 }
530 }
531
532 // Not calling ignoreSslErrors will make the SSL handshake fail.
533 return;
534 }
535 }
536
slotCredentialsFetched()537 void Account::slotCredentialsFetched()
538 {
539 if (_davUser.isEmpty()) {
540 qCDebug(lcAccount) << "User id not set. Fetch it.";
541 const auto fetchUserNameJob = new JsonApiJob(sharedFromThis(), QStringLiteral("/ocs/v1.php/cloud/user"));
542 connect(fetchUserNameJob, &JsonApiJob::jsonReceived, this, [this, fetchUserNameJob](const QJsonDocument &json, int statusCode) {
543 fetchUserNameJob->deleteLater();
544 if (statusCode != 100) {
545 qCWarning(lcAccount) << "Could not fetch user id. Login will probably not work.";
546 emit credentialsFetched(_credentials.data());
547 return;
548 }
549
550 const auto objData = json.object().value("ocs").toObject().value("data").toObject();
551 const auto userId = objData.value("id").toString("");
552 setDavUser(userId);
553 emit credentialsFetched(_credentials.data());
554 });
555 fetchUserNameJob->start();
556 } else {
557 qCDebug(lcAccount) << "User id already fetched.";
558 emit credentialsFetched(_credentials.data());
559 }
560 }
561
slotCredentialsAsked()562 void Account::slotCredentialsAsked()
563 {
564 emit credentialsAsked(_credentials.data());
565 }
566
handleInvalidCredentials()567 void Account::handleInvalidCredentials()
568 {
569 // Retrieving password will trigger remote wipe check job
570 retrieveAppPassword();
571
572 emit invalidCredentials();
573 }
574
clearQNAMCache()575 void Account::clearQNAMCache()
576 {
577 _am->clearAccessCache();
578 }
579
capabilities() const580 const Capabilities &Account::capabilities() const
581 {
582 return _capabilities;
583 }
584
setCapabilities(const QVariantMap & caps)585 void Account::setCapabilities(const QVariantMap &caps)
586 {
587 _capabilities = Capabilities(caps);
588
589 setupUserStatusConnector();
590 trySetupPushNotifications();
591 }
592
setupUserStatusConnector()593 void Account::setupUserStatusConnector()
594 {
595 _userStatusConnector = std::make_shared<OcsUserStatusConnector>(sharedFromThis());
596 connect(_userStatusConnector.get(), &UserStatusConnector::userStatusFetched, this, [this](const UserStatus &) {
597 emit userStatusChanged();
598 });
599 connect(_userStatusConnector.get(), &UserStatusConnector::messageCleared, this, [this] {
600 emit userStatusChanged();
601 });
602 }
603
serverVersion() const604 QString Account::serverVersion() const
605 {
606 return _serverVersion;
607 }
608
serverVersionInt() const609 int Account::serverVersionInt() const
610 {
611 // FIXME: Use Qt 5.5 QVersionNumber
612 auto components = serverVersion().split('.');
613 return makeServerVersion(components.value(0).toInt(),
614 components.value(1).toInt(),
615 components.value(2).toInt());
616 }
617
makeServerVersion(int majorVersion,int minorVersion,int patchVersion)618 int Account::makeServerVersion(int majorVersion, int minorVersion, int patchVersion)
619 {
620 return (majorVersion << 16) + (minorVersion << 8) + patchVersion;
621 }
622
serverVersionUnsupported() const623 bool Account::serverVersionUnsupported() const
624 {
625 if (serverVersionInt() == 0) {
626 // not detected yet, assume it is fine.
627 return false;
628 }
629 return serverVersionInt() < makeServerVersion(NEXTCLOUD_SERVER_VERSION_MIN_SUPPORTED_MAJOR,
630 NEXTCLOUD_SERVER_VERSION_MIN_SUPPORTED_MINOR, NEXTCLOUD_SERVER_VERSION_MIN_SUPPORTED_PATCH);
631 }
632
isUsernamePrefillSupported() const633 bool Account::isUsernamePrefillSupported() const
634 {
635 return serverVersionInt() >= makeServerVersion(usernamePrefillServerVersinMinSupportedMajor, 0, 0);
636 }
637
setServerVersion(const QString & version)638 void Account::setServerVersion(const QString &version)
639 {
640 if (version == _serverVersion) {
641 return;
642 }
643
644 auto oldServerVersion = _serverVersion;
645 _serverVersion = version;
646 emit serverVersionChanged(this, oldServerVersion, version);
647 }
648
writeAppPasswordOnce(QString appPassword)649 void Account::writeAppPasswordOnce(QString appPassword){
650 if(_wroteAppPassword)
651 return;
652
653 // Fix: Password got written from Account Wizard, before finish.
654 // Only write the app password for a connected account, else
655 // there'll be a zombie keychain slot forever, never used again ;p
656 //
657 // Also don't write empty passwords (Log out -> Relaunch)
658 if(id().isEmpty() || appPassword.isEmpty())
659 return;
660
661 const QString kck = AbstractCredentials::keychainKey(
662 url().toString(),
663 davUser() + app_password,
664 id()
665 );
666
667 auto *job = new WritePasswordJob(Theme::instance()->appName());
668 job->setInsecureFallback(false);
669 job->setKey(kck);
670 job->setBinaryData(appPassword.toLatin1());
671 connect(job, &WritePasswordJob::finished, [this](Job *incoming) {
672 auto *writeJob = static_cast<WritePasswordJob *>(incoming);
673 if (writeJob->error() == NoError)
674 qCInfo(lcAccount) << "appPassword stored in keychain";
675 else
676 qCWarning(lcAccount) << "Unable to store appPassword in keychain" << writeJob->errorString();
677
678 // We don't try this again on error, to not raise CPU consumption
679 _wroteAppPassword = true;
680 });
681 job->start();
682 }
683
retrieveAppPassword()684 void Account::retrieveAppPassword(){
685 const QString kck = AbstractCredentials::keychainKey(
686 url().toString(),
687 credentials()->user() + app_password,
688 id()
689 );
690
691 auto *job = new ReadPasswordJob(Theme::instance()->appName());
692 job->setInsecureFallback(false);
693 job->setKey(kck);
694 connect(job, &ReadPasswordJob::finished, [this](Job *incoming) {
695 auto *readJob = static_cast<ReadPasswordJob *>(incoming);
696 QString pwd("");
697 // Error or no valid public key error out
698 if (readJob->error() == NoError &&
699 readJob->binaryData().length() > 0) {
700 pwd = readJob->binaryData();
701 }
702
703 emit appPasswordRetrieved(pwd);
704 });
705 job->start();
706 }
707
deleteAppPassword()708 void Account::deleteAppPassword()
709 {
710 const QString kck = AbstractCredentials::keychainKey(
711 url().toString(),
712 credentials()->user() + app_password,
713 id()
714 );
715
716 if (kck.isEmpty()) {
717 qCDebug(lcAccount) << "appPassword is empty";
718 return;
719 }
720
721 auto *job = new DeletePasswordJob(Theme::instance()->appName());
722 job->setInsecureFallback(false);
723 job->setKey(kck);
724 connect(job, &DeletePasswordJob::finished, [this](Job *incoming) {
725 auto *deleteJob = static_cast<DeletePasswordJob *>(incoming);
726 if (deleteJob->error() == NoError)
727 qCInfo(lcAccount) << "appPassword deleted from keychain";
728 else
729 qCWarning(lcAccount) << "Unable to delete appPassword from keychain" << deleteJob->errorString();
730
731 // Allow storing a new app password on re-login
732 _wroteAppPassword = false;
733 });
734 job->start();
735 }
736
deleteAppToken()737 void Account::deleteAppToken()
738 {
739 const auto deleteAppTokenJob = new DeleteJob(sharedFromThis(), QStringLiteral("/ocs/v2.php/core/apppassword"));
740 connect(deleteAppTokenJob, &DeleteJob::finishedSignal, this, [this]() {
741 if (const auto deleteJob = qobject_cast<DeleteJob *>(QObject::sender())) {
742 const auto httpCode = deleteJob->reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
743 if (httpCode != 200) {
744 qCWarning(lcAccount) << "AppToken remove failed for user: " << displayName() << " with code: " << httpCode;
745 } else {
746 qCInfo(lcAccount) << "AppToken for user: " << displayName() << " has been removed.";
747 }
748 } else {
749 Q_ASSERT(false);
750 qCWarning(lcAccount) << "The sender is not a DeleteJob instance.";
751 }
752 });
753 deleteAppTokenJob->start();
754 }
755
fetchDirectEditors(const QUrl & directEditingURL,const QString & directEditingETag)756 void Account::fetchDirectEditors(const QUrl &directEditingURL, const QString &directEditingETag)
757 {
758 if(directEditingURL.isEmpty() || directEditingETag.isEmpty())
759 return;
760
761 // Check for the directEditing capability
762 if (!directEditingURL.isEmpty() &&
763 (directEditingETag.isEmpty() || directEditingETag != _lastDirectEditingETag)) {
764 // Fetch the available editors and their mime types
765 auto *job = new JsonApiJob(sharedFromThis(), QLatin1String("ocs/v2.php/apps/files/api/v1/directEditing"));
766 QObject::connect(job, &JsonApiJob::jsonReceived, this, &Account::slotDirectEditingRecieved);
767 job->start();
768 }
769 }
770
slotDirectEditingRecieved(const QJsonDocument & json)771 void Account::slotDirectEditingRecieved(const QJsonDocument &json)
772 {
773 auto data = json.object().value("ocs").toObject().value("data").toObject();
774 auto editors = data.value("editors").toObject();
775
776 foreach (auto editorKey, editors.keys()) {
777 auto editor = editors.value(editorKey).toObject();
778
779 const QString id = editor.value("id").toString();
780 const QString name = editor.value("name").toString();
781
782 if(!id.isEmpty() && !name.isEmpty()) {
783 auto mimeTypes = editor.value("mimetypes").toArray();
784 auto optionalMimeTypes = editor.value("optionalMimetypes").toArray();
785
786 auto *directEditor = new DirectEditor(id, name);
787
788 foreach(auto mimeType, mimeTypes) {
789 directEditor->addMimetype(mimeType.toString().toLatin1());
790 }
791
792 foreach(auto optionalMimeType, optionalMimeTypes) {
793 directEditor->addOptionalMimetype(optionalMimeType.toString().toLatin1());
794 }
795
796 _capabilities.addDirectEditor(directEditor);
797 }
798 }
799 }
800
pushNotifications() const801 PushNotifications *Account::pushNotifications() const
802 {
803 return _pushNotifications;
804 }
805
userStatusConnector() const806 std::shared_ptr<UserStatusConnector> Account::userStatusConnector() const
807 {
808 return _userStatusConnector;
809 }
810
811 } // namespace OCC
812