client: Port old HighlightRule to ExpressionMatch
[quassel.git] / src / qtui / coreconfigwizard.cpp
1 /***************************************************************************
2  *   Copyright (C) 2005-2018 by the Quassel Project                        *
3  *   devel@quassel-irc.org                                                 *
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) version 3.                                           *
9  *                                                                         *
10  *   This program is distributed in the hope that it will be useful,       *
11  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
12  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
13  *   GNU General Public License for more details.                          *
14  *                                                                         *
15  *   You should have received a copy of the GNU General Public License     *
16  *   along with this program; if not, write to the                         *
17  *   Free Software Foundation, Inc.,                                       *
18  *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.         *
19  ***************************************************************************/
20
21 #include "coreconfigwizard.h"
22
23 #include <QDebug>
24 #include <QAbstractButton>
25 #include <QCoreApplication>
26 #include <QFormLayout>
27 #include <QSpinBox>
28
29 #include "client.h"
30 #include "coreconnection.h"
31 #include "icon.h"
32
33 namespace {
34
35 QGroupBox *createDescriptionBox(const QString &description)
36 {
37     auto box = new QGroupBox;
38     auto layout = new QVBoxLayout(box);
39     auto label = new QLabel(description, box);
40     label->setWordWrap(true);
41     layout->addWidget(label);
42     layout->setAlignment(label, Qt::AlignTop);
43     box->setTitle(QCoreApplication::translate("CoreConfigWizard", "Description"));
44     return box;
45 }
46
47
48 template<typename FieldInfo>
49 QGroupBox *createFieldBox(const QString &title, const std::vector<FieldInfo> &fieldInfos)
50 {
51     // Create a config UI based on the field types sent from the backend
52     // We make some assumptions here (like integer range and password field names) that may not
53     // hold true for future authenticator types - but the only way around it for now would be to
54     // provide specialized config widgets for those (which may be a good idea anyway, e.g. if we
55     // think about client-side translations...)
56
57     QGroupBox *fieldBox = new QGroupBox;
58     fieldBox->setTitle(title);
59
60     QFormLayout *formLayout = new QFormLayout(fieldBox);
61     for (auto &&fieldInfo : fieldInfos) {
62         QWidget *widget {nullptr};
63         switch (std::get<2>(fieldInfo).type()) {
64             case QVariant::Int:
65                 widget = new QSpinBox(fieldBox);
66                 // Here we assume that int fields are always in 16 bit range, like ports
67                 static_cast<QSpinBox *>(widget)->setMinimum(0);
68                 static_cast<QSpinBox *>(widget)->setMaximum(65535);
69                 static_cast<QSpinBox *>(widget)->setValue(std::get<2>(fieldInfo).toInt());
70                 break;
71             case QVariant::String:
72                 widget = new QLineEdit(std::get<2>(fieldInfo).toString(), fieldBox);
73                 // Here we assume that fields named something with "password" are actual password inputs
74                 if (std::get<0>(fieldInfo).toLower().contains("password"))
75                     static_cast<QLineEdit *>(widget)->setEchoMode(QLineEdit::Password);
76                 break;
77             default:
78                 qWarning() << "Unsupported type for backend property" << std::get<0>(fieldInfo);
79         }
80         if (widget) {
81             widget->setObjectName(std::get<0>(fieldInfo));
82             formLayout->addRow(std::get<1>(fieldInfo) + ":", widget);
83         }
84     }
85     return fieldBox;
86 }
87
88
89 template<typename FieldInfo>
90 QVariantMap propertiesFromFieldWidgets(QGroupBox *fieldBox, const std::vector<FieldInfo> &fieldInfos)
91 {
92     QVariantMap properties;
93     if (!fieldBox)
94         return properties;
95
96     for (auto &&fieldInfo : fieldInfos) {
97         QString key = std::get<0>(fieldInfo);
98         QVariant value;
99         switch (std::get<2>(fieldInfo).type()) {
100             case QVariant::Int: {
101                 QSpinBox *spinBox = fieldBox->findChild<QSpinBox *>(key);
102                 if (spinBox)
103                     value = spinBox->value();
104                 else
105                     qWarning() << "Could not find child widget for field" << key;
106                 break;
107             }
108             case QVariant::String: {
109                 QLineEdit *lineEdit = fieldBox->findChild<QLineEdit *>(key);
110                 if (lineEdit)
111                     value = lineEdit->text();
112                 else
113                     qWarning() << "Could not find child widget for field" << key;
114                 break;
115             }
116             default:
117                 qWarning() << "Unsupported type for backend property" << key;
118         }
119         properties[key] = std::move(value);
120     }
121     return properties;
122 }
123
124 } // anon
125
126
127 CoreConfigWizard::CoreConfigWizard(CoreConnection *connection, const QVariantList &backendInfos, const QVariantList &authInfos, QWidget *parent)
128     : QWizard(parent),
129     _connection{connection}
130 {
131     setModal(true);
132     setAttribute(Qt::WA_DeleteOnClose);
133
134     setPage(IntroPage, new CoreConfigWizardPages::IntroPage(this));
135     setPage(AdminUserPage, new CoreConfigWizardPages::AdminUserPage(this));
136     setPage(AuthenticationSelectionPage, new CoreConfigWizardPages::AuthenticationSelectionPage(authInfos, this));
137     setPage(StorageSelectionPage, new CoreConfigWizardPages::StorageSelectionPage(backendInfos, this));
138     syncPage = new CoreConfigWizardPages::SyncPage(this);
139     connect(syncPage, SIGNAL(setupCore(const QString &, const QVariantMap &, const QString &, const QVariantMap &)),
140             SLOT(prepareCoreSetup(const QString &, const QVariantMap &, const QString &, const QVariantMap &)));
141     setPage(SyncPage, syncPage);
142     syncRelayPage = new CoreConfigWizardPages::SyncRelayPage(this);
143     connect(syncRelayPage, SIGNAL(startOver()), this, SLOT(startOver()));
144     setPage(SyncRelayPage, syncRelayPage);
145
146     setStartId(IntroPage);
147
148 #ifndef Q_OS_MAC
149     setWizardStyle(ModernStyle);
150 #endif
151
152     setOption(HaveHelpButton, false);
153     setOption(NoBackButtonOnStartPage, true);
154     setOption(HaveNextButtonOnLastPage, false);
155     setOption(HaveFinishButtonOnEarlyPages, false);
156     setOption(NoCancelButton, true);
157     setOption(IndependentPages, true);
158
159     setModal(true);
160
161     setWindowTitle(CoreConfigWizard::tr("Core Configuration Wizard"));
162     setPixmap(QWizard::LogoPixmap, icon::get("quassel").pixmap(48));
163
164     connect(connection, SIGNAL(coreSetupSuccess()), SLOT(coreSetupSuccess()));
165     connect(connection, SIGNAL(coreSetupFailed(QString)), SLOT(coreSetupFailed(QString)));
166     connect(connection, SIGNAL(synchronized()), SLOT(syncFinished()));
167     connect(this, SIGNAL(rejected()), connection, SLOT(disconnectFromCore()));
168
169
170     // Resize all pages to the size hint of the largest one, so the wizard is large enough
171     QSize maxSize;
172     for (int id : pageIds()) {
173         auto p = page(id);
174         p->adjustSize();
175         maxSize = maxSize.expandedTo(p->sizeHint());
176     }
177     for (int id : pageIds()) {
178         page(id)->setFixedSize(maxSize);
179     }
180 }
181
182
183 void CoreConfigWizard::prepareCoreSetup(const QString &backend, const QVariantMap &properties, const QString &authenticator, const QVariantMap &authProperties)
184 {
185     // Prevent the user from changing any settings he already specified...
186     for (auto &&idx : visitedPages())
187         page(idx)->setEnabled(false);
188
189     // FIXME? We need to be able to set up older cores that don't have auth backend support.
190     // So if the core doesn't support that feature, don't pass those parameters.
191     if (!Client::isCoreFeatureEnabled(Quassel::Feature::Authenticators)) {
192         coreConnection()->setupCore(Protocol::SetupData(field("adminUser.user").toString(), field("adminUser.password").toString(), backend, properties));
193     }
194     else {
195         coreConnection()->setupCore(Protocol::SetupData(field("adminUser.user").toString(), field("adminUser.password").toString(), backend, properties, authenticator, authProperties));
196     }
197 }
198
199
200 void CoreConfigWizard::coreSetupSuccess()
201 {
202     syncPage->setStatus(tr("Your core has been successfully configured. Logging you in..."));
203     syncPage->setError(false);
204     syncRelayPage->setMode(CoreConfigWizardPages::SyncRelayPage::Success);
205     coreConnection()->loginToCore(field("adminUser.user").toString(), field("adminUser.password").toString(), field("adminUser.rememberPasswd").toBool());
206 }
207
208
209 void CoreConfigWizard::coreSetupFailed(const QString &error)
210 {
211     syncPage->setStatus(tr("Core configuration failed:<br><b>%1</b><br>Press <em>Next</em> to start over.").arg(error));
212     syncPage->setError(true);
213     syncRelayPage->setMode(CoreConfigWizardPages::SyncRelayPage::Error);
214     //foreach(int idx, visitedPages()) page(idx)->setEnabled(true);
215     //setStartId(SyncPage);
216     //restart();
217 }
218
219
220 void CoreConfigWizard::startOver()
221 {
222     foreach(int idx, visitedPages()) page(idx)->setEnabled(true);
223     setStartId(CoreConfigWizard::AdminUserPage);
224     restart();
225 }
226
227
228 void CoreConfigWizard::syncFinished()
229 {
230     accept();
231 }
232
233
234 namespace CoreConfigWizardPages {
235 /*** Intro Page ***/
236
237 IntroPage::IntroPage(QWidget *parent) : QWizardPage(parent)
238 {
239     ui.setupUi(this);
240     setTitle(tr("Introduction"));
241     //setSubTitle(tr("foobar"));
242     //setPixmap(QWizard::WatermarkPixmap, QPixmap(":icons/quassel-icon.png"));
243 }
244
245
246 int IntroPage::nextId() const
247 {
248     return CoreConfigWizard::AdminUserPage;
249 }
250
251
252 /*** Admin User Page ***/
253
254 AdminUserPage::AdminUserPage(QWidget *parent) : QWizardPage(parent)
255 {
256     ui.setupUi(this);
257     setTitle(tr("Create Admin User"));
258     setSubTitle(tr("First, we will create a user on the core. This first user will have administrator privileges."));
259
260     registerField("adminUser.user*", ui.user);
261     registerField("adminUser.password*", ui.password);
262     registerField("adminUser.password2*", ui.password2);
263     registerField("adminUser.rememberPasswd", ui.rememberPasswd);
264 }
265
266
267 int AdminUserPage::nextId() const
268 {
269     // If the core doesn't support auth backends, skip that page!
270     if (!Client::isCoreFeatureEnabled(Quassel::Feature::Authenticators)) {
271         return CoreConfigWizard::StorageSelectionPage;
272     }
273     else {
274         return CoreConfigWizard::AuthenticationSelectionPage;
275     }
276 }
277
278
279 bool AdminUserPage::isComplete() const
280 {
281     bool ok = !ui.user->text().isEmpty() && !ui.password->text().isEmpty() && ui.password->text() == ui.password2->text();
282     return ok;
283 }
284
285 /*** Authentication Selection Page ***/
286
287 AuthenticationSelectionPage::AuthenticationSelectionPage(const QVariantList &authInfos, QWidget *parent)
288     : QWizardPage(parent)
289 {
290     ui.setupUi(this);
291
292     setTitle(tr("Select Authentication Backend"));
293     setSubTitle(tr("Please select a backend for Quassel Core to use for authenticating users."));
294
295     registerField("authentication.backend", ui.backendList);
296
297     for (auto &&authInfo : authInfos) {
298         auto props = authInfo.toMap();
299         // Extract field infos to avoid having to reparse the list
300         std::vector<FieldInfo> fields;
301         const auto &list = props["SetupData"].toList();
302         for (int i = 0; i + 2 < list.size(); i += 3) {
303             fields.emplace_back(std::make_tuple(list[i].toString(), list[i+1].toString(), list[i+2]));
304         }
305         props.remove("SetupData");
306
307         _authProperties.emplace_back(std::move(props));
308         _authFields.emplace_back(std::move(fields));
309
310         // Create widgets
311         ui.backendList->addItem(_authProperties.back()["DisplayName"].toString(), _authProperties.back()["BackendId"].toString());
312         ui.descriptionStack->addWidget(createDescriptionBox(_authProperties.back()["Description"].toString()));
313         ui.authSettingsStack->addWidget(createFieldBox(tr("Authentication Settings"), _authFields.back()));
314     }
315
316     // Do some trickery to make the page large enough
317     setSizePolicy({QSizePolicy::Fixed, QSizePolicy::Fixed});
318
319     QSizePolicy sp{QSizePolicy::MinimumExpanding, QSizePolicy::Fixed};
320 #if QT_VERSION >= 0x050200
321     sp.setRetainSizeWhenHidden(true);
322 #else
323     ui.authSettingsStack->setVisible(true);  // ugly hack that will show an empty box, but we'll deprecate Qt4 soon anyway
324 #endif
325     ui.descriptionStack->setSizePolicy(sp);
326     ui.authSettingsStack->setSizePolicy(sp);
327
328     ui.descriptionStack->adjustSize();
329     ui.authSettingsStack->adjustSize();
330
331     ui.backendList->setCurrentIndex(0);
332 }
333
334
335 int AuthenticationSelectionPage::nextId() const
336 {
337     return CoreConfigWizard::StorageSelectionPage;
338 }
339
340
341 QString AuthenticationSelectionPage::displayName() const
342 {
343     return ui.backendList->currentText();
344 }
345
346
347 QString AuthenticationSelectionPage::authenticator() const
348 {
349 #if QT_VERSION >= 0x050200
350     return ui.backendList->currentData().toString();
351 #else
352     return ui.backendList->itemData(ui.backendList->currentIndex()).toString();
353 #endif
354 }
355
356
357 QVariantMap AuthenticationSelectionPage::authProperties() const
358 {
359     return propertiesFromFieldWidgets(qobject_cast<QGroupBox *>(ui.authSettingsStack->currentWidget()),
360                                       _authFields[ui.backendList->currentIndex()]);
361 }
362
363
364 void AuthenticationSelectionPage::on_backendList_currentIndexChanged(int index)
365 {
366     ui.descriptionStack->setCurrentIndex(index);
367     ui.authSettingsStack->setCurrentIndex(index);
368     ui.authSettingsStack->setVisible(!_authFields[index].empty());
369 }
370
371 /*** Storage Selection Page ***/
372
373 StorageSelectionPage::StorageSelectionPage(const QVariantList &backendInfos, QWidget *parent)
374     : QWizardPage(parent)
375 {
376     ui.setupUi(this);
377
378     setTitle(tr("Select Storage Backend"));
379     setSubTitle(tr("Please select a storage backend for Quassel Core."));
380     setCommitPage(true);
381
382     registerField("storage.backend", ui.backendList);
383
384     int defaultIndex {0};  // Legacy cores send backend infos in arbitrary order
385
386     for (auto &&backendInfo : backendInfos) {
387         auto props = backendInfo.toMap();
388         // Extract field infos to avoid having to reparse the list
389         std::vector<FieldInfo> fields;
390
391         // Legacy cores (prior to 0.13) didn't send SetupData for storage backends; deal with this
392         if (!props.contains("SetupData")) {
393             const auto &defaultValues = props["SetupDefaults"].toMap();
394             for (auto &&key : props["SetupKeys"].toStringList()) {
395                 fields.emplace_back(std::make_tuple(key, key, defaultValues.value(key, QString{})));
396             }
397             if (props.value("IsDefault", false).toBool()) {
398                 defaultIndex = ui.backendList->count();
399             }
400         }
401         else {
402             const auto &list = props["SetupData"].toList();
403             for (int i = 0; i + 2 < list.size(); i += 3) {
404                 fields.emplace_back(std::make_tuple(list[i].toString(), list[i+1].toString(), list[i+2]));
405             }
406             props.remove("SetupData");
407         }
408         props.remove("SetupKeys");
409         props.remove("SetupDefaults");
410         // Legacy cores (prior to 0.13) don't send the BackendId property
411         if (!props.contains("BackendId"))
412             props["BackendId"] = props["DisplayName"];
413         _backendProperties.emplace_back(std::move(props));
414         _backendFields.emplace_back(std::move(fields));
415
416         // Create widgets
417         ui.backendList->addItem(_backendProperties.back()["DisplayName"].toString(), _backendProperties.back()["BackendId"].toString());
418         ui.descriptionStack->addWidget(createDescriptionBox(_backendProperties.back()["Description"].toString()));
419         ui.storageSettingsStack->addWidget(createFieldBox(tr("Storage Settings"), _backendFields.back()));
420     }
421
422     // Do some trickery to make the page large enough
423     setSizePolicy({QSizePolicy::Fixed, QSizePolicy::Fixed});
424
425     QSizePolicy sp{QSizePolicy::MinimumExpanding, QSizePolicy::Fixed};
426 #if QT_VERSION >= 0x050200
427     sp.setRetainSizeWhenHidden(true);
428 #else
429     ui.storageSettingsStack->setVisible(true);  // ugly hack that will show an empty box, but we'll deprecate Qt4 soon anyway
430 #endif
431     ui.descriptionStack->setSizePolicy(sp);
432     ui.storageSettingsStack->setSizePolicy(sp);
433
434     ui.descriptionStack->adjustSize();
435     ui.storageSettingsStack->adjustSize();
436
437     ui.backendList->setCurrentIndex(defaultIndex);
438 }
439
440
441 int StorageSelectionPage::nextId() const
442 {
443     return CoreConfigWizard::SyncPage;
444 }
445
446
447 QString StorageSelectionPage::displayName() const
448 {
449     return ui.backendList->currentText();
450 }
451
452
453 QString StorageSelectionPage::backend() const
454 {
455 #if QT_VERSION >= 0x050200
456     return ui.backendList->currentData().toString();
457 #else
458     return ui.backendList->itemData(ui.backendList->currentIndex()).toString();
459 #endif
460 }
461
462
463 QVariantMap StorageSelectionPage::backendProperties() const
464 {
465     return propertiesFromFieldWidgets(qobject_cast<QGroupBox *>(ui.storageSettingsStack->currentWidget()),
466                                       _backendFields[ui.backendList->currentIndex()]);
467 }
468
469
470 void StorageSelectionPage::on_backendList_currentIndexChanged(int index)
471 {
472     ui.descriptionStack->setCurrentIndex(index);
473     ui.storageSettingsStack->setCurrentIndex(index);
474     ui.storageSettingsStack->setVisible(!_backendFields[index].empty());
475 }
476
477
478 /*** Sync Page ***/
479
480 SyncPage::SyncPage(QWidget *parent) : QWizardPage(parent)
481 {
482     ui.setupUi(this);
483     setTitle(tr("Storing Your Settings"));
484     setSubTitle(tr("Your settings are now being stored in the core, and you will be logged in automatically."));
485 }
486
487
488 void SyncPage::initializePage()
489 {
490     _complete = false;
491     _hasError = false;
492     emit completeChanged();
493
494     // Fill in sync info about the storage layer.
495     StorageSelectionPage *storagePage = qobject_cast<StorageSelectionPage *>(wizard()->page(CoreConfigWizard::StorageSelectionPage));
496     QString backend = storagePage->backend();
497     QVariantMap backendProperties = storagePage->backendProperties();
498     ui.backend->setText(storagePage->displayName());
499
500     // Fill in sync info about the authentication layer.
501     AuthenticationSelectionPage *authPage = qobject_cast<AuthenticationSelectionPage *>(wizard()->page(CoreConfigWizard::AuthenticationSelectionPage));
502     QString authenticator = authPage->authenticator();
503     QVariantMap authProperties = authPage->authProperties();
504     ui.authenticator->setText(authPage->displayName());
505
506     ui.user->setText(wizard()->field("adminUser.user").toString());
507
508     emit setupCore(backend, backendProperties, authenticator, authProperties);
509 }
510
511
512 int SyncPage::nextId() const
513 {
514     if (!_hasError)
515         return -1;
516     return CoreConfigWizard::SyncRelayPage;
517 }
518
519
520 bool SyncPage::isComplete() const
521 {
522     return _complete || _hasError;
523 }
524
525
526 void SyncPage::setStatus(const QString &status)
527 {
528     ui.status->setText(status);
529 }
530
531
532 void SyncPage::setError(bool e)
533 {
534     _hasError = e;
535     setFinalPage(!e);
536     emit completeChanged();
537 }
538
539
540 void SyncPage::setComplete(bool c)
541 {
542     _complete = c;
543     completeChanged();
544 }
545
546
547 /*** Sync Relay Page ***/
548
549 SyncRelayPage::SyncRelayPage(QWidget *parent) : QWizardPage(parent)
550 {
551     mode = Success;
552 }
553
554
555 void SyncRelayPage::setMode(Mode m)
556 {
557     mode = m;
558 }
559
560 int SyncRelayPage::nextId() const
561 {
562     emit startOver();
563     return 0;
564 }
565 };  /* namespace CoreConfigWizardPages */