1 /*
2     SPDX-FileCopyrightText: 2010 Tobias Koenig <tokoe@kde.org>
3 
4     SPDX-License-Identifier: GPL-2.0-or-later
5 */
6 
7 #include "setupwizard.h"
8 
9 #include <KAuthorized>
10 #include <KDAV/DavCollectionsMultiFetchJob>
11 #include <KDesktopFile>
12 #include <KFileUtils>
13 #include <KLocalizedString>
14 #include <KPasswordLineEdit>
15 #include <KService>
16 #include <QIcon>
17 #include <QLineEdit>
18 #include <QTextBrowser>
19 
20 #include <QButtonGroup>
21 #include <QCheckBox>
22 #include <QComboBox>
23 #include <QFormLayout>
24 #include <QHBoxLayout>
25 #include <QLabel>
26 #include <QPushButton>
27 #include <QRadioButton>
28 #include <QRegularExpressionValidator>
29 #include <QStandardPaths>
30 #include <QUrl>
31 
32 enum GroupwareServers {
33     Citadel,
34     DAVical,
35     eGroupware,
36     OpenGroupware,
37     ScalableOGo,
38     Scalix,
39     Zarafa,
40     Zimbra,
41 };
42 
settingsToUrl(const QWizard * wizard,const QString & protocol)43 static QString settingsToUrl(const QWizard *wizard, const QString &protocol)
44 {
45     const QString desktopFilePath = wizard->property("providerDesktopFilePath").toString();
46     if (desktopFilePath.isEmpty()) {
47         return QString();
48     }
49 
50     KService::Ptr service = KService::serviceByStorageId(desktopFilePath);
51     if (!service) {
52         return QString();
53     }
54 
55     const QStringList supportedProtocols = service->property(QStringLiteral("X-DavGroupware-SupportedProtocols")).toStringList();
56     if (!supportedProtocols.contains(protocol)) {
57         return QString();
58     }
59 
60 
61     const QString pathPropertyName(QStringLiteral("X-DavGroupware-") + protocol + QStringLiteral("Path"));
62     if (service->property(pathPropertyName).isNull()) {
63         return QString();
64     }
65 
66     QString pathPattern;
67     pathPattern.append(service->property(pathPropertyName).toString() + QLatin1Char('/'));
68 
69     QString username = wizard->field(QStringLiteral("credentialsUserName")).toString();
70     QString localPart(username);
71     localPart.remove(QRegularExpression(QStringLiteral("@.*$")));
72     pathPattern.replace(QLatin1String("$user$"), username);
73     pathPattern.replace(QLatin1String("$localpart$"), localPart);
74     QString providerName;
75     if (!service->property(QStringLiteral("X-DavGroupware-Provider")).isNull()) {
76         providerName = service->property(QStringLiteral("X-DavGroupware-Provider")).toString();
77     }
78     QString localPath = wizard->field(QStringLiteral("installationPath")).toString();
79     if (!localPath.isEmpty()) {
80         if (providerName == QLatin1String("davical")) {
81             if (!localPath.endsWith(QLatin1Char('/'))) {
82                 pathPattern.append(localPath + QLatin1Char('/'));
83             } else {
84                 pathPattern.append(localPath);
85             }
86         } else {
87             if (!localPath.startsWith(QLatin1Char('/'))) {
88                 pathPattern.prepend(QLatin1Char('/') + localPath);
89             } else {
90                 pathPattern.prepend(localPath);
91             }
92         }
93     }
94     QUrl url;
95 
96     if (!wizard->property("usePredefinedProvider").isNull()) {
97         if (service->property(QStringLiteral("X-DavGroupware-ProviderUsesSSL")).toBool()) {
98             url.setScheme(QStringLiteral("https"));
99         } else {
100             url.setScheme(QStringLiteral("http"));
101         }
102 
103         QString hostPropertyName(QStringLiteral("X-DavGroupware-") + protocol + QStringLiteral("Host"));
104         if (service->property(hostPropertyName).isNull()) {
105             return QString();
106         }
107 
108         url.setHost(service->property(hostPropertyName).toString());
109         url.setPath(pathPattern);
110     } else {
111         if (wizard->field(QStringLiteral("connectionUseSecureConnection")).toBool()) {
112             url.setScheme(QStringLiteral("https"));
113         } else {
114             url.setScheme(QStringLiteral("http"));
115         }
116 
117         const QString host = wizard->field(QStringLiteral("connectionHost")).toString();
118         if (host.isEmpty()) {
119             return QString();
120         }
121         const QStringList hostParts = host.split(QLatin1Char(':'));
122         url.setHost(hostParts.at(0));
123         url.setPath(pathPattern);
124 
125         if (hostParts.size() == 2) {
126             int port = hostParts.at(1).toInt();
127             if (port) {
128                 url.setPort(port);
129             }
130         }
131     }
132     return url.toString();
133 }
134 
135 /*
136  * SetupWizard
137  */
138 
SetupWizard(QWidget * parent)139 SetupWizard::SetupWizard(QWidget *parent)
140     : QWizard(parent)
141 {
142     setWindowTitle(i18nc("@title:window", "DAV groupware configuration wizard"));
143     setWindowIcon(QIcon::fromTheme(QStringLiteral("folder-remote")));
144     setPage(W_CredentialsPage, new CredentialsPage);
145     setPage(W_PredefinedProviderPage, new PredefinedProviderPage);
146     setPage(W_ServerTypePage, new ServerTypePage);
147     setPage(W_ConnectionPage, new ConnectionPage);
148     setPage(W_CheckPage, new CheckPage);
149 }
150 
displayName() const151 QString SetupWizard::displayName() const
152 {
153     QString desktopFilePath = property("providerDesktopFilePath").toString();
154     if (desktopFilePath.isEmpty()) {
155         return QString();
156     }
157 
158     KService::Ptr service = KService::serviceByStorageId(desktopFilePath);
159     if (!service) {
160         return QString();
161     }
162 
163     return service->name();
164 }
165 
urls() const166 SetupWizard::Url::List SetupWizard::urls() const
167 {
168     Url::List urls;
169 
170     const QString desktopFilePath = property("providerDesktopFilePath").toString();
171     if (desktopFilePath.isEmpty()) {
172         return urls;
173     }
174 
175     KService::Ptr service = KService::serviceByStorageId(desktopFilePath);
176     if (!service) {
177         return urls;
178     }
179 
180     const QStringList supportedProtocols = service->property(QStringLiteral("X-DavGroupware-SupportedProtocols")).toStringList();
181     for (const QString &protocol : supportedProtocols) {
182         Url url;
183 
184         if (protocol == QLatin1String("CalDav")) {
185             url.protocol = KDAV::CalDav;
186         } else if (protocol == QLatin1String("CardDav")) {
187             url.protocol = KDAV::CardDav;
188         } else if (protocol == QLatin1String("GroupDav")) {
189             url.protocol = KDAV::GroupDav;
190         } else {
191             return urls;
192         }
193 
194         QString urlStr = settingsToUrl(this, protocol);
195 
196         if (!urlStr.isEmpty()) {
197             url.url = urlStr;
198             url.userName = QStringLiteral("$default$");
199             urls << url;
200         }
201     }
202 
203     return urls;
204 }
205 
206 /*
207  * CredentialsPage
208  */
209 
CredentialsPage(QWidget * parent)210 CredentialsPage::CredentialsPage(QWidget *parent)
211     : QWizardPage(parent)
212 {
213     setTitle(i18n("Login Credentials"));
214     setSubTitle(i18n("Enter your credentials to login to the groupware server"));
215 
216     auto layout = new QFormLayout(this);
217 
218     mUserName = new QLineEdit;
219     layout->addRow(i18n("User:"), mUserName);
220     registerField(QStringLiteral("credentialsUserName*"), mUserName);
221 
222     mPassword = new KPasswordLineEdit;
223     mPassword->setRevealPasswordAvailable(KAuthorized::authorize(QStringLiteral("lineedit_reveal_password")));
224     layout->addRow(i18n("Password:"), mPassword);
225     registerField(QStringLiteral("credentialsPassword*"), mPassword, "password", SIGNAL(passwordChanged(QString)));
226 }
227 
nextId() const228 int CredentialsPage::nextId() const
229 {
230     QString userName = field(QStringLiteral("credentialsUserName")).toString();
231     if (userName.endsWith(QLatin1String("@yahoo.com"))) {
232         const QString maybeYahooFile =
233             QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kservices5/akonadi/davgroupware-providers/yahoo.desktop"));
234 
235         if (maybeYahooFile.isEmpty()) {
236             return SetupWizard::W_ServerTypePage;
237         }
238 
239         const KDesktopFile yahooProvider(maybeYahooFile);
240 
241         wizard()->setProperty("usePredefinedProvider", true);
242         wizard()->setProperty("predefinedProviderName", yahooProvider.readName());
243         wizard()->setProperty("providerDesktopFilePath", maybeYahooFile);
244         return SetupWizard::W_PredefinedProviderPage;
245     } else {
246         return SetupWizard::W_ServerTypePage;
247     }
248 }
249 
250 /*
251  * PredefinedProviderPage
252  */
253 
PredefinedProviderPage(QWidget * parent)254 PredefinedProviderPage::PredefinedProviderPage(QWidget *parent)
255     : QWizardPage(parent)
256 {
257     setTitle(i18n("Predefined provider found"));
258     setSubTitle(i18n("Select if you want to use the auto-detected provider"));
259 
260     auto layout = new QVBoxLayout(this);
261 
262     mLabel = new QLabel;
263     layout->addWidget(mLabel);
264 
265     mProviderGroup = new QButtonGroup(this);
266     mProviderGroup->setExclusive(true);
267 
268     mUseProvider = new QRadioButton;
269     mProviderGroup->addButton(mUseProvider);
270     mUseProvider->setChecked(true);
271     layout->addWidget(mUseProvider);
272 
273     mDontUseProvider = new QRadioButton(i18n("No, choose another server"));
274     mProviderGroup->addButton(mDontUseProvider);
275     layout->addWidget(mDontUseProvider);
276 }
277 
initializePage()278 void PredefinedProviderPage::initializePage()
279 {
280     mLabel->setText(
281         i18n("Based on the email address you used as a login, this wizard\n"
282              "can configure automatically an account for %1 services.\n"
283              "Do you wish to do so?",
284              wizard()->property("predefinedProviderName").toString()));
285 
286     mUseProvider->setText(i18n("Yes, use %1 as provider", wizard()->property("predefinedProviderName").toString()));
287 }
288 
nextId() const289 int PredefinedProviderPage::nextId() const
290 {
291     if (mUseProvider->isChecked()) {
292         return SetupWizard::W_CheckPage;
293     } else {
294         wizard()->setProperty("usePredefinedProvider", QVariant());
295         wizard()->setProperty("providerDesktopFilePath", QVariant());
296         return SetupWizard::W_ServerTypePage;
297     }
298 }
299 
300 /*
301  * ServerTypePage
302  */
303 
compareServiceOffers(const QPair<QString,QString> & off1,const QPair<QString,QString> & off2)304 bool compareServiceOffers(const QPair<QString, QString> &off1, const QPair<QString, QString> &off2)
305 {
306     return off1.first.toLower() < off2.first.toLower();
307 }
308 
ServerTypePage(QWidget * parent)309 ServerTypePage::ServerTypePage(QWidget *parent)
310     : QWizardPage(parent)
311 {
312     setTitle(i18n("Groupware Server"));
313     setSubTitle(i18n("Select the groupware server the resource shall be configured for"));
314 
315     mProvidersCombo = new QComboBox(this);
316     mProvidersCombo->setSizeAdjustPolicy(QComboBox::AdjustToContents);
317 
318     const QStringList dirs = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation,
319                                                        QStringLiteral("kservices5/akonadi/davgroupware-providers"),
320                                                        QStandardPaths::LocateDirectory);
321     const QStringList providers = KFileUtils::findAllUniqueFiles(dirs, QStringList{QStringLiteral("*.desktop")});
322 
323     QList<QPair<QString, QString>> offers;
324     offers.reserve(providers.count());
325     for (const QString &fileName : providers) {
326         const KDesktopFile provider(fileName);
327         offers.append(QPair<QString, QString>(provider.readName(), fileName));
328     }
329     std::sort(offers.begin(), offers.end(), compareServiceOffers);
330     QListIterator<QPair<QString, QString>> it(offers);
331     while (it.hasNext()) {
332         QPair<QString, QString> p = it.next();
333         mProvidersCombo->addItem(p.first, p.second);
334     }
335     registerField(QStringLiteral("provider"), mProvidersCombo, "currentText");
336 
337     auto layout = new QVBoxLayout(this);
338 
339     mServerGroup = new QButtonGroup(this);
340     mServerGroup->setExclusive(true);
341 
342     auto hLayout = new QHBoxLayout;
343     auto button = new QRadioButton(i18n("Use one of those servers:"));
344     registerField(QStringLiteral("templateConfiguration"), button);
345     mServerGroup->addButton(button);
346     mServerGroup->setId(button, 0);
347     button->setChecked(true);
348     hLayout->addWidget(button);
349     hLayout->addWidget(mProvidersCombo);
350     layout->addLayout(hLayout);
351 
352     button = new QRadioButton(i18n("Configure the resource manually"));
353     connect(button, &QRadioButton::toggled, this, &ServerTypePage::manualConfigToggled);
354     registerField(QStringLiteral("manualConfiguration"), button);
355     mServerGroup->addButton(button);
356     mServerGroup->setId(button, 1);
357     layout->addWidget(button);
358 
359     layout->addStretch(1);
360 }
361 
manualConfigToggled(bool state)362 void ServerTypePage::manualConfigToggled(bool state)
363 {
364     setFinalPage(state);
365     wizard()->button(QWizard::NextButton)->setEnabled(!state);
366 }
367 
validatePage()368 bool ServerTypePage::validatePage()
369 {
370     QVariant desktopFilePath = mProvidersCombo->itemData(mProvidersCombo->currentIndex());
371     if (desktopFilePath.isNull()) {
372         return false;
373     } else {
374         wizard()->setProperty("providerDesktopFilePath", desktopFilePath);
375         return true;
376     }
377 }
378 
379 /*
380  * ConnectionPage
381  */
382 
ConnectionPage(QWidget * parent)383 ConnectionPage::ConnectionPage(QWidget *parent)
384     : QWizardPage(parent)
385     , mPreviewLayout(nullptr)
386     , mCalDavUrlPreview(nullptr)
387     , mCardDavUrlPreview(nullptr)
388     , mGroupDavUrlPreview(nullptr)
389 {
390     setTitle(i18n("Connection"));
391     setSubTitle(i18n("Enter the connection information for the groupware server"));
392 
393     mLayout = new QFormLayout(this);
394     const QRegularExpression hostnameRegexp(QStringLiteral("^[a-z0-9][.a-z0-9-]*[a-z0-9](?::[0-9]+)?$"));
395     mHost = new QLineEdit;
396     registerField(QStringLiteral("connectionHost*"), mHost);
397     mHost->setValidator(new QRegularExpressionValidator(hostnameRegexp, this));
398     mLayout->addRow(i18n("Host"), mHost);
399 
400     mPath = new QLineEdit;
401     mLayout->addRow(i18n("Installation path"), mPath);
402     registerField(QStringLiteral("installationPath"), mPath);
403 
404     mUseSecureConnection = new QCheckBox(i18n("Use secure connection"));
405     mUseSecureConnection->setChecked(true);
406     registerField(QStringLiteral("connectionUseSecureConnection"), mUseSecureConnection);
407     mLayout->addRow(QString(), mUseSecureConnection);
408 
409     connect(mHost, &QLineEdit::textChanged, this, &ConnectionPage::urlElementChanged);
410     connect(mPath, &QLineEdit::textChanged, this, &ConnectionPage::urlElementChanged);
411     connect(mUseSecureConnection, &QCheckBox::toggled, this, &ConnectionPage::urlElementChanged);
412 }
413 
initializePage()414 void ConnectionPage::initializePage()
415 {
416     KService::Ptr service = KService::serviceByStorageId(wizard()->property("providerDesktopFilePath").toString());
417     if (!service) {
418         return;
419     }
420 
421     QString providerInstallationPath = service->property(QStringLiteral("X-DavGroupware-InstallationPath")).toString();
422     if (!providerInstallationPath.isEmpty()) {
423         mPath->setText(providerInstallationPath);
424     }
425 
426     QStringList supportedProtocols = service->property(QStringLiteral("X-DavGroupware-SupportedProtocols")).toStringList();
427 
428     mPreviewLayout = new QFormLayout;
429     mLayout->addRow(mPreviewLayout);
430 
431     if (supportedProtocols.contains(QLatin1String("CalDav"))) {
432         mCalDavUrlLabel = new QLabel(i18n("Final URL (CalDav)"));
433         mCalDavUrlPreview = new QLabel;
434         mPreviewLayout->addRow(mCalDavUrlLabel, mCalDavUrlPreview);
435     }
436     if (supportedProtocols.contains(QLatin1String("CardDav"))) {
437         mCardDavUrlLabel = new QLabel(i18n("Final URL (CardDav)"));
438         mCardDavUrlPreview = new QLabel;
439         mPreviewLayout->addRow(mCardDavUrlLabel, mCardDavUrlPreview);
440     }
441     if (supportedProtocols.contains(QLatin1String("GroupDav"))) {
442         mGroupDavUrlLabel = new QLabel(i18n("Final URL (GroupDav)"));
443         mGroupDavUrlPreview = new QLabel;
444         mPreviewLayout->addRow(mGroupDavUrlLabel, mGroupDavUrlPreview);
445     }
446 }
447 
cleanupPage()448 void ConnectionPage::cleanupPage()
449 {
450     delete mPreviewLayout;
451 
452     if (mCalDavUrlPreview) {
453         delete mCalDavUrlLabel;
454         delete mCalDavUrlPreview;
455         mCalDavUrlPreview = nullptr;
456     }
457 
458     if (mCardDavUrlPreview) {
459         delete mCardDavUrlLabel;
460         delete mCardDavUrlPreview;
461         mCardDavUrlPreview = nullptr;
462     }
463 
464     if (mGroupDavUrlPreview) {
465         delete mGroupDavUrlLabel;
466         delete mGroupDavUrlPreview;
467         mGroupDavUrlPreview = nullptr;
468     }
469 
470     QWizardPage::cleanupPage();
471 }
472 
urlElementChanged()473 void ConnectionPage::urlElementChanged()
474 {
475     if (mHost->text().isEmpty()) {
476         if (mCalDavUrlPreview) {
477             mCalDavUrlPreview->setText(QStringLiteral("-"));
478         }
479         if (mCardDavUrlPreview) {
480             mCardDavUrlPreview->setText(QStringLiteral("-"));
481         }
482         if (mGroupDavUrlPreview) {
483             mGroupDavUrlPreview->setText(QStringLiteral("-"));
484         }
485     } else {
486         if (mCalDavUrlPreview) {
487             mCalDavUrlPreview->setText(settingsToUrl(this->wizard(), QStringLiteral("CalDav")));
488         }
489         if (mCardDavUrlPreview) {
490             mCardDavUrlPreview->setText(settingsToUrl(this->wizard(), QStringLiteral("CardDav")));
491         }
492         if (mGroupDavUrlPreview) {
493             mGroupDavUrlPreview->setText(settingsToUrl(this->wizard(), QStringLiteral("GroupDav")));
494         }
495     }
496 }
497 
498 /*
499  * CheckPage
500  */
501 
CheckPage(QWidget * parent)502 CheckPage::CheckPage(QWidget *parent)
503     : QWizardPage(parent)
504 {
505     setTitle(i18n("Test Connection"));
506     setSubTitle(i18n("You can test now whether the groupware server can be accessed with the current configuration"));
507     setFinalPage(true);
508 
509     auto layout = new QVBoxLayout(this);
510 
511     auto button = new QPushButton(i18n("Test Connection"));
512     layout->addWidget(button);
513 
514     mStatusLabel = new QTextBrowser;
515     layout->addWidget(mStatusLabel);
516 
517     connect(button, &QRadioButton::clicked, this, &CheckPage::checkConnection);
518 }
519 
checkConnection()520 void CheckPage::checkConnection()
521 {
522     mStatusLabel->clear();
523 
524     KDAV::DavUrl::List davUrls;
525 
526     // convert list of SetupWizard::Url to list of KDAV::DavUrl
527     const SetupWizard::Url::List urls = static_cast<SetupWizard *>(wizard())->urls();
528     for (const SetupWizard::Url &url : urls) {
529         KDAV::DavUrl davUrl;
530         davUrl.setProtocol(url.protocol);
531 
532         QUrl serverUrl(url.url);
533         serverUrl.setUserName(wizard()->field(QStringLiteral("credentialsUserName")).toString());
534         serverUrl.setPassword(wizard()->field(QStringLiteral("credentialsPassword")).toString());
535         davUrl.setUrl(serverUrl);
536 
537         davUrls << davUrl;
538     }
539 
540     // start the dav collections fetch job to test connectivity
541     auto job = new KDAV::DavCollectionsMultiFetchJob(davUrls, this);
542     connect(job, &KDAV::DavCollectionsMultiFetchJob::result, this, &CheckPage::onFetchDone);
543     job->start();
544 }
545 
onFetchDone(KJob * job)546 void CheckPage::onFetchDone(KJob *job)
547 {
548     QString msg;
549     QPixmap icon;
550 
551     if (job->error()) {
552         msg = i18n("An error occurred: %1", job->errorText());
553         icon = QIcon::fromTheme(QStringLiteral("dialog-close")).pixmap(16, 16);
554     } else {
555         msg = i18n("Connected successfully");
556         icon = QIcon::fromTheme(QStringLiteral("dialog-ok-apply")).pixmap(16, 16);
557     }
558 
559     mStatusLabel->setHtml(QStringLiteral("<html><body><img src=\"icon\"> %1</body></html>").arg(msg));
560     mStatusLabel->document()->addResource(QTextDocument::ImageResource, QUrl(QStringLiteral("icon")), QVariant(icon));
561 }
562