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