mirror of
https://github.com/systemd/systemd.git
synced 2025-01-13 17:18:18 +03:00
Merge pull request #31206 from AdrianVovk/user-record-fields
Added some more user record fields
This commit is contained in:
commit
64e18af731
@ -310,11 +310,22 @@ string. The string should be a `tzdata` compatible location string, for
|
||||
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)
|
||||
@ -744,7 +755,7 @@ that may be used in this section are identical to the equally named ones in the
|
||||
`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`,
|
||||
|
@ -366,10 +366,11 @@
|
||||
<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>
|
||||
|
@ -146,6 +146,9 @@ _homectl() {
|
||||
--cifs-user-name)
|
||||
comps=$(compgen -A user -- "$cur" )
|
||||
;;
|
||||
--language)
|
||||
comps=$(localectl list-locales 2>/dev/null)
|
||||
;;
|
||||
esac
|
||||
COMPREPLY=( $(compgen -W '$comps' -- "$cur") )
|
||||
return 0
|
||||
|
@ -260,7 +260,10 @@ bool locale_is_valid(const char *name) {
|
||||
if (!filename_is_valid(name))
|
||||
return false;
|
||||
|
||||
if (!string_is_safe(name))
|
||||
/* Locales look like: ll_CC.ENC@variant, where ll and CC are alphabetic, ENC is alphanumeric with
|
||||
* dashes, and variant seems to be alphabetic.
|
||||
* See: https://www.gnu.org/software/gettext/manual/html_node/Locale-Names.html */
|
||||
if (!in_charset(name, ALPHANUMERICAL "_.-@"))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
|
@ -2435,7 +2435,7 @@ static int help(int argc, char *argv[], void *userdata) {
|
||||
" --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"
|
||||
@ -2547,6 +2547,7 @@ static int help(int argc, char *argv[], void *userdata) {
|
||||
}
|
||||
|
||||
static int parse_argv(int argc, char *argv[]) {
|
||||
_cleanup_strv_free_ char **arg_languages = NULL;
|
||||
|
||||
enum {
|
||||
ARG_VERSION = 0x100,
|
||||
@ -3121,26 +3122,46 @@ static int parse_argv(int argc, char *argv[]) {
|
||||
|
||||
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:
|
||||
@ -4021,6 +4042,25 @@ static int parse_argv(int argc, char *argv[]) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -606,6 +606,7 @@ static int apply_user_record_settings(
|
||||
bool debug,
|
||||
uint64_t default_capability_bounding_set,
|
||||
uint64_t default_capability_ambient_set) {
|
||||
_cleanup_strv_free_ char **langs = NULL;
|
||||
int r;
|
||||
|
||||
assert(handle);
|
||||
@ -617,48 +618,25 @@ static int apply_user_record_settings(
|
||||
}
|
||||
|
||||
STRV_FOREACH(i, ur->environment) {
|
||||
_cleanup_free_ char *n = NULL;
|
||||
const char *e;
|
||||
|
||||
assert_se(e = strchr(*i, '=')); /* environment was already validated while parsing JSON record, this thus must hold */
|
||||
|
||||
n = strndup(*i, e - *i);
|
||||
if (!n)
|
||||
return pam_log_oom(handle);
|
||||
|
||||
if (pam_getenv(handle, n)) {
|
||||
pam_debug_syslog(handle, debug,
|
||||
"PAM environment variable $%s already set, not changing based on record.", *i);
|
||||
continue;
|
||||
}
|
||||
|
||||
r = pam_putenv_and_log(handle, *i, debug);
|
||||
if (r != PAM_SUCCESS)
|
||||
return r;
|
||||
}
|
||||
|
||||
if (ur->email_address) {
|
||||
if (pam_getenv(handle, "EMAIL"))
|
||||
pam_debug_syslog(handle, debug,
|
||||
"PAM environment variable $EMAIL already set, not changing based on user record.");
|
||||
else {
|
||||
_cleanup_free_ char *joined = NULL;
|
||||
_cleanup_free_ char *joined = NULL;
|
||||
|
||||
joined = strjoin("EMAIL=", ur->email_address);
|
||||
if (!joined)
|
||||
return pam_log_oom(handle);
|
||||
joined = strjoin("EMAIL=", ur->email_address);
|
||||
if (!joined)
|
||||
return pam_log_oom(handle);
|
||||
|
||||
r = pam_putenv_and_log(handle, joined, debug);
|
||||
if (r != PAM_SUCCESS)
|
||||
return r;
|
||||
}
|
||||
r = pam_putenv_and_log(handle, joined, debug);
|
||||
if (r != PAM_SUCCESS)
|
||||
return r;
|
||||
}
|
||||
|
||||
if (ur->time_zone) {
|
||||
if (pam_getenv(handle, "TZ"))
|
||||
pam_debug_syslog(handle, debug,
|
||||
"PAM environment variable $TZ already set, not changing based on user record.");
|
||||
else if (!timezone_is_valid(ur->time_zone, LOG_DEBUG))
|
||||
if (!timezone_is_valid(ur->time_zone, LOG_DEBUG))
|
||||
pam_debug_syslog(handle, debug,
|
||||
"Time zone specified in user record is not valid locally, not setting $TZ.");
|
||||
else {
|
||||
@ -674,21 +652,38 @@ static int apply_user_record_settings(
|
||||
}
|
||||
}
|
||||
|
||||
if (ur->preferred_language) {
|
||||
if (pam_getenv(handle, "LANG"))
|
||||
pam_debug_syslog(handle, debug,
|
||||
"PAM environment variable $LANG already set, not changing based on user record.");
|
||||
else 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;
|
||||
|
||||
joined = strjoin("LANG=", ur->preferred_language);
|
||||
lang = strjoin("LANG=", langs[0]);
|
||||
if (!lang)
|
||||
return pam_log_oom(handle);
|
||||
|
||||
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;
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ const char *user_record_state_color(const char *state) {
|
||||
}
|
||||
|
||||
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;
|
||||
@ -237,15 +238,15 @@ void user_record_show(UserRecord *hr, bool show_full_group_info) {
|
||||
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));
|
||||
|
@ -10,6 +10,7 @@
|
||||
#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"
|
||||
@ -146,6 +147,7 @@ static UserRecord* user_record_free(UserRecord *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);
|
||||
@ -535,6 +537,62 @@ static int json_dispatch_environment(const char *name, JsonVariant *variant, Jso
|
||||
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);
|
||||
|
||||
@ -1171,7 +1229,8 @@ static int dispatch_per_machine(const char *name, JsonVariant *variant, JsonDisp
|
||||
{ "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 },
|
||||
@ -1506,7 +1565,8 @@ int user_record_load(UserRecord *h, JsonVariant *v, UserRecordLoadFlags load_fla
|
||||
{ "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 },
|
||||
@ -2034,6 +2094,27 @@ uint64_t user_record_capability_ambient_set(UserRecord *h) {
|
||||
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);
|
||||
|
||||
|
@ -252,6 +252,7 @@ typedef struct UserRecord {
|
||||
char **environment;
|
||||
char *time_zone;
|
||||
char *preferred_language;
|
||||
char **additional_languages;
|
||||
int nice_level;
|
||||
struct rlimit *rlimits[_RLIMIT_MAX];
|
||||
|
||||
@ -415,6 +416,7 @@ AutoResizeMode user_record_auto_resize_mode(UserRecord *h);
|
||||
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);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user