cmake: avoid de-duplication of user's CXXFLAGS
[quassel.git] / src / qtui / coreconfigwizard.cpp
1 /***************************************************************************
2  *   Copyright (C) 2005-2022 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 <QAbstractButton>
24 #include <QCoreApplication>
25 #include <QDebug>
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 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 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             auto* 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             auto* 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 }  // namespace
125
126 CoreConfigWizard::CoreConfigWizard(CoreConnection* connection, const QVariantList& backendInfos, const QVariantList& authInfos, QWidget* parent)
127     : QWizard(parent)
128     , _connection{connection}
129 {
130     setModal(true);
131     setAttribute(Qt::WA_DeleteOnClose);
132
133     setPage(IntroPage, new CoreConfigWizardPages::IntroPage(this));
134     setPage(AdminUserPage, new CoreConfigWizardPages::AdminUserPage(this));
135     setPage(AuthenticationSelectionPage, new CoreConfigWizardPages::AuthenticationSelectionPage(authInfos, this));
136     setPage(StorageSelectionPage, new CoreConfigWizardPages::StorageSelectionPage(backendInfos, this));
137     syncPage = new CoreConfigWizardPages::SyncPage(this);
138     connect(syncPage, &CoreConfigWizardPages::SyncPage::setupCore, this, &CoreConfigWizard::prepareCoreSetup);
139     setPage(SyncPage, syncPage);
140     syncRelayPage = new CoreConfigWizardPages::SyncRelayPage(this);
141     connect(syncRelayPage, &CoreConfigWizardPages::SyncRelayPage::startOver, this, &CoreConfigWizard::startOver);
142     setPage(SyncRelayPage, syncRelayPage);
143
144     setStartId(IntroPage);
145
146 #ifndef Q_OS_MAC
147     setWizardStyle(ModernStyle);
148 #endif
149
150     setOption(HaveHelpButton, false);
151     setOption(NoBackButtonOnStartPage, true);
152     setOption(HaveNextButtonOnLastPage, false);
153     setOption(HaveFinishButtonOnEarlyPages, false);
154     setOption(NoCancelButton, true);
155     setOption(IndependentPages, true);
156
157     setModal(true);
158
159     setWindowTitle(CoreConfigWizard::tr("Core Configuration Wizard"));
160     setPixmap(QWizard::LogoPixmap, icon::get("quassel").pixmap(48));
161
162     connect(connection, &CoreConnection::coreSetupSuccess, this, &CoreConfigWizard::coreSetupSuccess);
163     connect(connection, &CoreConnection::coreSetupFailed, this, &CoreConfigWizard::coreSetupFailed);
164     connect(connection, &CoreConnection::synchronized, this, &CoreConfigWizard::syncFinished);
165     connect(this, &QDialog::rejected, connection, selectOverload<>(&CoreConnection::disconnectFromCore));
166
167     // Resize all pages to the size hint of the largest one, so the wizard is large enough
168     QSize maxSize;
169     for (int id : pageIds()) {
170         auto p = page(id);
171         p->adjustSize();
172         maxSize = maxSize.expandedTo(p->sizeHint());
173     }
174     for (int id : pageIds()) {
175         page(id)->setFixedSize(maxSize);
176     }
177 }
178
179 void CoreConfigWizard::prepareCoreSetup(const QString& backend,
180                                         const QVariantMap& properties,
181                                         const QString& authenticator,
182                                         const QVariantMap& authProperties)
183 {
184     // Prevent the user from changing any settings he already specified...
185     for (auto&& idx : visitedPages())
186         page(idx)->setEnabled(false);
187
188     // FIXME? We need to be able to set up older cores that don't have auth backend support.
189     // So if the core doesn't support that feature, don't pass those parameters.
190     if (!Client::isCoreFeatureEnabled(Quassel::Feature::Authenticators)) {
191         coreConnection()->setupCore(
192             Protocol::SetupData(field("adminUser.user").toString(), field("adminUser.password").toString(), backend, properties));
193     }
194     else {
195         coreConnection()->setupCore(Protocol::SetupData(field("adminUser.user").toString(),
196                                                         field("adminUser.password").toString(),
197                                                         backend,
198                                                         properties,
199                                                         authenticator,
200                                                         authProperties));
201     }
202 }
203
204 void CoreConfigWizard::coreSetupSuccess()
205 {
206     syncPage->setStatus(tr("Your core has been successfully configured. Logging you in..."));
207     syncPage->setError(false);
208     syncRelayPage->setMode(CoreConfigWizardPages::SyncRelayPage::Success);
209     coreConnection()->loginToCore(field("adminUser.user").toString(),
210                                   field("adminUser.password").toString(),
211                                   field("adminUser.rememberPasswd").toBool());
212 }
213
214 void CoreConfigWizard::coreSetupFailed(const QString& error)
215 {
216     syncPage->setStatus(tr("Core configuration failed:<br><b>%1</b><br>Press <em>Next</em> to start over.").arg(error));
217     syncPage->setError(true);
218     syncRelayPage->setMode(CoreConfigWizardPages::SyncRelayPage::Error);
219     // foreach(int idx, visitedPages()) page(idx)->setEnabled(true);
220     // setStartId(SyncPage);
221     // restart();
222 }
223
224 void CoreConfigWizard::startOver()
225 {
226     foreach (int idx, visitedPages())
227         page(idx)->setEnabled(true);
228     setStartId(CoreConfigWizard::AdminUserPage);
229     restart();
230 }
231
232 void CoreConfigWizard::syncFinished()
233 {
234     accept();
235 }
236
237 namespace CoreConfigWizardPages {
238 /*** Intro Page ***/
239
240 IntroPage::IntroPage(QWidget* parent)
241     : QWizardPage(parent)
242 {
243     ui.setupUi(this);
244     setTitle(tr("Introduction"));
245     // setSubTitle(tr("foobar"));
246     // setPixmap(QWizard::WatermarkPixmap, QPixmap(":icons/quassel-icon.png"));
247 }
248
249 int IntroPage::nextId() const
250 {
251     return CoreConfigWizard::AdminUserPage;
252 }
253
254 /*** Admin User Page ***/
255
256 AdminUserPage::AdminUserPage(QWidget* parent)
257     : QWizardPage(parent)
258 {
259     ui.setupUi(this);
260     setTitle(tr("Create Admin User"));
261     setSubTitle(tr("First, we will create a user on the core. This first user will have administrator privileges."));
262
263     registerField("adminUser.user*", ui.user);
264     registerField("adminUser.password*", ui.password);
265     registerField("adminUser.password2*", ui.password2);
266     registerField("adminUser.rememberPasswd", ui.rememberPasswd);
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 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     sp.setRetainSizeWhenHidden(true);
322     ui.descriptionStack->setSizePolicy(sp);
323     ui.authSettingsStack->setSizePolicy(sp);
324
325     ui.descriptionStack->adjustSize();
326     ui.authSettingsStack->adjustSize();
327
328     ui.backendList->setCurrentIndex(0);
329 }
330
331 int AuthenticationSelectionPage::nextId() const
332 {
333     return CoreConfigWizard::StorageSelectionPage;
334 }
335
336 QString AuthenticationSelectionPage::displayName() const
337 {
338     return ui.backendList->currentText();
339 }
340
341 QString AuthenticationSelectionPage::authenticator() const
342 {
343     return ui.backendList->currentData().toString();
344 }
345
346 QVariantMap AuthenticationSelectionPage::authProperties() const
347 {
348     return propertiesFromFieldWidgets(qobject_cast<QGroupBox*>(ui.authSettingsStack->currentWidget()),
349                                       _authFields[ui.backendList->currentIndex()]);
350 }
351
352 void AuthenticationSelectionPage::on_backendList_currentIndexChanged(int index)
353 {
354     ui.descriptionStack->setCurrentIndex(index);
355     ui.authSettingsStack->setCurrentIndex(index);
356     ui.authSettingsStack->setVisible(!_authFields[index].empty());
357 }
358
359 /*** Storage Selection Page ***/
360
361 StorageSelectionPage::StorageSelectionPage(const QVariantList& backendInfos, QWidget* parent)
362     : QWizardPage(parent)
363 {
364     ui.setupUi(this);
365
366     setTitle(tr("Select Storage Backend"));
367     setSubTitle(tr("Please select a storage backend for Quassel Core."));
368     setCommitPage(true);
369
370     registerField("storage.backend", ui.backendList);
371
372     int defaultIndex{0};  // Legacy cores send backend infos in arbitrary order
373
374     for (auto&& backendInfo : backendInfos) {
375         auto props = backendInfo.toMap();
376         // Extract field infos to avoid having to reparse the list
377         std::vector<FieldInfo> fields;
378
379         // Legacy cores (prior to 0.13) didn't send SetupData for storage backends; deal with this
380         if (!props.contains("SetupData")) {
381             const auto& defaultValues = props["SetupDefaults"].toMap();
382             for (auto&& key : props["SetupKeys"].toStringList()) {
383                 fields.emplace_back(std::make_tuple(key, key, defaultValues.value(key, QString{})));
384             }
385             if (props.value("IsDefault", false).toBool()) {
386                 defaultIndex = ui.backendList->count();
387             }
388         }
389         else {
390             const auto& list = props["SetupData"].toList();
391             for (int i = 0; i + 2 < list.size(); i += 3) {
392                 fields.emplace_back(std::make_tuple(list[i].toString(), list[i + 1].toString(), list[i + 2]));
393             }
394             props.remove("SetupData");
395         }
396         props.remove("SetupKeys");
397         props.remove("SetupDefaults");
398         // Legacy cores (prior to 0.13) don't send the BackendId property
399         if (!props.contains("BackendId"))
400             props["BackendId"] = props["DisplayName"];
401         _backendProperties.emplace_back(std::move(props));
402         _backendFields.emplace_back(std::move(fields));
403
404         // Create widgets
405         ui.backendList->addItem(_backendProperties.back()["DisplayName"].toString(), _backendProperties.back()["BackendId"].toString());
406         ui.descriptionStack->addWidget(createDescriptionBox(_backendProperties.back()["Description"].toString()));
407         ui.storageSettingsStack->addWidget(createFieldBox(tr("Storage Settings"), _backendFields.back()));
408     }
409
410     // Do some trickery to make the page large enough
411     setSizePolicy({QSizePolicy::Fixed, QSizePolicy::Fixed});
412
413     QSizePolicy sp{QSizePolicy::MinimumExpanding, QSizePolicy::Fixed};
414     sp.setRetainSizeWhenHidden(true);
415     ui.descriptionStack->setSizePolicy(sp);
416     ui.storageSettingsStack->setSizePolicy(sp);
417
418     ui.descriptionStack->adjustSize();
419     ui.storageSettingsStack->adjustSize();
420
421     ui.backendList->setCurrentIndex(defaultIndex);
422 }
423
424 int StorageSelectionPage::nextId() const
425 {
426     return CoreConfigWizard::SyncPage;
427 }
428
429 QString StorageSelectionPage::displayName() const
430 {
431     return ui.backendList->currentText();
432 }
433
434 QString StorageSelectionPage::backend() const
435 {
436     return ui.backendList->currentData().toString();
437 }
438
439 QVariantMap StorageSelectionPage::backendProperties() const
440 {
441     return propertiesFromFieldWidgets(qobject_cast<QGroupBox*>(ui.storageSettingsStack->currentWidget()),
442                                       _backendFields[ui.backendList->currentIndex()]);
443 }
444
445 void StorageSelectionPage::on_backendList_currentIndexChanged(int index)
446 {
447     ui.descriptionStack->setCurrentIndex(index);
448     ui.storageSettingsStack->setCurrentIndex(index);
449     ui.storageSettingsStack->setVisible(!_backendFields[index].empty());
450 }
451
452 /*** Sync Page ***/
453
454 SyncPage::SyncPage(QWidget* parent)
455     : QWizardPage(parent)
456 {
457     ui.setupUi(this);
458     setTitle(tr("Storing Your Settings"));
459     setSubTitle(tr("Your settings are now being stored in the core, and you will be logged in automatically."));
460 }
461
462 void SyncPage::initializePage()
463 {
464     _complete = false;
465     _hasError = false;
466     emit completeChanged();
467
468     // Fill in sync info about the storage layer.
469     auto* storagePage = qobject_cast<StorageSelectionPage*>(wizard()->page(CoreConfigWizard::StorageSelectionPage));
470     QString backend = storagePage->backend();
471     QVariantMap backendProperties = storagePage->backendProperties();
472     ui.backend->setText(storagePage->displayName());
473
474     // Fill in sync info about the authentication layer.
475     auto* authPage = qobject_cast<AuthenticationSelectionPage*>(wizard()->page(CoreConfigWizard::AuthenticationSelectionPage));
476     QString authenticator = authPage->authenticator();
477     QVariantMap authProperties = authPage->authProperties();
478     ui.authenticator->setText(authPage->displayName());
479
480     ui.user->setText(wizard()->field("adminUser.user").toString());
481
482     emit setupCore(backend, backendProperties, authenticator, authProperties);
483 }
484
485 int SyncPage::nextId() const
486 {
487     if (!_hasError)
488         return -1;
489     return CoreConfigWizard::SyncRelayPage;
490 }
491
492 bool SyncPage::isComplete() const
493 {
494     return _complete || _hasError;
495 }
496
497 void SyncPage::setStatus(const QString& status)
498 {
499     ui.status->setText(status);
500 }
501
502 void SyncPage::setError(bool e)
503 {
504     _hasError = e;
505     setFinalPage(!e);
506     emit completeChanged();
507 }
508
509 void SyncPage::setComplete(bool c)
510 {
511     _complete = c;
512     completeChanged();
513 }
514
515 /*** Sync Relay Page ***/
516
517 SyncRelayPage::SyncRelayPage(QWidget* parent)
518     : QWizardPage(parent)
519 {
520     mode = Success;
521 }
522
523 void SyncRelayPage::setMode(Mode m)
524 {
525     mode = m;
526 }
527
528 int SyncRelayPage::nextId() const
529 {
530     emit startOver();
531     return 0;
532 }
533 } /* namespace CoreConfigWizardPages */