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