1 /*
2  * Copyright (C) by Klaas Freitag <freitag@owncloud.com>
3  * Copyright (C) by Krzesimir Nowak <krzesimir@endocode.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 <QDir>
17 #include <QFileDialog>
18 #include <QUrl>
19 #include <QTimer>
20 #include <QPushButton>
21 #include <QMessageBox>
22 #include <QSsl>
23 #include <QSslCertificate>
24 #include <QNetworkAccessManager>
25 #include <QPropertyAnimation>
26 #include <QGraphicsPixmapItem>
27 #include <QBuffer>
28 
29 #include "QProgressIndicator.h"
30 
31 #include "wizard/owncloudwizardcommon.h"
32 #include "wizard/owncloudsetuppage.h"
33 #include "wizard/owncloudconnectionmethoddialog.h"
34 #include "wizard/slideshow.h"
35 #include "theme.h"
36 #include "account.h"
37 #include "config.h"
38 
39 namespace OCC {
40 
OwncloudSetupPage(QWidget * parent)41 OwncloudSetupPage::OwncloudSetupPage(QWidget *parent)
42     : QWizardPage()
43     , _progressIndi(new QProgressIndicator(this))
44     , _ocWizard(qobject_cast<OwncloudWizard *>(parent))
45 {
46     _ui.setupUi(this);
47 
48     setupServerAddressDescriptionLabel();
49 
50     Theme *theme = Theme::instance();
51     if (theme->overrideServerUrl().isEmpty()) {
52         _ui.leUrl->setPostfix(theme->wizardUrlPostfix());
53         _ui.leUrl->setPlaceholderText(theme->wizardUrlHint());
54     } else if (Theme::instance()->forceOverrideServerUrl()) {
55         _ui.leUrl->setEnabled(false);
56     }
57 
58 
59     registerField(QLatin1String("OCUrl*"), _ui.leUrl);
60 
61     auto sizePolicy = _progressIndi->sizePolicy();
62     sizePolicy.setRetainSizeWhenHidden(true);
63     _progressIndi->setSizePolicy(sizePolicy);
64 
65     _ui.progressLayout->addWidget(_progressIndi);
66     stopSpinner();
67 
68     setupCustomization();
69 
70     slotUrlChanged(QLatin1String("")); // don't jitter UI
71     connect(_ui.leUrl, &QLineEdit::textChanged, this, &OwncloudSetupPage::slotUrlChanged);
72     connect(_ui.leUrl, &QLineEdit::editingFinished, this, &OwncloudSetupPage::slotUrlEditFinished);
73 
74     addCertDial = new AddCertificateDialog(this);
75     connect(addCertDial, &QDialog::accepted, this, &OwncloudSetupPage::slotCertificateAccepted);
76 }
77 
setLogo()78 void OwncloudSetupPage::setLogo()
79 {
80     _ui.logoLabel->setPixmap(Theme::instance()->wizardApplicationLogo());
81 }
82 
setupServerAddressDescriptionLabel()83 void OwncloudSetupPage::setupServerAddressDescriptionLabel()
84 {
85     const auto appName = Theme::instance()->appNameGUI();
86     _ui.serverAddressDescriptionLabel->setText(tr("The link to your %1 web interface when you open it in the browser.", "%1 will be replaced with the application name").arg(appName));
87 }
88 
setServerUrl(const QString & newUrl)89 void OwncloudSetupPage::setServerUrl(const QString &newUrl)
90 {
91     _ocWizard->setRegistration(false);
92     _oCUrl = newUrl;
93     if (_oCUrl.isEmpty()) {
94         _ui.leUrl->clear();
95         return;
96     }
97 
98     _ui.leUrl->setText(_oCUrl);
99 }
100 
setupCustomization()101 void OwncloudSetupPage::setupCustomization()
102 {
103     // set defaults for the customize labels.
104     _ui.topLabel->hide();
105     _ui.bottomLabel->hide();
106 
107     Theme *theme = Theme::instance();
108     QVariant variant = theme->customMedia(Theme::oCSetupTop);
109     if (!variant.isNull()) {
110         WizardCommon::setupCustomMedia(variant, _ui.topLabel);
111     }
112 
113     variant = theme->customMedia(Theme::oCSetupBottom);
114     WizardCommon::setupCustomMedia(variant, _ui.bottomLabel);
115 
116     auto leUrlPalette = _ui.leUrl->palette();
117     leUrlPalette.setColor(QPalette::Text, Qt::black);
118     leUrlPalette.setColor(QPalette::Base, Qt::white);
119     _ui.leUrl->setPalette(leUrlPalette);
120 }
121 
122 // slot hit from textChanged of the url entry field.
slotUrlChanged(const QString & url)123 void OwncloudSetupPage::slotUrlChanged(const QString &url)
124 {
125     // Need to set next button as default button here because
126     // otherwise the on OSX the next button does not stay the default
127     // button
128     auto nextButton = qobject_cast<QPushButton *>(_ocWizard->button(QWizard::NextButton));
129     if (nextButton) {
130         nextButton->setDefault(true);
131     }
132 
133     _authTypeKnown = false;
134 
135     QString newUrl = url;
136     if (url.endsWith("index.php")) {
137         newUrl.chop(9);
138     }
139     if (_ocWizard && _ocWizard->account()) {
140         QString webDavPath = _ocWizard->account()->davPath();
141         if (url.endsWith(webDavPath)) {
142             newUrl.chop(webDavPath.length());
143         }
144         if (webDavPath.endsWith(QLatin1Char('/'))) {
145             webDavPath.chop(1); // cut off the slash
146             if (url.endsWith(webDavPath)) {
147                 newUrl.chop(webDavPath.length());
148             }
149         }
150     }
151     if (newUrl != url) {
152         _ui.leUrl->setText(newUrl);
153     }
154 }
155 
slotUrlEditFinished()156 void OwncloudSetupPage::slotUrlEditFinished()
157 {
158     QString url = _ui.leUrl->fullText();
159     if (QUrl(url).isRelative() && !url.isEmpty()) {
160         // no scheme defined, set one
161         url.prepend("https://");
162         _ui.leUrl->setFullText(url);
163     }
164 }
165 
isComplete() const166 bool OwncloudSetupPage::isComplete() const
167 {
168     return !_ui.leUrl->text().isEmpty() && !_checking;
169 }
170 
initializePage()171 void OwncloudSetupPage::initializePage()
172 {
173     customizeStyle();
174 
175     WizardCommon::initErrorLabel(_ui.errorLabel);
176 
177     _authTypeKnown = false;
178     _checking = false;
179 
180     QAbstractButton *nextButton = wizard()->button(QWizard::NextButton);
181     auto *pushButton = qobject_cast<QPushButton *>(nextButton);
182     if (pushButton) {
183         pushButton->setDefault(true);
184     }
185 
186     _ui.leUrl->setFocus();
187 
188     const auto isServerUrlOverridden = !Theme::instance()->overrideServerUrl().isEmpty();
189     if (isServerUrlOverridden && !Theme::instance()->forceOverrideServerUrl()) {
190         // If the url is overwritten but we don't force to use that url
191         // Just focus the next button to let the user navigate quicker
192         if (nextButton) {
193             nextButton->setFocus();
194         }
195     } else if (isServerUrlOverridden) {
196         // If the overwritten url is not empty and we force this overwritten url
197         // we just check the server type and switch to next page
198         // immediately.
199         setCommitPage(true);
200         // Hack: setCommitPage() changes caption, but after an error this page could still be visible
201         setButtonText(QWizard::CommitButton, tr("&Next >"));
202         validatePage();
203         setVisible(false);
204     }
205 }
206 
nextId() const207 int OwncloudSetupPage::nextId() const
208 {
209     switch (_authType) {
210     case DetermineAuthTypeJob::Basic:
211         return WizardCommon::Page_HttpCreds;
212     case DetermineAuthTypeJob::OAuth:
213         return WizardCommon::Page_OAuthCreds;
214     case DetermineAuthTypeJob::LoginFlowV2:
215         return WizardCommon::Page_Flow2AuthCreds;
216 #ifdef WITH_WEBENGINE
217     case DetermineAuthTypeJob::WebViewFlow:
218         return WizardCommon::Page_WebView;
219 #endif // WITH_WEBENGINE
220     case DetermineAuthTypeJob::NoAuthType:
221         return WizardCommon::Page_HttpCreds;
222     }
223     Q_UNREACHABLE();
224 }
225 
url() const226 QString OwncloudSetupPage::url() const
227 {
228     QString url = _ui.leUrl->fullText().simplified();
229     return url;
230 }
231 
validatePage()232 bool OwncloudSetupPage::validatePage()
233 {
234     if (!_authTypeKnown) {
235         slotUrlEditFinished();
236         QString u = url();
237         QUrl qurl(u);
238         if (!qurl.isValid() || qurl.host().isEmpty()) {
239             setErrorString(tr("Server address does not seem to be valid"), false);
240             return false;
241         }
242 
243         setErrorString(QString(), false);
244         _checking = true;
245         startSpinner();
246         emit completeChanged();
247 
248         emit determineAuthType(u);
249         return false;
250     } else {
251         // connecting is running
252         stopSpinner();
253         _checking = false;
254         emit completeChanged();
255         return true;
256     }
257 }
258 
setAuthType(DetermineAuthTypeJob::AuthType type)259 void OwncloudSetupPage::setAuthType(DetermineAuthTypeJob::AuthType type)
260 {
261     _authTypeKnown = true;
262     _authType = type;
263     stopSpinner();
264 }
265 
setErrorString(const QString & err,bool retryHTTPonly)266 void OwncloudSetupPage::setErrorString(const QString &err, bool retryHTTPonly)
267 {
268     if (err.isEmpty()) {
269         _ui.errorLabel->setVisible(false);
270     } else {
271         if (retryHTTPonly) {
272             QUrl url(_ui.leUrl->fullText());
273             if (url.scheme() == "https") {
274                 // Ask the user how to proceed when connecting to a https:// URL fails.
275                 // It is possible that the server is secured with client-side TLS certificates,
276                 // but that it has no way of informing the owncloud client that this is the case.
277 
278                 OwncloudConnectionMethodDialog dialog;
279                 dialog.setUrl(url);
280                 // FIXME: Synchronous dialogs are not so nice because of event loop recursion
281                 int retVal = dialog.exec();
282 
283                 switch (retVal) {
284                 case OwncloudConnectionMethodDialog::No_TLS: {
285                     url.setScheme("http");
286                     _ui.leUrl->setFullText(url.toString());
287                     // skip ahead to next page, since the user would expect us to retry automatically
288                     wizard()->next();
289                 } break;
290                 case OwncloudConnectionMethodDialog::Client_Side_TLS:
291                     addCertDial->show();
292                     break;
293                 case OwncloudConnectionMethodDialog::Closed:
294                 case OwncloudConnectionMethodDialog::Back:
295                 default:
296                     // No-op.
297                     break;
298                 }
299             }
300         }
301 
302         _ui.errorLabel->setVisible(true);
303         _ui.errorLabel->setText(err);
304     }
305     _checking = false;
306     emit completeChanged();
307     stopSpinner();
308 }
309 
startSpinner()310 void OwncloudSetupPage::startSpinner()
311 {
312     _ui.progressLayout->setEnabled(true);
313     _progressIndi->setVisible(true);
314     _progressIndi->startAnimation();
315 }
316 
stopSpinner()317 void OwncloudSetupPage::stopSpinner()
318 {
319     _ui.progressLayout->setEnabled(false);
320     _progressIndi->setVisible(false);
321     _progressIndi->stopAnimation();
322 }
323 
subjectInfoHelper(const QSslCertificate & cert,const QByteArray & qa)324 QString subjectInfoHelper(const QSslCertificate &cert, const QByteArray &qa)
325 {
326     return cert.subjectInfo(qa).join(QLatin1Char('/'));
327 }
328 
329 //called during the validation of the client certificate.
slotCertificateAccepted()330 void OwncloudSetupPage::slotCertificateAccepted()
331 {
332     QFile certFile(addCertDial->getCertificatePath());
333     certFile.open(QFile::ReadOnly);
334     QByteArray certData = certFile.readAll();
335     QByteArray certPassword = addCertDial->getCertificatePasswd().toLocal8Bit();
336 
337     QBuffer certDataBuffer(&certData);
338     certDataBuffer.open(QIODevice::ReadOnly);
339     if (QSslCertificate::importPkcs12(&certDataBuffer,
340             &_ocWizard->_clientSslKey, &_ocWizard->_clientSslCertificate,
341             &_ocWizard->_clientSslCaCertificates, certPassword)) {
342         _ocWizard->_clientCertBundle = certData;
343         _ocWizard->_clientCertPassword = certPassword;
344 
345         addCertDial->reinit(); // FIXME: Why not just have this only created on use?
346 
347         // The extracted SSL key and cert gets added to the QSslConfiguration in checkServer()
348         validatePage();
349     } else {
350         addCertDial->showErrorMessage(tr("Could not load certificate. Maybe wrong password?"));
351         addCertDial->show();
352     }
353 }
354 
355 OwncloudSetupPage::~OwncloudSetupPage() = default;
356 
slotStyleChanged()357 void OwncloudSetupPage::slotStyleChanged()
358 {
359     customizeStyle();
360 }
361 
customizeStyle()362 void OwncloudSetupPage::customizeStyle()
363 {
364     setLogo();
365 
366     if (_progressIndi) {
367         const auto isDarkBackground = Theme::isDarkColor(palette().window().color());
368         if (isDarkBackground) {
369             _progressIndi->setColor(Qt::white);
370         } else {
371             _progressIndi->setColor(Qt::black);
372         }
373     }
374 
375 
376     WizardCommon::customizeHintLabel(_ui.serverAddressDescriptionLabel);
377 }
378 
379 } // namespace OCC
380