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