diff --git a/src/modules/users/Config.cpp b/src/modules/users/Config.cpp index 1acbca1a1..cebe45452 100644 --- a/src/modules/users/Config.cpp +++ b/src/modules/users/Config.cpp @@ -20,6 +20,8 @@ #include "utils/String.h" #include "utils/Variant.h" +#include + #include #include #include @@ -305,6 +307,12 @@ Config::hostnameStatus() const return QString(); } +static QString +cleanupForHostname( const QString& s ) +{ + QRegExp dmirx( "[^a-zA-Z0-9]", Qt::CaseInsensitive ); + return s.toLower().replace( dmirx, " " ).remove( ' ' ); +} /** @brief Guess the machine's name * @@ -319,16 +327,11 @@ guessProductName() 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( ' ' ); + dmiProduct = cleanupForHostname( QString::fromLocal8Bit( dmiFile.readAll().simplified().data() ) ); } if ( dmiProduct.isEmpty() ) { @@ -386,17 +389,37 @@ makeLoginNameSuggestion( const QStringList& parts ) return USERNAME_RX.indexIn( usernameSuggestion ) != -1 ? usernameSuggestion : QString(); } +/** @brief Return an invalid string for use in a hostname, if @p s is empty + * + * Maps empty to "^" (which is invalid in a hostname), everything else + * returns @p s itself. + */ static QString -makeHostnameSuggestion( const QStringList& parts ) +invalidEmpty( const QString& s ) { - static const QRegExp HOSTNAME_RX( "^[a-zA-Z0-9][-a-zA-Z0-9_]*$" ); - if ( parts.isEmpty() || parts.first().isEmpty() ) - { - return QString(); - } + return s.isEmpty() ? QStringLiteral( "^" ) : s; +} - QString productName = guessProductName(); - QString hostnameSuggestion = QStringLiteral( "%1-%2" ).arg( parts.first() ).arg( productName ); +STATICTEST QString +makeHostnameSuggestion( const QString& templateString, const QStringList& fullNameParts, const QString& loginName ) +{ + QHash< QString, QString > replace; + // User data + replace.insert( QStringLiteral( "first" ), + invalidEmpty( fullNameParts.isEmpty() ? QString() : cleanupForHostname( fullNameParts.first() ) ) ); + replace.insert( QStringLiteral( "name" ), invalidEmpty( cleanupForHostname( fullNameParts.join( QString() ) ) ) ); + replace.insert( QStringLiteral( "login" ), invalidEmpty( cleanupForHostname( loginName ) ) ); + // Hardware data + replace.insert( QStringLiteral( "product" ), guessProductName() ); + replace.insert( QStringLiteral( "product2" ), cleanupForHostname( QSysInfo::prettyProductName() ) ); + replace.insert( QStringLiteral( "cpu" ), cleanupForHostname( QSysInfo::currentCpuArchitecture() ) ); + // Hostname data + replace.insert( QStringLiteral( "host" ), invalidEmpty( cleanupForHostname( QSysInfo::machineHostName() ) ) ); + + QString hostnameSuggestion = KMacroExpander::expandMacros( templateString, replace, '$' ); + + // RegExp for valid hostnames; if the suggestion produces a valid name, return it + static const QRegExp HOSTNAME_RX( "^[a-zA-Z0-9][-a-zA-Z0-9_]*$" ); return HOSTNAME_RX.indexIn( hostnameSuggestion ) != -1 ? hostnameSuggestion : QString(); } @@ -427,18 +450,18 @@ Config::setFullName( const QString& name ) // Build login and hostname, if needed static QRegExp rx( "[^a-zA-Z0-9 ]", Qt::CaseInsensitive ); - QString cleanName = CalamaresUtils::removeDiacritics( transliterate( name ) ) - .replace( QRegExp( "[-']" ), "" ) - .replace( rx, " " ) - .toLower() - .simplified(); + const QString cleanName = CalamaresUtils::removeDiacritics( transliterate( name ) ) + .replace( QRegExp( "[-']" ), "" ) + .replace( rx, " " ) + .toLower() + .simplified(); QStringList cleanParts = cleanName.split( ' ' ); if ( !m_customLoginName ) { - QString login = makeLoginNameSuggestion( cleanParts ); + const QString login = makeLoginNameSuggestion( cleanParts ); if ( !login.isEmpty() && login != m_loginName ) { setLoginName( login ); @@ -448,7 +471,7 @@ Config::setFullName( const QString& name ) } if ( !m_customHostName ) { - QString hostname = makeHostnameSuggestion( cleanParts ); + const QString hostname = makeHostnameSuggestion( m_hostnameTemplate, cleanParts, loginName() ); if ( !hostname.isEmpty() && hostname != m_hostname ) { setHostName( hostname ); @@ -877,6 +900,8 @@ Config::setConfigurationMap( const QVariantMap& configurationMap ) copyLegacy( configurationMap, "writeHostsFile", hostnameSettings, "writeHostsFile" ); m_hostnameAction = getHostNameAction( hostnameSettings ); m_writeEtcHosts = CalamaresUtils::getBool( hostnameSettings, "writeHostsFile", true ); + m_hostnameTemplate + = CalamaresUtils::getString( hostnameSettings, "template", QStringLiteral( "${first}-${product}" ) ); } setConfigurationDefaultGroups( configurationMap, m_defaultGroups ); diff --git a/src/modules/users/Config.h b/src/modules/users/Config.h index 6ddc72c67..c395dc1d4 100644 --- a/src/modules/users/Config.h +++ b/src/modules/users/Config.h @@ -345,6 +345,8 @@ private: HostNameAction m_hostnameAction = HostNameAction::EtcHostname; bool m_writeEtcHosts = false; + QString m_hostnameTemplate; + PasswordCheckList m_passwordChecks; }; diff --git a/src/modules/users/Tests.cpp b/src/modules/users/Tests.cpp index d9ce3636d..9c410cb1f 100644 --- a/src/modules/users/Tests.cpp +++ b/src/modules/users/Tests.cpp @@ -19,6 +19,8 @@ extern void setConfigurationDefaultGroups( const QVariantMap& map, QList< GroupDescription >& defaultGroups ); extern HostNameAction getHostNameAction( const QVariantMap& configurationMap ); extern bool addPasswordCheck( const QString& key, const QVariant& value, PasswordCheckList& passwordChecks ); +extern QString +makeHostnameSuggestion( const QString& templateString, const QStringList& fullNameParts, const QString& loginName ); /** @brief Test Config object methods and internals * @@ -43,6 +45,9 @@ private Q_SLOTS: void testHostActions_data(); void testHostActions(); void testHostActions2(); + void testHostSuggestions_data(); + void testHostSuggestions(); + void testPasswordChecks(); void testUserPassword(); @@ -279,6 +284,34 @@ UserTests::testHostActions2() } +void +UserTests::testHostSuggestions_data() +{ + QTest::addColumn< QString >( "templateString" ); + QTest::addColumn< QString >( "result" ); + + QTest::newRow( "unset " ) << QString() << QString(); + QTest::newRow( "const " ) << QStringLiteral( "derp" ) << QStringLiteral( "derp" ); + QTest::newRow( "escaped" ) << QStringLiteral( "$$" ) << QString(); // Because invalid + QTest::newRow( "default" ) << QStringLiteral( "${first}-pc" ) + << QStringLiteral( "chuck-pc" ); // Avoid ${product} because it's DMI-based + QTest::newRow( "full " ) << QStringLiteral( "${name}" ) << QStringLiteral( "chuckyeager" ); + QTest::newRow( "login+ " ) << QStringLiteral( "${login}-${first}" ) << QStringLiteral( "bill-chuck" ); +} + +void +UserTests::testHostSuggestions() +{ + const QStringList fullName { "Chuck", "Yeager" }; + const QString login { "bill" }; + + QFETCH( QString, templateString ); + QFETCH( QString, result ); + + QCOMPARE( makeHostnameSuggestion( templateString, fullName, login ), result ); +} + + void UserTests::testPasswordChecks() {