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 "account.h"
18#include "creds/abstractcredentials.h"
19#include "creds/httpcredentials.h"
20#include "logger.h"
21#include "configfile.h"
22
23#include <QSettings>
24#include <QTimer>
25#include <qfontmetrics.h>
26
27namespace OCC {
28
29Q_LOGGING_CATEGORY(lcAccountState, "gui.account.state", QtInfoMsg)
30
31AccountState::AccountState(AccountPtr account)
32 : QObject()
33 , _account(account)
34 , _state(AccountState::Disconnected)
35 , _connectionStatus(ConnectionValidator::Undefined)
36 , _waitingForNewCredentials(false)
37 , _maintenanceToConnectedDelay(60000 + (qrand() % (4 * 60000))) // 1-5min delay
38{
39 qRegisterMetaType<AccountState *>("AccountState*");
40
41 connect(account.data(), &Account::invalidCredentials,
42 this, &AccountState::slotInvalidCredentials);
43 connect(account.data(), &Account::credentialsFetched,
44 this, &AccountState::slotCredentialsFetched);
45 connect(account.data(), &Account::credentialsAsked,
46 this, &AccountState::slotCredentialsAsked);
47 _timeSinceLastETagCheck.invalidate();
48}
49
50AccountState::~AccountState()
51{
52}
53
54AccountState *AccountState::loadFromSettings(AccountPtr account, QSettings & /*settings*/)
55{
56 auto accountState = new AccountState(account);
57 return accountState;
58}
59
60void AccountState::writeToSettings(QSettings & /*settings*/)
61{
62}
63
64AccountPtr AccountState::account() const
65{
66 return _account;
67}
68
69AccountState::ConnectionStatus AccountState::connectionStatus() const
70{
71 return _connectionStatus;
72}
73
74QStringList AccountState::connectionErrors() const
75{
76 return _connectionErrors;
77}
78
79AccountState::State AccountState::state() const
80{
81 return _state;
82}
83
84void AccountState::setState(State state)
85{
86 if (_state != state) {
87 qCInfo(lcAccountState) << "AccountState state change: "
88 << stateString(_state) << "->" << stateString(state);
89 State oldState = _state;
90 _state = state;
91
92 if (_state == SignedOut) {
93 _connectionStatus = ConnectionValidator::Undefined;
94 _connectionErrors.clear();
95 } else if (oldState == SignedOut && _state == Disconnected) {
96 // If we stop being voluntarily signed-out, try to connect and
97 // auth right now!
98 checkConnectivity();
99 } else if (_state == ServiceUnavailable) {
100 // Check if we are actually down for maintenance.
101 // To do this we must clear the connection validator that just
102 // produced the 503. It's finished anyway and will delete itself.
103 _connectionValidator.clear();
104 checkConnectivity();
105 }
106 if (oldState == Connected || _state == Connected) {
107 emit isConnectedChanged();
108 }
109 }
110
111 // might not have changed but the underlying _connectionErrors might have
112 emit stateChanged(_state);
113}
114
115QString AccountState::stateString(State state)
116{
117 switch (state) {
118 case SignedOut:
119 return tr("Signed out");
120 case Disconnected:
121 return tr("Disconnected");
122 case Connected:
123 return tr("Connected");
124 case ServiceUnavailable:
125 return tr("Service unavailable");
126 case MaintenanceMode:
127 return tr("Maintenance mode");
128 case NetworkError:
129 return tr("Network error");
130 case ConfigurationError:
131 return tr("Configuration error");
132 case AskingCredentials:
133 return tr("Asking Credentials");
134 }
135 return tr("Unknown account state");
136}
137
138bool AccountState::isSignedOut() const
139{
140 return _state == SignedOut;
141}
142
143void AccountState::signOutByUi()
144{
145 account()->credentials()->forgetSensitiveData();
146 setState(SignedOut);
147}
148
149void AccountState::freshConnectionAttempt()
150{
151 if (isConnected())
152 setState(Disconnected);
153 checkConnectivity();
154}
155
156void AccountState::signIn()
157{
158 if (_state == SignedOut) {
159 _waitingForNewCredentials = false;
160 setState(Disconnected);
161 }
162}
163
164bool AccountState::isConnected() const
165{
166 return _state == Connected;
167}
168
169void AccountState::tagLastSuccessfullETagRequest()
170{
171 _timeSinceLastETagCheck.start();
172}
173
174void AccountState::checkConnectivity()
175{
176 if (isSignedOut() || _waitingForNewCredentials) {
177 return;
178 }
179
180 if (_connectionValidator) {
181 qCWarning(lcAccountState) << "ConnectionValidator already running, ignoring" << account()->displayName();
182 return;
183 }
184
185 // If we never fetched credentials, do that now - otherwise connection attempts
186 // make little sense, we might be missing client certs.
187 if (!account()->credentials()->wasFetched()) {
188 _waitingForNewCredentials = true;
189 account()->credentials()->fetchFromKeychain();
190 return;
191 }
192
193 // IF the account is connected the connection check can be skipped
194 // if the last successful etag check job is not so long ago.
195 ConfigFile cfg;
196 std::chrono::milliseconds polltime = cfg.remotePollInterval();
197
198 if (isConnected() && _timeSinceLastETagCheck.isValid()
199 && _timeSinceLastETagCheck.hasExpired(polltime.count())) {
200 qCDebug(lcAccountState) << account()->displayName() << "The last ETag check succeeded within the last " << polltime.count() / 1000 << " secs. No connection check needed!";
201 return;
202 }
203
204 ConnectionValidator *conValidator = new ConnectionValidator(account());
205 _connectionValidator = conValidator;
206 connect(conValidator, &ConnectionValidator::connectionResult,
207 this, &AccountState::slotConnectionValidatorResult);
208 if (isConnected()) {
209 // Use a small authed propfind as a minimal ping when we're
210 // already connected.
211 conValidator->checkAuthentication();
212 } else {
213 // Check the server and then the auth.
214
215 // Let's try this for all OS and see if it fixes the Qt issues we have on Linux #4720 #3888 #4051
216 //#ifdef Q_OS_WIN
217 // There seems to be a bug in Qt on Windows where QNAM sometimes stops
218 // working correctly after the computer woke up from sleep. See #2895 #2899
219 // and #2973.
220 // As an attempted workaround, reset the QNAM regularly if the account is
221 // disconnected.
222 account()->resetNetworkAccessManager();
223
224 // If we don't reset the ssl config a second CheckServerJob can produce a
225 // ssl config that does not have a sensible certificate chain.
226 account()->setSslConfiguration(QSslConfiguration());
227 //#endif
228 conValidator->checkServerAndAuth();
229 }
230}
231
232void AccountState::slotConnectionValidatorResult(ConnectionValidator::Status status, const QStringList &errors)
233{
234 if (isSignedOut()) {
235 qCWarning(lcAccountState) << "Signed out, ignoring" << status << _account->url().toString();
236 return;
237 }
238
239 // Come online gradually from 503 or maintenance mode
240 if (status == ConnectionValidator::Connected
241 && (_connectionStatus == ConnectionValidator::ServiceUnavailable
242 || _connectionStatus == ConnectionValidator::MaintenanceMode)) {
243 if (!_timeSinceMaintenanceOver.isValid()) {
244 qCInfo(lcAccountState) << "AccountState reconnection: delaying for"
245 << _maintenanceToConnectedDelay << "ms";
246 _timeSinceMaintenanceOver.start();
247 QTimer::singleShot(_maintenanceToConnectedDelay + 100, this, &AccountState::checkConnectivity);
248 return;
249 } else if (_timeSinceMaintenanceOver.elapsed() < _maintenanceToConnectedDelay) {
250 qCInfo(lcAccountState) << "AccountState reconnection: only"
251 << _timeSinceMaintenanceOver.elapsed() << "ms have passed";
252 return;
253 }
254 }
255
256 if (_connectionStatus != status) {
257 qCInfo(lcAccountState) << "AccountState connection status change: "
258 << _connectionStatus << "->"
259 << status;
260 _connectionStatus = status;
261 }
262 _connectionErrors = errors;
263
264 switch (status) {
265 case ConnectionValidator::Connected:
266 if (_state != Connected) {
267 setState(Connected);
268 }
269 break;
270 case ConnectionValidator::Undefined:
271 case ConnectionValidator::NotConfigured:
272 setState(Disconnected);
273 break;
274 case ConnectionValidator::ServerVersionMismatch:
275 setState(ConfigurationError);
276 break;
277 case ConnectionValidator::StatusNotFound:
278 // This can happen either because the server does not exist
279 // or because we are having network issues. The latter one is
280 // much more likely, so keep trying to connect.
281 setState(NetworkError);
282 break;
283 case ConnectionValidator::CredentialsWrong:
284 case ConnectionValidator::CredentialsNotReady:
285 slotInvalidCredentials();
286 break;
287 case ConnectionValidator::SslError:
288 setState(SignedOut);
289 break;
290 case ConnectionValidator::ServiceUnavailable:
291 _timeSinceMaintenanceOver.invalidate();
292 setState(ServiceUnavailable);
293 break;
294 case ConnectionValidator::MaintenanceMode:
295 _timeSinceMaintenanceOver.invalidate();
296 setState(MaintenanceMode);
297 break;
298 case ConnectionValidator::Timeout:
299 setState(NetworkError);
300 break;
301 }
302}
303
304void AccountState::slotInvalidCredentials()
305{
306 if (isSignedOut() || _waitingForNewCredentials)
307 return;
308
309 qCInfo(lcAccountState) << "Invalid credentials for" << _account->url().toString()
310 << "asking user";
311
312 _waitingForNewCredentials = true;
313 setState(AskingCredentials);
314
315 if (account()->credentials()->ready()) {
316 account()->credentials()->invalidateToken();
317 }
318 if (auto creds = qobject_cast<HttpCredentials *>(account()->credentials())) {
319 if (creds->refreshAccessToken())
320 return;
321 }
322 account()->credentials()->askFromUser();
323}
324
325void AccountState::slotCredentialsFetched(AbstractCredentials *)
326{
327 // Make a connection attempt, no matter whether the credentials are
328 // ready or not - we want to check whether we can get an SSL connection
329 // going before bothering the user for a password.
330 qCInfo(lcAccountState) << "Fetched credentials for" << _account->url().toString()
331 << "attempting to connect";
332 _waitingForNewCredentials = false;
333 checkConnectivity();
334}
335
336void AccountState::slotCredentialsAsked(AbstractCredentials *credentials)
337{
338 qCInfo(lcAccountState) << "Credentials asked for" << _account->url().toString()
339 << "are they ready?" << credentials->ready();
340
341 _waitingForNewCredentials = false;
342
343 if (!credentials->ready()) {
344 // User canceled the connection or did not give a password
345 setState(SignedOut);
346 return;
347 }
348
349 if (_connectionValidator) {
350 // When new credentials become available we always want to restart the
351 // connection validation, even if it's currently running.
352 _connectionValidator->deleteLater();
353 _connectionValidator = 0;
354 }
355
356 checkConnectivity();
357}
358
359std::unique_ptr<QSettings> AccountState::settings()
360{
361 auto s = ConfigFile::settingsWithGroup(QLatin1String("Accounts"));
362 s->beginGroup(_account->id());
363 return s;
364}
365
366} // namespace OCC
367