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