1 /*
2 SPDX-FileCopyrightText: 2018 Daniel Vrátil <dvratil@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
5 */
6
7 #include "accountmanager.h"
8 #include "authjob.h"
9 #include "accountstorage_p.h"
10 #include "../debug.h"
11
12 #include <QTimer>
13 #include <QDateTime>
14
15 #include <functional>
16
17 namespace KGAPI2 {
18
19 AccountManager *AccountManager::sInstance = nullptr;
20
21 class AccountPromise::Private
22 {
23 public:
Private(AccountPromise * q)24 Private(AccountPromise *q)
25 : q(q)
26
27 {}
28
setError(const QString & error)29 void setError(const QString &error)
30 {
31 this->error = error;
32 emitFinished();
33 }
34
setAccount(const AccountPtr & account)35 void setAccount(const AccountPtr &account)
36 {
37 this->account = account;
38 emitFinished();
39 }
40
setRunning()41 void setRunning()
42 {
43 mRunning = true;
44 }
45
isRunning() const46 bool isRunning() const
47 {
48 return mRunning;
49 }
50
51
52 QString error;
53 AccountPtr account;
54 private:
emitFinished()55 void emitFinished()
56 {
57 QTimer::singleShot(0, q, [this]() {
58 Q_EMIT q->finished(q);
59 q->deleteLater();
60 });
61 }
62
63 bool mRunning = false;
64 AccountPromise * const q;
65 };
66
67 class AccountManager::Private
68 {
69 public:
Private(AccountManager * q)70 Private(AccountManager *q)
71 : q(q)
72 {}
73
updateAccount(AccountPromise * promise,const QString & apiKey,const QString & apiSecret,const AccountPtr & account,const QList<QUrl> & requestedScopes)74 void updateAccount(AccountPromise *promise, const QString &apiKey, const QString &apiSecret,
75 const AccountPtr &account, const QList<QUrl> &requestedScopes)
76 {
77 if (!requestedScopes.isEmpty()) {
78 auto currentScopes = account->scopes();
79 for (const auto &requestedScope : requestedScopes) {
80 if (!currentScopes.contains(requestedScope)) {
81 currentScopes.push_back(requestedScope);
82 }
83 }
84 if (currentScopes != account->scopes()) {
85 account->setScopes(currentScopes);
86 }
87 }
88 auto *job = new AuthJob(account, apiKey, apiSecret);
89 job->setUsername(account->accountName());
90 connect(job, &AuthJob::finished,
91 [=]() {
92 if (job->error() != KGAPI2::NoError) {
93 promise->d->setError(tr("Failed to authenticate additional scopes"));
94 return;
95 }
96
97 mStore->storeAccount(apiKey, job->account());
98 promise->d->setAccount(job->account());
99 });
100 }
101
createAccount(AccountPromise * promise,const QString & apiKey,const QString & apiSecret,const QString & accountName,const QList<QUrl> & scopes)102 void createAccount(AccountPromise *promise, const QString &apiKey, const QString &apiSecret,
103 const QString &accountName, const QList<QUrl> &scopes)
104 {
105 const auto account = AccountPtr::create(accountName, QString{}, QString{}, scopes);
106 updateAccount(promise, apiKey, apiSecret, account, {});
107 }
108
compareScopes(const QList<QUrl> & currentScopes,const QList<QUrl> & requestedScopes) const109 bool compareScopes(const QList<QUrl> ¤tScopes, const QList<QUrl> &requestedScopes) const {
110 for (const auto &scope : std::as_const(requestedScopes)) {
111 if (!currentScopes.contains(scope)) {
112 return false;
113 }
114 }
115 return true;
116 }
117
ensureStore(const std::function<void (bool)> & callback)118 void ensureStore(const std::function<void(bool)> &callback)
119 {
120 if (!mStore) {
121 mStore = AccountStorageFactory::instance()->create();
122 }
123 if (!mStore->opened()) {
124 mStore->open(callback);
125 } else {
126 callback(true);
127 }
128 }
129
createPromise(const QString & apiKey,const QString & accountName)130 AccountPromise *createPromise(const QString &apiKey, const QString &accountName)
131 {
132 const QString key = apiKey + accountName;
133 auto promise = mPendingPromises.value(key, nullptr);
134 if (!promise) {
135 promise = new AccountPromise(q);
136 QObject::connect(promise, &QObject::destroyed,
137 q, [key, this]() {
138 mPendingPromises.remove(key);
139 });
140 mPendingPromises.insert(key, promise);
141 }
142 return promise;
143 }
144 public:
145 AccountStorage *mStore = nullptr;
146
147 private:
148 QHash<QString, AccountPromise*> mPendingPromises;
149
150 AccountManager * const q;
151 };
152
153 }
154
155 using namespace KGAPI2;
156
157
AccountPromise(QObject * parent)158 AccountPromise::AccountPromise(QObject *parent)
159 : QObject(parent)
160 , d(new Private(this))
161 {
162 }
163
~AccountPromise()164 AccountPromise::~AccountPromise()
165 {
166 }
167
account() const168 AccountPtr AccountPromise::account() const
169 {
170 return d->account;
171 }
172
hasError() const173 bool AccountPromise::hasError() const
174 {
175 return !d->error.isNull();
176 }
177
errorText() const178 QString AccountPromise::errorText() const
179 {
180 return d->error;
181 }
182
183
AccountManager(QObject * parent)184 AccountManager::AccountManager(QObject *parent)
185 : QObject(parent)
186 , d(new Private(this))
187 {
188 }
189
~AccountManager()190 AccountManager::~AccountManager()
191 {
192 }
193
instance()194 AccountManager *AccountManager::instance()
195 {
196 if (!sInstance) {
197 sInstance = new AccountManager;
198 }
199 return sInstance;
200 }
201
getAccount(const QString & apiKey,const QString & apiSecret,const QString & accountName,const QList<QUrl> & scopes)202 AccountPromise *AccountManager::getAccount(const QString &apiKey, const QString &apiSecret,
203 const QString &accountName,
204 const QList<QUrl> &scopes)
205 {
206 auto promise = d->createPromise(apiKey, accountName);
207 if (!promise->d->isRunning()) {
208 // Start the process asynchronously so that caller has a chance to connect
209 // to AccountPromise signals.
210 QTimer::singleShot(0, this, [=]() {
211 d->ensureStore([=](bool storeOpened) {
212 if (!storeOpened) {
213 promise->d->setError(tr("Failed to open account store"));
214 return;
215 }
216
217 const auto account = d->mStore->getAccount(apiKey, accountName);
218 if (!account) {
219 d->createAccount(promise, apiKey, apiSecret, accountName, scopes);
220 } else {
221 if (d->compareScopes(account->scopes(), scopes)) {
222 // Don't hand out obviously expired tokens
223 if (account->expireDateTime() <= QDateTime::currentDateTime()) {
224 d->updateAccount(promise, apiKey, apiSecret, account, scopes);
225 } else {
226 promise->d->setAccount(account);
227 }
228 } else {
229 // Since installed apps can't keep the API secret truly a secret
230 // incremental authorization is not allowed by Google so we need
231 // to request a completely new token from scratch.
232 account->setAccessToken({});
233 account->setRefreshToken({});
234 account->setExpireDateTime({});
235 d->updateAccount(promise, apiKey, apiSecret, account, scopes);
236 }
237 }
238 });
239 });
240 promise->d->setRunning();
241 }
242 return promise;
243 }
244
refreshTokens(const QString & apiKey,const QString & apiSecret,const QString & accountName)245 AccountPromise *AccountManager::refreshTokens(const QString &apiKey, const QString &apiSecret,
246 const QString &accountName)
247 {
248 auto promise = d->createPromise(apiKey, accountName);
249 if (!promise->d->isRunning()) {
250 QTimer::singleShot(0, this, [=]() {
251 d->ensureStore([=](bool storeOpened) {
252 if (!storeOpened) {
253 promise->d->setError(tr("Failed to open account store"));
254 return;
255 }
256
257 const auto account = d->mStore->getAccount(apiKey, accountName);
258 if (!account) {
259 promise->d->setAccount({});
260 } else {
261 d->updateAccount(promise, apiKey, apiSecret, account, {});
262 }
263 });
264 });
265 promise->d->setRunning();
266 }
267 return promise;
268 }
269
270
findAccount(const QString & apiKey,const QString & accountName,const QList<QUrl> & scopes)271 AccountPromise *AccountManager::findAccount(const QString &apiKey, const QString &accountName,
272 const QList<QUrl> &scopes)
273 {
274 auto promise = d->createPromise(apiKey, accountName);
275 if (!promise->d->isRunning()) {
276 QTimer::singleShot(0, this, [=]() {
277 d->ensureStore([=](bool storeOpened) {
278 if (!storeOpened) {
279 promise->d->setError(tr("Failed to open account store"));
280 return;
281 }
282
283 const auto account = d->mStore->getAccount(apiKey, accountName);
284 if (!account) {
285 promise->d->setAccount({});
286 } else {
287 const auto currentScopes = account->scopes();
288 if (scopes.isEmpty() || d->compareScopes(currentScopes, scopes)) {
289 promise->d->setAccount(account);
290 } else {
291 promise->d->setAccount({});
292 }
293 }
294 });
295 });
296 promise->d->setRunning();
297 }
298 return promise;
299 }
300
removeScopes(const QString & apiKey,const QString & accountName,const QList<QUrl> & removedScopes)301 void AccountManager::removeScopes(const QString &apiKey, const QString &accountName,
302 const QList<QUrl> &removedScopes)
303 {
304 d->ensureStore([=](bool storeOpened) {
305 if (!storeOpened) {
306 return;
307 }
308
309 const auto account = d->mStore->getAccount(apiKey, accountName);
310 if (!account) {
311 return;
312 }
313
314 for (const auto &scope : removedScopes) {
315 account->removeScope(scope);
316 }
317 if (account->scopes().isEmpty()) {
318 d->mStore->removeAccount(apiKey, account->accountName());
319 } else {
320 // Since installed apps can't keep the API secret truly a secret
321 // incremental authorization is not allowed by Google so we need
322 // to request a completely new token from scratch.
323 account->setAccessToken({});
324 account->setRefreshToken({});
325 account->setExpireDateTime({});
326 d->mStore->storeAccount(apiKey, account);
327 }
328 });
329 }
330