summaryrefslogtreecommitdiff
path: root/extensions/session.vala
blob: 8307d5cce87884eac629a5a99c257765f81202d9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
/*
 Copyright (C) 2013-2018 Christian Dywan <christian@twotoats.de>

 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; either
 version 2.1 of the License, or (at your option) any later version.

 See the file COPYING for the full license text.
*/

namespace Tabby {
    class SessionDatabase : Midori.Database {
        static SessionDatabase? _default = null;
        // Note: Using string instead of int64 because it's a hashable type
        HashTable<string, Midori.Browser> browsers;

        public static SessionDatabase get_default () throws Midori.DatabaseError {
            if (_default == null) {
                _default = new SessionDatabase ();
            }
            return _default;
        }

        SessionDatabase () throws Midori.DatabaseError {
            Object (path: "tabby.db", table: "tabs");
            init ();
            browsers = new HashTable<string, Midori.Browser> (str_hash, str_equal);
        }

        async List<int64?> get_sessions () throws Midori.DatabaseError {
            string sqlcmd = """
                SELECT id, closed FROM sessions WHERE closed = 0
                UNION
                SELECT * FROM (SELECT id, closed FROM sessions WHERE closed = 1 ORDER BY tstamp DESC LIMIT 1)
                ORDER BY closed;
            """;
            var sessions = new List<int64?> ();
            var statement = prepare (sqlcmd);
            while (statement.step ()) {
                int64 id = statement.get_int64 ("id");
                int64 closed = statement.get_int64 ("closed");
                if (closed == 0 || sessions.length () == 0) {
                    sessions.append (id);
                }
            }
            return sessions;
        }

        async List<Midori.DatabaseItem>? get_items (int64 session_id, string? filter=null, int64 max_items=15, Cancellable? cancellable=null) throws Midori.DatabaseError {
            string where = filter != null ? "AND (uri LIKE :filter OR title LIKE :filter)" : "";
            string sqlcmd = """
                SELECT id, uri, title, tstamp FROM %s
                WHERE session_id = :session_id %s
                ORDER BY tstamp DESC LIMIT :limit
                """.printf (table, where);
            var statement = prepare (sqlcmd,
                ":session_id", typeof (int64), session_id,
                ":limit", typeof (int64), max_items);
            if (filter != null) {
                string real_filter = "%" + filter.replace (" ", "%") + "%";
                statement.bind (":filter", typeof (string), real_filter);
            }

            var items = new List<Midori.DatabaseItem> ();
            while (statement.step ()) {
                string uri = statement.get_string ("uri");
                string title = statement.get_string ("title");
                int64 date = statement.get_int64 ("tstamp");
                var item = new Midori.DatabaseItem (uri, title, date);
                item.database = this;
                item.id = statement.get_int64 ("id");
                item.set_data<int64> ("session_id", session_id);
                items.append (item);

                uint src = Idle.add (get_items.callback);
                yield;
                Source.remove (src);

                if (cancellable != null && cancellable.is_cancelled ())
                    return null;
            }

            if (cancellable != null && cancellable.is_cancelled ())
                return null;
            return items;
        }

        public async override List<Midori.DatabaseItem>? query (string? filter=null, int64 max_items=15, Cancellable? cancellable=null) throws Midori.DatabaseError {
            var items = new List<Midori.DatabaseItem> ();
            foreach (int64 session_id in yield get_sessions ()) {
                foreach (var item in yield get_items (session_id, filter, max_items, cancellable)) {
                    items.append (item);
                }
            }

            if (cancellable != null && cancellable.is_cancelled ())
                return null;
            return items;
        }

        public async override bool insert (Midori.DatabaseItem item) throws Midori.DatabaseError {
            item.database = this;

            string sqlcmd = """
                INSERT INTO %s (crdate, tstamp, session_id, uri, title)
                VALUES (:crdate, :tstamp, :session_id, :uri, :title)
                """.printf (table);

            var statement = prepare (sqlcmd,
                ":crdate", typeof (int64), item.date,
                ":tstamp", typeof (int64), item.date,
                ":session_id", typeof (int64), item.get_data<int64> ("session_id"),
                ":uri", typeof (string), item.uri,
                ":title", typeof (string), item.title);
            if (statement.exec ()) {
                item.id = statement.row_id ();
                return true;
            }
            return false;
        }

        public async override bool update (Midori.DatabaseItem item) throws Midori.DatabaseError {
            string sqlcmd = """
                UPDATE %s SET uri = :uri, title = :title, tstamp = :tstamp WHERE id = :id
                """.printf (table);
            try {
                var statement = prepare (sqlcmd,
                    ":id", typeof (int64), item.id,
                    ":uri", typeof (string), item.uri,
                    ":title", typeof (string), item.title,
                    ":tstamp", typeof (int64), new DateTime.now_local ().to_unix ());
                if (statement.exec ()) {
                    return true;
                }
            } catch (Midori.DatabaseError error) {
                critical ("Failed to update %s: %s", table, error.message);
            }
            return false;
        }

        public async override bool delete (Midori.DatabaseItem item) throws Midori.DatabaseError {
            string sqlcmd = """
                DELETE FROM %s WHERE id = :id
                """.printf (table);
            var statement = prepare (sqlcmd,
                ":id", typeof (int64), item.id);
            if (statement.exec ()) {
                return true;
            }
            return false;
        }

        int64 insert_session () {
            string sqlcmd = """
                INSERT INTO sessions (tstamp) VALUES (:tstamp)
                """;
            try {
                var statement = prepare (sqlcmd,
                    ":tstamp", typeof (int64), new DateTime.now_local ().to_unix ());
                statement.exec ();
                debug ("Added session: %s", statement.row_id ().to_string ());
                return statement.row_id ();
            } catch (Midori.DatabaseError error) {
                critical ("Failed to add session: %s", error.message);
            }
            return -1;
         }

        void update_session (int64 id, bool closed) {
            string sqlcmd = """
                UPDATE sessions SET closed=:closed, tstamp=:tstamp WHERE id = :id
                """;
            try {
                var statement = prepare (sqlcmd,
                    ":id", typeof (int64), id,
                    ":tstamp", typeof (int64), new DateTime.now_local ().to_unix (),
                    ":closed", typeof (int64), closed ? 1 : 0);
                statement.exec ();
            } catch (Midori.DatabaseError error) {
                critical ("Failed to update session: %s", error.message);
            }
        }

        public async override bool clear (TimeSpan timespan) throws Midori.DatabaseError {
            // Note: TimeSpan is defined in microseconds
            int64 maximum_age = new DateTime.now_local ().to_unix () - timespan / 1000000;

            string sqlcmd = """
                DELETE FROM %s WHERE tstamp >= :maximum_age;
                DELETE FROM sessions WHERE tstamp >= :maximum_age;
                """.printf (table);
            var statement = prepare (sqlcmd,
                ":maximum_age", typeof (int64), maximum_age);
            return statement.exec ();
        }

        public async bool restore_windows (Midori.Browser default_browser) throws Midori.DatabaseError {
            bool restored = false;

            // Restore existing session(s) that weren't closed, or the last closed one
            foreach (var item in yield query (null, int64.MAX - 1)) {
                Midori.Browser browser;
                int64 id = item.get_data<int64> ("session_id");
                if (!restored) {
                    browser = default_browser;
                    restored = true;
                    connect_browser (browser, id);
                    foreach (var widget in browser.tabs.get_children ()) {
                        yield tab_added (widget as Midori.Tab, id);
                    }
                } else {
                    var app = (Midori.App)default_browser.get_application ();
                    browser = browser_for_session (app, id);
                }
                var tab = new Midori.Tab (null, browser.web_context,
                                          item.uri, item.title);
                connect_tab (tab, item);
                browser.add (tab);
            }
            return restored;
        }

        Midori.Browser browser_for_session (Midori.App app, int64 id) {
            var browser = browsers.lookup (id.to_string ());
            if (browser == null) {
                debug ("Restoring session %s", id.to_string ());
                browser = new Midori.Browser (app);
                browser.show ();
                connect_browser (browser, id);
            }
            return browser;
        }

        public void connect_browser (Midori.Browser browser, int64 id=-1) {
            if (id < 0) {
                id = insert_session ();
            } else {
                update_session (id, false);
            }

            browsers.insert (id.to_string (), browser);
            browser.set_data<bool> ("tabby_connected", true);
            foreach (var widget in browser.tabs.get_children ()) {
                tab_added.begin (widget as Midori.Tab, id);
            }
            browser.tabs.add.connect ((widget) => { tab_added.begin (widget as Midori.Tab, id); });
            browser.delete_event.connect ((event) => {
                debug ("Closing session %s", id.to_string ());
                update_session (id, true);
                return false;
            });
        }

        void connect_tab (Midori.Tab tab, Midori.DatabaseItem item) {
            debug ("Connecting %s to session %s", item.uri, item.get_data<int64> ("session_id").to_string ());
            tab.set_data<Midori.DatabaseItem?> ("tabby-item", item);
            tab.notify["uri"].connect ((pspec) => { item.uri = tab.uri; update.begin (item); });
            tab.notify["title"].connect ((pspec) => { item.title = tab.title; });
            tab.close.connect (() => { tab_removed (tab); });
        }

        bool tab_is_connected (Midori.Tab tab) {
            return tab.get_data<Midori.DatabaseItem?> ("tabby-item") != null;
        }

        async void tab_added (Midori.Tab tab, int64 id) {
            if (tab_is_connected (tab)) {
                return;
            }
            var item = new Midori.DatabaseItem (tab.display_uri, tab.display_title,
                                                new DateTime.now_local ().to_unix ());
            item.set_data<int64> ("session_id", id);
            try {
                yield insert (item);
                connect_tab (tab, item);
            } catch (Midori.DatabaseError error) {
                critical ("Failed add tab to session database: %s", error.message);
            }
        }

        void tab_removed (Midori.Tab tab) {
            var item = tab.get_data<Midori.DatabaseItem?> ("tabby-item");
            debug ("Trashing tab %s:%s", item.get_data<int64> ("session_id").to_string (), tab.display_uri);
            item.delete.begin ();
        }
    }

    public class Session : Peas.ExtensionBase, Midori.BrowserActivatable {
        public Midori.Browser browser { owned get; set; }

        static bool session_restored = false;

        public void activate () {
            // Don't track locked (app) or private windows
            if (browser.is_locked || browser.web_context.is_ephemeral ()) {
                return;
            }
            // Skip windows already in the session
            if (browser.get_data<bool> ("tabby_connected")) {
                return;
            }

            browser.default_tab.connect (restore_or_connect);
            try {
                var session = SessionDatabase.get_default ();
                if (session_restored) {
                    session.connect_browser (browser);
                    browser.activate_action ("tab-new", null);
                } else {
                    session_restored = true;
                    restore_session.begin (session);
                }
            } catch (Midori.DatabaseError error) {
                critical ("Failed to restore session: %s", error.message);
            }
        }

        bool restore_or_connect () {
            try {
                var session = SessionDatabase.get_default ();
                var settings = Midori.CoreSettings.get_default ();
                if (settings.load_on_startup == Midori.StartupType.SPEED_DIAL) {
                    session.connect_browser (browser);
                } else if (settings.load_on_startup == Midori.StartupType.HOMEPAGE) {
                    session.connect_browser (browser);
                    browser.activate_action ("homepage", null);
                    return true;
                } else {
                    return true;
                }
            } catch (Midori.DatabaseError error) {
                critical ("Failed to restore session: %s", error.message);
            }
            return false;
        }

        async void restore_session (SessionDatabase session) {
            try {
                bool restored = yield session.restore_windows (browser);
                if (!restored) {
                    browser.add (new Midori.Tab (null, browser.web_context));
                    session.connect_browser (browser);
                }
            } catch (Midori.DatabaseError error) {
                critical ("Failed to restore session: %s", error.message);
            }
        }
    }

    public class Preferences : Object, Midori.PreferencesActivatable {
        public Midori.Preferences preferences { owned get; set; }

        public void activate () {
            var settings = Midori.CoreSettings.get_default ();
            var box = new Midori.LabelWidget (_("Startup"));
            var combo = new Gtk.ComboBoxText ();
            combo.append ("0", _("Show Speed Dial"));
            combo.append ("1", _("Show Homepage"));
            combo.append ("2", _("Show last open tabs"));
            settings.bind_property ("load-on-startup", combo, "active", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL);
            var button = new Midori.LabelWidget (_("When Midori starts:"), combo);
            box.add (button);
            box.show_all ();
            preferences.add (_("Browsing"), box);
            deactivate.connect (() => {
                box.destroy ();
            });
        }
    }

    public class ClearSession : Peas.ExtensionBase, Midori.ClearPrivateDataActivatable {
        public Gtk.Box box { owned get; set; }

        Gtk.CheckButton button;

        public void activate () {
            button = new Gtk.CheckButton.with_mnemonic (_("Last open _tabs"));
            button.show ();
            box.add (button);
        }

        public async void clear (TimeSpan timespan) {
            if (!button.active) {
                return;
            }

            try {
                yield SessionDatabase.get_default ().clear (timespan);
            } catch (Midori.DatabaseError error) {
                critical ("Failed to clear session: %s", error.message);
            }
        }
    }
}

[ModuleInit]
public void peas_register_types(TypeModule module) {
    ((Peas.ObjectModule)module).register_extension_type (
        typeof (Midori.BrowserActivatable), typeof (Tabby.Session));
    ((Peas.ObjectModule)module).register_extension_type (
        typeof (Midori.PreferencesActivatable), typeof (Tabby.Preferences));
    ((Peas.ObjectModule)module).register_extension_type (
        typeof (Midori.ClearPrivateDataActivatable), typeof (Tabby.ClearSession));

}