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