diff --git a/src/modules/users/CMakeLists.txt b/src/modules/users/CMakeLists.txt index a449d39ac..bf93eb26a 100644 --- a/src/modules/users/CMakeLists.txt +++ b/src/modules/users/CMakeLists.txt @@ -28,6 +28,7 @@ calamares_add_plugin( users UsersPage.cpp SetHostNameJob.cpp CheckPWQuality.cpp + Config.cpp UI page_usersetup.ui RESOURCES diff --git a/src/modules/users/CheckPWQuality.cpp b/src/modules/users/CheckPWQuality.cpp index ab3eed84f..f06137c9b 100644 --- a/src/modules/users/CheckPWQuality.cpp +++ b/src/modules/users/CheckPWQuality.cpp @@ -55,7 +55,7 @@ DEFINE_CHECK_FUNC( minLength ) { cDebug() << Logger::SubEntry << "minLength set to" << minLength; checks.push_back( PasswordCheck( []() { return QCoreApplication::translate( "PWQ", "Password is too short" ); }, - [ minLength ]( const QString& s ) { return s.length() >= minLength; }, + [minLength]( const QString& s ) { return s.length() >= minLength; }, PasswordCheck::Weight( 10 ) ) ); } } @@ -71,7 +71,7 @@ DEFINE_CHECK_FUNC( maxLength ) { cDebug() << Logger::SubEntry << "maxLength set to" << maxLength; checks.push_back( PasswordCheck( []() { return QCoreApplication::translate( "PWQ", "Password is too long" ); }, - [ maxLength ]( const QString& s ) { return s.length() <= maxLength; }, + [maxLength]( const QString& s ) { return s.length() <= maxLength; }, PasswordCheck::Weight( 10 ) ) ); } } @@ -349,8 +349,8 @@ DEFINE_CHECK_FUNC( libpwquality ) /* Something actually added? */ if ( requirement_count ) { - checks.push_back( PasswordCheck( [ settings ]() { return settings->explanation(); }, - [ settings ]( const QString& s ) { + checks.push_back( PasswordCheck( [settings]() { return settings->explanation(); }, + [settings]( const QString& s ) { int r = settings->check( s ); if ( r < 0 ) { diff --git a/src/modules/users/Config.cpp b/src/modules/users/Config.cpp new file mode 100644 index 000000000..bb739cbd1 --- /dev/null +++ b/src/modules/users/Config.cpp @@ -0,0 +1,372 @@ +/* === This file is part of Calamares - === + * + * SPDX-FileCopyrightText: 2020 Adriaan de Groot + * SPDX-License-Identifier: GPL-3.0-or-later + * License-Filename: LICENSE + * + * Calamares is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Calamares is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Calamares. If not, see . + */ + +#include "Config.h" + +#include "GlobalStorage.h" +#include "JobQueue.h" +#include "utils/Logger.h" +#include "utils/String.h" +#include "utils/Variant.h" + +#include +#include +#include + +static const QRegExp USERNAME_RX( "^[a-z_][a-z0-9_-]*[$]?$" ); +static constexpr const int USERNAME_MAX_LENGTH = 31; + +static const QRegExp HOSTNAME_RX( "^[a-zA-Z0-9][-a-zA-Z0-9_]*$" ); +static constexpr const int HOSTNAME_MIN_LENGTH = 2; +static constexpr const int HOSTNAME_MAX_LENGTH = 63; + +Config::Config( QObject* parent ) + : QObject( parent ) +{ +} + +Config::~Config() {} + +void +Config::setUserShell( const QString& shell ) +{ + if ( !shell.isEmpty() && !shell.startsWith( '/' ) ) + { + cWarning() << "User shell" << shell << "is not an absolute path."; + return; + } + // The shell is put into GS because the CreateUser job expects it there + Calamares::JobQueue::instance()->globalStorage()->insert( "userShell", shell ); +} + +static inline void +insertInGlobalStorage( const QString& key, const QString& group ) +{ + auto* gs = Calamares::JobQueue::instance()->globalStorage(); + if ( !gs || group.isEmpty() ) + { + return; + } + gs->insert( key, group ); +} + +void +Config::setAutologinGroup( const QString& group ) +{ + insertInGlobalStorage( QStringLiteral( "autologinGroup" ), group ); + emit autologinGroupChanged( group ); +} + +void +Config::setSudoersGroup( const QString& group ) +{ + insertInGlobalStorage( QStringLiteral( "sudoersGroup" ), group ); + emit sudoersGroupChanged( group ); +} + + +void +Config::setLoginName( const QString& login ) +{ + if ( login != m_loginName ) + { + Calamares::GlobalStorage* gs = Calamares::JobQueue::instance()->globalStorage(); + if ( login.isEmpty() ) + { + gs->remove( "username" ); + } + else + { + gs->insert( "username", login ); + } + + m_customLoginName = !login.isEmpty(); + m_loginName = login; + emit loginNameChanged( login ); + emit loginNameStatusChanged( loginNameStatus() ); + } +} + +const QStringList& +Config::forbiddenLoginNames() +{ + static QStringList forbidden { "root" }; + return forbidden; +} + +QString +Config::loginNameStatus() const +{ + // An empty login is "ok", even if it isn't really + if ( m_loginName.isEmpty() ) + { + return QString(); + } + + if ( m_loginName.length() > USERNAME_MAX_LENGTH ) + { + return tr( "Your username is too long." ); + } + for ( const QString& badName : forbiddenLoginNames() ) + { + if ( 0 == QString::compare( badName, m_loginName, Qt::CaseSensitive ) ) + { + return tr( "'%1' is not allowed as username." ).arg( badName ); + } + } + + QString login( m_loginName ); // make a copy because validate() doesn't take const& + QRegExpValidator validateEntireLoginName( USERNAME_RX ); + QRegExpValidator validateFirstLetter( QRegExp( "[a-z_].*" ) ); // anchors are implicit in QRegExpValidator + int pos = -1; + if ( validateFirstLetter.validate( login, pos ) == QValidator::Invalid ) + { + return tr( "Your username must start with a lowercase letter or underscore." ); + } + if ( validateEntireLoginName.validate( login, pos ) == QValidator::Invalid ) + { + return tr( "Only lowercase letters, numbers, underscore and hyphen are allowed." ); + } + + return QString(); +} + +void +Config::setHostName( const QString& host ) +{ + if ( host != m_hostName ) + { + Calamares::GlobalStorage* gs = Calamares::JobQueue::instance()->globalStorage(); + if ( host.isEmpty() ) + { + gs->remove( "hostname" ); + } + else + { + gs->insert( "hostname", host ); + } + + m_customHostName = !host.isEmpty(); + m_hostName = host; + emit hostNameChanged( host ); + emit hostNameStatusChanged( hostNameStatus() ); + } +} + +const QStringList& +Config::forbiddenHostNames() +{ + static QStringList forbidden { "localhost" }; + return forbidden; +} + +QString +Config::hostNameStatus() const +{ + // An empty hostname is "ok", even if it isn't really + if ( m_hostName.isEmpty() ) + { + return QString(); + } + + if ( m_hostName.length() < HOSTNAME_MIN_LENGTH ) + { + return tr( "Your hostname is too short." ); + } + if ( m_hostName.length() > HOSTNAME_MAX_LENGTH ) + { + return tr( "Your hostname is too long." ); + } + for ( const QString& badName : forbiddenHostNames() ) + { + if ( 0 == QString::compare( badName, m_hostName, Qt::CaseSensitive ) ) + { + return tr( "'%1' is not allowed as hostname." ).arg( badName ); + } + } + + QString text = m_hostName; + QRegExpValidator val( HOSTNAME_RX ); + int pos = -1; + + if ( val.validate( text, pos ) == QValidator::Invalid ) + { + return tr( "Only letters, numbers, underscore and hyphen are allowed." ); + } + + return QString(); +} + + +/** @brief Guess the machine's name + * + * If there is DMI data, use that; otherwise, just call the machine "-pc". + * Reads the DMI data just once. + */ +static QString +guessProductName() +{ + static bool tried = false; + static QString dmiProduct; + + if ( !tried ) + { + // yes validateHostnameText() but these files can be a mess + QRegExp dmirx( "[^a-zA-Z0-9]", Qt::CaseInsensitive ); + QFile dmiFile( QStringLiteral( "/sys/devices/virtual/dmi/id/product_name" ) ); + + if ( dmiFile.exists() && dmiFile.open( QIODevice::ReadOnly ) ) + { + dmiProduct = QString::fromLocal8Bit( dmiFile.readAll().simplified().data() ) + .toLower() + .replace( dmirx, " " ) + .remove( ' ' ); + } + if ( dmiProduct.isEmpty() ) + { + dmiProduct = QStringLiteral( "pc" ); + } + tried = true; + } + return dmiProduct; +} + +static QString +makeLoginNameSuggestion( const QStringList& parts ) +{ + if ( parts.isEmpty() || parts.first().isEmpty() ) + { + return QString(); + } + + QString usernameSuggestion = parts.first(); + for ( int i = 1; i < parts.length(); ++i ) + { + if ( !parts.value( i ).isEmpty() ) + { + usernameSuggestion.append( parts.value( i ).at( 0 ) ); + } + } + + return USERNAME_RX.indexIn( usernameSuggestion ) != -1 ? usernameSuggestion : QString(); +} + +static QString +makeHostnameSuggestion( const QStringList& parts ) +{ + static const QRegExp HOSTNAME_RX( "^[a-zA-Z0-9][-a-zA-Z0-9_]*$" ); + if ( parts.isEmpty() || parts.first().isEmpty() ) + { + return QString(); + } + + QString productName = guessProductName(); + QString hostnameSuggestion = QStringLiteral( "%1-%2" ).arg( parts.first() ).arg( productName ); + return HOSTNAME_RX.indexIn( hostnameSuggestion ) != -1 ? hostnameSuggestion : QString(); +} + +void +Config::setFullName( const QString& name ) +{ + if ( name.isEmpty() && !m_fullName.isEmpty() ) + { + if ( !m_customHostName ) + { + setHostName( name ); + } + if ( !m_customLoginName ) + { + setLoginName( name ); + } + m_fullName = name; + emit fullNameChanged( name ); + } + + if ( name != m_fullName ) + { + m_fullName = name; + emit fullNameChanged( name ); + + // Build login and hostname, if needed + QRegExp rx( "[^a-zA-Z0-9 ]", Qt::CaseInsensitive ); + QString cleanName = CalamaresUtils::removeDiacritics( name ).toLower().replace( rx, " " ).simplified(); + QStringList cleanParts = cleanName.split( ' ' ); + + if ( !m_customLoginName ) + { + QString login = makeLoginNameSuggestion( cleanParts ); + if ( !login.isEmpty() && login != m_loginName ) + { + m_loginName = login; + emit loginNameChanged( login ); + emit loginNameStatusChanged( loginNameStatus() ); + } + } + if ( !m_customHostName ) + { + QString hostname = makeHostnameSuggestion( cleanParts ); + if ( !hostname.isEmpty() && hostname != m_hostName ) + { + m_hostName = hostname; + emit hostNameChanged( hostname ); + emit hostNameStatusChanged( hostNameStatus() ); + } + } + } +} + +void +Config::setAutoLogin( bool b ) +{ + if ( b != m_doAutoLogin ) + { + Calamares::GlobalStorage* gs = Calamares::JobQueue::instance()->globalStorage(); + if ( b ) + { + gs->insert( "autologinUser", loginName() ); + } + else + { + gs->remove( "autologinUser" ); + } + m_doAutoLogin = b; + emit autoLoginChanged( b ); + } +} + +void +Config::setConfigurationMap( const QVariantMap& configurationMap ) +{ + QString shell( QLatin1String( "/bin/bash" ) ); // as if it's not set at all + if ( configurationMap.contains( "userShell" ) ) + { + shell = CalamaresUtils::getString( configurationMap, "userShell" ); + } + // Now it might be explicitly set to empty, which is ok + setUserShell( shell ); + + setAutologinGroup( CalamaresUtils::getString( configurationMap, "autologinGroup" ) ); + setSudoersGroup( CalamaresUtils::getString( configurationMap, "sudoersGroup" ) ); + + m_doAutoLogin = CalamaresUtils::getBool( configurationMap, "doAutologin", false ); + + m_writeRootPassword = CalamaresUtils::getBool( configurationMap, "setRootPassword", true ); + Calamares::JobQueue::instance()->globalStorage()->insert( "setRootPassword", m_writeRootPassword ); +} diff --git a/src/modules/users/Config.h b/src/modules/users/Config.h new file mode 100644 index 000000000..d32ddc8f2 --- /dev/null +++ b/src/modules/users/Config.h @@ -0,0 +1,135 @@ +/* === This file is part of Calamares - === + * + * SPDX-FileCopyrightText: 2020 Adriaan de Groot + * SPDX-License-Identifier: GPL-3.0-or-later + * License-Filename: LICENSE + * + * Calamares is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Calamares is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Calamares. If not, see . + */ + +#ifndef USERS_CONFIG_H +#define USERS_CONFIG_H + +#include +#include + +class Config : public QObject +{ + Q_OBJECT + + Q_PROPERTY( QString userShell READ userShell WRITE setUserShell NOTIFY userShellChanged ) + + Q_PROPERTY( QString autologinGroup READ autologinGroup WRITE setAutologinGroup NOTIFY autologinGroupChanged ) + Q_PROPERTY( QString sudoersGroup READ sudoersGroup WRITE setSudoersGroup NOTIFY sudoersGroupChanged ) + + Q_PROPERTY( bool doAutoLogin READ doAutoLogin WRITE setAutoLogin NOTIFY autoLoginChanged ) + + Q_PROPERTY( QString fullName READ fullName WRITE setFullName NOTIFY fullNameChanged ) + Q_PROPERTY( QString loginName READ loginName WRITE setLoginName NOTIFY loginNameChanged ) + Q_PROPERTY( QString loginNameStatus READ loginNameStatus NOTIFY loginNameStatusChanged ) + + Q_PROPERTY( QString hostName READ hostName WRITE setHostName NOTIFY hostNameChanged ) + Q_PROPERTY( QString hostNameStatus READ hostNameStatus NOTIFY hostNameStatusChanged ) + +public: + Config( QObject* parent = nullptr ); + ~Config(); + + void setConfigurationMap( const QVariantMap& ); + + /** @brief Full path to the user's shell executable + * + * Typically this will be /bin/bash, but it can be set from + * the config file with the *userShell* setting. + */ + QString userShell() const { return m_userShell; } + + /// The group of which auto-login users must be a member + QString autologinGroup() const { return m_autologinGroup; } + /// The group of which users who can "sudo" must be a member + QString sudoersGroup() const { return m_sudoersGroup; } + + /// The full (GECOS) name of the user + QString fullName() const { return m_fullName; } + /// The login name of the user + QString loginName() const { return m_loginName; } + /// Status message about login -- empty for "ok" + QString loginNameStatus() const; + + /// The host name (name for the system) + QString hostName() const { return m_hostName; } + /// Status message about hostname -- empty for "ok" + QString hostNameStatus() const; + + /// Should the user be automatically logged-in? + bool doAutoLogin() const { return m_doAutoLogin; } + /// Should the root password be written (if false, no password is set and the root account is disabled for login) + bool writeRootPassword() const { return m_writeRootPassword; } + + static const QStringList& forbiddenLoginNames(); + static const QStringList& forbiddenHostNames(); + +public Q_SLOTS: + /** @brief Sets the user's shell if possible + * + * If the path is empty, that's ok: no shell will be explicitly set, + * so the user will get whatever shell is set to default in the target. + * + * The given non-empty @p path must be an absolute path (for use inside + * the target system!); if it is not, the shell is not changed. + */ + void setUserShell( const QString& path ); + + /// Sets the autologin group; empty is ignored + void setAutologinGroup( const QString& group ); + /// Sets the sudoer group; empty is ignored + void setSudoersGroup( const QString& group ); + + /// Sets the full name, may guess a loginName + void setFullName( const QString& name ); + /// Sets the login name (flags it as "custom") + void setLoginName( const QString& login ); + + /// Sets the host name (flags it as "custom") + void setHostName( const QString& host ); + + /// Sets the autologin flag + void setAutoLogin( bool b ); + +signals: + void userShellChanged( const QString& ); + void autologinGroupChanged( const QString& ); + void sudoersGroupChanged( const QString& ); + void fullNameChanged( const QString& ); + void loginNameChanged( const QString& ); + void loginNameStatusChanged( const QString& ); + void hostNameChanged( const QString& ); + void hostNameStatusChanged( const QString& ); + void autoLoginChanged( bool ); + +private: + QString m_userShell; + QString m_autologinGroup; + QString m_sudoersGroup; + QString m_fullName; + QString m_loginName; + QString m_hostName; + bool m_doAutoLogin = false; + bool m_writeRootPassword = true; + + bool m_customLoginName = false; + bool m_customHostName = false; +}; + +#endif diff --git a/src/modules/users/PasswordTests.cpp b/src/modules/users/PasswordTests.cpp index b33526162..0c1b4bffc 100644 --- a/src/modules/users/PasswordTests.cpp +++ b/src/modules/users/PasswordTests.cpp @@ -24,9 +24,9 @@ QTEST_GUILESS_MAIN( PasswordTests ) -PasswordTests::PasswordTests() { } +PasswordTests::PasswordTests() {} -PasswordTests::~PasswordTests() { } +PasswordTests::~PasswordTests() {} void PasswordTests::initTestCase() diff --git a/src/modules/users/Tests.cpp b/src/modules/users/Tests.cpp index 75c5e6d5f..196fd9d68 100644 --- a/src/modules/users/Tests.cpp +++ b/src/modules/users/Tests.cpp @@ -37,7 +37,7 @@ class UsersTests : public QObject Q_OBJECT public: UsersTests(); - virtual ~UsersTests() { } + virtual ~UsersTests() {} private Q_SLOTS: void initTestCase(); diff --git a/src/modules/users/UsersPage.cpp b/src/modules/users/UsersPage.cpp index 5c649d622..c4502256a 100644 --- a/src/modules/users/UsersPage.cpp +++ b/src/modules/users/UsersPage.cpp @@ -25,16 +25,12 @@ #include "UsersPage.h" +#include "Config.h" #include "ui_page_usersetup.h" -#include "CreateUserJob.h" -#include "SetHostNameJob.h" -#include "SetPasswordJob.h" - #include "GlobalStorage.h" #include "JobQueue.h" #include "Settings.h" - #include "utils/CalamaresUtilsGui.h" #include "utils/Logger.h" #include "utils/Retranslator.h" @@ -44,14 +40,6 @@ #include #include #include -#include -#include - -static const QRegExp USERNAME_RX( "^[a-z_][a-z0-9_-]*[$]?$" ); -static const QRegExp HOSTNAME_RX( "^[a-zA-Z0-9][-a-zA-Z0-9_]*$" ); -static constexpr const int USERNAME_MAX_LENGTH = 31; -static constexpr const int HOSTNAME_MIN_LENGTH = 2; -static constexpr const int HOSTNAME_MAX_LENGTH = 63; /** @brief How bad is the error for labelError() ? */ enum class Badness @@ -79,54 +67,87 @@ labelOk( QLabel* pix, QLabel* label ) pix->setPixmap( CalamaresUtils::defaultPixmap( CalamaresUtils::Yes, CalamaresUtils::Original, label->size() ) ); } -UsersPage::UsersPage( QWidget* parent ) +/** Indicate error, update @p ok based on @p status */ +static inline void +labelStatus( QLabel* pix, QLabel* label, const QString& value, const QString& status, bool& ok ) +{ + if ( status.isEmpty() ) + { + if ( value.isEmpty() ) + { + // This is different from labelOK() because no checkmark is shown + label->clear(); + pix->clear(); + ok = false; + } + else + { + labelOk( pix, label ); + ok = true; + } + } + else + { + labelError( pix, label, status ); + ok = false; + } +} + +UsersPage::UsersPage( Config* config, QWidget* parent ) : QWidget( parent ) , ui( new Ui::Page_UserSetup ) + , m_config( config ) , m_readyFullName( false ) , m_readyUsername( false ) , m_readyHostname( false ) , m_readyPassword( false ) , m_readyRootPassword( false ) - , m_writeRootPassword( true ) { ui->setupUi( this ); // Connect signals and slots - connect( ui->textBoxFullName, &QLineEdit::textEdited, this, &UsersPage::onFullNameTextEdited ); - connect( ui->textBoxUsername, &QLineEdit::textEdited, this, &UsersPage::onUsernameTextEdited ); - connect( ui->textBoxHostname, &QLineEdit::textEdited, this, &UsersPage::onHostnameTextEdited ); connect( ui->textBoxUserPassword, &QLineEdit::textChanged, this, &UsersPage::onPasswordTextChanged ); connect( ui->textBoxUserVerifiedPassword, &QLineEdit::textChanged, this, &UsersPage::onPasswordTextChanged ); connect( ui->textBoxRootPassword, &QLineEdit::textChanged, this, &UsersPage::onRootPasswordTextChanged ); connect( ui->textBoxVerifiedRootPassword, &QLineEdit::textChanged, this, &UsersPage::onRootPasswordTextChanged ); - connect( ui->checkBoxValidatePassword, &QCheckBox::stateChanged, this, [ this ]( int ) { + connect( ui->checkBoxValidatePassword, &QCheckBox::stateChanged, this, [this]( int ) { onPasswordTextChanged( ui->textBoxUserPassword->text() ); onRootPasswordTextChanged( ui->textBoxRootPassword->text() ); checkReady( isReady() ); } ); - connect( ui->checkBoxReusePassword, &QCheckBox::stateChanged, this, [ this ]( int checked ) { + connect( ui->checkBoxReusePassword, &QCheckBox::stateChanged, this, [this]( const int checked ) { /* When "reuse" is checked, hide the fields for explicitly * entering the root password. However, if we're going to * disable the root password anyway, hide them all regardless of * the checkbox -- so when writeRoot is false, checked needs * to be true, to hide them all. */ - if ( !m_writeRootPassword ) - { - checked = true; - } - ui->labelChooseRootPassword->setVisible( !checked ); - ui->labelRootPassword->setVisible( !checked ); - ui->labelRootPasswordError->setVisible( !checked ); - ui->textBoxRootPassword->setVisible( !checked ); - ui->textBoxVerifiedRootPassword->setVisible( !checked ); + const bool visible = m_config->writeRootPassword() ? !checked : false; + ui->labelChooseRootPassword->setVisible( visible ); + ui->labelRootPassword->setVisible( visible ); + ui->labelRootPasswordError->setVisible( visible ); + ui->textBoxRootPassword->setVisible( visible ); + ui->textBoxVerifiedRootPassword->setVisible( visible ); checkReady( isReady() ); } ); - m_customUsername = false; - m_customHostname = false; + connect( ui->textBoxFullName, &QLineEdit::textEdited, config, &Config::setFullName ); + connect( config, &Config::fullNameChanged, this, &UsersPage::onFullNameTextEdited ); + + connect( ui->textBoxHostName, &QLineEdit::textEdited, config, &Config::setHostName ); + connect( config, &Config::hostNameChanged, ui->textBoxHostName, &QLineEdit::setText ); + connect( config, &Config::hostNameStatusChanged, this, &UsersPage::reportHostNameStatus ); + + connect( ui->textBoxLoginName, &QLineEdit::textEdited, config, &Config::setLoginName ); + connect( config, &Config::loginNameChanged, ui->textBoxLoginName, &QLineEdit::setText ); + connect( config, &Config::loginNameStatusChanged, this, &UsersPage::reportLoginNameStatus ); - setWriteRootPassword( true ); + connect( ui->checkBoxDoAutoLogin, &QCheckBox::stateChanged, this, [this]( int checked ) { + m_config->setAutoLogin( checked != Qt::Unchecked ); + } ); + connect( config, &Config::autoLoginChanged, ui->checkBoxDoAutoLogin, &QCheckBox::setChecked ); + + ui->checkBoxReusePassword->setVisible( m_config->writeRootPassword() ); ui->checkBoxReusePassword->setChecked( true ); ui->checkBoxValidatePassword->setChecked( true ); @@ -146,15 +167,15 @@ UsersPage::retranslate() ui->retranslateUi( this ); if ( Calamares::Settings::instance()->isSetupMode() ) { - ui->textBoxUsername->setToolTip( tr( "If more than one person will " - "use this computer, you can create multiple " - "accounts after setup." ) ); + ui->textBoxLoginName->setToolTip( tr( "If more than one person will " + "use this computer, you can create multiple " + "accounts after setup." ) ); } else { - ui->textBoxUsername->setToolTip( tr( "If more than one person will " - "use this computer, you can create multiple " - "accounts after installation." ) ); + ui->textBoxLoginName->setToolTip( tr( "If more than one person will " + "use this computer, you can create multiple " + "accounts after installation." ) ); } // Re-do password checks (with output messages) as well. // .. the password-checking methods get their values from the text boxes, @@ -165,27 +186,19 @@ UsersPage::retranslate() bool -UsersPage::isReady() +UsersPage::isReady() const { bool readyFields = m_readyFullName && m_readyHostname && m_readyPassword && m_readyUsername; - if ( !m_writeRootPassword || ui->checkBoxReusePassword->isChecked() ) - { - return readyFields; - } - - return readyFields && m_readyRootPassword; -} - -QString -UsersPage::getHostname() const -{ - return ui->textBoxHostname->text(); + // If we're going to write a root password, we need a valid one (or reuse the user's password) + readyFields + &= m_config->writeRootPassword() ? ( m_readyRootPassword || ui->checkBoxReusePassword->isChecked() ) : true; + return readyFields; } QString UsersPage::getRootPassword() const { - if ( m_writeRootPassword ) + if ( m_config->writeRootPassword() ) { if ( ui->checkBoxReusePassword->isChecked() ) { @@ -205,42 +218,24 @@ UsersPage::getRootPassword() const QPair< QString, QString > UsersPage::getUserPassword() const { - return QPair< QString, QString >( ui->textBoxUsername->text(), ui->textBoxUserPassword->text() ); + return QPair< QString, QString >( m_config->loginName(), ui->textBoxUserPassword->text() ); } -QList< Calamares::job_ptr > -UsersPage::createJobs( const QStringList& defaultGroupsList ) +void +UsersPage::fillGlobalStorage() const { - QList< Calamares::job_ptr > list; if ( !isReady() ) { - return list; + return; } Calamares::GlobalStorage* gs = Calamares::JobQueue::instance()->globalStorage(); - Calamares::Job* j; - j = new CreateUserJob( ui->textBoxUsername->text(), - ui->textBoxFullName->text().isEmpty() ? ui->textBoxUsername->text() - : ui->textBoxFullName->text(), - ui->checkBoxAutoLogin->isChecked(), - defaultGroupsList ); - list.append( Calamares::job_ptr( j ) ); - - if ( m_writeRootPassword ) + if ( m_config->writeRootPassword() ) { gs->insert( "reuseRootPassword", ui->checkBoxReusePassword->isChecked() ); } - gs->insert( "hostname", ui->textBoxHostname->text() ); - if ( ui->checkBoxAutoLogin->isChecked() ) - { - gs->insert( "autologinUser", ui->textBoxUsername->text() ); - } - - gs->insert( "username", ui->textBoxUsername->text() ); gs->insert( "password", CalamaresUtils::obscure( ui->textBoxUserPassword->text() ) ); - - return list; } @@ -254,222 +249,23 @@ UsersPage::onActivate() void -UsersPage::setWriteRootPassword( bool write ) -{ - m_writeRootPassword = write; - ui->checkBoxReusePassword->setVisible( write ); -} - - -void -UsersPage::onFullNameTextEdited( const QString& textRef ) +UsersPage::onFullNameTextEdited( const QString& fullName ) { - if ( textRef.isEmpty() ) - { - ui->labelFullNameError->clear(); - ui->labelFullName->clear(); - if ( !m_customUsername ) - { - ui->textBoxUsername->clear(); - } - if ( !m_customHostname ) - { - ui->textBoxHostname->clear(); - } - m_readyFullName = false; - } - else - { - ui->labelFullName->setPixmap( - CalamaresUtils::defaultPixmap( CalamaresUtils::Yes, CalamaresUtils::Original, ui->labelFullName->size() ) ); - m_readyFullName = true; - fillSuggestions(); - } + labelStatus( ui->labelFullName, ui->labelFullNameError, fullName, QString(), m_readyFullName ); checkReady( isReady() ); } -/** @brief Guess the machine's name - * - * If there is DMI data, use that; otherwise, just call the machine "-pc". - * Reads the DMI data just once. - */ -static QString -guessProductName() -{ - static bool tried = false; - static QString dmiProduct; - - if ( !tried ) - { - // yes validateHostnameText() but these files can be a mess - QRegExp dmirx( "[^a-zA-Z0-9]", Qt::CaseInsensitive ); - QFile dmiFile( QStringLiteral( "/sys/devices/virtual/dmi/id/product_name" ) ); - - if ( dmiFile.exists() && dmiFile.open( QIODevice::ReadOnly ) ) - { - dmiProduct = QString::fromLocal8Bit( dmiFile.readAll().simplified().data() ) - .toLower() - .replace( dmirx, " " ) - .remove( ' ' ); - } - if ( dmiProduct.isEmpty() ) - { - dmiProduct = QStringLiteral( "-pc" ); - } - tried = true; - } - return dmiProduct; -} - -void -UsersPage::fillSuggestions() -{ - QString fullName = ui->textBoxFullName->text(); - QRegExp rx( "[^a-zA-Z0-9 ]", Qt::CaseInsensitive ); - QString cleanName = CalamaresUtils::removeDiacritics( fullName ).toLower().replace( rx, " " ).simplified(); - QStringList cleanParts = cleanName.split( ' ' ); - - if ( !m_customUsername ) - { - if ( !cleanParts.isEmpty() && !cleanParts.first().isEmpty() ) - { - QString usernameSuggestion = cleanParts.first(); - for ( int i = 1; i < cleanParts.length(); ++i ) - { - if ( !cleanParts.value( i ).isEmpty() ) - { - usernameSuggestion.append( cleanParts.value( i ).at( 0 ) ); - } - } - if ( USERNAME_RX.indexIn( usernameSuggestion ) != -1 ) - { - ui->textBoxUsername->setText( usernameSuggestion ); - validateUsernameText( usernameSuggestion ); - m_customUsername = false; - } - } - } - - if ( !m_customHostname ) - { - if ( !cleanParts.isEmpty() && !cleanParts.first().isEmpty() ) - { - QString hostnameSuggestion; - QString productName = guessProductName(); - hostnameSuggestion = QString( "%1-%2" ).arg( cleanParts.first() ).arg( productName ); - if ( HOSTNAME_RX.indexIn( hostnameSuggestion ) != -1 ) - { - ui->textBoxHostname->setText( hostnameSuggestion ); - validateHostnameText( hostnameSuggestion ); - m_customHostname = false; - } - } - } -} - - -void -UsersPage::onUsernameTextEdited( const QString& textRef ) -{ - m_customUsername = true; - validateUsernameText( textRef ); -} - - void -UsersPage::validateUsernameText( const QString& textRef ) +UsersPage::reportLoginNameStatus( const QString& status ) { - QString text( textRef ); - QRegExpValidator val_whole( USERNAME_RX ); - QRegExpValidator val_start( QRegExp( "[a-z_].*" ) ); // anchors are implicit in QRegExpValidator - int pos = -1; - - if ( text.isEmpty() ) - { - ui->labelUsernameError->clear(); - ui->labelUsername->clear(); - m_readyUsername = false; - } - else if ( text.length() > USERNAME_MAX_LENGTH ) - { - labelError( ui->labelUsername, ui->labelUsernameError, tr( "Your username is too long." ) ); - m_readyUsername = false; - } - else if ( val_start.validate( text, pos ) == QValidator::Invalid ) - { - labelError( ui->labelUsername, - ui->labelUsernameError, - tr( "Your username must start with a lowercase letter or underscore." ) ); - m_readyUsername = false; - } - else if ( val_whole.validate( text, pos ) == QValidator::Invalid ) - { - labelError( ui->labelUsername, - ui->labelUsernameError, - tr( "Only lowercase letters, numbers, underscore and hyphen are allowed." ) ); - m_readyUsername = false; - } - else if ( 0 == QString::compare("root", text, Qt::CaseSensitive ) ) - { - labelError( ui->labelUsername, - ui->labelUsernameError, - tr( "'root' is not allowed as user name." ) ); - m_readyUsername = false; - } - else - { - labelOk( ui->labelUsername, ui->labelUsernameError ); - m_readyUsername = true; - } - + labelStatus( ui->labelUsername, ui->labelUsernameError, m_config->loginName(), status, m_readyUsername ); emit checkReady( isReady() ); } - -void -UsersPage::onHostnameTextEdited( const QString& textRef ) -{ - m_customHostname = true; - validateHostnameText( textRef ); -} - - void -UsersPage::validateHostnameText( const QString& textRef ) +UsersPage::reportHostNameStatus( const QString& status ) { - QString text = textRef; - QRegExpValidator val( HOSTNAME_RX ); - int pos = -1; - - if ( text.isEmpty() ) - { - ui->labelHostnameError->clear(); - ui->labelHostname->clear(); - m_readyHostname = false; - } - else if ( text.length() < HOSTNAME_MIN_LENGTH ) - { - labelError( ui->labelHostname, ui->labelHostnameError, tr( "Your hostname is too short." ) ); - m_readyHostname = false; - } - else if ( text.length() > HOSTNAME_MAX_LENGTH ) - { - labelError( ui->labelHostname, ui->labelHostnameError, tr( "Your hostname is too long." ) ); - m_readyHostname = false; - } - else if ( val.validate( text, pos ) == QValidator::Invalid ) - { - labelError( ui->labelHostname, - ui->labelHostnameError, - tr( "Only letters, numbers, underscore and hyphen are allowed." ) ); - m_readyHostname = false; - } - else - { - labelOk( ui->labelHostname, ui->labelHostnameError ); - m_readyHostname = true; - } - + labelStatus( ui->labelHostname, ui->labelHostnameError, m_config->hostName(), status, m_readyHostname ); emit checkReady( isReady() ); } @@ -553,13 +349,6 @@ UsersPage::setValidatePasswordDefault( bool checked ) emit checkReady( isReady() ); } -void -UsersPage::setAutologinDefault( bool checked ) -{ - ui->checkBoxAutoLogin->setChecked( checked ); - emit checkReady( isReady() ); -} - void UsersPage::setReusePasswordDefault( bool checked ) { diff --git a/src/modules/users/UsersPage.h b/src/modules/users/UsersPage.h index 3382e9335..b8cb0f06a 100644 --- a/src/modules/users/UsersPage.h +++ b/src/modules/users/UsersPage.h @@ -25,10 +25,11 @@ #define USERSPAGE_H #include "CheckPWQuality.h" -#include "Job.h" #include +class Config; + class QLabel; namespace Ui @@ -40,19 +41,17 @@ class UsersPage : public QWidget { Q_OBJECT public: - explicit UsersPage( QWidget* parent = nullptr ); + explicit UsersPage( Config* config, QWidget* parent = nullptr ); virtual ~UsersPage(); - bool isReady(); + bool isReady() const; - Calamares::JobList createJobs( const QStringList& defaultGroupsList ); + void fillGlobalStorage() const; void onActivate(); - void setWriteRootPassword( bool show ); void setPasswordCheckboxVisible( bool visible ); void setValidatePasswordDefault( bool checked ); - void setAutologinDefault( bool checked ); void setReusePasswordDefault( bool checked ); /** @brief Process entries in the passwordRequirements config entry @@ -63,8 +62,6 @@ public: */ void addPasswordCheck( const QString& key, const QVariant& value ); - ///@brief Hostname as entered / auto-filled - QString getHostname() const; ///@brief Root password, depends on settings, may be empty QString getRootPassword() const; ///@brief User name and password @@ -72,11 +69,8 @@ public: protected slots: void onFullNameTextEdited( const QString& ); - void fillSuggestions(); - void onUsernameTextEdited( const QString& ); - void validateUsernameText( const QString& ); - void onHostnameTextEdited( const QString& ); - void validateHostnameText( const QString& ); + void reportLoginNameStatus( const QString& ); + void reportHostNameStatus( const QString& ); void onPasswordTextChanged( const QString& ); void onRootPasswordTextChanged( const QString& ); @@ -95,19 +89,16 @@ private: void retranslate(); Ui::Page_UserSetup* ui; + Config* m_config; PasswordCheckList m_passwordChecks; bool m_passwordChecksChanged = false; bool m_readyFullName; bool m_readyUsername; - bool m_customUsername; bool m_readyHostname; - bool m_customHostname; bool m_readyPassword; bool m_readyRootPassword; - - bool m_writeRootPassword; }; #endif // USERSPAGE_H diff --git a/src/modules/users/UsersViewStep.cpp b/src/modules/users/UsersViewStep.cpp index fe633b1c2..ddaf4837b 100644 --- a/src/modules/users/UsersViewStep.cpp +++ b/src/modules/users/UsersViewStep.cpp @@ -20,17 +20,18 @@ #include "UsersViewStep.h" +#include "Config.h" +#include "CreateUserJob.h" #include "SetHostNameJob.h" #include "SetPasswordJob.h" #include "UsersPage.h" +#include "GlobalStorage.h" +#include "JobQueue.h" #include "utils/Logger.h" #include "utils/NamedEnum.h" #include "utils/Variant.h" -#include "GlobalStorage.h" -#include "JobQueue.h" - CALAMARES_PLUGIN_FACTORY_DEFINITION( UsersViewStepFactory, registerPlugin< UsersViewStep >(); ) static const NamedEnumTable< SetHostNameJob::Action >& @@ -53,11 +54,11 @@ hostnameActions() UsersViewStep::UsersViewStep( QObject* parent ) : Calamares::ViewStep( parent ) - , m_widget( new UsersPage() ) + , m_widget( nullptr ) , m_actions( SetHostNameJob::Action::None ) + , m_config( new Config( this ) ) { emit nextStatusChanged( true ); - connect( m_widget, &UsersPage::checkReady, this, &UsersViewStep::nextStatusChanged ); } @@ -80,6 +81,11 @@ UsersViewStep::prettyName() const QWidget* UsersViewStep::widget() { + if ( !m_widget ) + { + m_widget = new UsersPage( m_config ); + connect( m_widget, &UsersPage::checkReady, this, &UsersViewStep::nextStatusChanged ); + } return m_widget; } @@ -87,7 +93,7 @@ UsersViewStep::widget() bool UsersViewStep::isNextEnabled() const { - return m_widget->isReady(); + return m_widget ? m_widget->isReady() : true; } @@ -122,7 +128,10 @@ UsersViewStep::jobs() const void UsersViewStep::onActivate() { - m_widget->onActivate(); + if ( m_widget ) + { + m_widget->onActivate(); + } } @@ -130,9 +139,16 @@ void UsersViewStep::onLeave() { m_jobs.clear(); - m_jobs.append( m_widget->createJobs( m_defaultGroups ) ); + if ( !m_widget || !m_widget->isReady() ) + { + return; + } Calamares::Job* j; + j = new CreateUserJob( m_config->loginName(), + m_config->fullName().isEmpty() ? m_config->loginName() : m_config->fullName(), + m_config->doAutoLogin(), + m_defaultGroups ); auto userPW = m_widget->getUserPassword(); j = new SetPasswordJob( userPW.first, userPW.second ); @@ -141,14 +157,18 @@ UsersViewStep::onLeave() j = new SetPasswordJob( "root", m_widget->getRootPassword() ); m_jobs.append( Calamares::job_ptr( j ) ); - j = new SetHostNameJob( m_widget->getHostname(), m_actions ); + j = new SetHostNameJob( m_config->hostName(), m_actions ); m_jobs.append( Calamares::job_ptr( j ) ); + + m_widget->fillGlobalStorage(); } void UsersViewStep::setConfigurationMap( const QVariantMap& configurationMap ) { + // Create the widget, after all .. as long as writing configuration to the UI is needed + (void)this->widget(); using CalamaresUtils::getBool; if ( configurationMap.contains( "defaultGroups" ) @@ -162,25 +182,6 @@ UsersViewStep::setConfigurationMap( const QVariantMap& configurationMap ) m_defaultGroups = QStringList { "lp", "video", "network", "storage", "wheel", "audio" }; } - if ( configurationMap.contains( "autologinGroup" ) - && configurationMap.value( "autologinGroup" ).type() == QVariant::String ) - { - Calamares::JobQueue::instance()->globalStorage()->insert( - "autologinGroup", configurationMap.value( "autologinGroup" ).toString() ); - } - - if ( configurationMap.contains( "sudoersGroup" ) - && configurationMap.value( "sudoersGroup" ).type() == QVariant::String ) - { - Calamares::JobQueue::instance()->globalStorage()->insert( "sudoersGroup", - configurationMap.value( "sudoersGroup" ).toString() ); - } - - bool setRootPassword = getBool( configurationMap, "setRootPassword", true ); - Calamares::JobQueue::instance()->globalStorage()->insert( "setRootPassword", setRootPassword ); - - m_widget->setWriteRootPassword( setRootPassword ); - m_widget->setAutologinDefault( getBool( configurationMap, "doAutologin", false ) ); m_widget->setReusePasswordDefault( getBool( configurationMap, "doReusePassword", false ) ); if ( configurationMap.contains( "passwordRequirements" ) @@ -197,15 +198,6 @@ UsersViewStep::setConfigurationMap( const QVariantMap& configurationMap ) m_widget->setPasswordCheckboxVisible( getBool( configurationMap, "allowWeakPasswords", false ) ); m_widget->setValidatePasswordDefault( !getBool( configurationMap, "allowWeakPasswordsDefault", false ) ); - QString shell( QLatin1String( "/bin/bash" ) ); // as if it's not set at all - if ( configurationMap.contains( "userShell" ) ) - { - shell = CalamaresUtils::getString( configurationMap, "userShell" ); - } - // Now it might be explicitly set to empty, which is ok - - Calamares::JobQueue::instance()->globalStorage()->insert( "userShell", shell ); - using Action = SetHostNameJob::Action; QString hostnameActionString = CalamaresUtils::getString( configurationMap, "setHostname" ); @@ -222,4 +214,6 @@ UsersViewStep::setConfigurationMap( const QVariantMap& configurationMap ) Action hostsfileAction = getBool( configurationMap, "writeHostsFile", true ) ? Action::WriteEtcHosts : Action::None; m_actions = hostsfileAction | hostnameAction; + + m_config->setConfigurationMap( configurationMap ); } diff --git a/src/modules/users/UsersViewStep.h b/src/modules/users/UsersViewStep.h index 03cc83819..aacf11f2a 100644 --- a/src/modules/users/UsersViewStep.h +++ b/src/modules/users/UsersViewStep.h @@ -29,6 +29,7 @@ #include #include +class Config; class UsersPage; class PLUGINDLLEXPORT UsersViewStep : public Calamares::ViewStep @@ -62,6 +63,8 @@ private: QStringList m_defaultGroups; SetHostNameJob::Actions m_actions; + + Config* m_config; }; CALAMARES_PLUGIN_FACTORY_DECLARATION( UsersViewStepFactory ) diff --git a/src/modules/users/page_usersetup.ui b/src/modules/users/page_usersetup.ui index b778647d8..d880673b8 100644 --- a/src/modules/users/page_usersetup.ui +++ b/src/modules/users/page_usersetup.ui @@ -127,7 +127,7 @@ - + 0 @@ -226,7 +226,7 @@ - + 0 @@ -456,7 +456,7 @@ - + Log in automatically without asking for the password.