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 "accountstate.h"
16 #include "accountmanager.h"
17 #include "remotewipe.h"
18 #include "account.h"
19 #include "creds/abstractcredentials.h"
20 #include "creds/httpcredentials.h"
21 #include "logger.h"
22 #include "configfile.h"
23 #include "ocsnavigationappsjob.h"
24 
25 #include <QSettings>
26 #include <QTimer>
27 #include <qfontmetrics.h>
28 
29 #include <QJsonDocument>
30 #include <QJsonObject>
31 #include <QJsonArray>
32 #include <QNetworkRequest>
33 #include <QBuffer>
34 
35 namespace OCC {
36 
37 Q_LOGGING_CATEGORY(lcAccountState, "nextcloud.gui.account.state", QtInfoMsg)
38 
AccountState(AccountPtr account)39 AccountState::AccountState(AccountPtr account)
40     : QObject()
41     , _account(account)
42     , _state(AccountState::Disconnected)
43     , _connectionStatus(ConnectionValidator::Undefined)
44     , _waitingForNewCredentials(false)
45     , _maintenanceToConnectedDelay(60000 + (qrand() % (4 * 60000))) // 1-5min delay
46     , _remoteWipe(new RemoteWipe(_account))
47     , _isDesktopNotificationsAllowed(true)
48 {
49     qRegisterMetaType<AccountState *>("AccountState*");
50 
51     connect(account.data(), &Account::invalidCredentials,
52         this, &AccountState::slotHandleRemoteWipeCheck);
53     connect(account.data(), &Account::credentialsFetched,
54         this, &AccountState::slotCredentialsFetched);
55     connect(account.data(), &Account::credentialsAsked,
56         this, &AccountState::slotCredentialsAsked);
57 
58     connect(this, &AccountState::isConnectedChanged, [=]{
59         // Get the Apps available on the server if we're now connected.
60         if (isConnected()) {
61             fetchNavigationApps();
62         }
63     });
64 }
65 
66 AccountState::~AccountState() = default;
67 
loadFromSettings(AccountPtr account,QSettings &)68 AccountState *AccountState::loadFromSettings(AccountPtr account, QSettings & /*settings*/)
69 {
70     auto accountState = new AccountState(account);
71     return accountState;
72 }
73 
writeToSettings(QSettings &)74 void AccountState::writeToSettings(QSettings & /*settings*/)
75 {
76 }
77 
account() const78 AccountPtr AccountState::account() const
79 {
80     return _account;
81 }
82 
connectionStatus() const83 AccountState::ConnectionStatus AccountState::connectionStatus() const
84 {
85     return _connectionStatus;
86 }
87 
connectionErrors() const88 QStringList AccountState::connectionErrors() const
89 {
90     return _connectionErrors;
91 }
92 
state() const93 AccountState::State AccountState::state() const
94 {
95     return _state;
96 }
97 
setState(State state)98 void AccountState::setState(State state)
99 {
100     if (_state != state) {
101         qCInfo(lcAccountState) << "AccountState state change: "
102                                << stateString(_state) << "->" << stateString(state);
103         State oldState = _state;
104         _state = state;
105 
106         if (_state == SignedOut) {
107             _connectionStatus = ConnectionValidator::Undefined;
108             _connectionErrors.clear();
109         } else if (oldState == SignedOut && _state == Disconnected) {
110             // If we stop being voluntarily signed-out, try to connect and
111             // auth right now!
112             checkConnectivity();
113         } else if (_state == ServiceUnavailable) {
114             // Check if we are actually down for maintenance.
115             // To do this we must clear the connection validator that just
116             // produced the 503. It's finished anyway and will delete itself.
117             _connectionValidator.clear();
118             checkConnectivity();
119         }
120         if (oldState == Connected || _state == Connected) {
121             emit isConnectedChanged();
122         }
123     }
124 
125     // might not have changed but the underlying _connectionErrors might have
126     emit stateChanged(_state);
127 }
128 
stateString(State state)129 QString AccountState::stateString(State state)
130 {
131     switch (state) {
132     case SignedOut:
133         return tr("Signed out");
134     case Disconnected:
135         return tr("Disconnected");
136     case Connected:
137         return tr("Connected");
138     case ServiceUnavailable:
139         return tr("Service unavailable");
140     case MaintenanceMode:
141         return tr("Maintenance mode");
142     case NetworkError:
143         return tr("Network error");
144     case ConfigurationError:
145         return tr("Configuration error");
146     case AskingCredentials:
147         return tr("Asking Credentials");
148     }
149     return tr("Unknown account state");
150 }
151 
isSignedOut() const152 bool AccountState::isSignedOut() const
153 {
154     return _state == SignedOut;
155 }
156 
signOutByUi()157 void AccountState::signOutByUi()
158 {
159     account()->credentials()->forgetSensitiveData();
160     account()->clearCookieJar();
161     setState(SignedOut);
162 }
163 
freshConnectionAttempt()164 void AccountState::freshConnectionAttempt()
165 {
166     if (isConnected())
167         setState(Disconnected);
168     checkConnectivity();
169 }
170 
signIn()171 void AccountState::signIn()
172 {
173     if (_state == SignedOut) {
174         _waitingForNewCredentials = false;
175         setState(Disconnected);
176     }
177 }
178 
isConnected() const179 bool AccountState::isConnected() const
180 {
181     return _state == Connected;
182 }
183 
tagLastSuccessfullETagRequest(const QDateTime & tp)184 void AccountState::tagLastSuccessfullETagRequest(const QDateTime &tp)
185 {
186     _timeOfLastETagCheck = tp;
187 }
188 
notificationsEtagResponseHeader() const189 QByteArray AccountState::notificationsEtagResponseHeader() const
190 {
191     return _notificationsEtagResponseHeader;
192 }
193 
setNotificationsEtagResponseHeader(const QByteArray & value)194 void AccountState::setNotificationsEtagResponseHeader(const QByteArray &value)
195 {
196     _notificationsEtagResponseHeader = value;
197 }
198 
navigationAppsEtagResponseHeader() const199 QByteArray AccountState::navigationAppsEtagResponseHeader() const
200 {
201     return _navigationAppsEtagResponseHeader;
202 }
203 
setNavigationAppsEtagResponseHeader(const QByteArray & value)204 void AccountState::setNavigationAppsEtagResponseHeader(const QByteArray &value)
205 {
206     _navigationAppsEtagResponseHeader = value;
207 }
208 
isDesktopNotificationsAllowed() const209 bool AccountState::isDesktopNotificationsAllowed() const
210 {
211     return _isDesktopNotificationsAllowed;
212 }
213 
setDesktopNotificationsAllowed(bool isAllowed)214 void AccountState::setDesktopNotificationsAllowed(bool isAllowed)
215 {
216     if (_isDesktopNotificationsAllowed == isAllowed) {
217         return;
218     }
219 
220     _isDesktopNotificationsAllowed = isAllowed;
221     emit desktopNotificationsAllowedChanged();
222 }
223 
checkConnectivity()224 void AccountState::checkConnectivity()
225 {
226     if (isSignedOut() || _waitingForNewCredentials) {
227         return;
228     }
229 
230     if (_connectionValidator) {
231         qCWarning(lcAccountState) << "ConnectionValidator already running, ignoring" << account()->displayName();
232         return;
233     }
234 
235     // If we never fetched credentials, do that now - otherwise connection attempts
236     // make little sense, we might be missing client certs.
237     if (!account()->credentials()->wasFetched()) {
238         _waitingForNewCredentials = true;
239         account()->credentials()->fetchFromKeychain();
240         return;
241     }
242 
243     // IF the account is connected the connection check can be skipped
244     // if the last successful etag check job is not so long ago.
245     const auto polltime = std::chrono::duration_cast<std::chrono::seconds>(ConfigFile().remotePollInterval());
246     const auto elapsed = _timeOfLastETagCheck.secsTo(QDateTime::currentDateTimeUtc());
247     if (isConnected() && _timeOfLastETagCheck.isValid()
248         && elapsed <= polltime.count()) {
249         qCDebug(lcAccountState) << account()->displayName() << "The last ETag check succeeded within the last " << polltime.count() << "s (" << elapsed << "s). No connection check needed!";
250         return;
251     }
252 
253     auto *conValidator = new ConnectionValidator(AccountStatePtr(this));
254     _connectionValidator = conValidator;
255     connect(conValidator, &ConnectionValidator::connectionResult,
256         this, &AccountState::slotConnectionValidatorResult);
257     if (isConnected()) {
258         // Use a small authed propfind as a minimal ping when we're
259         // already connected.
260         conValidator->checkAuthentication();
261     } else {
262         // Check the server and then the auth.
263 
264         // Let's try this for all OS and see if it fixes the Qt issues we have on Linux  #4720 #3888 #4051
265         //#ifdef Q_OS_WIN
266         // There seems to be a bug in Qt on Windows where QNAM sometimes stops
267         // working correctly after the computer woke up from sleep. See #2895 #2899
268         // and #2973.
269         // As an attempted workaround, reset the QNAM regularly if the account is
270         // disconnected.
271         account()->resetNetworkAccessManager();
272 
273         // If we don't reset the ssl config a second CheckServerJob can produce a
274         // ssl config that does not have a sensible certificate chain.
275         account()->setSslConfiguration(QSslConfiguration());
276         //#endif
277         conValidator->checkServerAndAuth();
278     }
279 }
280 
slotConnectionValidatorResult(ConnectionValidator::Status status,const QStringList & errors)281 void AccountState::slotConnectionValidatorResult(ConnectionValidator::Status status, const QStringList &errors)
282 {
283     if (isSignedOut()) {
284         qCWarning(lcAccountState) << "Signed out, ignoring" << status << _account->url().toString();
285         return;
286     }
287 
288     // Come online gradually from 503 or maintenance mode
289     if (status == ConnectionValidator::Connected
290         && (_connectionStatus == ConnectionValidator::ServiceUnavailable
291             || _connectionStatus == ConnectionValidator::MaintenanceMode)) {
292         if (!_timeSinceMaintenanceOver.isValid()) {
293             qCInfo(lcAccountState) << "AccountState reconnection: delaying for"
294                                    << _maintenanceToConnectedDelay << "ms";
295             _timeSinceMaintenanceOver.start();
296             QTimer::singleShot(_maintenanceToConnectedDelay + 100, this, &AccountState::checkConnectivity);
297             return;
298         } else if (_timeSinceMaintenanceOver.elapsed() < _maintenanceToConnectedDelay) {
299             qCInfo(lcAccountState) << "AccountState reconnection: only"
300                                    << _timeSinceMaintenanceOver.elapsed() << "ms have passed";
301             return;
302         }
303     }
304 
305     if (_connectionStatus != status) {
306         qCInfo(lcAccountState) << "AccountState connection status change: "
307                                << _connectionStatus << "->"
308                                << status;
309         _connectionStatus = status;
310     }
311     _connectionErrors = errors;
312 
313     switch (status) {
314     case ConnectionValidator::Connected:
315         if (_state != Connected) {
316             setState(Connected);
317 
318             // Get the Apps available on the server.
319             fetchNavigationApps();
320 
321             // Setup push notifications after a successful connection
322             account()->trySetupPushNotifications();
323         }
324         break;
325     case ConnectionValidator::Undefined:
326     case ConnectionValidator::NotConfigured:
327         setState(Disconnected);
328         break;
329     case ConnectionValidator::ServerVersionMismatch:
330         setState(ConfigurationError);
331         break;
332     case ConnectionValidator::StatusNotFound:
333         // This can happen either because the server does not exist
334         // or because we are having network issues. The latter one is
335         // much more likely, so keep trying to connect.
336         setState(NetworkError);
337         break;
338     case ConnectionValidator::CredentialsWrong:
339     case ConnectionValidator::CredentialsNotReady:
340         handleInvalidCredentials();
341         break;
342     case ConnectionValidator::SslError:
343         setState(SignedOut);
344         break;
345     case ConnectionValidator::ServiceUnavailable:
346         _timeSinceMaintenanceOver.invalidate();
347         setState(ServiceUnavailable);
348         break;
349     case ConnectionValidator::MaintenanceMode:
350         _timeSinceMaintenanceOver.invalidate();
351         setState(MaintenanceMode);
352         break;
353     case ConnectionValidator::Timeout:
354         setState(NetworkError);
355         break;
356     }
357 }
358 
slotHandleRemoteWipeCheck()359 void AccountState::slotHandleRemoteWipeCheck()
360 {
361     // make sure it changes account state and icons
362     signOutByUi();
363 
364     qCInfo(lcAccountState) << "Invalid credentials for" << _account->url().toString()
365                            << "checking for remote wipe request";
366 
367     _waitingForNewCredentials = false;
368     setState(SignedOut);
369 }
370 
371 
handleInvalidCredentials()372 void AccountState::handleInvalidCredentials()
373 {
374     if (isSignedOut() || _waitingForNewCredentials)
375         return;
376 
377     qCInfo(lcAccountState) << "Invalid credentials for" << _account->url().toString()
378                            << "asking user";
379 
380     _waitingForNewCredentials = true;
381     setState(AskingCredentials);
382 
383     if (account()->credentials()->ready()) {
384         account()->credentials()->invalidateToken();
385     }
386     if (auto creds = qobject_cast<HttpCredentials *>(account()->credentials())) {
387         if (creds->refreshAccessToken())
388             return;
389     }
390     account()->credentials()->askFromUser();
391 }
392 
393 
slotCredentialsFetched(AbstractCredentials *)394 void AccountState::slotCredentialsFetched(AbstractCredentials *)
395 {
396     // Make a connection attempt, no matter whether the credentials are
397     // ready or not - we want to check whether we can get an SSL connection
398     // going before bothering the user for a password.
399     qCInfo(lcAccountState) << "Fetched credentials for" << _account->url().toString()
400                            << "attempting to connect";
401     _waitingForNewCredentials = false;
402     checkConnectivity();
403 }
404 
slotCredentialsAsked(AbstractCredentials * credentials)405 void AccountState::slotCredentialsAsked(AbstractCredentials *credentials)
406 {
407     qCInfo(lcAccountState) << "Credentials asked for" << _account->url().toString()
408                            << "are they ready?" << credentials->ready();
409 
410     _waitingForNewCredentials = false;
411 
412     if (!credentials->ready()) {
413         // User canceled the connection or did not give a password
414         setState(SignedOut);
415         return;
416     }
417 
418     if (_connectionValidator) {
419         // When new credentials become available we always want to restart the
420         // connection validation, even if it's currently running.
421         _connectionValidator->deleteLater();
422         _connectionValidator = nullptr;
423     }
424 
425     checkConnectivity();
426 }
427 
settings()428 std::unique_ptr<QSettings> AccountState::settings()
429 {
430     auto s = ConfigFile::settingsWithGroup(QLatin1String("Accounts"));
431     s->beginGroup(_account->id());
432     return s;
433 }
434 
fetchNavigationApps()435 void AccountState::fetchNavigationApps(){
436     auto *job = new OcsNavigationAppsJob(_account);
437     job->addRawHeader("If-None-Match", navigationAppsEtagResponseHeader());
438     connect(job, &OcsNavigationAppsJob::appsJobFinished, this, &AccountState::slotNavigationAppsFetched);
439     connect(job, &OcsNavigationAppsJob::etagResponseHeaderReceived, this, &AccountState::slotEtagResponseHeaderReceived);
440     connect(job, &OcsNavigationAppsJob::ocsError, this, &AccountState::slotOcsError);
441     job->getNavigationApps();
442 }
443 
slotEtagResponseHeaderReceived(const QByteArray & value,int statusCode)444 void AccountState::slotEtagResponseHeaderReceived(const QByteArray &value, int statusCode){
445     if(statusCode == 200){
446         qCDebug(lcAccountState) << "New navigation apps ETag Response Header received " << value;
447         setNavigationAppsEtagResponseHeader(value);
448     }
449 }
450 
slotOcsError(int statusCode,const QString & message)451 void AccountState::slotOcsError(int statusCode, const QString &message)
452 {
453     qCDebug(lcAccountState) << "Error " << statusCode << " while fetching new navigation apps: " << message;
454 }
455 
slotNavigationAppsFetched(const QJsonDocument & reply,int statusCode)456 void AccountState::slotNavigationAppsFetched(const QJsonDocument &reply, int statusCode)
457 {
458     if(_account){
459         if (statusCode == 304) {
460             qCWarning(lcAccountState) << "Status code " << statusCode << " Not Modified - No new navigation apps.";
461         } else {
462             _apps.clear();
463 
464             if(!reply.isEmpty()){
465                 auto element = reply.object().value("ocs").toObject().value("data");
466                 const auto navLinks = element.toArray();
467 
468                 if(navLinks.size() > 0){
469                     for (const QJsonValue &value : navLinks) {
470                         auto navLink = value.toObject();
471 
472                         auto *app = new AccountApp(navLink.value("name").toString(), QUrl(navLink.value("href").toString()),
473                             navLink.value("id").toString(), QUrl(navLink.value("icon").toString()));
474 
475                         _apps << app;
476                     }
477                 }
478             }
479 
480             emit hasFetchedNavigationApps();
481         }
482     }
483 }
484 
appList() const485 AccountAppList AccountState::appList() const
486 {
487     return _apps;
488 }
489 
findApp(const QString & appId) const490 AccountApp* AccountState::findApp(const QString &appId) const
491 {
492     if(!appId.isEmpty()) {
493         const auto apps = appList();
494         const auto it = std::find_if(apps.cbegin(), apps.cend(), [appId](const auto &app) {
495             return app->id() == appId;
496         });
497         if (it != apps.cend()) {
498             return *it;
499         }
500     }
501 
502     return nullptr;
503 }
504 
505 /*-------------------------------------------------------------------------------------*/
506 
AccountApp(const QString & name,const QUrl & url,const QString & id,const QUrl & iconUrl,QObject * parent)507 AccountApp::AccountApp(const QString &name, const QUrl &url,
508     const QString &id, const QUrl &iconUrl,
509     QObject *parent)
510     : QObject(parent)
511     , _name(name)
512     , _url(url)
513     , _id(id)
514     , _iconUrl(iconUrl)
515 {
516 }
517 
name() const518 QString AccountApp::name() const
519 {
520     return _name;
521 }
522 
url() const523 QUrl AccountApp::url() const
524 {
525     return _url;
526 }
527 
id() const528 QString AccountApp::id() const
529 {
530     return _id;
531 }
532 
iconUrl() const533 QUrl AccountApp::iconUrl() const
534 {
535     return _iconUrl;
536 }
537 
538 /*-------------------------------------------------------------------------------------*/
539 
540 } // namespace OCC
541