1 | /* |
2 | * Copyright (C) by Klaas Freitag <freitag@kde.org> |
3 | * Copyright (C) by Olivier Goffart <ogoffart@woboq.com> |
4 | * |
5 | * This program is free software; you can redistribute it and/or modify |
6 | * it under the terms of the GNU General Public License as published by |
7 | * the Free Software Foundation; either version 2 of the License, or |
8 | * (at your option) any later version. |
9 | * |
10 | * This program is distributed in the hope that it will be useful, but |
11 | * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY |
12 | * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
13 | * for more details. |
14 | */ |
15 | |
16 | #include <QInputDialog> |
17 | #include <QLabel> |
18 | #include <QDesktopServices> |
19 | #include <QNetworkReply> |
20 | #include <QTimer> |
21 | #include <QBuffer> |
22 | #include "creds/httpcredentialsgui.h" |
23 | #include "theme.h" |
24 | #include "account.h" |
25 | #include "networkjobs.h" |
26 | #include <QMessageBox> |
27 | #include "common/asserts.h" |
28 | |
29 | using namespace QKeychain; |
30 | |
31 | namespace OCC { |
32 | |
33 | Q_LOGGING_CATEGORY(lcHttpCredentialsGui, "sync.credentials.http.gui" , QtInfoMsg) |
34 | |
35 | void HttpCredentialsGui::askFromUser() |
36 | { |
37 | // This function can be called from AccountState::slotInvalidCredentials, |
38 | // which (indirectly, through HttpCredentials::invalidateToken) schedules |
39 | // a cache wipe of the qnam. We can only execute a network job again once |
40 | // the cache has been cleared, otherwise we'd interfere with the job. |
41 | QTimer::singleShot(100, this, &HttpCredentialsGui::askFromUserAsync); |
42 | } |
43 | |
44 | void HttpCredentialsGui::askFromUserAsync() |
45 | { |
46 | // First, we will check what kind of auth we need. |
47 | auto job = new DetermineAuthTypeJob(_account->sharedFromThis(), this); |
48 | QObject::connect(job, &DetermineAuthTypeJob::authType, this, [this](DetermineAuthTypeJob::AuthType type) { |
49 | if (type == DetermineAuthTypeJob::OAuth) { |
50 | _asyncAuth.reset(new OAuth(_account, this)); |
51 | _asyncAuth->_expectedUser = _user; |
52 | connect(_asyncAuth.data(), &OAuth::result, |
53 | this, &HttpCredentialsGui::asyncAuthResult); |
54 | connect(_asyncAuth.data(), &OAuth::destroyed, |
55 | this, &HttpCredentialsGui::authorisationLinkChanged); |
56 | _asyncAuth->start(); |
57 | emit authorisationLinkChanged(); |
58 | } else if (type == DetermineAuthTypeJob::Basic) { |
59 | // Show the dialog |
60 | // We will re-enter the event loop, so better wait the next iteration |
61 | QMetaObject::invokeMethod(this, "showDialog" , Qt::QueuedConnection); |
62 | } else { |
63 | // Shibboleth? |
64 | qCWarning(lcHttpCredentialsGui) << "Bad http auth type:" << type; |
65 | emit asked(); |
66 | } |
67 | }); |
68 | job->start(); |
69 | } |
70 | |
71 | void HttpCredentialsGui::asyncAuthResult(OAuth::Result r, const QString &user, |
72 | const QString &token, const QString &refreshToken) |
73 | { |
74 | switch (r) { |
75 | case OAuth::NotSupported: |
76 | // We will re-enter the event loop, so better wait the next iteration |
77 | QMetaObject::invokeMethod(this, "showDialog" , Qt::QueuedConnection); |
78 | _asyncAuth.reset(0); |
79 | return; |
80 | case OAuth::Error: |
81 | _asyncAuth.reset(0); |
82 | emit asked(); |
83 | return; |
84 | case OAuth::LoggedIn: |
85 | break; |
86 | } |
87 | |
88 | ASSERT(_user == user); // ensured by _asyncAuth |
89 | |
90 | _password = token; |
91 | _refreshToken = refreshToken; |
92 | _ready = true; |
93 | persist(); |
94 | _asyncAuth.reset(0); |
95 | emit asked(); |
96 | } |
97 | |
98 | void HttpCredentialsGui::showDialog() |
99 | { |
100 | QString msg = tr("Please enter %1 password:<br>" |
101 | "<br>" |
102 | "User: %2<br>" |
103 | "Account: %3<br>" ) |
104 | .arg(Utility::escape(Theme::instance()->appNameGUI()), |
105 | Utility::escape(_user), |
106 | Utility::escape(_account->displayName())); |
107 | |
108 | QString reqTxt = requestAppPasswordText(_account); |
109 | if (!reqTxt.isEmpty()) { |
110 | msg += QLatin1String("<br>" ) + reqTxt + QLatin1String("<br>" ); |
111 | } |
112 | if (!_fetchErrorString.isEmpty()) { |
113 | msg += QLatin1String("<br>" ) |
114 | + tr("Reading from keychain failed with error: '%1'" ) |
115 | .arg(Utility::escape(_fetchErrorString)) |
116 | + QLatin1String("<br>" ); |
117 | } |
118 | |
119 | QInputDialog dialog; |
120 | dialog.setWindowTitle(tr("Enter Password" )); |
121 | dialog.setLabelText(msg); |
122 | dialog.setTextValue(_previousPassword); |
123 | dialog.setTextEchoMode(QLineEdit::Password); |
124 | if (QLabel *dialogLabel = dialog.findChild<QLabel *>()) { |
125 | dialogLabel->setOpenExternalLinks(true); |
126 | dialogLabel->setTextFormat(Qt::RichText); |
127 | } |
128 | |
129 | bool ok = dialog.exec(); |
130 | if (ok) { |
131 | _password = dialog.textValue(); |
132 | _refreshToken.clear(); |
133 | _ready = true; |
134 | persist(); |
135 | } |
136 | emit asked(); |
137 | } |
138 | |
139 | QString HttpCredentialsGui::requestAppPasswordText(const Account *account) |
140 | { |
141 | int version = account->serverVersionInt(); |
142 | QString path; |
143 | |
144 | // Version may not be available before login on new servers! |
145 | if (!version || version >= Account::makeServerVersion(10, 0, 0)) { |
146 | path = QLatin1String("/index.php/settings/personal?sectionid=security#apppasswords" ); |
147 | } else if (version >= Account::makeServerVersion(9, 1, 0)) { |
148 | path = QLatin1String("/index.php/settings/personal?section=apppasswords" ); |
149 | } else { |
150 | // Older server than 9.1 does not have the feature to request App Password |
151 | return QString(); |
152 | } |
153 | |
154 | auto baseUrl = account->url().toString(); |
155 | if (baseUrl.endsWith('/')) |
156 | baseUrl.chop(1); |
157 | return tr("<a href=\"%1\">Click here</a> to request an app password from the web interface." ) |
158 | .arg(baseUrl + path); |
159 | } |
160 | } // namespace OCC |
161 | |