example: `Europe/Berlin`.
`preferredLanguage` → A string indicating the preferred language/locale for the
-user. When logging in
+user. It is combined with the `additionalLanguages` field to initialize the `$LANG`
+and `$LANGUAGE` environment variables on login; see below for more details. This string
+should be in a format compatible with the `$LANG` environment variable, for example:
+`de_DE.UTF-8`.
+
+`additionalLanguages` → An array of strings indicating the preferred languages/locales
+that should be used in the event that translations for the `preferredLanguage` are
+missing, listed in order of descending priority. This allows multi-lingual users to
+specify all the languages that they know, so software lacking translations in the user's
+primary language can try another language that the user knows rather than falling back to
+the default English. All entries in this field must be valid locale names, compatible with
+the `$LANG` variable, for example: `de_DE.UTF-8`. When logging in
[`pam_systemd`](https://www.freedesktop.org/software/systemd/man/pam_systemd.html)
-will automatically initialize the `$LANG` environment variable from this
-string. The string hence should be in a format compatible with this environment
-variable, for example: `de_DE.UTF8`.
+will prepend `preferredLanguage` (if set) to this list (if set), remove duplicates,
+and then automatically initialize the `$LANGUAGE` variable with the resulting list.
+It will also initialize `$LANG` variable with the first entry in the resulting list.
`niceLevel` → An integer value in the range -20…19. When logging in
[`pam_systemd`](https://www.freedesktop.org/software/systemd/man/pam_systemd.html)
`regular` section (i.e. at the top-level object). Specifically, these are:
`iconName`, `location`, `shell`, `umask`, `environment`, `timeZone`,
-`preferredLanguage`, `niceLevel`, `resourceLimits`, `locked`, `notBeforeUSec`,
+`preferredLanguage`, `additionalLanguages`, `niceLevel`, `resourceLimits`, `locked`, `notBeforeUSec`,
`notAfterUSec`, `storage`, `diskSize`, `diskSizeRelative`, `skeletonDirectory`,
`accessMode`, `tasksMax`, `memoryHigh`, `memoryMax`, `cpuWeight`, `ioWeight`,
`mountNoDevices`, `mountNoSuid`, `mountNoExecute`, `cifsDomain`,
<varlistentry>
<term><option>--language=</option><replaceable>LANG</replaceable></term>
- <listitem><para>Takes a specifier indicating the preferred language of the user. The
- <varname>$LANG</varname> environment variable is initialized from this value on login, and thus a
- value suitable for this environment variable is accepted here, for example
- <option>--language=de_DE.UTF8</option>.</para>
+ <listitem><para>Takes a comma- or colon-separated list of languages preferred by the user, ordered
+ by descending priority. The <varname>$LANG</varname> and <varname>$LANGUAGE</varname> environment
+ variables are initialized from this value on login, and thus values suitible for these environment
+ variables are accepted here, for example <option>--language=de_DE.UTF-8</option>. This option may
+ be used more than once, in which case the language lists are concatenated.</para>
<xi:include href="version-info.xml" xpointer="v245"/></listitem>
</varlistentry>
--cifs-user-name)
comps=$(compgen -A user -- "$cur" )
;;
+ --language)
+ comps=$(localectl list-locales 2>/dev/null)
+ ;;
esac
COMPREPLY=( $(compgen -W '$comps' -- "$cur") )
return 0
" --shell=PATH Shell for account\n"
" --setenv=VARIABLE[=VALUE] Set an environment variable at log-in\n"
" --timezone=TIMEZONE Set a time-zone\n"
- " --language=LOCALE Set preferred language\n"
+ " --language=LOCALE Set preferred languages\n"
" --ssh-authorized-keys=KEYS\n"
" Specify SSH public keys\n"
" --pkcs11-token-uri=URI URI to PKCS#11 security token containing\n"
}
static int parse_argv(int argc, char *argv[]) {
+ _cleanup_strv_free_ char **arg_languages = NULL;
enum {
ARG_VERSION = 0x100,
break;
- case ARG_LANGUAGE:
- if (isempty(optarg)) {
- r = drop_from_identity("language");
+ case ARG_LANGUAGE: {
+ const char *p = optarg;
+
+ if (isempty(p)) {
+ r = drop_from_identity("preferredLanguage");
if (r < 0)
return r;
+ r = drop_from_identity("additionalLanguages");
+ if (r < 0)
+ return r;
+
+ arg_languages = strv_free(arg_languages);
break;
}
- if (!locale_is_valid(optarg))
- return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Locale '%s' is not valid.", optarg);
+ for (;;) {
+ _cleanup_free_ char *word = NULL;
- if (locale_is_installed(optarg) <= 0)
- log_warning("Locale '%s' is not installed, accepting anyway.", optarg);
+ r = extract_first_word(&p, &word, ",:", 0);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse locale list: %m");
+ if (r == 0)
+ break;
- r = json_variant_set_field_string(&arg_identity_extra, "preferredLanguage", optarg);
- if (r < 0)
- return log_error_errno(r, "Failed to set preferredLanguage field: %m");
+ if (!locale_is_valid(word))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Locale '%s' is not valid.", word);
+
+ if (locale_is_installed(word) <= 0)
+ log_warning("Locale '%s' is not installed, accepting anyway.", word);
+
+ r = strv_consume(&arg_languages, TAKE_PTR(word));
+ if (r < 0)
+ return log_oom();
+
+ strv_uniq(arg_languages);
+ }
break;
+ }
case ARG_NOSUID:
case ARG_NODEV:
if (arg_disk_size != UINT64_MAX || arg_disk_size_relative != UINT64_MAX)
arg_and_resize = true;
+ if (!strv_isempty(arg_languages)) {
+ char **additional;
+
+ r = json_variant_set_field_string(&arg_identity_extra, "preferredLanguage", arg_languages[0]);
+ if (r < 0)
+ return log_error_errno(r, "Failed to update preferred language: %m");
+
+ additional = strv_skip(arg_languages, 1);
+ if (!strv_isempty(additional)) {
+ r = json_variant_set_field_strv(&arg_identity_extra, "additionalLanguages", additional);
+ if (r < 0)
+ return log_error_errno(r, "Failed to update additional language list: %m");
+ } else {
+ r = drop_from_identity("additionalLanguages");
+ if (r < 0)
+ return r;
+ }
+ }
+
return 1;
}
bool debug,
uint64_t default_capability_bounding_set,
uint64_t default_capability_ambient_set) {
+ _cleanup_strv_free_ char **langs = NULL;
int r;
assert(handle);
}
}
- if (ur->preferred_language) {
- if (locale_is_installed(ur->preferred_language) <= 0)
- pam_debug_syslog(handle, debug,
- "Preferred language specified in user record is not valid or not installed, not setting $LANG.");
- else {
- _cleanup_free_ char *joined = NULL;
+ r = user_record_languages(ur, &langs);
+ if (r < 0)
+ pam_syslog_errno(handle, LOG_ERR, r,
+ "Failed to acquire user's language preferences, ignoring: %m");
+ else if (strv_isempty(langs))
+ ; /* User has no preference set so we do nothing */
+ else if (locale_is_installed(langs[0]) <= 0)
+ pam_debug_syslog(handle, debug,
+ "Preferred languages specified in user record are not installed locally, not setting $LANG or $LANGUAGE.");
+ else {
+ _cleanup_free_ char *lang = NULL;
+
+ lang = strjoin("LANG=", langs[0]);
+ if (!lang)
+ return pam_log_oom(handle);
- joined = strjoin("LANG=", ur->preferred_language);
+ r = pam_putenv_and_log(handle, lang, debug);
+ if (r != PAM_SUCCESS)
+ return r;
+
+ if (strv_length(langs) > 1) {
+ _cleanup_free_ char *joined = NULL, *language = NULL;
+
+ joined = strv_join(langs, ":");
if (!joined)
return pam_log_oom(handle);
- r = pam_putenv_and_log(handle, joined, debug);
+ language = strjoin("LANGUAGE=", joined);
+ if (!language)
+ return pam_log_oom(handle);
+
+ r = pam_putenv_and_log(handle, language, debug);
if (r != PAM_SUCCESS)
return r;
}
}
void user_record_show(UserRecord *hr, bool show_full_group_info) {
+ _cleanup_strv_free_ char **langs = NULL;
const char *hd, *ip, *shell;
UserStorage storage;
usec_t t;
if (hr->time_zone)
printf(" Time Zone: %s\n", hr->time_zone);
- if (hr->preferred_language)
- printf(" Language: %s\n", hr->preferred_language);
-
- if (!strv_isempty(hr->environment))
- STRV_FOREACH(i, hr->environment) {
- printf(i == hr->environment ?
- " Environment: %s\n" :
- " %s\n", *i);
- }
+ r = user_record_languages(hr, &langs);
+ if (r < 0) {
+ errno = -r;
+ printf(" Languages: (can't acquire: %m)\n");
+ } else if (!strv_isempty(langs)) {
+ STRV_FOREACH(i, langs)
+ printf(i == langs ? " Languages: %s" : ", %s", *i);
+ printf("\n");
+ }
if (hr->locked >= 0)
printf(" Locked: %s\n", yes_no(hr->locked));
#include "glyph-util.h"
#include "hexdecoct.h"
#include "hostname-util.h"
+#include "locale-util.h"
#include "memory-util.h"
#include "path-util.h"
#include "pkcs11-util.h"
strv_free(h->environment);
free(h->time_zone);
free(h->preferred_language);
+ strv_free(h->additional_languages);
rlimit_free_all(h->rlimits);
free(h->skeleton_directory);
return strv_free_and_replace(*l, n);
}
+static int json_dispatch_locale(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+ char **s = userdata;
+ const char *n;
+ int r;
+
+ if (json_variant_is_null(variant)) {
+ *s = mfree(*s);
+ return 0;
+ }
+
+ if (!json_variant_is_string(variant))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name));
+
+ n = json_variant_string(variant);
+
+ if (!locale_is_valid(n))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a valid locale.", strna(name));
+
+ r = free_and_strdup(s, n);
+ if (r < 0)
+ return json_log(variant, flags, r, "Failed to allocate string: %m");
+
+ return 0;
+}
+
+static int json_dispatch_locales(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+ _cleanup_strv_free_ char **n = NULL;
+ char ***l = userdata;
+ const char *locale;
+ JsonVariant *e;
+ int r;
+
+ if (json_variant_is_null(variant)) {
+ *l = strv_free(*l);
+ return 0;
+ }
+
+ if (!json_variant_is_array(variant))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array of strings.", strna(name));
+
+ JSON_VARIANT_ARRAY_FOREACH(e, variant) {
+ if (!json_variant_is_string(e))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array of strings.", strna(name));
+
+ locale = json_variant_string(e);
+ if (!locale_is_valid(locale))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array of valid locales.", strna(name));
+
+ r = strv_extend(&n, locale);
+ if (r < 0)
+ return json_log_oom(variant, flags);
+ }
+
+ return strv_free_and_replace(*l, n);
+}
+
JSON_DISPATCH_ENUM_DEFINE(json_dispatch_user_disposition, UserDisposition, user_disposition_from_string);
static JSON_DISPATCH_ENUM_DEFINE(json_dispatch_user_storage, UserStorage, user_storage_from_string);
{ "umask", JSON_VARIANT_UNSIGNED, json_dispatch_umask, offsetof(UserRecord, umask), 0 },
{ "environment", JSON_VARIANT_ARRAY, json_dispatch_environment, offsetof(UserRecord, environment), 0 },
{ "timeZone", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, time_zone), JSON_SAFE },
- { "preferredLanguage", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, preferred_language), JSON_SAFE },
+ { "preferredLanguage", JSON_VARIANT_STRING, json_dispatch_locale, offsetof(UserRecord, preferred_language), 0 },
+ { "additionalLanguages", JSON_VARIANT_ARRAY, json_dispatch_locales, offsetof(UserRecord, additional_languages), 0 },
{ "niceLevel", _JSON_VARIANT_TYPE_INVALID, json_dispatch_nice, offsetof(UserRecord, nice_level), 0 },
{ "resourceLimits", _JSON_VARIANT_TYPE_INVALID, json_dispatch_rlimits, offsetof(UserRecord, rlimits), 0 },
{ "locked", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, locked), 0 },
{ "umask", JSON_VARIANT_UNSIGNED, json_dispatch_umask, offsetof(UserRecord, umask), 0 },
{ "environment", JSON_VARIANT_ARRAY, json_dispatch_environment, offsetof(UserRecord, environment), 0 },
{ "timeZone", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, time_zone), JSON_SAFE },
- { "preferredLanguage", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, preferred_language), JSON_SAFE },
+ { "preferredLanguage", JSON_VARIANT_STRING, json_dispatch_locale, offsetof(UserRecord, preferred_language), 0 },
+ { "additionalLanguages", JSON_VARIANT_ARRAY, json_dispatch_locales, offsetof(UserRecord, additional_languages), 0 },
{ "niceLevel", _JSON_VARIANT_TYPE_INVALID, json_dispatch_nice, offsetof(UserRecord, nice_level), 0 },
{ "resourceLimits", _JSON_VARIANT_TYPE_INVALID, json_dispatch_rlimits, offsetof(UserRecord, rlimits), 0 },
{ "locked", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, locked), 0 },
return parse_caps_strv(h->capability_ambient_set) & user_record_capability_bounding_set(h);
}
+int user_record_languages(UserRecord *h, char ***ret) {
+ _cleanup_strv_free_ char **l = NULL;
+ int r;
+
+ assert(h);
+ assert(ret);
+
+ if (h->preferred_language) {
+ l = strv_new(h->preferred_language);
+ if (!l)
+ return -ENOMEM;
+ }
+
+ r = strv_extend_strv(&l, h->additional_languages, /* filter_duplicates= */ true);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(l);
+ return 0;
+}
+
uint64_t user_record_ratelimit_next_try(UserRecord *h) {
assert(h);
char **environment;
char *time_zone;
char *preferred_language;
+ char **additional_languages;
int nice_level;
struct rlimit *rlimits[_RLIMIT_MAX];
uint64_t user_record_rebalance_weight(UserRecord *h);
uint64_t user_record_capability_bounding_set(UserRecord *h);
uint64_t user_record_capability_ambient_set(UserRecord *h);
+int user_record_languages(UserRecord *h, char ***ret);
int user_record_build_image_path(UserStorage storage, const char *user_name_and_realm, char **ret);