1/*
2 * Copyright (C) by Olivier Goffart <ogoffart@woboq.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 "accountmanager.h"
16#include "configfile.h"
17#include "sslerrordialog.h"
18#include "proxyauthhandler.h"
19#include <theme.h>
20#include <creds/credentialsfactory.h>
21#include <creds/abstractcredentials.h>
22#include <cookiejar.h>
23#include <QSettings>
24#include <QDir>
25#include <QNetworkAccessManager>
26
27namespace {
28static const char urlC[] = "url";
29static const char authTypeC[] = "authType";
30static const char userC[] = "user";
31static const char httpUserC[] = "http_user";
32static const char caCertsKeyC[] = "CaCertificates";
33static const char accountsC[] = "Accounts";
34static const char versionC[] = "version";
35static const char serverVersionC[] = "serverVersion";
36
37// The maximum versions that this client can read
38static const int maxAccountsVersion = 2;
39static const int maxAccountVersion = 1;
40}
41
42
43namespace OCC {
44
45Q_LOGGING_CATEGORY(lcAccountManager, "gui.account.manager", QtInfoMsg)
46
47AccountManager *AccountManager::instance()
48{
49 static AccountManager instance;
50 return &instance;
51}
52
53bool AccountManager::restore()
54{
55 QStringList skipSettingsKeys;
56 backwardMigrationSettingsKeys(&skipSettingsKeys, &skipSettingsKeys);
57
58 auto settings = ConfigFile::settingsWithGroup(QLatin1String(accountsC));
59 if (settings->status() != QSettings::NoError) {
60 qCWarning(lcAccountManager) << "Could not read settings from" << settings->fileName()
61 << settings->status();
62 return false;
63 }
64
65 if (skipSettingsKeys.contains(settings->group())) {
66 // Should not happen: bad container keys should have been deleted
67 qCWarning(lcAccountManager) << "Accounts structure is too new, ignoring";
68 return true;
69 }
70
71 // If there are no accounts, check the old format.
72 if (settings->childGroups().isEmpty()
73 && !settings->contains(QLatin1String(versionC))) {
74 restoreFromLegacySettings();
75 return true;
76 }
77
78 foreach (const auto &accountId, settings->childGroups()) {
79 settings->beginGroup(accountId);
80 if (!skipSettingsKeys.contains(settings->group())) {
81 if (auto acc = loadAccountHelper(*settings)) {
82 acc->_id = accountId;
83 if (auto accState = AccountState::loadFromSettings(acc, *settings)) {
84 addAccountState(accState);
85 }
86 }
87 } else {
88 qCInfo(lcAccountManager) << "Account" << accountId << "is too new, ignoring";
89 _additionalBlockedAccountIds.insert(accountId);
90 }
91 settings->endGroup();
92 }
93
94 return true;
95}
96
97void AccountManager::backwardMigrationSettingsKeys(QStringList *deleteKeys, QStringList *ignoreKeys)
98{
99 auto settings = ConfigFile::settingsWithGroup(QLatin1String(accountsC));
100 const int accountsVersion = settings->value(QLatin1String(versionC)).toInt();
101 if (accountsVersion <= maxAccountsVersion) {
102 foreach (const auto &accountId, settings->childGroups()) {
103 settings->beginGroup(accountId);
104 const int accountVersion = settings->value(QLatin1String(versionC), 1).toInt();
105 if (accountVersion > maxAccountVersion) {
106 ignoreKeys->append(settings->group());
107 }
108 settings->endGroup();
109 }
110 } else {
111 deleteKeys->append(settings->group());
112 }
113}
114
115bool AccountManager::restoreFromLegacySettings()
116{
117 qCInfo(lcAccountManager) << "Migrate: restoreFromLegacySettings, checking settings group"
118 << Theme::instance()->appName();
119
120 // try to open the correctly themed settings
121 auto settings = ConfigFile::settingsWithGroup(Theme::instance()->appName());
122
123 // if the settings file could not be opened, the childKeys list is empty
124 // then try to load settings from a very old place
125 if (settings->childKeys().isEmpty()) {
126 // Now try to open the original ownCloud settings to see if they exist.
127 QString oCCfgFile = QDir::fromNativeSeparators(settings->fileName());
128 // replace the last two segments with ownCloud/owncloud.cfg
129 oCCfgFile = oCCfgFile.left(oCCfgFile.lastIndexOf('/'));
130 oCCfgFile = oCCfgFile.left(oCCfgFile.lastIndexOf('/'));
131 oCCfgFile += QLatin1String("/ownCloud/owncloud.cfg");
132
133 qCInfo(lcAccountManager) << "Migrate: checking old config " << oCCfgFile;
134
135 QFileInfo fi(oCCfgFile);
136 if (fi.isReadable()) {
137 std::unique_ptr<QSettings> oCSettings(new QSettings(oCCfgFile, QSettings::IniFormat));
138 oCSettings->beginGroup(QLatin1String("ownCloud"));
139
140 // Check the theme url to see if it is the same url that the oC config was for
141 QString overrideUrl = Theme::instance()->overrideServerUrl();
142 if (!overrideUrl.isEmpty()) {
143 if (overrideUrl.endsWith('/')) {
144 overrideUrl.chop(1);
145 }
146 QString oCUrl = oCSettings->value(QLatin1String(urlC)).toString();
147 if (oCUrl.endsWith('/')) {
148 oCUrl.chop(1);
149 }
150
151 // in case the urls are equal reset the settings object to read from
152 // the ownCloud settings object
153 qCInfo(lcAccountManager) << "Migrate oC config if " << oCUrl << " == " << overrideUrl << ":"
154 << (oCUrl == overrideUrl ? "Yes" : "No");
155 if (oCUrl == overrideUrl) {
156 settings = std::move(oCSettings);
157 }
158 }
159 }
160 }
161
162 // Try to load the single account.
163 if (!settings->childKeys().isEmpty()) {
164 if (auto acc = loadAccountHelper(*settings)) {
165 addAccount(acc);
166 return true;
167 }
168 }
169 return false;
170}
171
172void AccountManager::save(bool saveCredentials)
173{
174 auto settings = ConfigFile::settingsWithGroup(QLatin1String(accountsC));
175 settings->setValue(QLatin1String(versionC), maxAccountsVersion);
176 foreach (const auto &acc, _accounts) {
177 settings->beginGroup(acc->account()->id());
178 saveAccountHelper(acc->account().data(), *settings, saveCredentials);
179 acc->writeToSettings(*settings);
180 settings->endGroup();
181 }
182
183 settings->sync();
184 qCInfo(lcAccountManager) << "Saved all account settings, status:" << settings->status();
185}
186
187void AccountManager::saveAccount(Account *a)
188{
189 qCInfo(lcAccountManager) << "Saving account" << a->url().toString();
190 auto settings = ConfigFile::settingsWithGroup(QLatin1String(accountsC));
191 settings->beginGroup(a->id());
192 saveAccountHelper(a, *settings, false); // don't save credentials they might not have been loaded yet
193 settings->endGroup();
194
195 settings->sync();
196 qCInfo(lcAccountManager) << "Saved account settings, status:" << settings->status();
197}
198
199void AccountManager::saveAccountState(AccountState *a)
200{
201 qCInfo(lcAccountManager) << "Saving account state" << a->account()->url().toString();
202 auto settings = ConfigFile::settingsWithGroup(QLatin1String(accountsC));
203 settings->beginGroup(a->account()->id());
204 a->writeToSettings(*settings);
205 settings->endGroup();
206
207 settings->sync();
208 qCInfo(lcAccountManager) << "Saved account state settings, status:" << settings->status();
209}
210
211void AccountManager::saveAccountHelper(Account *acc, QSettings &settings, bool saveCredentials)
212{
213 settings.setValue(QLatin1String(versionC), maxAccountVersion);
214 settings.setValue(QLatin1String(urlC), acc->_url.toString());
215 settings.setValue(QLatin1String(serverVersionC), acc->_serverVersion);
216 if (acc->_credentials) {
217 if (saveCredentials) {
218 // Only persist the credentials if the parameter is set, on migration from 1.8.x
219 // we want to save the accounts but not overwrite the credentials
220 // (This is easier than asynchronously fetching the credentials from keychain and then
221 // re-persisting them)
222 acc->_credentials->persist();
223 }
224 Q_FOREACH (QString key, acc->_settingsMap.keys()) {
225 settings.setValue(key, acc->_settingsMap.value(key));
226 }
227 settings.setValue(QLatin1String(authTypeC), acc->_credentials->authType());
228
229 // HACK: Save http_user also as user
230 if (acc->_settingsMap.contains(httpUserC))
231 settings.setValue(userC, acc->_settingsMap.value(httpUserC));
232 }
233
234 // Save accepted certificates.
235 settings.beginGroup(QLatin1String("General"));
236 qCInfo(lcAccountManager) << "Saving " << acc->approvedCerts().count() << " unknown certs.";
237 QByteArray certs;
238 Q_FOREACH (const QSslCertificate &cert, acc->approvedCerts()) {
239 certs += cert.toPem() + '\n';
240 }
241 if (!certs.isEmpty()) {
242 settings.setValue(QLatin1String(caCertsKeyC), certs);
243 }
244 settings.endGroup();
245
246 // Save cookies.
247 if (acc->_am) {
248 CookieJar *jar = qobject_cast<CookieJar *>(acc->_am->cookieJar());
249 if (jar) {
250 qCInfo(lcAccountManager) << "Saving cookies." << acc->cookieJarPath();
251 jar->save(acc->cookieJarPath());
252 }
253 }
254}
255
256AccountPtr AccountManager::loadAccountHelper(QSettings &settings)
257{
258 auto urlConfig = settings.value(QLatin1String(urlC));
259 if (!urlConfig.isValid()) {
260 // No URL probably means a corrupted entry in the account settings
261 qCWarning(lcAccountManager) << "No URL for account " << settings.group();
262 return AccountPtr();
263 }
264
265 auto acc = createAccount();
266
267 QString authType = settings.value(QLatin1String(authTypeC)).toString();
268
269 // There was an account-type saving bug when 'skip folder config' was used
270 // See #5408. This attempts to fix up the "dummy" authType
271 if (authType == QLatin1String("dummy")) {
272 if (settings.contains(QLatin1String("http_user"))) {
273 authType = "http";
274 } else if (settings.contains(QLatin1String("shibboleth_shib_user"))) {
275 authType = "shibboleth";
276 }
277 }
278
279 QString overrideUrl = Theme::instance()->overrideServerUrl();
280 QString forceAuth = Theme::instance()->forceConfigAuthType();
281 if (!forceAuth.isEmpty() && !overrideUrl.isEmpty()) {
282 // If forceAuth is set, this might also mean the overrideURL has changed.
283 // See enterprise issues #1126
284 acc->setUrl(overrideUrl);
285 authType = forceAuth;
286 } else {
287 acc->setUrl(urlConfig.toUrl());
288 }
289
290 qCInfo(lcAccountManager) << "Account for" << acc->url() << "using auth type" << authType;
291
292 acc->_serverVersion = settings.value(QLatin1String(serverVersionC)).toString();
293
294 // We want to only restore settings for that auth type and the user value
295 acc->_settingsMap.insert(QLatin1String(userC), settings.value(userC));
296 QString authTypePrefix = authType + "_";
297 Q_FOREACH (QString key, settings.childKeys()) {
298 if (!key.startsWith(authTypePrefix))
299 continue;
300 acc->_settingsMap.insert(key, settings.value(key));
301 }
302
303 acc->setCredentials(CredentialsFactory::create(authType));
304
305 // now the server cert, it is in the general group
306 settings.beginGroup(QLatin1String("General"));
307 acc->setApprovedCerts(QSslCertificate::fromData(settings.value(caCertsKeyC).toByteArray()));
308 settings.endGroup();
309
310 return acc;
311}
312
313AccountStatePtr AccountManager::account(const QString &name)
314{
315 foreach (const auto &acc, _accounts) {
316 if (acc->account()->displayName() == name) {
317 return acc;
318 }
319 }
320 return AccountStatePtr();
321}
322
323AccountState *AccountManager::addAccount(const AccountPtr &newAccount)
324{
325 auto id = newAccount->id();
326 if (id.isEmpty() || !isAccountIdAvailable(id)) {
327 id = generateFreeAccountId();
328 }
329 newAccount->_id = id;
330
331 auto newAccountState = new AccountState(newAccount);
332 addAccountState(newAccountState);
333 return newAccountState;
334}
335
336void AccountManager::deleteAccount(AccountState *account)
337{
338 auto it = std::find(_accounts.begin(), _accounts.end(), account);
339 if (it == _accounts.end()) {
340 return;
341 }
342 auto copy = *it; // keep a reference to the shared pointer so it does not delete it just yet
343 _accounts.erase(it);
344
345 // Forget account credentials, cookies
346 account->account()->credentials()->forgetSensitiveData();
347 QFile::remove(account->account()->cookieJarPath());
348
349 auto settings = ConfigFile::settingsWithGroup(QLatin1String(accountsC));
350 settings->remove(account->account()->id());
351
352 emit accountRemoved(account);
353}
354
355AccountPtr AccountManager::createAccount()
356{
357 AccountPtr acc = Account::create();
358 acc->setSslErrorHandler(new SslDialogErrorHandler);
359 connect(acc.data(), &Account::proxyAuthenticationRequired,
360 ProxyAuthHandler::instance(), &ProxyAuthHandler::handleProxyAuthenticationRequired);
361 return acc;
362}
363
364void AccountManager::shutdown()
365{
366 auto accountsCopy = _accounts;
367 _accounts.clear();
368 foreach (const auto &acc, accountsCopy) {
369 emit accountRemoved(acc.data());
370 }
371}
372
373bool AccountManager::isAccountIdAvailable(const QString &id) const
374{
375 foreach (const auto &acc, _accounts) {
376 if (acc->account()->id() == id) {
377 return false;
378 }
379 }
380 if (_additionalBlockedAccountIds.contains(id))
381 return false;
382 return true;
383}
384
385QString AccountManager::generateFreeAccountId() const
386{
387 int i = 0;
388 forever {
389 QString id = QString::number(i);
390 if (isAccountIdAvailable(id)) {
391 return id;
392 }
393 ++i;
394 }
395}
396
397void AccountManager::addAccountState(AccountState *accountState)
398{
399 QObject::connect(accountState->account().data(),
400 &Account::wantsAccountSaved,
401 this, &AccountManager::saveAccount);
402
403 AccountStatePtr ptr(accountState);
404 _accounts << ptr;
405 emit accountAdded(accountState);
406}
407}
408