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> &currentScopes, 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