diff --git a/CHANGES b/CHANGES index 720908ca4..2b0c3210a 100644 --- a/CHANGES +++ b/CHANGES @@ -21,6 +21,7 @@ This release contains contributions from (alphabetically by first name): ## Modules ## - A new QML-based *finishedq* module has been added. (Thanks Anke) + - The *users* module now can set a fixed username and prevent editing. # 3.2.37 (2021-02-23) # diff --git a/src/libcalamares/CMakeLists.txt b/src/libcalamares/CMakeLists.txt index 700eff37b..826f0bc41 100644 --- a/src/libcalamares/CMakeLists.txt +++ b/src/libcalamares/CMakeLists.txt @@ -48,9 +48,11 @@ set( libSources locale/TranslatableString.cpp # Modules + modulesystem/Config.cpp modulesystem/Descriptor.cpp modulesystem/InstanceKey.cpp modulesystem/Module.cpp + modulesystem/Preset.cpp modulesystem/RequirementsChecker.cpp modulesystem/RequirementsModel.cpp diff --git a/src/libcalamares/modulesystem/Config.cpp b/src/libcalamares/modulesystem/Config.cpp new file mode 100644 index 000000000..71210a9cb --- /dev/null +++ b/src/libcalamares/modulesystem/Config.cpp @@ -0,0 +1,122 @@ +/* === This file is part of Calamares - === + * + * SPDX-FileCopyrightText: 2021 Adriaan de Groot + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Calamares is Free Software: see the License-Identifier above. + * + */ + +#include "Config.h" + +#include "Preset.h" +#include "utils/Logger.h" +#include "utils/Variant.h" + +namespace Calamares +{ +namespace ModuleSystem +{ + + +class Config::Private +{ +public: + std::unique_ptr< Presets > m_presets; +}; + + +Config::Config( QObject* parent ) + : QObject( parent ) + , d( std::make_unique< Private >() ) +{ +} + +Config::~Config() {} + +bool +Config::isEditable( const QString& fieldName ) const +{ + if ( m_unlocked ) + { + return true; + } + if ( d && d->m_presets ) + { + return d->m_presets->isEditable( fieldName ); + } + else + { + cWarning() << "Checking isEditable, but no presets are configured."; + } + return true; +} + +Config::ApplyPresets::ApplyPresets( Calamares::ModuleSystem::Config& c, const QVariantMap& configurationMap ) + : m_c( c ) + , m_bogus( true ) + , m_map( CalamaresUtils::getSubMap( configurationMap, "presets", m_bogus ) ) +{ + c.m_unlocked = true; + if ( !c.d->m_presets ) + { + c.d->m_presets = std::make_unique< Presets >(); + } +} + +Config::ApplyPresets::~ApplyPresets() +{ + m_c.m_unlocked = false; + + // Check that there's no **settings** (from the configuration map) + // that have not been consumed by apply() -- if they are there, + // that means the configuration map specifies things that the + // Config object does not expect. + bool haveWarned = false; + for ( const auto& k : m_map.keys() ) + { + if ( !m_c.d->m_presets->find( k ).isValid() ) + { + if ( !haveWarned ) + { + cWarning() << "Preset configuration contains unused keys"; + haveWarned = true; + } + cDebug() << Logger::SubEntry << "Unused key" << k; + } + } +} + +Config::ApplyPresets& +Config::ApplyPresets::apply( const char* fieldName ) +{ + const auto prop = m_c.property( fieldName ); + if ( !prop.isValid() ) + { + cWarning() << "Applying invalid property" << fieldName; + } + else + { + const QString key( fieldName ); + if ( !key.isEmpty() && m_c.d->m_presets->find( key ).isValid() ) + { + cWarning() << "Applying duplicate property" << fieldName; + } + else if ( !key.isEmpty() && m_map.contains( key ) ) + { + QVariantMap m = CalamaresUtils::getSubMap( m_map, key, m_bogus ); + QVariant value = m[ "value" ]; + bool editable = CalamaresUtils::getBool( m, "editable", true ); + + if ( value.isValid() ) + { + m_c.setProperty( fieldName, value ); + } + m_c.d->m_presets->append( PresetField { key, value, editable } ); + } + } + return *this; +} + +} // namespace ModuleSystem +} // namespace Calamares diff --git a/src/libcalamares/modulesystem/Config.h b/src/libcalamares/modulesystem/Config.h new file mode 100644 index 000000000..5facae3cd --- /dev/null +++ b/src/libcalamares/modulesystem/Config.h @@ -0,0 +1,147 @@ +/* === This file is part of Calamares - === + * + * SPDX-FileCopyrightText: 2021 Adriaan de Groot + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Calamares is Free Software: see the License-Identifier above. + * + */ + +#ifndef CALAMARES_MODULESYSTEM_CONFIG_H +#define CALAMARES_MODULESYSTEM_CONFIG_H + +#include "DllMacro.h" + +#include +#include +#include + +#include + +namespace Calamares +{ +namespace ModuleSystem +{ +/** @brief Base class for Config-objects + * + * This centralizes the things every Config-object should + * do and provides one source of preset-data. A Config-object + * for a module can **optionally** inherit from this class + * to get presets-support. + * + * TODO:3.3 This is not optional + * TODO:3.3 Put consistent i18n for Configurations in here too + */ +class DLLEXPORT Config : public QObject +{ + Q_OBJECT +public: + Config( QObject* parent = nullptr ); + ~Config() override; + + /** @brief Set the configuration from the config file + * + * Subclasses must implement this to load configuration data; + * that subclass **should** also call loadPresets() with the + * same map, to pick up the "presets" key consistently. + */ + virtual void setConfigurationMap( const QVariantMap& ) = 0; + +public Q_SLOTS: + /** @brief Checks if a @p fieldName is editable according to presets + * + * If the field is named as a preset, **and** the field is set + * to not-editable, returns @c false. Otherwise, return @c true. + * Calling this with an unknown field (one for which no presets + * are accepted) will print a warning and return @c true. + * + * @see CONFIG_PREVENT_EDITING + * + * Most setters will call isEditable() to check if the field should + * be editable. Do not count on the setter not being called: the + * UI might not have set the field to fixed / constant / not-editable + * and then you can have the setter called by changes in the UI. + * + * To prevent the UI from changing **and** to make sure that the UI + * reflects the unchanged value (rather than the changed value it + * sent to the Config object), use CONFIG_PREVENT_EDITING, like so: + * + * CONFIG_PREVENT_EDITING( type, "propertyName" ); + * + * The ; is necessary. is the type of the property; for instance + * QString. The name of the property is a (constant) string. The + * macro will return (out of the setter it is used in) if the field + * is not editable, and will send a notification event with the old + * value as soon as the event loop resumes. + */ + bool isEditable( const QString& fieldName ) const; + +protected: + friend class ApplyPresets; + /** @brief "Builder" class for presets + * + * Derived classes should instantiate this (with themselves, + * and the whole configuration map that is passed to + * setConfigurationMap()) and then call .apply() to apply + * the presets specified in the configuration to the **named** + * QObject properties. + */ + class ApplyPresets + { + public: + /** @brief Create a preset-applier for this config + * + * The @p configurationMap should be the one passed in to + * setConfigurationMap() . Presets are extracted from the + * standard key *presets* and can be applied to the configuration + * with apply() or operator<<. + */ + ApplyPresets( Config& c, const QVariantMap& configurationMap ); + ~ApplyPresets(); + + /** @brief Add a preset for the given @p fieldName + * + * This checks for preset-entries in the configuration map that was + * passed in to the constructor. + */ + ApplyPresets& apply( const char* fieldName ); + /** @brief Alternate way of writing apply() + */ + ApplyPresets& operator<<( const char* fieldName ) { return apply( fieldName ); } + + private: + Config& m_c; + bool m_bogus = true; + const QVariantMap m_map; + }; + +private: + class Private; + std::unique_ptr< Private > d; + bool m_unlocked = false; +}; +} // namespace ModuleSystem +} // namespace Calamares + +/// @see Config::isEditable() +// +// This needs to be a macro, because Q_ARG() is a macro that stringifies +// the type name. +#define CONFIG_PREVENT_EDITING( type, fieldName ) \ + do \ + { \ + if ( !isEditable( QStringLiteral( fieldName ) ) ) \ + { \ + auto prop = property( fieldName ); \ + const auto& metaobject = metaObject(); \ + auto metaprop = metaobject->property( metaobject->indexOfProperty( fieldName ) ); \ + if ( metaprop.hasNotifySignal() ) \ + { \ + metaprop.notifySignal().invoke( this, Qt::QueuedConnection, Q_ARG( type, prop.value< type >() ) ); \ + } \ + return; \ + } \ + } while ( 0 ) + + +#endif diff --git a/src/libcalamares/modulesystem/Preset.cpp b/src/libcalamares/modulesystem/Preset.cpp new file mode 100644 index 000000000..a2e3f3264 --- /dev/null +++ b/src/libcalamares/modulesystem/Preset.cpp @@ -0,0 +1,82 @@ +/* === This file is part of Calamares - === + * + * SPDX-FileCopyrightText: 2021 Adriaan de Groot + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Calamares is Free Software: see the License-Identifier above. + * + */ + +#include "Preset.h" + +#include "utils/Logger.h" +#include "utils/Variant.h" + +static void +loadPresets( Calamares::ModuleSystem::Presets& preset, + const QVariantMap& configurationMap, + std::function< bool( const QString& ) > pred ) +{ + cDebug() << "Creating presets" << preset.capacity(); + for ( auto it = configurationMap.cbegin(); it != configurationMap.cend(); ++it ) + { + if ( !it.key().isEmpty() && pred( it.key() ) ) + { + QVariantMap m = it.value().toMap(); + QString value = CalamaresUtils::getString( m, "value" ); + bool editable = CalamaresUtils::getBool( m, "editable", true ); + + preset.append( Calamares::ModuleSystem::PresetField { it.key(), value, editable } ); + + cDebug() << Logger::SubEntry << "Preset for" << it.key() << "applied editable?" << editable; + } + } +} + +namespace Calamares +{ +namespace ModuleSystem +{ +Presets::Presets( const QVariantMap& configurationMap ) +{ + reserve( configurationMap.count() ); + loadPresets( *this, configurationMap, []( const QString& ) { return true; } ); +} + +Presets::Presets( const QVariantMap& configurationMap, const QStringList& recognizedKeys ) +{ + reserve( recognizedKeys.size() ); + loadPresets( + *this, configurationMap, [&recognizedKeys]( const QString& s ) { return recognizedKeys.contains( s ); } ); +} + +bool +Presets::isEditable( const QString& fieldName ) const +{ + for ( const auto& p : *this ) + { + if ( p.fieldName == fieldName ) + { + return p.editable; + } + } + cWarning() << "Checking isEditable for unknown field" << fieldName; + return true; +} + +PresetField +Presets::find( const QString& fieldName ) const +{ + for ( const auto& p : *this ) + { + if ( p.fieldName == fieldName ) + { + return p; + } + } + + return PresetField(); +} + +} // namespace ModuleSystem +} // namespace Calamares diff --git a/src/libcalamares/modulesystem/Preset.h b/src/libcalamares/modulesystem/Preset.h new file mode 100644 index 000000000..b768a31c9 --- /dev/null +++ b/src/libcalamares/modulesystem/Preset.h @@ -0,0 +1,91 @@ +/* === This file is part of Calamares - === + * + * SPDX-FileCopyrightText: 2021 Adriaan de Groot + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Calamares is Free Software: see the License-Identifier above. + * + */ + +#ifndef CALAMARES_MODULESYSTEM_PRESET_H +#define CALAMARES_MODULESYSTEM_PRESET_H + +#include +#include +#include + +namespace Calamares +{ +namespace ModuleSystem +{ +/** @brief The settings for a single field + * + * The settings apply to a single field; **often** this will + * correspond to a single value or property of a Config + * object, but there is no guarantee of a correspondence + * between names here and names in the code. + * + * The value is stored as a string; consumers (e.g. the UI) + * will need to translate the value to whatever is actually + * used (e.g. in the case of an integer field). + * + * By default, presets are still editable. Set that to @c false + * to make the field unchangeable (again, the UI is responsible + * for setting that up). + */ +struct PresetField +{ + QString fieldName; + QVariant value; + bool editable = true; + + bool isValid() const { return !fieldName.isEmpty(); } +}; + +/** @brief All the presets for one UI entity + * + * This is a collection of presets read from a module + * configuration file, one setting per field. + */ +class Presets : public QVector< PresetField > +{ +public: + /** @brief Reads preset entries from the map + * + * The map's keys are used as field name, and each value entry + * should specify an initial value and whether the entry is editable. + * Fields are editable by default. + */ + explicit Presets( const QVariantMap& configurationMap ); + /** @brief Reads preset entries from the @p configurationMap + * + * As above, but only field names that occur in @p recognizedKeys + * are kept; others are discarded. + */ + Presets( const QVariantMap& configurationMap, const QStringList& recognizedKeys ); + + /** @brief Creates an empty presets map + * + * This constructor is primarily intended for use by the ApplyPresets + * helper class, which will reserve suitable space and load + * presets on-demand. + */ + Presets() = default; + + /** @brief Is the given @p fieldName editable? + * + * Fields are editable by default, so if there is no explicit setting, + * returns @c true. + */ + bool isEditable( const QString& fieldName ) const; + + /** @brief Finds the settings for a field @p fieldName + * + * If there is no such field, returns an invalid PresetField. + */ + PresetField find( const QString& fieldName ) const; +}; +} // namespace ModuleSystem +} // namespace Calamares + +#endif diff --git a/src/modules/users/Config.cpp b/src/modules/users/Config.cpp index 165e21b9c..7aa1cb65c 100644 --- a/src/modules/users/Config.cpp +++ b/src/modules/users/Config.cpp @@ -22,7 +22,9 @@ #include #include +#include #include +#include #ifdef HAVE_ICU #include @@ -91,7 +93,7 @@ hostNameActionNames() } Config::Config( QObject* parent ) - : QObject( parent ) + : Calamares::ModuleSystem::Config( parent ) { emit readyChanged( m_isReady ); // false @@ -105,7 +107,7 @@ Config::Config( QObject* parent ) connect( this, &Config::requireStrongPasswordsChanged, this, &Config::checkReady ); } -Config::~Config() { } +Config::~Config() {} void Config::setUserShell( const QString& shell ) @@ -183,6 +185,7 @@ Config::setSudoersGroup( const QString& group ) void Config::setLoginName( const QString& login ) { + CONFIG_PREVENT_EDITING( QString, "loginName" ); if ( login != m_loginName ) { m_customLoginName = !login.isEmpty(); @@ -393,6 +396,8 @@ makeHostnameSuggestion( const QStringList& parts ) void Config::setFullName( const QString& name ) { + CONFIG_PREVENT_EDITING( QString, "fullName" ); + if ( name.isEmpty() && !m_fullName.isEmpty() ) { if ( !m_customHostName ) @@ -836,6 +841,9 @@ Config::setConfigurationMap( const QVariantMap& configurationMap ) updateGSAutoLogin( doAutoLogin(), loginName() ); checkReady(); + + ApplyPresets( *this, configurationMap ) << "fullName" + << "loginName"; } void diff --git a/src/modules/users/Config.h b/src/modules/users/Config.h index d4bfee4a4..28f0c73d7 100644 --- a/src/modules/users/Config.h +++ b/src/modules/users/Config.h @@ -13,6 +13,7 @@ #include "CheckPWQuality.h" #include "Job.h" +#include "modulesystem/Config.h" #include "utils/NamedEnum.h" #include @@ -85,7 +86,7 @@ private: }; -class PLUGINDLLEXPORT Config : public QObject +class PLUGINDLLEXPORT Config : public Calamares::ModuleSystem::Config { Q_OBJECT @@ -161,7 +162,7 @@ public: Config( QObject* parent = nullptr ); ~Config() override; - void setConfigurationMap( const QVariantMap& ); + void setConfigurationMap( const QVariantMap& ) override; /** @brief Fill Global Storage with some settings * diff --git a/src/modules/users/UsersPage.cpp b/src/modules/users/UsersPage.cpp index 6ea03f8ef..be9e63498 100644 --- a/src/modules/users/UsersPage.cpp +++ b/src/modules/users/UsersPage.cpp @@ -101,6 +101,7 @@ UsersPage::UsersPage( Config* config, QWidget* parent ) connect( config, &Config::rootPasswordSecondaryChanged, ui->textBoxVerifiedRootPassword, &QLineEdit::setText ); connect( config, &Config::rootPasswordStatusChanged, this, &UsersPage::reportRootPasswordStatus ); + ui->textBoxFullName->setText( config->fullName() ); connect( ui->textBoxFullName, &QLineEdit::textEdited, config, &Config::setFullName ); connect( config, &Config::fullNameChanged, this, &UsersPage::onFullNameTextEdited ); @@ -108,6 +109,7 @@ UsersPage::UsersPage( Config* config, QWidget* parent ) connect( config, &Config::hostNameChanged, ui->textBoxHostName, &QLineEdit::setText ); connect( config, &Config::hostNameStatusChanged, this, &UsersPage::reportHostNameStatus ); + ui->textBoxLoginName->setText( config->loginName() ); connect( ui->textBoxLoginName, &QLineEdit::textEdited, config, &Config::setLoginName ); connect( config, &Config::loginNameChanged, ui->textBoxLoginName, &QLineEdit::setText ); connect( config, &Config::loginNameStatusChanged, this, &UsersPage::reportLoginNameStatus ); @@ -140,6 +142,11 @@ UsersPage::UsersPage( Config* config, QWidget* parent ) CALAMARES_RETRANSLATE_SLOT( &UsersPage::retranslate ) onReuseUserPasswordChanged( m_config->reuseUserPasswordForRoot() ); + onFullNameTextEdited( m_config->fullName() ); + reportLoginNameStatus( m_config->loginNameStatus() ); + + ui->textBoxLoginName->setEnabled( m_config->isEditable( "loginName" ) ); + ui->textBoxFullName->setEnabled( m_config->isEditable( "fullName" ) ); } UsersPage::~UsersPage() diff --git a/src/modules/users/users.conf b/src/modules/users/users.conf index 2e09ae123..87227898f 100644 --- a/src/modules/users/users.conf +++ b/src/modules/users/users.conf @@ -159,3 +159,11 @@ setHostname: EtcFile # (also adds localhost and some ipv6 standard entries). # Defaults to *true*. writeHostsFile: true + +presets: + fullName: + value: "OEM User" + editable: false + loginName: + value: "oem" + editable: false diff --git a/src/modules/users/users.schema.yaml b/src/modules/users/users.schema.yaml index 81088032c..5870a7e0a 100644 --- a/src/modules/users/users.schema.yaml +++ b/src/modules/users/users.schema.yaml @@ -43,6 +43,18 @@ properties: setHostname: { type: string, enum: [ None, EtcFile, Hostnamed ] } writeHostsFile: { type: boolean, default: true } + # Presets + # + # TODO: lift up somewhere, since this will return in many modules; + # the type for each field (fullname, loginname) is a + # preset-description (value, editable). + presets: + type: object + additionalProperties: false + properties: + fullname: { type: object } + loginname: { type: object } + required: - defaultGroups - autologinGroup