Description: Save user language and regional formats settings to ~/.pam_environment.
Author: Gunnar Hjalmarsson <gunnarhj@ubuntu.com>

Index: accountsservice-0.6.35/src/user.c
===================================================================
--- accountsservice-0.6.35.orig/src/user.c	2013-11-13 15:16:53.000125338 +0100
+++ accountsservice-0.6.35/src/user.c	2013-11-13 15:16:52.996125337 +0100
@@ -1071,6 +1071,297 @@
         return TRUE;
 }
 
+static gboolean
+user_drop_privileges_to_user (User *user)
+{
+        if (setresgid (user->gid, user->gid, -1) != 0) {
+                g_warning ("setresgid() failed");
+                return FALSE;
+        }
+        if (setresuid (user->uid, user->uid, -1) != 0) {
+                g_warning ("setresuid() failed");
+                return FALSE;
+        }
+        return TRUE;
+}
+
+static void
+user_regain_privileges ()
+{
+        if (setresuid (0, 0, -1) != 0)
+                g_warning ("setresuid() failed");
+        if (setresgid (0, 0, -1) != 0)
+                g_warning ("setresgid() failed");
+}
+
+static gchar *
+user_locale_utf8_fix (const gchar *locale)
+{
+        if (locale == NULL || !g_strrstr (locale, ".utf8"))
+                return g_strdup (locale);
+
+        gchar **tokens = g_strsplit_set (locale, ".8", 3);
+        gchar *fixed_locale = g_strconcat (tokens[0], ".UTF-8", tokens[2], NULL);
+        g_strfreev (tokens);
+
+        return fixed_locale;
+}
+
+static gchar *
+user_language_validate (User        *user,
+                        const gchar *lang)
+{
+        gboolean     ret;
+        const gchar *program = "/usr/share/language-tools/language-validate";
+        gchar       *command = g_strconcat (program, " ", lang, NULL);
+        gchar       *validated_language;
+        GError      *error = NULL;
+
+        if (!user_drop_privileges_to_user (user))
+                return NULL;
+        ret = g_spawn_command_line_sync (command, &validated_language, NULL, NULL, &error);
+        user_regain_privileges ();
+
+        g_free (command);
+        if (!ret) {
+                g_warning ("Couldn't get validated language: %s", error->message);
+                g_error_free (error);
+                return NULL;
+        }
+        return g_strchomp (validated_language);
+}
+
+static gchar *
+user_locale_validate (User                  *user,
+                      const gchar           *locale,
+                      GDBusMethodInvocation *context)
+{
+        gchar    *validated_locale = NULL;
+        gchar    *tmp_locale = NULL;
+        gchar    *tmp_locale_utf8 = NULL;
+        gboolean  ret = FALSE;
+        gchar    *output = NULL;
+        GError   *error = NULL;
+        gboolean  found = FALSE;
+
+        if (locale == NULL || strlen (locale) < 2)
+                goto out;
+        tmp_locale = g_strdup (locale);
+        g_strchomp (tmp_locale);
+
+        /* When a new language has been installed, we want to be able to instantly
+           set a locale for that language to denote the regional formats. For that
+           reason we make use of 'locale -a' instead of calling setlocale(). */
+
+        if (!user_drop_privileges_to_user (user))
+                goto out;
+        ret = g_spawn_command_line_sync ("locale -a", &output, NULL, NULL, &error);
+        user_regain_privileges ();
+        if (!ret) {
+                throw_error (context, ERROR_FAILED, "Failed to run 'locale -a': %s", error->message);
+                g_clear_error (&error);
+                goto out;
+        }
+
+        /* Replace 'UTF-8' with 'utf8' since 'locale -a' outputs the latter. */
+        tmp_locale_utf8 = g_strdup (tmp_locale);
+        if (g_strrstr (tmp_locale_utf8, ".UTF-8")) {
+                gchar **tokens = g_strsplit_set (tmp_locale_utf8, ".8", 3);
+                g_free (tmp_locale_utf8);
+                tmp_locale_utf8 = g_strconcat (tokens[0], ".utf8", tokens[2], NULL);
+                g_strfreev (tokens);
+        }
+
+        gchar **tokens;
+        int i;
+        g_strchomp (output);
+        tokens = g_strsplit (output, "\n", -1);
+        for (i = 0; tokens[i]; i++) {
+                if (g_strcmp0 (tokens[i], "C") == 0 ||
+                    g_strcmp0 (tokens[i], "C.UTF-8") == 0 ||
+                    g_strcmp0 (tokens[i], "POSIX") == 0)
+                        continue;
+
+                if (g_strcmp0 (tokens[i], tmp_locale_utf8) == 0) {
+                        found = TRUE;
+                        break;
+                }
+        }
+        g_strfreev (tokens);
+
+        if (!found) {
+                throw_error (context, ERROR_FAILED, "'%s' is not a valid locale name", tmp_locale);
+                goto out;
+        }
+        validated_locale = user_locale_utf8_fix (tmp_locale);
+
+out:
+        g_free (tmp_locale);
+        g_free (tmp_locale_utf8);
+        g_free (output);
+
+        return validated_locale;
+}
+
+static gchar *
+strip_quotes (const gchar *str)
+{
+        if ((g_str_has_prefix (str, "\"") && g_str_has_suffix (str, "\""))
+          || (g_str_has_prefix (str, "'") && g_str_has_suffix (str, "'")))
+                return g_strndup (str + 1, strlen (str) - 2);
+        else
+                return g_strdup (str);
+}
+
+static gchar *
+user_get_fallback_value (User        *user,
+                         const gchar *property)
+{
+        gchar *fallback_value = NULL;
+        gchar *language = NULL;
+        gchar *lang = NULL;
+        gchar *lctime = NULL;
+
+        FILE  *fp;
+        if ((fp = fopen ("/etc/default/locale", "r"))) {
+                gchar line[50];
+                while ((fgets (line, 50, fp)) != NULL) {
+                        g_strchomp (line);
+                        if (g_str_has_prefix (line, "LANGUAGE="))
+                                language = strip_quotes (line + strlen ("LANGUAGE="));
+                        else if (g_str_has_prefix (line, "LANG="))
+                                lang = strip_quotes (line + strlen ("LANG="));
+                        else if (g_str_has_prefix (line, "LC_TIME="))
+                                lctime = strip_quotes (line + strlen ("LC_TIME="));
+                }
+                fclose (fp);
+        }
+
+        if (g_strcmp0 (property, "Language") == 0) {
+                gchar *tmp = NULL;
+                if (language)
+                        tmp = language;
+                else if (lang)
+                        tmp = lang;
+                fallback_value = user_language_validate (user, tmp);
+        }
+        
+        if (g_strcmp0 (property, "FormatsLocale") == 0) {
+                gchar *tmp = NULL;
+                if (lctime)
+                        tmp = lctime;
+                else if (lang)
+                        tmp = lang;
+                fallback_value = user_locale_utf8_fix (tmp);
+        }
+
+        g_free (language);
+        g_free (lang);
+        g_free (lctime);
+
+        return fallback_value;
+}
+
+static gchar *
+user_update_environment (User                  *user,
+                         gchar                 *data,
+                         const gchar           *script,
+                         GDBusMethodInvocation *context)
+
+/* This function updates ~/.pam_environment by means of the help files in /usr/share/language-tools. */
+{
+        gchar        *program;
+        gchar        *validated_data = NULL;
+        gint          i;
+        const gchar  *allowed_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890._+-:/ @";
+        gboolean      ret;
+        GError       *error = NULL;
+
+        if (data == NULL)
+               return NULL;
+
+        program = g_build_path ("/", "/usr/share/language-tools", script, NULL);
+        gchar *command[] = { program, user->home_dir, data, NULL };
+
+        /* test for odd characters in arguments */
+        for (i = 1; i <= 2; i++) {
+                if (strlen (command[i]) != strspn (command[i], allowed_chars)) {
+                        throw_error (context, ERROR_FAILED, "non-permitted character(s) in argument");
+                        goto out;
+                }
+        }
+
+        /* set applicable environment variables in ~/.pam_environment */
+        if (!user_drop_privileges_to_user (user))
+                goto out;
+        ret = g_spawn_sync ( NULL,
+                             command,
+                             NULL,
+                             G_SPAWN_STDERR_TO_DEV_NULL,
+                             NULL,
+                             NULL,
+                             &validated_data,
+                             NULL,
+                             NULL,
+                             &error );
+        user_regain_privileges ();
+        if (!ret) {
+                throw_error (context, ERROR_FAILED, "running '%s' failed: %s", program, error->message);
+                g_error_free (error);
+                validated_data = NULL;
+                goto out;
+        }
+        if (validated_data == NULL || strlen (validated_data) <= 1) {
+                throw_error (context, ERROR_FAILED, "running '%s' failed: no output", program);
+                validated_data = NULL;
+                goto out;
+        }
+
+out:
+        g_free (program);
+        if (validated_data == NULL)
+                return NULL;
+
+        return g_strchomp (validated_data);
+}
+
+static gboolean
+user_HOME_available (User *user)
+{
+        /* run these tests as user to prevent incorrect
+           failures when HOME is on NFS */
+        user_drop_privileges_to_user (user);
+
+        GDir* homedir = g_dir_open (user->home_dir, 0, NULL);
+
+        if (homedir == NULL) {  // HOME not found
+                user_regain_privileges ();
+                return FALSE;
+        } else {
+                do {
+                        const gchar *entry = g_dir_read_name (homedir);
+                        gchar *path_to_entry = g_build_path ("/", user->home_dir, entry, NULL);
+                        if (g_file_test (path_to_entry, G_FILE_TEST_IS_SYMLINK)) {
+                                g_free (path_to_entry);
+                                continue;
+                        }
+                        if (g_strcmp0 (entry, NULL) == 0) {  // HOME contains only symlinks
+                                g_free (path_to_entry);
+                                g_dir_close (homedir);
+                                user_regain_privileges ();
+                                return FALSE;
+                        } else
+                                g_free (path_to_entry);
+                                break;
+                } while (TRUE);
+        }
+
+        g_dir_close (homedir);
+        user_regain_privileges ();
+
+        return TRUE;
+}
+
 static void
 user_change_language_authorized_cb (Daemon                *daemon,
                                     User                  *user,
@@ -1078,11 +1369,78 @@
                                     gpointer               data)
 
 {
-        gchar *language = data;
+        const gchar *fallback_language = user_get_fallback_value (user, "Language");
+        gboolean     is_language_changed = (user->language && g_strcmp0 (user->language, data) != 0)
+                         || (!user->language && g_strcmp0 (data, fallback_language) != 0);
+
+        if (!user_HOME_available (user)) {
+
+                /* SetLanguage was probably called from a login greeter,
+                   and HOME not mounted and/or not decrypted.
+                   Hence don't save anything, or else accountsservice
+                   and ~/.pam_environment would become out of sync. */
+                throw_error (context, ERROR_FAILED, "not access to HOME yet so language not saved");
+                goto out2;
+        }
+
+        if (is_language_changed) {
+                gchar *language = user_update_environment (user,
+                                                           data,
+                                                           "set-language-helper",
+                                                           context);
+                if (language != NULL) {
+                        g_free (user->language);
+                        user->language = language;
+                }
+
+                gchar *locale = NULL;
 
-        if (g_strcmp0 (user->language, language) != 0) {
-                g_free (user->language);
-                user->language = g_strdup (language);
+                if (!user->formats_locale) {
+
+                        /* set the user formats (certain LC_* variables) explicitly
+                           in order to prevent surprises when LANG is changed */
+                        FILE *fp;
+                        if ((fp = fopen ("/etc/default/locale", "r"))) {
+                                gchar line[50];
+                                while ((fgets (line, 50, fp)) != NULL) {
+                                        if (g_str_has_prefix (line, "LC_TIME=")) {
+                                                g_strchomp (line);
+                                                locale = strip_quotes (line + strlen ("LC_TIME="));
+                                                break;
+                                        }
+                                }
+                                fclose (fp);
+                        }
+                        if (locale == NULL) {
+                                GError *error = NULL;
+                                const gchar *program = "/usr/share/language-tools/language2locale";
+                                if (!user_drop_privileges_to_user (user))
+                                        goto out1;
+                                gchar *command = g_strconcat (program, " ", fallback_language, NULL);
+                                gboolean ret = g_spawn_command_line_sync (command, &locale, NULL, NULL, &error);
+                                user_regain_privileges ();
+                                if (!ret) {
+                                        throw_error (context, ERROR_FAILED,
+                                            "language-to-locale conversion failed: %s", error->message);
+                                        g_error_free (error);
+                                        locale = NULL;
+                                }
+                                g_free (command);
+                        }
+                }
+
+                if (locale != NULL && strlen (locale) > 0) {
+                        gchar *formats_locale = user_update_environment (user,
+                                                                         user_locale_validate (user, locale, context),
+                                                                         "save-to-pam-env",
+                                                                         context);
+                        if (formats_locale != NULL) {
+                                g_free (user->formats_locale);
+                                user->formats_locale = formats_locale;
+                        }
+                }
+out1:
+                g_free (locale);
 
                 save_extra_data (user);
 
@@ -1091,6 +1449,7 @@
                 g_object_notify (G_OBJECT (user), "language");
         }
 
+out2:
         accounts_user_complete_set_language (ACCOUNTS_USER (user), context);
 }
 
@@ -1134,11 +1493,15 @@
                                           gpointer               data)
 
 {
-        gchar *formats_locale = data;
-
-        if (g_strcmp0 (user->formats_locale, formats_locale) != 0) {
-                g_free (user->formats_locale);
-                user->formats_locale = g_strdup (formats_locale);
+        if (g_strcmp0 (user->formats_locale, data) != 0) {
+                gchar *formats_locale = user_update_environment (user,
+                                                                 user_locale_validate (user, data, context),
+                                                                 "save-to-pam-env",
+                                                                 context);
+                if (formats_locale != NULL) {
+                        g_free (user->formats_locale);
+                        user->formats_locale = formats_locale;
+                }
 
                 save_extra_data (user);
 
@@ -2325,10 +2688,16 @@
                 g_value_set_string (value, user->email);
                 break;
         case PROP_LANGUAGE:
-                g_value_set_string (value, user->language);
+                if (user->language)
+                        g_value_set_string (value, user->language);
+                else
+                        g_value_set_string (value, user_get_fallback_value (user, "Language"));
                 break;
         case PROP_FORMATS_LOCALE:
-                g_value_set_string (value, user->formats_locale);
+                if (user->formats_locale)
+                        g_value_set_string (value, user->formats_locale);
+                else
+                        g_value_set_string (value, user_get_fallback_value (user, "FormatsLocale"));
                 break;
         case PROP_X_SESSION:
                 g_value_set_string (value, user->x_session);
