1 /*
2  *    SPDX-FileCopyrightText: 2017 Daniel Vrátil <dvratil@kde.org>
3  *
4  *    SPDX-License-Identifier: GPL-3.0-or-later
5  */
6 
7 #include "tokenjobs.h"
8 #include "graph.h"
9 #include "resource_debug.h"
10 
11 #include <QContextMenuEvent>
12 #include <QDialog>
13 #include <QLineEdit>
14 #include <QMessageBox>
15 #include <QProgressBar>
16 #include <QTimer>
17 #include <QToolButton>
18 #include <QVBoxLayout>
19 
20 #include <QJsonDocument>
21 #include <QUrlQuery>
22 
23 #include <QWebEngineCertificateError>
24 #include <QWebEngineCookieStore>
25 #include <QWebEnginePage>
26 #include <QWebEngineProfile>
27 #include <QWebEngineView>
28 
29 #include <KWallet>
30 
31 #include <KLocalizedString>
32 
33 namespace
34 {
35 static const auto KWalletFolder = QStringLiteral("Facebook");
36 static const auto KWalletKeyToken = QStringLiteral("token");
37 static const auto KWalletKeyName = QStringLiteral("name");
38 static const auto KWalletKeyId = QStringLiteral("id");
39 static const auto KWalletKeyCookies = QStringLiteral("cookies");
40 
41 class TokenManager
42 {
43 public:
~TokenManager()44     ~TokenManager()
45     {
46         delete wallet;
47         wallet = nullptr;
48     }
49 
50     KWallet::Wallet *wallet = nullptr;
51     QString token;
52     QString userName;
53     QString id;
54     QByteArray cookies;
55 };
56 
57 Q_GLOBAL_STATIC(TokenManager, d)
58 
59 class WebView : public QWebEngineView
60 {
61     Q_OBJECT
62 public:
WebView(QWidget * parent=nullptr)63     explicit WebView(QWidget *parent = nullptr)
64         : QWebEngineView(parent)
65     {
66     }
67 
contextMenuEvent(QContextMenuEvent * e)68     void contextMenuEvent(QContextMenuEvent *e) override
69     {
70         e->accept();
71     }
72 };
73 
74 class WebPage : public QWebEnginePage
75 {
76     Q_OBJECT
77 public:
WebPage(QWebEngineProfile * profile,QObject * parent=nullptr)78     explicit WebPage(QWebEngineProfile *profile, QObject *parent = nullptr)
79         : QWebEnginePage(profile, parent)
80     {
81     }
82 
lastCeritificateError() const83     QWebEngineCertificateError *lastCeritificateError() const
84     {
85         return mLastError;
86     }
87 
certificateError(const QWebEngineCertificateError & err)88     bool certificateError(const QWebEngineCertificateError &err) override
89     {
90         delete mLastError;
91         mLastError = new QWebEngineCertificateError(err.error(), err.url(), err.isOverridable(), err.errorDescription());
92         Q_EMIT sslError({});
93 
94         return false;
95     }
96 
97 Q_SIGNALS:
98     void sslError(QPrivateSignal);
99 
100 private:
101     QWebEngineCertificateError *mLastError = nullptr;
102 };
103 
104 class AuthDialog : public QDialog
105 {
106     Q_OBJECT
107 
108 public:
AuthDialog(const QByteArray & cookies,const QString & resourceIdentifier,QWidget * parent=nullptr)109     AuthDialog(const QByteArray &cookies, const QString &resourceIdentifier, QWidget *parent = nullptr)
110         : QDialog(parent)
111     {
112         setModal(true);
113         setMinimumSize(1080, 880); // minimal size to fit in facebook login screen
114                                    // without scrollbars
115 
116         auto v = new QVBoxLayout(this);
117 
118         auto h = new QHBoxLayout(this);
119         h->setSpacing(0);
120         mSslIndicator = new QToolButton(this);
121         connect(mSslIndicator, &QToolButton::clicked, this, [this]() {
122             auto page = qobject_cast<WebPage *>(mView->page());
123             if (auto err = page->lastCeritificateError()) {
124                 QMessageBox msg;
125                 msg.setIconPixmap(QIcon::fromTheme(QStringLiteral("security-low")).pixmap(64));
126                 msg.setText(err->errorDescription());
127                 msg.addButton(QMessageBox::Ok);
128                 msg.exec();
129             }
130         });
131         h->addWidget(mSslIndicator);
132 
133         mUrlEdit = new QLineEdit(this);
134         mUrlEdit->setReadOnly(true);
135         h->addWidget(mUrlEdit);
136 
137         v->addLayout(h);
138 
139         auto progressBar = new QProgressBar(this);
140         progressBar->setMinimum(0);
141         progressBar->setMaximum(100);
142         progressBar->setValue(0);
143         v->addWidget(progressBar);
144 
145         // Create a special profile just for us
146         auto profile = new QWebEngineProfile(resourceIdentifier, this);
147         auto cookieStore = profile->cookieStore();
148         cookieStore->deleteAllCookies(); // delete all cookies from it
149         const auto parsedCookies = QNetworkCookie::parseCookies(cookies);
150         for (const auto &parsedCookie : parsedCookies) {
151             cookieStore->setCookie(parsedCookie, QUrl(QStringLiteral("https://www.facebook.com")));
152             mCookies.insert(parsedCookie.name(), parsedCookie.toRawForm());
153         }
154         connect(cookieStore, &QWebEngineCookieStore::cookieAdded, this, [this](const QNetworkCookie &cookie) {
155             if (cookie.domain() == QLatin1String(".facebook.com")) {
156                 mCookies.insert(cookie.name(), cookie.toRawForm());
157             }
158         });
159         connect(cookieStore, &QWebEngineCookieStore::cookieRemoved, this, [this](const QNetworkCookie &cookie) {
160             mCookies.remove(cookie.name());
161         });
162 
163         mView = new WebView(this);
164         auto webpage = new WebPage(profile, mView);
165         connect(webpage, &WebPage::sslError, this, [this]() {
166             setSslIcon(QStringLiteral("security-low"));
167         });
168         mView->setPage(webpage);
169         v->addWidget(mView);
170 
171         connect(mView, &WebView::loadProgress, progressBar, &QProgressBar::setValue);
172         connect(mView, &WebView::urlChanged, this, &AuthDialog::onUrlChanged);
173 
174         mShowTimer = new QTimer(this);
175         mShowTimer->setSingleShot(true);
176         mShowTimer->setInterval(1000);
177         connect(mShowTimer, &QTimer::timeout, this, &QWidget::show);
178     }
179 
~AuthDialog()180     ~AuthDialog()
181     {
182     }
183 
run()184     void run()
185     {
186         QUrl url(QStringLiteral("https://www.facebook.com/v2.9/dialog/oauth"));
187         QUrlQuery query;
188         query.addQueryItem(QStringLiteral("client_id"), Graph::appId());
189         query.addQueryItem(QStringLiteral("redirect_uri"), QStringLiteral("https://www.facebook.com/connect/login_success.html"));
190         query.addQueryItem(QStringLiteral("response_type"), QStringLiteral("token"));
191         query.addQueryItem(QStringLiteral("scope"), Graph::scopes());
192         url.setQuery(query);
193 
194         mView->load(url);
195 
196         // Don't show the dialog here, we will only show it if we are stuck on
197         // login.php for longer than a second
198     }
199 
token() const200     QString token() const
201     {
202         return mToken;
203     }
204 
cookies() const205     QByteArray cookies() const
206     {
207         QByteArray rv;
208         for (auto it = mCookies.cbegin(), end = mCookies.cend(); it != end; ++it) {
209             rv += it.value() + '\n';
210         }
211         return rv;
212     }
213 
214 Q_SIGNALS:
215     void authDone();
216 
217 private Q_SLOTS:
onUrlChanged(const QUrl & newUrl)218     void onUrlChanged(const QUrl &newUrl)
219     {
220         mUrlEdit->setText(newUrl.toDisplayString(QUrl::PrettyDecoded));
221         mUrlEdit->setCursorPosition(0);
222 
223         if (!newUrl.host().contains(QLatin1String(".facebook.com"))) {
224             setSslIcon(QStringLiteral("security-medium"));
225             return;
226         }
227 
228         if (qobject_cast<WebPage *>(mView->page())->lastCeritificateError()) {
229             setSslIcon(QStringLiteral("security-low"));
230         } else {
231             setSslIcon(QStringLiteral("security-high"));
232         }
233 
234         if (newUrl.path() == QLatin1String("/login.php")) {
235             if (!isVisible() && !mShowTimer->isActive()) {
236                 // If we get stuck on login.php for at least a second, then it means
237                 // facebook wants user login, otherwise we are just immediately redirected
238                 // to login_success.html, which causes the webview to just flash on the
239                 // screen, which is not nice, and can be confusing if it happens randomly
240                 // when the resource is syncing in the background
241                 mShowTimer->start();
242             }
243         } else if (newUrl.path() == QLatin1String("/connect/login_success.html")) {
244             mShowTimer->stop();
245             QUrlQuery query(newUrl.fragment());
246             mToken = query.queryItemValue(QStringLiteral("access_token"));
247             hide();
248 
249             Q_EMIT authDone();
250         }
251     }
252 
setSslIcon(const QString & iconName)253     void setSslIcon(const QString &iconName)
254     {
255         // FIXME: workaround for silly Breeze icons: the small 22x22 icons are
256         // monochromatic, which is absolutely useless since we are trying to security
257         // information here, so instead we force use the bigger 48x48 icons which
258         // have colors and downscale them
259         mSslIndicator->setIcon(QIcon::fromTheme(iconName).pixmap(48));
260     }
261 
262 private:
263     QWebEngineView *mView = nullptr;
264     QTimer *mShowTimer = nullptr;
265     QToolButton *mSslIndicator = nullptr;
266     QLineEdit *mUrlEdit = nullptr;
267     QString mToken;
268     QMap<QByteArray, QByteArray> mCookies;
269 };
270 } // namespace
271 
TokenJob(const QString & identifier,QObject * parent)272 TokenJob::TokenJob(const QString &identifier, QObject *parent)
273     : KJob(parent)
274     , mIdentifier(identifier)
275 {
276 }
277 
~TokenJob()278 TokenJob::~TokenJob()
279 {
280 }
281 
start()282 void TokenJob::start()
283 {
284     if (!d->wallet) {
285         d->wallet = KWallet::Wallet::openWallet(KWallet::Wallet::NetworkWallet(), 0, KWallet::Wallet::Asynchronous);
286         if (!d->wallet) {
287             emitError(i18n("Failed to open KWallet"));
288             return;
289         }
290     }
291 
292     if (d->wallet->isOpen()) {
293         doStart();
294     } else {
295         connect(d->wallet, &KWallet::Wallet::walletOpened, this, [this]() {
296             if (!d->wallet->isOpen()) {
297                 delete d->wallet;
298                 d->wallet = nullptr;
299                 emitError(i18n("Failed to open KWallet"));
300                 return;
301             }
302 
303             if (!d->wallet->hasFolder(KWalletFolder)) {
304                 d->wallet->createFolder(KWalletFolder);
305             }
306 
307             d->wallet->setFolder(KWalletFolder);
308 
309             doStart();
310         });
311     }
312 }
313 
emitError(const QString & text)314 void TokenJob::emitError(const QString &text)
315 {
316     setError(KJob::UserDefinedError);
317     setErrorText(text);
318     emitResult();
319 }
320 
LoginJob(const QString & identifier,QObject * parent)321 LoginJob::LoginJob(const QString &identifier, QObject *parent)
322     : TokenJob(identifier, parent)
323 {
324 }
325 
~LoginJob()326 LoginJob::~LoginJob()
327 {
328 }
329 
token() const330 QString LoginJob::token() const
331 {
332     return d->token;
333 }
334 
doStart()335 void LoginJob::doStart()
336 {
337     auto dlg = new AuthDialog(d->cookies, mIdentifier);
338     connect(dlg, &AuthDialog::authDone, this, [this, dlg]() {
339         dlg->deleteLater();
340         d->token = dlg->token();
341         d->cookies = dlg->cookies();
342         if (d->token.isEmpty()) {
343             emitError(i18n("Failed to obtain access token from Facebook"));
344             return;
345         }
346 
347         fetchUserInfo();
348     });
349 
350     dlg->run();
351 }
352 
fetchUserInfo()353 void LoginJob::fetchUserInfo()
354 {
355     auto job = Graph::job(QStringLiteral("me"), d->token, {QStringLiteral("id"), QStringLiteral("name")});
356     connect(job, &KJob::result, this, [this, job]() {
357         if (job->error()) {
358             emitError(job->errorText());
359             return;
360         }
361 
362         const auto json = QJsonDocument::fromJson(qobject_cast<KIO::StoredTransferJob *>(job)->data());
363         const auto me = json.object();
364 
365         d->userName = me.value(QStringLiteral("name")).toString();
366         d->id = me.value(QStringLiteral("id")).toString();
367         d->wallet->writeMap(
368             mIdentifier,
369             {{KWalletKeyToken, d->token}, {KWalletKeyName, d->userName}, {KWalletKeyId, d->id}, {KWalletKeyCookies, QString::fromUtf8(d->cookies)}});
370         emitResult();
371     });
372     job->start();
373 }
374 
LogoutJob(const QString & identifier,QObject * parent)375 LogoutJob::LogoutJob(const QString &identifier, QObject *parent)
376     : TokenJob(identifier, parent)
377 {
378 }
379 
~LogoutJob()380 LogoutJob::~LogoutJob()
381 {
382 }
383 
doStart()384 void LogoutJob::doStart()
385 {
386     d->token.clear();
387     d->userName.clear();
388     d->id.clear();
389     d->cookies.clear();
390 
391     if (!d->wallet->isOpen()) {
392         emitError(i18n("Failed to open KWallet"));
393         return;
394     }
395 
396     d->wallet->removeEntry(mIdentifier);
397     emitResult();
398 }
399 
GetTokenJob(const QString & identifier,QObject * parent)400 GetTokenJob::GetTokenJob(const QString &identifier, QObject *parent)
401     : TokenJob(identifier, parent)
402 {
403 }
404 
~GetTokenJob()405 GetTokenJob::~GetTokenJob()
406 {
407 }
408 
token() const409 QString GetTokenJob::token() const
410 {
411     return d->token;
412 }
413 
userId() const414 QString GetTokenJob::userId() const
415 {
416     return d->id;
417 }
418 
userName() const419 QString GetTokenJob::userName() const
420 {
421     return d->userName;
422 }
423 
cookies() const424 QByteArray GetTokenJob::cookies() const
425 {
426     return d->cookies;
427 }
428 
start()429 void GetTokenJob::start()
430 {
431     // Already have token, so we are done
432     if (!d->token.isEmpty()) {
433         QTimer::singleShot(0, this, [this]() {
434             emitResult();
435         });
436         return;
437     }
438 
439     TokenJob::start();
440 }
441 
doStart()442 void GetTokenJob::doStart()
443 {
444     if (!d->wallet->isOpen()) {
445         emitError(i18n("Failed to open KWallet"));
446         return;
447     }
448 
449     const auto key = mIdentifier;
450     QMap<QString, QString> entries;
451     d->wallet->readMap(key, entries);
452     d->token = entries.value(KWalletKeyToken);
453     d->userName = entries.value(KWalletKeyName);
454     d->id = entries.value(KWalletKeyId);
455     d->cookies = entries.value(KWalletKeyCookies).toUtf8();
456 
457     emitResult();
458 }
459 
460 #include "tokenjobs.moc"
461