/*
* evolution-source-registry-autoconfig.c
*
* This library is free software: you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation.
*
* This library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this library. If not, see .
*
*/
#include "evolution-data-server-config.h"
#include
#include
#include
#include
#include
#include "evolution-source-registry-methods.h"
typedef struct _MergeSourceData {
gchar *source_filename;
gchar *path;
GKeyFile *key_file;
} MergeSourceData;
typedef void (*MergeSourcePopulateHashtableFunc)(GHashTable *source,
GKeyFile *key_file,
const gchar *basename,
const gchar *filename);
static void
e_autoconfig_free_merge_source_data (gpointer mem)
{
MergeSourceData *source_data = (MergeSourceData *) mem;
if (source_data == NULL)
return;
g_free (source_data->source_filename);
g_free (source_data->path);
g_key_file_unref (source_data->key_file);
g_free (source_data);
}
static void
populate_hashtable_autoconfig (GHashTable *sources,
GKeyFile *key_file,
const gchar *basename,
const gchar *filename)
{
MergeSourceData *data;
GError *local_error = NULL;
gchar *uid, *val;
val = g_key_file_get_value (key_file, E_SOURCE_EXTENSION_AUTOCONFIG, "Revision", &local_error);
if (val == NULL) {
e_source_registry_debug_print (
"Autoconfig: Failed to read '%s': %s.\n",
filename,
local_error->message);
g_error_free (local_error);
return;
}
g_free (val);
uid = g_strndup (basename, strlen (basename) - 7);
data = g_new0 (MergeSourceData, 1);
data->key_file = g_key_file_ref (key_file);
data->path = g_strdup (filename);
g_hash_table_insert (sources, uid, data);
e_source_registry_debug_print (
"Autoconfig: Found autoconfig source '%s'.\n",
filename);
}
static void
populate_hashtable_home (GHashTable *sources,
GKeyFile *key_file,
const gchar *basename,
const gchar *filename)
{
MergeSourceData *data;
gchar *uid;
if (!g_key_file_has_group (key_file, E_SOURCE_EXTENSION_AUTOCONFIG))
return;
uid = g_strndup (basename, strlen (basename) - 7);
data = g_new0 (MergeSourceData, 1);
data->key_file = g_key_file_ref (key_file);
data->path = g_strdup (filename);
g_hash_table_insert (sources, uid, data);
}
static gboolean
e_autoconfig_read_directory (const gchar *path,
GHashTable *sources,
MergeSourcePopulateHashtableFunc func,
GError **error)
{
GDir *dir;
const gchar *basename;
dir = g_dir_open (path, 0, error);
if (dir == NULL) {
return FALSE;
}
while ((basename = g_dir_read_name (dir)) != NULL) {
GKeyFile *key_file;
gchar *filename;
if (!g_str_has_suffix (basename, ".source"))
continue;
filename = g_build_filename (path, basename, NULL);
key_file = g_key_file_new ();
if (!g_key_file_load_from_file (key_file, filename, G_KEY_FILE_NONE, error)) {
g_prefix_error (error, "Failed to load key file '%s': ", filename);
g_free (filename);
g_dir_close (dir);
g_key_file_unref (key_file);
return FALSE;
}
func (sources, key_file, basename, filename);
g_free (filename);
g_key_file_unref (key_file);
}
g_dir_close (dir);
return TRUE;
}
static gchar *
autoconfig_build_signature_filename (const gchar *path,
const gchar *source_filename)
{
const gchar *use_path;
gchar *filename, *tmp_basename, *tmp_path = NULL;
tmp_basename = g_path_get_basename (source_filename);
g_return_val_if_fail (tmp_basename != NULL, NULL);
g_return_val_if_fail (g_str_has_suffix (tmp_basename, ".source"), NULL);
/* Remove the ".source" extension */
tmp_basename[strlen (tmp_basename) - 7] = '\0';
if (path) {
use_path = path;
} else {
const gchar *config_dir = e_get_user_config_dir ();
tmp_path = g_build_filename (config_dir, "signatures", NULL);
g_mkdir_with_parents (tmp_path, 0700);
use_path = tmp_path;
}
filename = g_build_filename (use_path, tmp_basename, NULL);
g_free (tmp_basename);
g_free (tmp_path);
return filename;
}
static void
e_autoconfig_clean_orphans (GHashTable *autoconfig_sources,
GHashTable *home_sources)
{
GList *keys;
GList *index;
keys = g_hash_table_get_keys (home_sources);
for (index = keys; index != NULL; index = g_list_next (index)) {
gchar *key = index->data;
if (!g_hash_table_contains (autoconfig_sources, key)) {
MergeSourceData *data = g_hash_table_lookup (home_sources, key);
if (data != NULL) {
/* if we fail to remove it, keep going */
if (g_unlink (data->path) == -1) {
e_source_registry_debug_print (
"Autoconfig: Error removing orphan source '%s': %s.\n",
data->path,
g_strerror (errno));
} else {
e_source_registry_debug_print (
"Autoconfig: Removed orphan source '%s'.\n",
data->path);
}
g_hash_table_remove (home_sources, data->path);
if (g_key_file_has_group (data->key_file, E_SOURCE_EXTENSION_MAIL_SIGNATURE)) {
gchar *filename;
filename = autoconfig_build_signature_filename (NULL, data->path);
if (filename && g_file_test (filename, G_FILE_TEST_EXISTS)) {
if (g_unlink (filename) == -1) {
e_source_registry_debug_print (
"Autoconfig: Error removing orphan signature '%s': %s.\n",
filename,
g_strerror (errno));
} else {
e_source_registry_debug_print (
"Autoconfig: Removed orphan signature '%s'.\n",
filename);
}
} else if (filename) {
e_source_registry_debug_print (
"Autoconfig: Error removing orphan signature '%s': File not found.\n",
filename);
}
g_free (filename);
}
}
}
}
g_list_free (keys);
}
typedef struct _ReplaceVariablesData {
const gchar *source_path;
GHashTable *user_variables;
} ReplaceVariablesData;
static gboolean
e_autoconfig_replace_vars_eval_cb (const GMatchInfo *match_info,
GString *result,
gpointer user_data)
{
gchar *var_name;
const gchar *val = NULL;
ReplaceVariablesData *rvd = user_data;
g_return_val_if_fail (rvd != NULL, FALSE);
var_name = g_match_info_fetch (match_info, 1);
if (var_name) {
val = g_hash_table_lookup (rvd->user_variables, var_name);
if (!val)
val = g_getenv (var_name);
}
if (val != NULL) {
g_string_append (result, val);
} else {
/* env var will be replaced by an empty string */
e_source_registry_debug_print (
"Autoconfig: Variable '${%s}' not found, used in '%s'.\n",
var_name,
rvd->source_path);
}
g_free (var_name);
return FALSE;
}
static gchar *
e_autoconfig_replace_vars (const gchar *old,
const gchar *source_path,
GHashTable *user_variables)
{
GRegex *regex;
gchar *new;
ReplaceVariablesData rvd;
GError *local_error = NULL;
g_return_val_if_fail (old != NULL, NULL);
regex = g_regex_new ("\\$\\{(\\w+)\\}", 0, 0, NULL);
g_return_val_if_fail (regex != NULL, g_strdup (old));
rvd.source_path = source_path;
rvd.user_variables = user_variables;
new = g_regex_replace_eval (
regex,
old,
-1,
0,
0,
e_autoconfig_replace_vars_eval_cb,
&rvd,
&local_error);
g_regex_unref (regex);
if (new == NULL) {
e_source_registry_debug_print (
"Autoconfig: Replacing variables failed: %s.\n",
local_error ? local_error->message : "Unknown error");
g_error_free (local_error);
return g_strdup (old);
}
return new;
}
static void
e_autoconfig_copy_source (MergeSourceData *target,
MergeSourceData *source,
GHashTable *user_variables)
{
gchar **groups = NULL;
gsize ngroups;
gint ii;
groups = g_key_file_get_groups (source->key_file, &ngroups);
for (ii = 0; ii < ngroups; ii++) {
gsize nkeys;
gint jj;
gchar **keys;
keys = g_key_file_get_keys (source->key_file, groups[ii], &nkeys, NULL);
for (jj = 0; jj < nkeys; jj++) {
gchar *new_val;
gchar *val = g_key_file_get_value (source->key_file, groups[ii], keys[jj], NULL);
new_val = e_autoconfig_replace_vars (val, source->path, user_variables);
g_free (val);
g_key_file_set_value (target->key_file, groups[ii], keys[jj], new_val);
g_free (new_val);
}
g_strfreev (keys);
}
g_strfreev (groups);
}
static gboolean
e_autoconfig_merge_source (GHashTable *home_sources,
const gchar *key,
MergeSourceData *autoconfig_key_file,
GList **key_files_to_copy,
GHashTable *user_variables,
GError **error)
{
GKeyFile *new_keyfile;
MergeSourceData *home_key_file;
MergeSourceData *new_data;
gboolean skip_copy;
gchar *autoconfig_revision, *home_revision;
g_return_val_if_fail (key_files_to_copy != NULL, FALSE);
home_key_file = g_hash_table_lookup (home_sources, key);
autoconfig_revision = g_key_file_get_value (
autoconfig_key_file->key_file,
E_SOURCE_EXTENSION_AUTOCONFIG,
"Revision",
error);
if (autoconfig_revision == NULL) {
g_prefix_error (
error,
"Failed to get revision of key file '%s': ",
autoconfig_key_file->path);
return FALSE;
}
home_revision = g_key_file_get_value (
home_key_file->key_file,
E_SOURCE_EXTENSION_AUTOCONFIG,
"Revision",
error);
if (home_revision == NULL) {
g_prefix_error (
error,
"Failed to get revision of key file '%s': ",
home_key_file->path);
g_free (autoconfig_revision);
return FALSE;
}
skip_copy = g_strcmp0 (autoconfig_revision, home_revision) == 0;
if (skip_copy) {
e_source_registry_debug_print (
"Autoconfig: Skipping update, revisions of '%s' and '%s' are the same ('%s').\n",
home_key_file->path,
autoconfig_key_file->path,
home_revision);
g_free (autoconfig_revision);
g_free (home_revision);
return TRUE;
}
e_source_registry_debug_print (
"Autoconfig: Going to update '%s' (Revision '%s') with '%s' (Revision '%s').\n",
home_key_file->path,
home_revision,
autoconfig_key_file->path,
autoconfig_revision);
g_free (autoconfig_revision);
g_free (home_revision);
new_keyfile = g_key_file_new ();
if (!g_key_file_load_from_file (new_keyfile, home_key_file->path, G_KEY_FILE_NONE, error)) {
g_prefix_error (
error,
"Failed to load key file '%s': ",
home_key_file->path);
g_key_file_unref (new_keyfile);
return FALSE;
}
new_data = g_new0 (MergeSourceData, 1);
new_data->source_filename = g_strdup (autoconfig_key_file->path);
new_data->path = g_strdup (home_key_file->path);
new_data->key_file = new_keyfile;
e_autoconfig_copy_source (new_data, autoconfig_key_file, user_variables);
*key_files_to_copy = g_list_prepend (*key_files_to_copy, new_data);
return TRUE;
}
static void
e_autoconfig_generate_source_from_autoconfig (const gchar *key,
MergeSourceData *autoconfig_key_file,
GList **key_files_to_copy,
GHashTable *user_variables)
{
GKeyFile *new_keyfile;
MergeSourceData *new_data;
gchar *dest_source_filename, *dest_source_path;
g_return_if_fail (key_files_to_copy != NULL);
dest_source_filename = g_strdup_printf ("%s.source", key);
dest_source_path = g_build_filename (
e_server_side_source_get_user_dir (),
dest_source_filename,
NULL);
g_free (dest_source_filename);
/* If we're here it means there was no file in home sources with an
* Autoconfig section that corresponds with autoconfig_key_file.
* In the unlikely event there's an existing file in home sources with
* the same name as autoconfig_key file, let's not include it in the
* list of files to copy to avoid data loss. */
if (g_file_test (dest_source_path, G_FILE_TEST_EXISTS)) {
e_source_registry_debug_print (
"Autoconfig: Skipping '%s' due to an existing '%s' without '%s' section.\n",
autoconfig_key_file->path, dest_source_path, E_SOURCE_EXTENSION_AUTOCONFIG);
g_free (dest_source_path);
return;
}
new_keyfile = g_key_file_new ();
new_data = g_new0 (MergeSourceData, 1);
new_data->source_filename = g_strdup (autoconfig_key_file->path);
new_data->path = dest_source_path;
new_data->key_file = new_keyfile;
e_autoconfig_copy_source (new_data, autoconfig_key_file, user_variables);
*key_files_to_copy = g_list_prepend (*key_files_to_copy, new_data);
e_source_registry_debug_print (
"Autoconfig: New source '%s'. It will be copied to '%s'.\n",
autoconfig_key_file->path,
new_data->path);
}
static GList *
e_autoconfig_merge_sources (GHashTable *autoconfig_sources,
GHashTable *home_sources,
GHashTable *user_variables)
{
GHashTableIter iter;
GList *key_files_to_copy = NULL;
gpointer key, value;
e_autoconfig_clean_orphans (autoconfig_sources, home_sources);
g_hash_table_iter_init (&iter, autoconfig_sources);
while (g_hash_table_iter_next (&iter, &key, &value)) {
MergeSourceData *autoconfig_key_file = value;
if (g_hash_table_contains (home_sources, key)) {
GError *local_error = NULL;
if (!e_autoconfig_merge_source (home_sources, key, autoconfig_key_file, &key_files_to_copy, user_variables, &local_error)) {
e_source_registry_debug_print (
"Autoconfig: Merge source failed: %s.\n",
local_error ? local_error->message : "Unknown error");
g_clear_error (&local_error);
continue;
}
} else {
e_autoconfig_generate_source_from_autoconfig (key, autoconfig_key_file, &key_files_to_copy, user_variables);
}
}
return key_files_to_copy;
}
static gboolean
e_autoconfig_write_key_file (MergeSourceData *key_file_data,
GError **error)
{
return g_key_file_save_to_file (key_file_data->key_file, key_file_data->path, error);
}
static gboolean
e_autoconfig_write_signature (MergeSourceData *key_file_data,
GHashTable *user_variables,
GError **error)
{
gchar *tmp, *signature_src, *signature_dest;
gboolean success;
GError *local_error = NULL;
g_return_val_if_fail (key_file_data != NULL, FALSE);
g_return_val_if_fail (key_file_data->source_filename != NULL, FALSE);
g_return_val_if_fail (g_str_has_suffix (key_file_data->source_filename, ".source"), FALSE);
/* filename with path, without the ".source" extension */
tmp = g_strndup (key_file_data->source_filename, strlen (key_file_data->source_filename) - 7);
signature_src = g_strconcat (tmp, ".signature", NULL);
g_free (tmp);
if (!g_file_test (signature_src, G_FILE_TEST_EXISTS)) {
e_source_registry_debug_print ("Autoconfig: Missing signature file '%s', skipping it.", signature_src);
g_free (signature_src);
/* return TRUE, to keep going */
return TRUE;
}
signature_dest = autoconfig_build_signature_filename (NULL, key_file_data->source_filename);
tmp = NULL;
success = g_file_get_contents (signature_src, &tmp, NULL, &local_error);
if (success) {
gchar *value;
value = e_autoconfig_replace_vars (tmp, signature_src, user_variables);
if (value) {
success = g_file_set_contents (signature_dest, value, -1, &local_error);
if (!success) {
e_source_registry_debug_print ("Autoconfig: Failed to write signature file '%s': %s",
signature_dest, local_error ? local_error->message : "Unknown error");
}
g_free (value);
} else {
success = FALSE;
e_source_registry_debug_print ("Autoconfig: Failed to replace variables in signature file '%s'",
signature_src);
}
} else {
e_source_registry_debug_print ("Autoconfig: Failed to read signature file '%s': %s",
signature_src, local_error ? local_error->message : "Unknown error");
}
if (success) {
GStatBuf sb;
if (g_stat (signature_src, &sb) != -1 &&
g_chmod (signature_dest, sb.st_mode) == -1) {
e_source_registry_debug_print (
"Autoconfig: Failed to chmod() for '%s': %s\n",
signature_dest, g_strerror (errno));
}
}
if (local_error)
g_propagate_error (error, local_error);
g_free (signature_src);
g_free (signature_dest);
g_free (tmp);
if (success)
success = e_autoconfig_write_key_file (key_file_data, error);
return success;
}
static gboolean
e_autoconfig_write_key_files (GList *list,
GHashTable *user_variables,
GError **error)
{
GList *index;
gboolean success = TRUE;
for (index = list; index && success; index = g_list_next (index)) {
MergeSourceData *data;
data = index->data;
if (data == NULL)
continue;
if (g_key_file_has_group (data->key_file, E_SOURCE_EXTENSION_MAIL_SIGNATURE))
success = e_autoconfig_write_signature (data, user_variables, error);
else
success = e_autoconfig_write_key_file (data, error);
}
return TRUE;
}
static GHashTable *
e_autoconfig_read_user_variables (GSettings *settings)
{
GHashTable *variables;
gchar **strv;
gint ii;
g_return_val_if_fail (G_IS_SETTINGS (settings), NULL);
variables = g_hash_table_new_full (camel_strcase_hash, camel_strcase_equal, g_free, g_free);
strv = g_settings_get_strv (settings, "autoconfig-variables");
if (!strv || !strv[0]) {
g_strfreev (strv);
return variables;
}
for (ii = 0; strv[ii]; ii++) {
const gchar *line = strv[ii];
gchar *name, *value, *sep;
if (!*line)
continue;
sep = strchr (line, '=');
if (!sep || sep == line) {
e_source_registry_debug_print ("Autoconfig: GSettings' autoconfig-variables line '%s' doesn't conform format 'name=value'.\n", line);
continue;
}
name = g_strdup (line);
sep = strchr (name, '=');
if (!sep || sep == name) {
g_free (name);
g_warn_if_reached ();
continue;
}
*sep = '\0';
value = sep + 1;
if (g_hash_table_contains (variables, name))
e_source_registry_debug_print ("Autoconfig: GSettings' autoconfig-variables key contains multiple '%s' variables.\n", name);
g_hash_table_insert (variables, name, g_strdup (value));
}
g_strfreev (strv);
return variables;
}
gboolean
evolution_source_registry_merge_autoconfig_sources (ESourceRegistryServer *server,
GError **error)
{
GHashTable *home_sources = NULL, *autoconfig_sources = NULL, *user_variables = NULL;
GList *key_files_to_copy = NULL;
GSettings *settings;
GError *local_error = NULL;
gboolean success = FALSE;
const gchar * const *config_dirs;
gchar *autoconfig_directory;
gint ii;
settings = g_settings_new ("org.gnome.evolution-data-server");
autoconfig_sources = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, e_autoconfig_free_merge_source_data);
config_dirs = g_get_system_config_dirs ();
for (ii = 0; config_dirs[ii]; ii++) {
gchar *path;
path = g_build_filename (config_dirs[ii], "evolution-data-server", "autoconfig", NULL);
success = e_autoconfig_read_directory (path, autoconfig_sources, populate_hashtable_autoconfig, &local_error);
g_free (path);
if (!success) {
if (local_error != NULL &&
g_error_matches (local_error, G_FILE_ERROR, G_FILE_ERROR_NOENT)) {
g_clear_error (&local_error);
continue;
}
goto exit;
}
}
autoconfig_directory = g_settings_get_string (settings, "autoconfig-directory");
if (autoconfig_directory && *autoconfig_directory) {
if (g_file_test (autoconfig_directory, G_FILE_TEST_IS_DIR)) {
success = e_autoconfig_read_directory (autoconfig_directory, autoconfig_sources, populate_hashtable_autoconfig, &local_error);
if (!success) {
if (local_error != NULL &&
g_error_matches (local_error, G_FILE_ERROR, G_FILE_ERROR_NOENT)) {
g_clear_error (&local_error);
} else {
g_free (autoconfig_directory);
goto exit;
}
}
} else {
e_source_registry_debug_print ("Autoconfig: Skipping GSettings' autoconfig-directory '%s', either it doesn't exist, or it's currently unavailable.\n", autoconfig_directory);
}
}
g_free (autoconfig_directory);
home_sources = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, e_autoconfig_free_merge_source_data);
success = e_autoconfig_read_directory (e_server_side_source_get_user_dir (), home_sources, populate_hashtable_home, &local_error);
if (!success)
goto exit;
user_variables = e_autoconfig_read_user_variables (settings);
g_warn_if_fail (user_variables != NULL);
/* Add these last, to override any user-specified */
g_hash_table_insert (user_variables, g_strdup ("USER"), g_strdup (g_get_user_name ()));
g_hash_table_insert (user_variables, g_strdup ("REALNAME"), g_strdup (g_get_real_name ()));
g_hash_table_insert (user_variables, g_strdup ("HOST"), g_strdup (g_get_host_name ()));
key_files_to_copy = e_autoconfig_merge_sources (autoconfig_sources, home_sources, user_variables);
success = e_autoconfig_write_key_files (key_files_to_copy, user_variables, error);
exit:
if (autoconfig_sources != NULL)
g_hash_table_unref (autoconfig_sources);
if (home_sources != NULL)
g_hash_table_unref (home_sources);
if (user_variables)
g_hash_table_unref (user_variables);
if (key_files_to_copy != NULL)
g_list_free_full (key_files_to_copy, e_autoconfig_free_merge_source_data);
g_clear_object (&settings);
if (local_error != NULL)
g_propagate_error (error, local_error);
return success;
}