/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/*
* Copyright © 2002 Jorn Baayen
* Copyright © 2003, 2004 Marco Pesenti Gritti
* Copyright © 2003, 2004, 2005, 2006, 2008 Christian Persch
* Copyright © 2012 Igalia S.L.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
#include "config.h"
#include "ephy-session.h"
#include "ephy-about-handler.h"
#include "ephy-debug.h"
#include "ephy-embed-container.h"
#include "ephy-embed-utils.h"
#include "ephy-embed.h"
#include "ephy-file-helpers.h"
#include "ephy-gui.h"
#include "ephy-notebook.h"
#include "ephy-prefs.h"
#include "ephy-private.h"
#include "ephy-settings.h"
#include "ephy-shell.h"
#include "ephy-string.h"
#include "ephy-window.h"
#include
#include
#include
#include
typedef struct {
EphyNotebook *notebook;
gint ref_count;
} NotebookTracker;
typedef struct {
NotebookTracker *notebook_tracker;
int position;
char *url;
WebKitWebViewSessionState *state;
} ClosedTab;
struct _EphySession {
GObject parent_instance;
GQueue *closed_tabs;
guint save_source_id;
GCancellable *save_cancellable;
guint closing : 1;
guint dont_save : 1;
};
#define SESSION_STATE "type:session_state"
#define MAX_CLOSED_TABS 10
enum {
PROP_0,
PROP_CAN_UNDO_TAB_CLOSED,
LAST_PROP
};
static GParamSpec *obj_properties[LAST_PROP];
static gboolean ephy_session_save_idle_cb (EphySession *session);
G_DEFINE_TYPE (EphySession, ephy_session, G_TYPE_OBJECT)
/* Helper functions */
static GFile *
get_session_file (const char *filename)
{
GFile *file;
char *path;
if (filename == NULL) {
return NULL;
}
if (strcmp (filename, SESSION_STATE) == 0) {
path = g_build_filename (ephy_dot_dir (),
"session_state.xml",
NULL);
} else {
path = g_strdup (filename);
}
file = g_file_new_for_path (path);
g_free (path);
return file;
}
static void
session_delete (EphySession *session)
{
GFile *file;
file = get_session_file (SESSION_STATE);
g_file_delete (file, NULL, NULL);
g_object_unref (file);
}
static void
load_changed_cb (WebKitWebView *view,
WebKitLoadEvent load_event,
EphySession *session)
{
if (!ephy_web_view_load_failed (EPHY_WEB_VIEW (view)))
ephy_session_save (session);
}
static void
notebook_tracker_set_notebook (NotebookTracker *tracker,
EphyNotebook *notebook)
{
if (tracker->notebook == notebook) {
return;
}
if (tracker->notebook) {
g_object_remove_weak_pointer (G_OBJECT (tracker->notebook), (gpointer *)&tracker->notebook);
}
tracker->notebook = notebook;
if (tracker->notebook) {
g_object_add_weak_pointer (G_OBJECT (tracker->notebook), (gpointer *)&tracker->notebook);
}
}
static NotebookTracker *
notebook_tracker_new (EphyNotebook *notebook)
{
NotebookTracker *tracker = g_slice_new0 (NotebookTracker);
tracker->ref_count = 1;
notebook_tracker_set_notebook (tracker, notebook);
return tracker;
}
static NotebookTracker *
notebook_tracker_ref (NotebookTracker *tracker)
{
g_atomic_int_inc (&tracker->ref_count);
return tracker;
}
static void
notebook_tracker_unref (NotebookTracker *tracker)
{
if (!g_atomic_int_dec_and_test (&tracker->ref_count))
return;
notebook_tracker_set_notebook (tracker, NULL);
g_slice_free (NotebookTracker, tracker);
}
static EphyNotebook *
closed_tab_get_notebook (ClosedTab *tab)
{
return tab->notebook_tracker->notebook;
}
static int
compare_func (ClosedTab *iter, EphyNotebook *notebook)
{
return GTK_NOTEBOOK (closed_tab_get_notebook (iter)) - GTK_NOTEBOOK (notebook);
}
static NotebookTracker *
ephy_session_ref_or_create_notebook_tracker (EphySession *session,
EphyNotebook *notebook)
{
GList *item = g_queue_find_custom (session->closed_tabs, notebook, (GCompareFunc)compare_func);
return item ? notebook_tracker_ref (((ClosedTab *)item->data)->notebook_tracker) : notebook_tracker_new (notebook);
}
static void
closed_tab_free (ClosedTab *tab)
{
g_free (tab->url);
notebook_tracker_unref (tab->notebook_tracker);
webkit_web_view_session_state_unref (tab->state);
g_slice_free (ClosedTab, tab);
}
static ClosedTab *
closed_tab_new (EphyWebView *web_view,
int position,
NotebookTracker *notebook_tracker)
{
ClosedTab *tab = g_slice_new0 (ClosedTab);
tab->url = g_strdup (ephy_web_view_get_address (web_view));
tab->position = position;
/* Takes the ownership of the tracker */
tab->notebook_tracker = notebook_tracker;
tab->state = webkit_web_view_get_session_state (WEBKIT_WEB_VIEW (web_view));
return tab;
}
void
ephy_session_undo_close_tab (EphySession *session)
{
EphyEmbed *embed, *new_tab;
WebKitWebView *web_view;
WebKitBackForwardList *bf_list;
WebKitBackForwardListItem *item;
ClosedTab *tab;
EphyWindow *window;
EphyNotebook *notebook;
EphyNewTabFlags flags = EPHY_NEW_TAB_JUMP;
g_return_if_fail (EPHY_IS_SESSION (session));
tab = g_queue_pop_head (session->closed_tabs);
if (tab == NULL)
return;
LOG ("UNDO CLOSE TAB: %s", tab->url);
notebook = closed_tab_get_notebook (tab);
if (notebook) {
if (tab->position > 0) {
/* Append in the n-th position. */
embed = EPHY_EMBED (gtk_notebook_get_nth_page (GTK_NOTEBOOK (notebook),
tab->position - 1));
flags |= EPHY_NEW_TAB_APPEND_AFTER;
} else {
/* Just prepend in the first position. */
embed = NULL;
flags |= EPHY_NEW_TAB_FIRST;
}
window = EPHY_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (notebook)));
new_tab = ephy_shell_new_tab (ephy_shell_get_default (),
window, embed,
flags);
} else {
window = ephy_window_new ();
new_tab = ephy_shell_new_tab (ephy_shell_get_default (),
window, NULL, flags);
notebook_tracker_set_notebook (tab->notebook_tracker,
EPHY_NOTEBOOK (ephy_window_get_notebook (window)));
}
web_view = WEBKIT_WEB_VIEW (ephy_embed_get_web_view (new_tab));
webkit_web_view_restore_session_state (web_view, tab->state);
bf_list = webkit_web_view_get_back_forward_list (web_view);
item = webkit_back_forward_list_get_current_item (bf_list);
if (item) {
webkit_web_view_go_to_back_forward_list_item (web_view, item);
} else {
ephy_web_view_load_url (ephy_embed_get_web_view (new_tab), tab->url);
}
gtk_widget_grab_focus (GTK_WIDGET (new_tab));
gtk_window_present (GTK_WINDOW (window));
closed_tab_free (tab);
if (g_queue_is_empty (session->closed_tabs))
g_object_notify_by_pspec (G_OBJECT (session), obj_properties[PROP_CAN_UNDO_TAB_CLOSED]);
}
static void
ephy_session_tab_closed (EphySession *session,
EphyNotebook *notebook,
EphyEmbed *embed,
gint position)
{
EphyWebView *view;
WebKitWebView *wk_view;
ClosedTab *tab;
view = ephy_embed_get_web_view (embed);
wk_view = WEBKIT_WEB_VIEW (view);
if (!webkit_web_view_can_go_back (wk_view) && !webkit_web_view_can_go_forward (wk_view) &&
(ephy_web_view_get_is_blank (view) || ephy_web_view_is_overview (view))) {
return;
}
if (g_queue_get_length (session->closed_tabs) == MAX_CLOSED_TABS) {
closed_tab_free (g_queue_pop_tail (session->closed_tabs));
}
tab = closed_tab_new (view, position,
ephy_session_ref_or_create_notebook_tracker (session, notebook));
g_queue_push_head (session->closed_tabs, tab);
if (g_queue_get_length (session->closed_tabs) == 1)
g_object_notify_by_pspec (G_OBJECT (session), obj_properties[PROP_CAN_UNDO_TAB_CLOSED]);
LOG ("Added: %s to the list (%d elements)",
ephy_web_view_get_address (view), g_queue_get_length (session->closed_tabs));
}
gboolean
ephy_session_get_can_undo_tab_closed (EphySession *session)
{
g_return_val_if_fail (EPHY_IS_SESSION (session), FALSE);
return g_queue_is_empty (session->closed_tabs) == FALSE;
}
static void
notebook_page_added_cb (GtkWidget *notebook,
EphyEmbed *embed,
guint position,
EphySession *session)
{
g_signal_connect (ephy_embed_get_web_view (embed), "load-changed",
G_CALLBACK (load_changed_cb), session);
}
static void
notebook_page_removed_cb (GtkWidget *notebook,
EphyEmbed *embed,
guint position,
EphySession *session)
{
ephy_session_save (session);
g_signal_handlers_disconnect_by_func
(ephy_embed_get_web_view (embed), G_CALLBACK (load_changed_cb),
session);
ephy_session_tab_closed (session, EPHY_NOTEBOOK (notebook), embed, position);
}
static void
notebook_page_reordered_cb (GtkWidget *notebook,
GtkWidget *tab,
guint position,
EphySession *session)
{
ephy_session_save (session);
}
static void
notebook_switch_page_cb (GtkNotebook *notebook,
GtkWidget *page,
guint page_num,
EphySession *session)
{
ephy_session_save (session);
}
static void
session_maybe_open_window (EphySession *session,
guint32 user_time)
{
EphyShell *shell = ephy_shell_get_default ();
/* FIXME: maybe just check for normal windows? */
if (ephy_shell_get_n_windows (shell) == 0) {
EphyWindow *window = ephy_window_new ();
EphyEmbed *embed;
embed = ephy_shell_new_tab_full (shell,
NULL /* title */,
NULL /* related view */,
window, NULL /* tab */,
0,
user_time);
ephy_web_view_load_homepage (ephy_embed_get_web_view (embed));
ephy_window_activate_location (window);
}
}
static void
window_added_cb (GtkApplication *application,
GtkWindow *window,
EphySession *session)
{
GtkWidget *notebook;
EphyWindow *ephy_window;
ephy_session_save (session);
if (!EPHY_IS_WINDOW (window))
return;
ephy_window = EPHY_WINDOW (window);
notebook = ephy_window_get_notebook (ephy_window);
g_signal_connect (notebook, "page-added",
G_CALLBACK (notebook_page_added_cb), session);
g_signal_connect (notebook, "page-removed",
G_CALLBACK (notebook_page_removed_cb), session);
g_signal_connect (notebook, "page-reordered",
G_CALLBACK (notebook_page_reordered_cb), session);
g_signal_connect_after (notebook, "switch-page",
G_CALLBACK (notebook_switch_page_cb), session);
/* Set unique identifier as role, so that on restore, the WM can
* place the window on the right workspace
*/
if (gtk_window_get_role (window) == NULL) {
/* I guess rand() is unique enough, otherwise we could use
* time + pid or something
*/
char *role;
role = g_strdup_printf ("epiphany-window-%x", rand ());
gtk_window_set_role (window, role);
g_free (role);
}
}
static void
window_removed_cb (GtkApplication *application,
GtkWindow *window,
EphySession *session)
{
ephy_session_save (session);
/* NOTE: since the window will be destroyed anyway, we don't need to
* disconnect our signal handlers from its components.
*/
}
/* Class implementation */
static void
ephy_session_init (EphySession *session)
{
EphyShell *shell;
LOG ("EphySession initialising");
session->closed_tabs = g_queue_new ();
shell = ephy_shell_get_default ();
g_signal_connect (shell, "window-added",
G_CALLBACK (window_added_cb), session);
g_signal_connect (shell, "window-removed",
G_CALLBACK (window_removed_cb), session);
}
static void
ephy_session_dispose (GObject *object)
{
EphySession *session = EPHY_SESSION (object);
LOG ("EphySession disposing");
g_queue_free_full (session->closed_tabs,
(GDestroyNotify)closed_tab_free);
G_OBJECT_CLASS (ephy_session_parent_class)->dispose (object);
}
static void
ephy_session_get_property (GObject *object,
guint property_id,
GValue *value,
GParamSpec *pspec)
{
EphySession *session = EPHY_SESSION (object);
switch (property_id) {
case PROP_CAN_UNDO_TAB_CLOSED:
g_value_set_boolean (value,
ephy_session_get_can_undo_tab_closed (session));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
}
}
static void
ephy_session_class_init (EphySessionClass *class)
{
GObjectClass *object_class = G_OBJECT_CLASS (class);
object_class->dispose = ephy_session_dispose;
object_class->get_property = ephy_session_get_property;
obj_properties[PROP_CAN_UNDO_TAB_CLOSED] =
g_param_spec_boolean ("can-undo-tab-closed",
"Can undo tab close",
"Session can undo a tab closure",
FALSE,
G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
g_object_class_install_properties (object_class, LAST_PROP, obj_properties);
}
/* Implementation */
void
ephy_session_close (EphySession *session)
{
EphyPrefsRestoreSessionPolicy policy;
g_return_if_fail (EPHY_IS_SESSION (session));
LOG ("ephy_session_close");
if (session->save_source_id) {
/* There's a save pending, cancel it and save the session now since
* after closing the session the saving is no longer allowed.
*/
g_source_remove (session->save_source_id);
session->save_source_id = 0;
}
if (session->closing)
return;
session->closing = TRUE;
policy = g_settings_get_enum (EPHY_SETTINGS_MAIN, EPHY_PREFS_RESTORE_SESSION_POLICY);
if (policy == EPHY_PREFS_RESTORE_SESSION_POLICY_ALWAYS) {
ephy_session_save_idle_cb (session);
} else {
session_delete (session);
}
session->dont_save = TRUE;
}
static void
get_window_geometry (GtkWindow *window,
GdkRectangle *rectangle)
{
gtk_window_get_size (window, &rectangle->width, &rectangle->height);
gtk_window_get_position (window, &rectangle->x, &rectangle->y);
}
typedef struct {
char *url;
char *title;
gboolean loading;
gboolean crashed;
WebKitWebViewSessionState *state;
} SessionTab;
static SessionTab *
session_tab_new (EphyEmbed *embed,
EphySession *session)
{
SessionTab *session_tab;
const char *address;
EphyWebView *web_view = ephy_embed_get_web_view (embed);
EphyWebViewErrorPage error_page = ephy_web_view_get_error_page (web_view);
session_tab = g_slice_new (SessionTab);
address = ephy_web_view_get_address (web_view);
/* Do not store ephy-about: URIs, they are not valid for loading. */
if (g_str_has_prefix (address, EPHY_ABOUT_SCHEME)) {
session_tab->url = g_strconcat ("about", address + EPHY_ABOUT_SCHEME_LEN, NULL);
} else if (g_str_equal (address, "about:blank")) {
/* EphyWebView address is NULL between load_uri() and WEBKIT_LOAD_STARTED,
* but WebKitWebView knows the pending API request URL, so use that instead of about:blank.
*/
session_tab->url = g_strdup (webkit_web_view_get_uri (WEBKIT_WEB_VIEW (web_view)));
} else {
session_tab->url = g_strdup (address);
}
session_tab->title = g_strdup (ephy_embed_get_title (embed));
session_tab->loading = (ephy_web_view_is_loading (web_view) &&
!ephy_embed_has_load_pending (embed) &&
!session->closing);
session_tab->crashed = (error_page == EPHY_WEB_VIEW_ERROR_PAGE_CRASH ||
error_page == EPHY_WEB_VIEW_ERROR_PROCESS_CRASH);
session_tab->state = webkit_web_view_get_session_state (WEBKIT_WEB_VIEW (web_view));
return session_tab;
}
static void
session_tab_free (SessionTab *tab)
{
g_free (tab->url);
g_free (tab->title);
g_clear_pointer (&tab->state, webkit_web_view_session_state_unref);
g_slice_free (SessionTab, tab);
}
typedef struct {
GdkRectangle geometry;
char *role;
GList *tabs;
gint active_tab;
} SessionWindow;
static SessionWindow *
session_window_new (EphyWindow *window,
EphySession *session)
{
SessionWindow *session_window;
GList *tabs, *l;
GtkNotebook *notebook;
tabs = ephy_embed_container_get_children (EPHY_EMBED_CONTAINER (window));
/* Do not save an empty EphyWindow.
* This only happens when the window was newly opened.
*/
if (!tabs) {
return NULL;
}
session_window = g_slice_new0 (SessionWindow);
get_window_geometry (GTK_WINDOW (window), &session_window->geometry);
session_window->role = g_strdup (gtk_window_get_role (GTK_WINDOW (window)));
for (l = tabs; l != NULL; l = l->next) {
SessionTab *tab;
tab = session_tab_new (EPHY_EMBED (l->data), session);
session_window->tabs = g_list_prepend (session_window->tabs, tab);
}
g_list_free (tabs);
session_window->tabs = g_list_reverse (session_window->tabs);
notebook = GTK_NOTEBOOK (ephy_window_get_notebook (window));
session_window->active_tab = gtk_notebook_get_current_page (notebook);
return session_window;
}
static void
session_window_free (SessionWindow *session_window)
{
g_free (session_window->role);
g_list_free_full (session_window->tabs, (GDestroyNotify)session_tab_free);
g_slice_free (SessionWindow, session_window);
}
typedef struct {
EphySession *session;
GList *windows;
} SaveData;
static SaveData *
save_data_new (EphySession *session)
{
SaveData *data;
EphyShell *shell = ephy_shell_get_default ();
GList *windows, *w;
data = g_slice_new0 (SaveData);
data->session = g_object_ref (session);
windows = gtk_application_get_windows (GTK_APPLICATION (shell));
for (w = windows; w != NULL; w = w->next) {
SessionWindow *session_window;
session_window = session_window_new (EPHY_WINDOW (w->data), session);
if (session_window)
data->windows = g_list_prepend (data->windows, session_window);
}
data->windows = g_list_reverse (data->windows);
return data;
}
static void
save_data_free (SaveData *data)
{
g_list_free_full (data->windows, (GDestroyNotify)session_window_free);
g_object_unref (data->session);
g_slice_free (SaveData, data);
}
static int
write_tab (xmlTextWriterPtr writer,
SessionTab *tab)
{
int ret;
ret = xmlTextWriterStartElement (writer, (xmlChar *)"embed");
if (ret < 0) return ret;
ret = xmlTextWriterWriteAttribute (writer, (xmlChar *)"url",
(const xmlChar *)tab->url);
if (ret < 0) return ret;
ret = xmlTextWriterWriteAttribute (writer, (xmlChar *)"title",
(const xmlChar *)tab->title);
if (ret < 0) return ret;
if (tab->loading) {
ret = xmlTextWriterWriteAttribute (writer,
(const xmlChar *)"loading",
(const xmlChar *)"true");
if (ret < 0) return ret;
}
if (tab->crashed) {
ret = xmlTextWriterWriteAttribute (writer,
(const xmlChar *)"crashed",
(const xmlChar *)"true");
if (ret < 0) return ret;
}
if (tab->state) {
GBytes *bytes;
bytes = webkit_web_view_session_state_serialize (tab->state);
if (bytes) {
gchar *base64;
gconstpointer data;
gsize data_length;
data = g_bytes_get_data (bytes, &data_length);
base64 = g_base64_encode (data, data_length);
ret = xmlTextWriterWriteAttribute (writer,
(const xmlChar *)"history",
(const xmlChar *)base64);
g_free (base64);
g_bytes_unref (bytes);
}
}
ret = xmlTextWriterEndElement (writer); /* embed */
return ret;
}
static int
write_window_geometry (xmlTextWriterPtr writer,
GdkRectangle *geometry)
{
int ret;
/* set window properties */
ret = xmlTextWriterWriteFormatAttribute (writer, (const xmlChar *)"x", "%d",
geometry->x);
if (ret < 0) return ret;
ret = xmlTextWriterWriteFormatAttribute (writer, (const xmlChar *)"y", "%d",
geometry->y);
if (ret < 0) return ret;
ret = xmlTextWriterWriteFormatAttribute (writer, (const xmlChar *)"width", "%d",
geometry->width);
if (ret < 0) return ret;
ret = xmlTextWriterWriteFormatAttribute (writer, (const xmlChar *)"height", "%d",
geometry->height);
return ret;
}
static int
write_ephy_window (xmlTextWriterPtr writer,
SessionWindow *window)
{
GList *l;
int ret;
ret = xmlTextWriterStartElement (writer, (xmlChar *)"window");
if (ret < 0) return ret;
ret = write_window_geometry (writer, &window->geometry);
if (ret < 0) return ret;
ret = xmlTextWriterWriteFormatAttribute (writer, (const xmlChar *)"active-tab", "%d",
window->active_tab);
if (ret < 0) return ret;
if (window->role != NULL) {
ret = xmlTextWriterWriteAttribute (writer,
(const xmlChar *)"role",
(const xmlChar *)window->role);
if (ret < 0) return ret;
}
for (l = window->tabs; l != NULL; l = l->next) {
SessionTab *tab = (SessionTab *)l->data;
ret = write_tab (writer, tab);
if (ret < 0) break;
}
if (ret < 0) return ret;
ret = xmlTextWriterEndElement (writer); /* window */
return ret;
}
static void
save_session_in_thread_finished_cb (GObject *source_object,
GAsyncResult *res,
gpointer user_data)
{
g_application_release (G_APPLICATION (ephy_shell_get_default ()));
}
static void
save_session_sync (GTask *task,
gpointer source_object,
gpointer task_data,
GCancellable *cancellable)
{
SaveData *data = (SaveData *)g_task_get_task_data (task);
xmlBufferPtr buffer;
xmlTextWriterPtr writer;
GList *w;
int ret = -1;
buffer = xmlBufferCreate ();
writer = xmlNewTextWriterMemory (buffer, 0);
if (writer == NULL) goto out;
ret = xmlTextWriterSetIndent (writer, 1);
if (ret < 0) goto out;
ret = xmlTextWriterSetIndentString (writer, (const xmlChar *)" ");
if (ret < 0) goto out;
START_PROFILER ("Saving session")
ret = xmlTextWriterStartDocument (writer, "1.0", NULL, NULL);
if (ret < 0) goto out;
/* create and set the root node for the session */
ret = xmlTextWriterStartElement (writer, (const xmlChar *)"session");
if (ret < 0) goto out;
/* iterate through all the windows */
for (w = data->windows; w != NULL && ret >= 0; w = w->next) {
ret = write_ephy_window (writer, (SessionWindow *)w->data);
}
if (ret < 0) goto out;
ret = xmlTextWriterEndElement (writer); /* session */
if (ret < 0) goto out;
ret = xmlTextWriterEndDocument (writer);
out:
if (writer)
xmlFreeTextWriter (writer);
if (ret >= 0 && !g_cancellable_is_cancelled (cancellable)) {
GError *error = NULL;
GFile *session_file;
session_file = get_session_file (SESSION_STATE);
if (!g_file_replace_contents (session_file,
(const char *)buffer->content,
buffer->use,
NULL, TRUE, 0, NULL,
cancellable, &error)) {
if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
g_warning ("Error saving session: %s", error->message);
}
g_error_free (error);
}
g_object_unref (session_file);
}
xmlBufferFree (buffer);
g_task_return_boolean (task, TRUE);
STOP_PROFILER ("Saving session")
}
static EphySession *
ephy_session_save_idle_started (EphySession *session)
{
g_application_hold (G_APPLICATION (ephy_shell_get_default ()));
return g_object_ref (session);
}
static void
ephy_session_save_idle_finished (EphySession *session)
{
g_application_release (G_APPLICATION (ephy_shell_get_default ()));
g_object_unref (session);
}
static gboolean
ephy_session_save_idle_cb (EphySession *session)
{
EphyShell *shell = ephy_shell_get_default ();
SaveData *data;
GTask *task;
session->save_source_id = 0;
if (session->save_cancellable) {
g_cancellable_cancel (session->save_cancellable);
g_object_unref (session->save_cancellable);
session->save_cancellable = NULL;
}
LOG ("ephy_sesion_save");
if (ephy_shell_get_n_windows (shell) == 0) {
session_delete (session);
return G_SOURCE_REMOVE;
}
g_application_hold (G_APPLICATION (ephy_shell_get_default ()));
session->save_cancellable = g_cancellable_new ();
data = save_data_new (session);
task = g_task_new (session, session->save_cancellable,
save_session_in_thread_finished_cb, NULL);
g_task_set_task_data (task, data, (GDestroyNotify)save_data_free);
g_task_run_in_thread (task, save_session_sync);
g_object_unref (task);
return G_SOURCE_REMOVE;
}
void
ephy_session_save (EphySession *session)
{
EphyPrefsRestoreSessionPolicy policy;
g_return_if_fail (EPHY_IS_SESSION (session));
if (session->save_source_id) {
return;
}
if (session->dont_save) {
return;
}
policy = g_settings_get_enum (EPHY_SETTINGS_MAIN, EPHY_PREFS_RESTORE_SESSION_POLICY);
if (policy == EPHY_PREFS_RESTORE_SESSION_POLICY_NEVER) {
return;
}
session->save_source_id = g_timeout_add_seconds_full (G_PRIORITY_DEFAULT_IDLE, 1,
(GSourceFunc)ephy_session_save_idle_cb,
ephy_session_save_idle_started (session),
(GDestroyNotify)ephy_session_save_idle_finished);
}
static void
confirm_before_recover (EphyWindow *window, const char *url, const char *title)
{
EphyEmbed *embed;
embed = ephy_shell_new_tab_full (ephy_shell_get_default (),
title, NULL,
window, NULL,
EPHY_NEW_TAB_APPEND_LAST,
0);
ephy_web_view_load_error_page (ephy_embed_get_web_view (embed), url,
EPHY_WEB_VIEW_ERROR_PAGE_CRASH, NULL);
}
static void
restore_geometry (GtkWindow *window,
GdkRectangle *geometry)
{
if (geometry->x >= 0 && geometry->y >= 0) {
gtk_window_move (window, geometry->x, geometry->y);
}
if (geometry->width > 0 && geometry->height > 0) {
gtk_window_set_default_size (window, geometry->width, geometry->height);
}
}
typedef struct {
EphySession *session;
guint32 user_time;
EphyWindow *window;
gboolean is_first_window;
gint active_tab;
gboolean is_first_tab;
} SessionParserContext;
static SessionParserContext *
session_parser_context_new (EphySession *session,
guint32 user_time)
{
SessionParserContext *context;
context = g_slice_new0 (SessionParserContext);
context->session = g_object_ref (session);
context->user_time = user_time;
context->is_first_window = TRUE;
return context;
}
static void
session_parser_context_free (SessionParserContext *context)
{
g_object_unref (context->session);
g_slice_free (SessionParserContext, context);
}
static void
session_parse_window (SessionParserContext *context,
const gchar **names,
const gchar **values)
{
GdkRectangle geometry = { -1, -1, 0, 0 };
guint i;
context->window = ephy_window_new ();
for (i = 0; names[i]; i++) {
gulong int_value;
if (strcmp (names[i], "x") == 0) {
ephy_string_to_int (values[i], &int_value);
geometry.x = int_value;
} else if (strcmp (names[i], "y") == 0) {
ephy_string_to_int (values[i], &int_value);
geometry.y = int_value;
} else if (strcmp (names[i], "width") == 0) {
ephy_string_to_int (values[i], &int_value);
geometry.width = int_value;
} else if (strcmp (names[i], "height") == 0) {
ephy_string_to_int (values[i], &int_value);
geometry.height = int_value;
} else if (strcmp (names[i], "role") == 0) {
gtk_window_set_role (GTK_WINDOW (context->window), values[i]);
} else if (strcmp (names[i], "active-tab") == 0) {
ephy_string_to_int (values[i], &int_value);
context->active_tab = int_value;
}
}
restore_geometry (GTK_WINDOW (context->window), &geometry);
}
static void
session_parse_embed (SessionParserContext *context,
const gchar **names,
const gchar **values)
{
const char *url = NULL;
const char *title = NULL;
const char *history = NULL;
gboolean was_loading = FALSE;
gboolean crashed = FALSE;
gboolean is_blank_page = FALSE;
guint i;
for (i = 0; names[i]; i++) {
if (strcmp (names[i], "url") == 0) {
url = values[i];
is_blank_page = (strcmp (url, "about:blank") == 0 ||
strcmp (url, "about:overview") == 0);
} else if (strcmp (names[i], "title") == 0) {
title = values[i];
} else if (strcmp (names[i], "loading") == 0) {
was_loading = strcmp (values[i], "true") == 0;
} else if (strcmp (names[i], "crashed") == 0) {
crashed = strcmp (values[i], "true") == 0;
} else if (strcmp (names[i], "history") == 0) {
history = values[i];
}
}
/* In the case that crash happens before we receive the URL from the server,
* this will open an about:blank tab.
* See http://bugzilla.gnome.org/show_bug.cgi?id=591294
* Otherwise, if the web was fully loaded, it is reloaded again.
*/
if ((!was_loading || is_blank_page) && !crashed) {
EphyNewTabFlags flags;
EphyEmbed *embed;
EphyWebView *web_view;
gboolean delay_loading;
WebKitWebViewSessionState *state = NULL;
delay_loading = g_settings_get_boolean (EPHY_SETTINGS_MAIN,
EPHY_PREFS_RESTORE_SESSION_DELAYING_LOADS);
flags = EPHY_NEW_TAB_APPEND_LAST;
embed = ephy_shell_new_tab_full (ephy_shell_get_default (),
title, NULL,
context->window, NULL, flags,
0);
web_view = ephy_embed_get_web_view (embed);
if (history) {
guchar *data;
gsize data_length;
GBytes *history_data;
data = g_base64_decode (history, &data_length);
history_data = g_bytes_new_take (data, data_length);
state = webkit_web_view_session_state_new (history_data);
g_bytes_unref (history_data);
}
if (delay_loading) {
WebKitURIRequest *request = webkit_uri_request_new (url);
ephy_embed_set_delayed_load_request (embed, request, state);
ephy_web_view_set_placeholder (web_view, url, title);
g_object_unref (request);
} else {
WebKitBackForwardList *bf_list;
WebKitBackForwardListItem *item;
if (state) {
webkit_web_view_restore_session_state (WEBKIT_WEB_VIEW (web_view), state);
}
bf_list = webkit_web_view_get_back_forward_list (WEBKIT_WEB_VIEW (web_view));
item = webkit_back_forward_list_get_current_item (bf_list);
if (item) {
webkit_web_view_go_to_back_forward_list_item (WEBKIT_WEB_VIEW (web_view), item);
} else {
ephy_web_view_load_url (web_view, url);
}
}
if (state) {
webkit_web_view_session_state_unref (state);
}
} else if (url && (was_loading || crashed)) {
/* This page was loading during a UI process crash
* (was_loading == TRUE) or a web process crash
* (crashed == TRUE) and might make Epiphany crash again.
*/
confirm_before_recover (context->window, url, title);
}
}
static void
session_start_element (GMarkupParseContext *ctx,
const gchar *element_name,
const gchar **names,
const gchar **values,
gpointer user_data,
GError **error)
{
SessionParserContext *context = (SessionParserContext *)user_data;
if (strcmp (element_name, "window") == 0) {
session_parse_window (context, names, values);
context->is_first_tab = TRUE;
} else if (strcmp (element_name, "embed") == 0) {
session_parse_embed (context, names, values);
}
}
static void
session_end_element (GMarkupParseContext *ctx,
const gchar *element_name,
gpointer user_data,
GError **error)
{
SessionParserContext *context = (SessionParserContext *)user_data;
if (strcmp (element_name, "window") == 0) {
GtkWidget *notebook;
EphyEmbedShell *shell = ephy_embed_shell_get_default ();
notebook = ephy_window_get_notebook (context->window);
gtk_notebook_set_current_page (GTK_NOTEBOOK (notebook), context->active_tab);
if (ephy_embed_shell_get_mode (ephy_embed_shell_get_default ()) != EPHY_EMBED_SHELL_MODE_TEST) {
EphyEmbed *active_child;
active_child = ephy_embed_container_get_active_child (EPHY_EMBED_CONTAINER (context->window));
gtk_widget_grab_focus (GTK_WIDGET (active_child));
gtk_widget_show (GTK_WIDGET (context->window));
}
ephy_embed_shell_restored_window (shell);
context->window = NULL;
context->active_tab = 0;
context->is_first_window = FALSE;
} else if (strcmp (element_name, "embed") == 0) {
context->is_first_tab = FALSE;
}
}
static const GMarkupParser session_parser = {
session_start_element,
session_end_element,
NULL,
NULL,
NULL
};
typedef struct {
EphyShell *shell;
GMarkupParseContext *parser;
char buffer[1024];
} LoadFromStreamAsyncData;
static LoadFromStreamAsyncData *
load_from_stream_async_data_new (GMarkupParseContext *parser)
{
LoadFromStreamAsyncData *data;
data = g_slice_new (LoadFromStreamAsyncData);
data->shell = g_object_ref (ephy_shell_get_default ());
data->parser = parser;
return data;
}
static void
load_from_stream_async_data_free (LoadFromStreamAsyncData *data)
{
g_object_unref (data->shell);
g_markup_parse_context_free (data->parser);
g_slice_free (LoadFromStreamAsyncData, data);
}
static void
load_stream_complete (GTask *task)
{
EphySession *session;
g_task_return_boolean (task, TRUE);
session = EPHY_SESSION (g_task_get_source_object (task));
session->dont_save = FALSE;
ephy_session_save (session);
g_object_unref (task);
g_application_release (G_APPLICATION (ephy_shell_get_default ()));
}
static void
load_stream_complete_error (GTask *task,
GError *error)
{
EphySession *session;
LoadFromStreamAsyncData *data;
SessionParserContext *context;
g_task_return_error (task, error);
session = EPHY_SESSION (g_task_get_source_object (task));
session->dont_save = FALSE;
/* If the session fails to load for whatever reason,
* delete the file and open an empty window.
*/
session_delete (session);
data = g_task_get_task_data (task);
context = (SessionParserContext *)g_markup_parse_context_get_user_data (data->parser);
session_maybe_open_window (session, context->user_time);
g_object_unref (task);
g_application_release (G_APPLICATION (ephy_shell_get_default ()));
}
static void
load_stream_read_cb (GObject *object,
GAsyncResult *result,
gpointer user_data)
{
GInputStream *stream = G_INPUT_STREAM (object);
GTask *task = G_TASK (user_data);
LoadFromStreamAsyncData *data;
gssize bytes_read;
GError *error = NULL;
bytes_read = g_input_stream_read_finish (stream, result, &error);
if (bytes_read < 0) {
load_stream_complete_error (task, error);
return;
}
data = g_task_get_task_data (task);
if (bytes_read == 0) {
if (!g_markup_parse_context_end_parse (data->parser, &error)) {
load_stream_complete_error (task, error);
} else {
load_stream_complete (task);
}
return;
}
if (!g_markup_parse_context_parse (data->parser, data->buffer, bytes_read, &error)) {
load_stream_complete_error (task, error);
return;
}
g_input_stream_read_async (stream, data->buffer, sizeof (data->buffer),
g_task_get_priority (task),
g_task_get_cancellable (task),
load_stream_read_cb, task);
}
/**
* ephy_session_load_from_stream:
* @session: an #EphySession
* @stream: a #GInputStream to read the session data from
* @user_time: a user time, or 0
* @cancellable: (allow-none): optional #GCancellable object, or %NULL
* @callback: (scope async): a #GAsyncReadyCallback to call when the
* request is satisfied
* @user_data: (closure): the data to pass to callback function
*
* Asynchronously loads the session reading the session data from @stream,
* restoring windows and their state.
*
* When the operation is finished, @callback will be called. You can
* then call ephy_session_load_from_stream_finish() to get the result of
* the operation.
**/
void
ephy_session_load_from_stream (EphySession *session,
GInputStream *stream,
guint32 user_time,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
GTask *task;
SessionParserContext *context;
GMarkupParseContext *parser;
LoadFromStreamAsyncData *data;
g_return_if_fail (EPHY_IS_SESSION (session));
g_return_if_fail (G_IS_INPUT_STREAM (stream));
g_application_hold (G_APPLICATION (ephy_shell_get_default ()));
session->dont_save = TRUE;
task = g_task_new (session, cancellable, callback, user_data);
/* Use a priority lower than drawing events (HIGH_IDLE + 20) to make sure
* the main window is shown as soon as possible at startup
*/
g_task_set_priority (task, G_PRIORITY_HIGH_IDLE + 30);
context = session_parser_context_new (session, user_time);
parser = g_markup_parse_context_new (&session_parser, 0, context, (GDestroyNotify)session_parser_context_free);
data = load_from_stream_async_data_new (parser);
g_task_set_task_data (task, data, (GDestroyNotify)load_from_stream_async_data_free);
g_input_stream_read_async (stream, data->buffer, sizeof (data->buffer),
g_task_get_priority (task), cancellable,
load_stream_read_cb, task);
}
/**
* ephy_session_load_from_stream_finish:
* @session: an #EphySession
* @result: a #GAsyncResult
* @error: a #GError
*
* Finishes an async session load operation started with
* ephy_session_load_from_stream().
*
* Returns: %TRUE if at least a window has been opened
**/
gboolean
ephy_session_load_from_stream_finish (EphySession *session,
GAsyncResult *result,
GError **error)
{
g_return_val_if_fail (g_task_is_valid (result, session), FALSE);
return g_task_propagate_boolean (G_TASK (result), error);
}
typedef struct {
guint32 user_time;
} LoadAsyncData;
static LoadAsyncData *
load_async_data_new (guint32 user_time)
{
LoadAsyncData *data;
data = g_slice_new (LoadAsyncData);
data->user_time = user_time;
return data;
}
static void
load_async_data_free (LoadAsyncData *data)
{
g_slice_free (LoadAsyncData, data);
}
static void
load_from_stream_cb (GObject *object,
GAsyncResult *result,
gpointer user_data)
{
EphySession *session = EPHY_SESSION (object);
GTask *task = G_TASK (user_data);
GError *error = NULL;
if (!ephy_session_load_from_stream_finish (session, result, &error)) {
g_task_return_error (task, error);
} else {
g_task_return_boolean (task, TRUE);
}
g_object_unref (task);
}
static void
session_read_cb (GObject *object,
GAsyncResult *result,
gpointer user_data)
{
GFileInputStream *stream;
GTask *task = G_TASK (user_data);
GError *error = NULL;
stream = g_file_read_finish (G_FILE (object), result, &error);
if (stream) {
EphySession *session;
LoadAsyncData *data;
session = EPHY_SESSION (g_task_get_source_object (task));
data = g_task_get_task_data (task);
ephy_session_load_from_stream (session, G_INPUT_STREAM (stream), data->user_time,
g_task_get_cancellable (task), load_from_stream_cb, task);
g_object_unref (stream);
} else {
g_task_return_error (task, error);
g_object_unref (task);
}
g_application_release (G_APPLICATION (ephy_shell_get_default ()));
}
/**
* ephy_session_load:
* @session: an #EphySession
* @filename: the path of the source file
* @user_time: a user time, or 0
* @cancellable: (allow-none): optional #GCancellable object, or %NULL
* @callback: (scope async): a #GAsyncReadyCallback to call when the
* request is satisfied
* @user_data: (closure): the data to pass to callback function
*
* Asynchronously loads the session reading the session data from @filename,
* restoring windows and their state.
*
* When the operation is finished, @callback will be called. You can
* then call ephy_session_load_finish() to get the result of
* the operation.
**/
void
ephy_session_load (EphySession *session,
const char *filename,
guint32 user_time,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
GFile *save_to_file;
GTask *task;
LoadAsyncData *data;
g_return_if_fail (EPHY_IS_SESSION (session));
g_return_if_fail (filename);
LOG ("ephy_sesion_load %s", filename);
g_application_hold (G_APPLICATION (ephy_shell_get_default ()));
task = g_task_new (session, cancellable, callback, user_data);
/* Use a priority lower than drawing events (HIGH_IDLE + 20) to make sure
* the main window is shown as soon as possible at startup
*/
g_task_set_priority (task, G_PRIORITY_HIGH_IDLE + 30);
save_to_file = get_session_file (filename);
data = load_async_data_new (user_time);
g_task_set_task_data (task, data, (GDestroyNotify)load_async_data_free);
g_file_read_async (save_to_file, g_task_get_priority (task), cancellable, session_read_cb, task);
g_object_unref (save_to_file);
}
/**
* ephy_session_load_finish:
* @session: an #EphySession
* @result: a #GAsyncResult
* @error: a #GError
*
* Finishes an async session load operation started with
* ephy_session_load().
*
* Returns: %TRUE if at least a window has been opened
**/
gboolean
ephy_session_load_finish (EphySession *session,
GAsyncResult *result,
GError **error)
{
g_return_val_if_fail (g_task_is_valid (result, session), FALSE);
return g_task_propagate_boolean (G_TASK (result), error);
}
static gboolean
session_state_file_exists (EphySession *session)
{
GFile *saved_session_file;
char *saved_session_file_path;
gboolean retval;
saved_session_file = get_session_file (SESSION_STATE);
saved_session_file_path = g_file_get_path (saved_session_file);
g_object_unref (saved_session_file);
retval = g_file_test (saved_session_file_path, G_FILE_TEST_EXISTS);
g_free (saved_session_file_path);
return retval;
}
static void
session_resumed_cb (GObject *object,
GAsyncResult *result,
gpointer user_data)
{
EphySession *session = EPHY_SESSION (object);
GTask *task = G_TASK (user_data);
GError *error = NULL;
if (!ephy_session_load_finish (session, result, &error)) {
g_task_return_error (task, error);
} else {
g_task_return_boolean (task, TRUE);
}
g_object_unref (task);
}
void
ephy_session_resume (EphySession *session,
guint32 user_time,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
GTask *task;
gboolean has_session_state;
EphyPrefsRestoreSessionPolicy policy;
EphyShell *shell;
LOG ("ephy_session_resume");
task = g_task_new (session, cancellable, callback, user_data);
has_session_state = session_state_file_exists (session);
policy = g_settings_get_enum (EPHY_SETTINGS_MAIN,
EPHY_PREFS_RESTORE_SESSION_POLICY);
shell = ephy_shell_get_default ();
if (has_session_state == FALSE ||
policy == EPHY_PREFS_RESTORE_SESSION_POLICY_NEVER) {
/* If we are auto-resuming, and we never want to
* restore the session, clobber the session state
* file. */
if (policy == EPHY_PREFS_RESTORE_SESSION_POLICY_NEVER)
session_delete (session);
session_maybe_open_window (session, user_time);
} else if (ephy_shell_get_n_windows (shell) == 0) {
ephy_session_load (session, SESSION_STATE, user_time, cancellable,
session_resumed_cb, task);
return;
}
g_task_return_boolean (task, TRUE);
g_object_unref (task);
}
gboolean
ephy_session_resume_finish (EphySession *session,
GAsyncResult *result,
GError **error)
{
g_return_val_if_fail (g_task_is_valid (result, session), FALSE);
return g_task_propagate_boolean (G_TASK (result), error);
}
void
ephy_session_clear (EphySession *session)
{
EphyShell *shell;
GList *windows, *p;
g_return_if_fail (EPHY_IS_SESSION (session));
shell = ephy_shell_get_default ();
windows = g_list_copy (gtk_application_get_windows (GTK_APPLICATION (shell)));
for (p = windows; p; p = p->next)
gtk_widget_destroy (GTK_WIDGET (p->data));
g_list_free (windows);
g_queue_foreach (session->closed_tabs,
(GFunc)closed_tab_free, NULL);
g_queue_clear (session->closed_tabs);
ephy_session_save (session);
}