1/*
2 * Copyright © 2016 Software Freedom Conservancy Inc.
3 * Copyright © 2020 Michael Gratton <mike@vee.net>
4 *
5 * This software is licensed under the GNU Lesser General Public License
6 * (version 2.1 or later). See the COPYING file in this distribution.
7n */
8
9namespace Util.Migrate {
10    private const string GROUP = "AccountInformation";
11    private const string PRIMARY_EMAIL_KEY = "primary_email";
12    private const string SETTINGS_FILENAME = Accounts.Manager.SETTINGS_FILENAME;
13    private const string MIGRATED_FILENAME = ".config_migrated";
14
15    /**
16     * Migrates geary.ini to the XDG configuration directory with the account's email address
17     *
18     * This function iterates through all the account directories in $XDG_DATA_DIR and copies over
19     * geary.ini to ~/.config/geary/<account>/geary.ini. Note that it leaves the
20     * original file untouched.
21     * It also appends a "primary_email" key to the new configuration file to reliaby keep
22     * track of the user's email address.
23     */
24    public static void xdg_config_dir(GLib.File user_config_dir,
25                                      GLib.File user_data_dir)
26        throws GLib.Error {
27        File new_config_dir;
28        File old_data_dir;
29        File new_config_file;
30        File old_config_file;
31
32        // Return if Geary has never been run (~/.local/share/geary does not exist).
33        if (!user_data_dir.query_exists())
34            return;
35
36        FileEnumerator enumerator;
37        enumerator = user_data_dir.enumerate_children ("standard::*",
38            FileQueryInfoFlags.NOFOLLOW_SYMLINKS, null);
39
40        FileInfo? info;
41        string email;
42        File is_migrated;
43        while ((info = enumerator.next_file(null)) != null) {
44            if (info.get_file_type() != FileType.DIRECTORY)
45                continue;
46
47            email = info.get_name();
48
49            // Skip the directory if its name is not a valid email address.
50            if (!Geary.RFC822.MailboxAddress.is_valid_address(email))
51                continue;
52
53            old_data_dir = user_data_dir.get_child(email);
54            new_config_dir = user_config_dir.get_child(email);
55
56            // Skip the directory if ~/.local/share/geary/<account>/geary.ini does not exist.
57            old_config_file = old_data_dir.get_child(SETTINGS_FILENAME);
58            if (!old_config_file.query_exists())
59                continue;
60
61            // Skip the directory if ~/.local/share/geary/<account>/.config_migrated exists.
62            is_migrated = old_data_dir.get_child(MIGRATED_FILENAME);
63            if (is_migrated.query_exists())
64                continue;
65
66            if (!new_config_dir.query_exists()) {
67                try {
68                    new_config_dir.make_directory_with_parents();
69                } catch (Error e) {
70                    debug("Cannot make directory, %s", e.message);
71                    continue;
72                }
73            }
74
75            new_config_file = new_config_dir.get_child(SETTINGS_FILENAME);
76            if (new_config_file.query_exists())
77                continue;
78
79            try {
80                old_config_file.copy(new_config_file, FileCopyFlags.NONE);
81            } catch (Error err) {
82                debug("Error copying over to %s", new_config_dir.get_path());
83                continue;
84            }
85            KeyFile key_file = new KeyFile();
86            try {
87                key_file.load_from_file(new_config_file.get_path(), KeyFileFlags.NONE);
88            } catch (Error err) {
89                debug("Error opening %s", new_config_file.get_path());
90                continue;
91            }
92
93            // Write the primary email key in the new config file.
94            key_file.set_value(GROUP, PRIMARY_EMAIL_KEY, email);
95            string data = key_file.to_data();
96            try {
97                new_config_file.replace_contents(data.data, null, false, FileCreateFlags.NONE,
98                null);
99            } catch (Error e) {
100                debug("Error writing email %s to config file", email);
101                continue;
102            }
103            is_migrated.create(FileCreateFlags.PRIVATE);
104        }
105    }
106
107    /**
108     * Migrates configuration from release build locations.
109     *
110     * This will migrate configuration from release build locations to
111     * the current config directory, if and only if the current config
112     * directory is empty. For example, from the standard
113     * distro-package config location to the current Flatpak location,
114     * or from either to a development config location.
115     */
116    public static void release_config(GLib.File[] search_path,
117                                      GLib.File config_dir)
118        throws GLib.Error {
119        if (is_directory_empty(config_dir)) {
120            GLib.File? most_recent = null;
121            GLib.DateTime most_recent_modified = null;
122            foreach (var source in search_path) {
123                if (!source.equal(config_dir)) {
124                    GLib.DateTime? src_modified = null;
125                    try {
126                        GLib.FileInfo? src_info = source.query_info(
127                            GLib.FileAttribute.TIME_MODIFIED, 0
128                        );
129                        if (src_info != null) {
130                            src_modified =
131                                src_info.get_modification_date_time();
132                        }
133                    } catch (GLib.IOError.NOT_FOUND err) {
134                        // fine
135                    } catch (GLib.Error err) {
136                        debug(
137                            "Error querying release config dir %s: %s",
138                            source.get_path(),
139                            err.message
140                        );
141                    }
142                    if (most_recent_modified == null ||
143                        (src_modified != null &&
144                         most_recent_modified.compare(src_modified) < 0)) {
145                        most_recent = source;
146                        most_recent_modified = src_modified;
147                    }
148                }
149            }
150
151            if (most_recent != null) {
152                try {
153                    debug(
154                        "Migrating release config from %s to %s",
155                        most_recent.get_path(),
156                        config_dir.get_path()
157                    );
158                    recursive_copy(most_recent, config_dir);
159                } catch (GLib.Error err) {
160                    debug("Error migrating release config: %s", err.message);
161                }
162            }
163        }
164    }
165
166    private bool is_directory_empty(GLib.File dir) {
167        bool is_empty = true;
168        GLib.FileEnumerator? existing = null;
169        try {
170            existing = dir.enumerate_children(
171                GLib.FileAttribute.STANDARD_TYPE, 0
172            );
173        } catch (GLib.IOError.NOT_FOUND err) {
174            // fine
175        } catch (GLib.Error err) {
176            debug(
177                "Error enumerating directory %s: %s",
178                dir.get_path(),
179                err.message
180            );
181        }
182
183        if (existing != null) {
184            try {
185                is_empty = existing.next_file() == null;
186            } catch (GLib.Error err) {
187                debug(
188                    "Error getting next child in directory %s: %s",
189                    dir.get_path(),
190                    err.message
191                );
192            }
193
194            try {
195                existing.close();
196            } catch (GLib.Error err) {
197                debug(
198                    "Error closing directory enumeration %s: %s",
199                    dir.get_path(),
200                    err.message
201                );
202            }
203        }
204
205        return is_empty;
206    }
207
208    private static void recursive_copy(GLib.File src,
209                                       GLib.File dest,
210                                       GLib.Cancellable? cancellable = null
211    ) throws GLib.Error {
212        switch (src.query_file_type(NONE, cancellable)) {
213        case DIRECTORY:
214            try {
215                dest.make_directory(cancellable);
216            } catch (GLib.IOError.EXISTS err) {
217                // fine
218            }
219            src.copy_attributes(dest, NONE, cancellable);
220
221            GLib.FileEnumerator children = src.enumerate_children(
222                GLib.FileAttribute.STANDARD_NAME,
223                NONE,
224                cancellable
225            );
226            GLib.FileInfo? child = children.next_file(cancellable);
227            while (child != null) {
228                recursive_copy(
229                    src.get_child(child.get_name()),
230                    dest.get_child(child.get_name())
231                );
232                child = children.next_file(cancellable);
233            }
234            break;
235
236        case REGULAR:
237            src.copy(dest, NONE, cancellable);
238            break;
239
240        default:
241            // no-op
242            break;
243        }
244    }
245
246    public const string OLD_APP_ID = "org.yorba.geary";
247    private const string MIGRATED_CONFIG_KEY = "migrated-config";
248
249    public static void old_app_config(Settings newSettings, string old_app_id = OLD_APP_ID) {
250        SettingsSchemaSource schemaSource = SettingsSchemaSource.get_default();
251        if (Application.Client.GSETTINGS_DIR != null) {
252            try {
253                schemaSource = new SettingsSchemaSource.from_directory(Application.Client.GSETTINGS_DIR, null, false);
254            } catch (Error e) {
255                // If it didn't work, do nothing (i.e. use the default GSettings dir)
256            }
257        }
258        SettingsSchema oldSettingsSchema = schemaSource.lookup(old_app_id, false);
259
260        if (newSettings.get_boolean(MIGRATED_CONFIG_KEY))
261            return;
262
263        if (oldSettingsSchema != null) {
264            Settings oldSettings = new Settings.full(oldSettingsSchema, null, null);
265            foreach (string key in newSettings.settings_schema.list_keys()) {
266                if (oldSettingsSchema.has_key(key)) {
267                    newSettings.set_value(key, oldSettings.get_value(key));
268                }
269            }
270        }
271
272        newSettings.set_boolean(MIGRATED_CONFIG_KEY, true);
273    }
274
275
276}
277