calamares/src/modules/users/UsersPage.cpp

597 lines
18 KiB
C++
Raw Normal View History

/* === This file is part of Calamares - <https://github.com/calamares> ===
2014-07-31 14:52:05 +02:00
*
* Copyright 2014-2017, Teo Mrnjavac <teo@kde.org>
* Copyright 2017-2018, Adriaan de Groot <groot@kde.org>
* Copyright 2019, Collabora Ltd <arnaud.ferraris@collabora.com>
* Copyright 2020, Gabriel Craciunescu <crazy@frugalware.org>
2014-07-31 14:52:05 +02:00
*
* Portions from the Manjaro Installation Framework
* by Roland Singer <roland@manjaro.org>
* Copyright (C) 2007 Free Software Foundation, Inc.
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "UsersPage.h"
2014-07-31 14:52:05 +02:00
#include "ui_page_usersetup.h"
#include "CreateUserJob.h"
2014-10-21 18:59:02 +02:00
#include "SetHostNameJob.h"
#include "SetPasswordJob.h"
#include "GlobalStorage.h"
#include "JobQueue.h"
#include "Settings.h"
2014-08-26 15:26:30 +02:00
#include "utils/CalamaresUtilsGui.h"
#include "utils/Logger.h"
2014-11-13 17:42:02 +01:00
#include "utils/Retranslator.h"
#include "utils/String.h"
2014-07-31 14:52:05 +02:00
#include <QBoxLayout>
#include <QFile>
2014-07-31 14:52:05 +02:00
#include <QLabel>
#include <QLineEdit>
#include <QRegExp>
#include <QRegExpValidator>
2019-10-29 16:21:26 +01:00
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
{
Fatal,
Warning
};
/** Add an error message and pixmap to a label. */
static inline void
labelError( QLabel* pix, QLabel* label, const QString& message, Badness bad = Badness::Fatal )
{
label->setText( message );
pix->setPixmap( CalamaresUtils::defaultPixmap( ( bad == Badness::Fatal ) ? CalamaresUtils::StatusError
: CalamaresUtils::StatusWarning,
CalamaresUtils::Original,
label->size() ) );
}
/** Clear error, indicate OK on a label. */
static inline void
labelOk( QLabel* pix, QLabel* label )
{
label->clear();
pix->setPixmap( CalamaresUtils::defaultPixmap( CalamaresUtils::Yes, CalamaresUtils::Original, label->size() ) );
}
2014-07-31 14:52:05 +02:00
UsersPage::UsersPage( QWidget* parent )
: QWidget( parent )
, ui( new Ui::Page_UserSetup )
, m_readyFullName( false )
2014-07-31 14:52:05 +02:00
, m_readyUsername( false )
, m_readyHostname( false )
, m_readyPassword( false )
, m_readyRootPassword( false )
, m_writeRootPassword( true )
2014-07-31 14:52:05 +02:00
{
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 );
2020-02-14 11:46:05 +01:00
connect( ui->checkBoxValidatePassword, &QCheckBox::stateChanged, this, [this]( int ) {
onPasswordTextChanged( ui->textBoxUserPassword->text() );
onRootPasswordTextChanged( ui->textBoxRootPassword->text() );
checkReady( isReady() );
} );
2020-02-14 11:46:05 +01:00
connect( ui->checkBoxReusePassword, &QCheckBox::stateChanged, this, [this]( 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 );
checkReady( isReady() );
} );
m_customUsername = false;
m_customHostname = false;
2014-11-13 17:42:02 +01:00
setWriteRootPassword( true );
ui->checkBoxReusePassword->setChecked( true );
ui->checkBoxValidatePassword->setChecked( true );
setPasswordCheckboxVisible( false );
CALAMARES_RETRANSLATE_SLOT( &UsersPage::retranslate );
2014-07-31 14:52:05 +02:00
}
UsersPage::~UsersPage()
{
delete ui;
}
void
UsersPage::retranslate()
{
ui->retranslateUi( this );
if ( Calamares::Settings::instance()->isSetupMode() )
{
ui->textBoxUsername->setToolTip( tr( "<small>If more than one person will "
"use this computer, you can create multiple "
"accounts after setup.</small>" ) );
}
else
{
ui->textBoxUsername->setToolTip( tr( "<small>If more than one person will "
"use this computer, you can create multiple "
"accounts after installation.</small>" ) );
}
// Re-do password checks (with output messages) as well.
// .. the password-checking methods get their values from the text boxes,
// not from their parameters.
onPasswordTextChanged( QString() );
onRootPasswordTextChanged( QString() );
}
2014-07-31 14:52:05 +02:00
bool
UsersPage::isReady()
{
bool readyFields = m_readyFullName && m_readyHostname && m_readyPassword && m_readyUsername;
if ( !m_writeRootPassword || ui->checkBoxReusePassword->isChecked() )
{
return readyFields;
}
return readyFields && m_readyRootPassword;
2014-07-31 14:52:05 +02:00
}
QString
UsersPage::getHostname() const
{
return ui->textBoxHostname->text();
}
QString
UsersPage::getRootPassword() const
{
if ( m_writeRootPassword )
{
if ( ui->checkBoxReusePassword->isChecked() )
{
return ui->textBoxUserPassword->text();
}
else
{
return ui->textBoxRootPassword->text();
}
}
else
{
return QString();
}
}
QPair< QString, QString >
UsersPage::getUserPassword() const
{
return QPair< QString, QString >( ui->textBoxUsername->text(), ui->textBoxUserPassword->text() );
}
2014-07-31 14:52:05 +02:00
QList< Calamares::job_ptr >
UsersPage::createJobs( const QStringList& defaultGroupsList )
{
QList< Calamares::job_ptr > list;
if ( !isReady() )
{
return list;
}
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 )
{
gs->insert( "reuseRootPassword", ui->checkBoxReusePassword->isChecked() );
}
2014-08-01 11:19:50 +02:00
gs->insert( "hostname", ui->textBoxHostname->text() );
if ( ui->checkBoxAutoLogin->isChecked() )
{
2014-08-01 11:19:50 +02:00
gs->insert( "autologinUser", ui->textBoxUsername->text() );
}
gs->insert( "username", ui->textBoxUsername->text() );
2015-09-25 15:24:35 +02:00
gs->insert( "password", CalamaresUtils::obscure( ui->textBoxUserPassword->text() ) );
return list;
}
2014-11-14 18:21:04 +01:00
void
UsersPage::onActivate()
2014-11-14 18:21:04 +01:00
{
ui->textBoxFullName->setFocus();
onPasswordTextChanged( QString() );
onRootPasswordTextChanged( QString() );
2014-11-14 18:21:04 +01:00
}
void
UsersPage::setWriteRootPassword( bool write )
{
m_writeRootPassword = write;
ui->checkBoxReusePassword->setVisible( write );
}
2014-07-31 14:52:05 +02:00
void
2015-02-26 01:57:19 +01:00
UsersPage::onFullNameTextEdited( const QString& textRef )
2014-07-31 14:52:05 +02:00
{
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();
}
checkReady( isReady() );
}
2020-02-14 11:45:45 +01:00
/** @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;
2020-02-14 11:45:45 +01:00
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 )
{
QString text( textRef );
QRegExpValidator val_whole( USERNAME_RX );
QRegExpValidator val_start( QRegExp( "[a-z_].*" ) ); // anchors are implicit in QRegExpValidator
2014-07-31 14:52:05 +02:00
int pos = -1;
if ( text.isEmpty() )
{
ui->labelUsernameError->clear();
ui->labelUsername->clear();
m_readyUsername = false;
}
else if ( text.length() > USERNAME_MAX_LENGTH )
2014-07-31 14:52:05 +02:00
{
labelError( ui->labelUsername, ui->labelUsernameError, tr( "Your username is too long." ) );
m_readyUsername = false;
}
else if ( val_start.validate( text, pos ) == QValidator::Invalid )
{
2019-11-01 13:39:16 +01:00
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 )
{
2019-11-01 13:39:16 +01:00
labelError( ui->labelUsername,
ui->labelUsernameError,
tr( "Only lowercase letters, numbers, underscore and hyphen are allowed." ) );
2014-07-31 14:52:05 +02:00
m_readyUsername = false;
}
else
{
labelOk( ui->labelUsername, ui->labelUsernameError );
2014-07-31 14:52:05 +02:00
m_readyUsername = true;
}
emit checkReady( isReady() );
}
void
UsersPage::onHostnameTextEdited( const QString& textRef )
{
m_customHostname = true;
validateHostnameText( textRef );
}
void
UsersPage::validateHostnameText( const QString& textRef )
2014-07-31 14:52:05 +02:00
{
QString text = textRef;
2019-10-29 16:21:26 +01:00
QRegExpValidator val( HOSTNAME_RX );
2014-07-31 14:52:05 +02:00
int pos = -1;
if ( text.isEmpty() )
{
ui->labelHostnameError->clear();
ui->labelHostname->clear();
m_readyHostname = false;
2014-07-31 14:52:05 +02:00
}
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;
}
2014-07-31 14:52:05 +02:00
else if ( val.validate( text, pos ) == QValidator::Invalid )
{
labelError( ui->labelHostname,
ui->labelHostnameError,
tr( "Only letters, numbers, underscore and hyphen are allowed." ) );
2014-07-31 14:52:05 +02:00
m_readyHostname = false;
}
else
{
labelOk( ui->labelHostname, ui->labelHostnameError );
2014-07-31 14:52:05 +02:00
m_readyHostname = true;
}
emit checkReady( isReady() );
}
bool
UsersPage::checkPasswordAcceptance( const QString& pw1, const QString& pw2, QLabel* badge, QLabel* message )
2014-07-31 14:52:05 +02:00
{
if ( pw1 != pw2 )
2014-07-31 14:52:05 +02:00
{
labelError( badge, message, tr( "Your passwords do not match!" ) );
return false;
2014-07-31 14:52:05 +02:00
}
else
{
bool failureIsFatal = ui->checkBoxValidatePassword->isChecked();
bool failureFound = false;
if ( m_passwordChecksChanged )
{
std::sort( m_passwordChecks.begin(), m_passwordChecks.end() );
m_passwordChecksChanged = false;
}
for ( auto pc : m_passwordChecks )
{
QString s = pc.filter( pw1 );
if ( !s.isEmpty() )
{
labelError( badge, message, s, failureIsFatal ? Badness::Fatal : Badness::Warning );
failureFound = true;
if ( failureIsFatal )
{
return false;
}
}
}
if ( !failureFound )
{
labelOk( badge, message );
}
// Here, if failureFound is true then we've found **warnings**,
// which is ok to continue but the user should know.
return true;
2014-07-31 14:52:05 +02:00
}
}
void
UsersPage::onPasswordTextChanged( const QString& )
{
m_readyPassword = checkPasswordAcceptance( ui->textBoxUserPassword->text(),
ui->textBoxUserVerifiedPassword->text(),
ui->labelUserPassword,
ui->labelUserPasswordError );
2014-07-31 14:52:05 +02:00
emit checkReady( isReady() );
}
void
UsersPage::onRootPasswordTextChanged( const QString& )
{
m_readyRootPassword = checkPasswordAcceptance( ui->textBoxRootPassword->text(),
ui->textBoxVerifiedRootPassword->text(),
ui->labelRootPassword,
ui->labelRootPasswordError );
2014-07-31 14:52:05 +02:00
emit checkReady( isReady() );
}
void
UsersPage::setPasswordCheckboxVisible( bool visible )
{
ui->checkBoxValidatePassword->setVisible( visible );
}
void
UsersPage::setValidatePasswordDefault( bool checked )
{
ui->checkBoxValidatePassword->setChecked( checked );
emit checkReady( isReady() );
}
void
UsersPage::setAutologinDefault( bool checked )
{
ui->checkBoxAutoLogin->setChecked( checked );
emit checkReady( isReady() );
}
void
2015-11-13 19:25:27 +01:00
UsersPage::setReusePasswordDefault( bool checked )
{
ui->checkBoxReusePassword->setChecked( checked );
emit checkReady( isReady() );
}
void
UsersPage::addPasswordCheck( const QString& key, const QVariant& value )
{
m_passwordChecksChanged = true;
if ( key == "minLength" )
{
add_check_minLength( m_passwordChecks, value );
}
else if ( key == "maxLength" )
{
add_check_maxLength( m_passwordChecks, value );
}
else if ( key == "nonempty" )
{
if ( value.toBool() )
{
2020-02-14 11:46:05 +01:00
m_passwordChecks.push_back(
PasswordCheck( []() { return QCoreApplication::translate( "PWQ", "Password is empty" ); },
[]( const QString& s ) { return !s.isEmpty(); },
PasswordCheck::Weight( 1 ) ) );
}
}
#ifdef CHECK_PWQUALITY
else if ( key == "libpwquality" )
{
add_check_libpwquality( m_passwordChecks, value );
}
#endif // CHECK_PWQUALITY
else
{
cWarning() << "Unknown password-check key" << key;
}
}