/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/*
* Copyright © 2012 Igalia S.L.
*
* This file is part of Epiphany.
*
* Epiphany 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 3 of the License, or
* (at your option) any later version.
*
* Epiphany 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 Epiphany. If not, see .
*/
#include "config.h"
#include "ephy-snapshot-service.h"
#include "ephy-file-helpers.h"
#include
#include
#include
#include
#include
#include
#include
struct _EphySnapshotService {
GObject parent_instance;
GHashTable *cache;
};
G_DEFINE_TYPE (EphySnapshotService, ephy_snapshot_service, G_TYPE_OBJECT)
typedef enum {
SNAPSHOT_STALE,
SNAPSHOT_FRESH
} EphySnapshotFreshness;
typedef struct {
char *path;
EphySnapshotFreshness freshness;
} SnapshotPathCachedData;
static void
snapshot_path_cached_data_free (SnapshotPathCachedData *data)
{
g_free (data->path);
g_free (data);
}
static void
ephy_snapshot_service_class_init (EphySnapshotServiceClass *klass)
{
}
static void
ephy_snapshot_service_init (EphySnapshotService *self)
{
self->cache = g_hash_table_new_full (g_str_hash, g_str_equal,
(GDestroyNotify)g_free,
(GDestroyNotify)snapshot_path_cached_data_free);
}
static char *
thumbnail_filename (const char *uri)
{
GChecksum *checksum;
guint8 digest[16];
gsize digest_len = sizeof (digest);
char *file;
checksum = g_checksum_new (G_CHECKSUM_MD5);
g_checksum_update (checksum, (const guchar *)uri, strlen (uri));
g_checksum_get_digest (checksum, digest, &digest_len);
g_assert (digest_len == 16);
file = g_strconcat (g_checksum_get_string (checksum), ".png", NULL);
g_checksum_free (checksum);
return file;
}
static gboolean
thumbnail_is_valid (GdkPixbuf *pixbuf,
const char *uri)
{
const char *thumb_uri;
thumb_uri = gdk_pixbuf_get_option (pixbuf, "tEXt::Thumb::URI");
if (g_strcmp0 (uri, thumb_uri) != 0)
return FALSE;
return TRUE;
}
static gboolean
validate_thumbnail_path (const char *path,
const char *uri)
{
GdkPixbuf *pixbuf;
pixbuf = gdk_pixbuf_new_from_file (path, NULL);
if (pixbuf == NULL || !thumbnail_is_valid (pixbuf, uri))
return FALSE;
g_object_unref (pixbuf);
return TRUE;
}
static char *
thumbnail_directory (void)
{
return g_build_filename (ephy_cache_dir (),
"thumbnails",
NULL);
}
static char *
thumbnail_path (const char *uri)
{
char *path, *file, *dir;
dir = thumbnail_directory ();
file = thumbnail_filename (uri);
path = g_build_filename (dir, file, NULL);
g_free (dir);
g_free (file);
return path;
}
static gboolean
save_thumbnail (GdkPixbuf *pixbuf,
const char *uri)
{
char *path;
char *dirname;
char *tmp_path = NULL;
int tmp_fd;
gboolean ret = FALSE;
GError *error = NULL;
const char *width, *height;
if (pixbuf == NULL)
return FALSE;
path = thumbnail_path (uri);
dirname = g_path_get_dirname (path);
if (g_mkdir_with_parents (dirname, 0700) != 0)
goto out;
tmp_path = g_strconcat (path, ".XXXXXX", NULL);
tmp_fd = g_mkstemp (tmp_path);
if (tmp_fd == -1)
goto out;
close (tmp_fd);
width = gdk_pixbuf_get_option (pixbuf, "tEXt::Thumb::Image::Width");
height = gdk_pixbuf_get_option (pixbuf, "tEXt::Thumb::Image::Height");
error = NULL;
if (width != NULL && height != NULL)
ret = gdk_pixbuf_save (pixbuf,
tmp_path,
"png", &error,
"tEXt::Thumb::Image::Width", width,
"tEXt::Thumb::Image::Height", height,
"tEXt::Thumb::URI", uri,
"tEXt::Software", "GNOME::Epiphany::ThumbnailFactory",
NULL);
else
ret = gdk_pixbuf_save (pixbuf,
tmp_path,
"png", &error,
"tEXt::Thumb::URI", uri,
"tEXt::Software", "GNOME::Epiphany::ThumbnailFactory",
NULL);
if (!ret)
goto out;
chmod (tmp_path, 0600);
rename (tmp_path, path);
out:
if (error != NULL) {
g_warning ("Failed to create thumbnail %s: %s", tmp_path, error->message);
g_error_free (error);
}
if (tmp_path != NULL)
unlink (tmp_path);
g_free (path);
g_free (tmp_path);
g_free (dirname);
return ret;
}
static GdkPixbuf *
ephy_snapshot_service_prepare_snapshot (cairo_surface_t *surface)
{
GdkPixbuf *snapshot, *scaled;
int orig_width, orig_height;
orig_width = cairo_image_surface_get_width (surface);
orig_height = cairo_image_surface_get_height (surface);
if (orig_width < EPHY_THUMBNAIL_WIDTH ||
orig_height < EPHY_THUMBNAIL_HEIGHT) {
snapshot = gdk_pixbuf_get_from_surface (surface,
0, 0,
orig_width, orig_height);
scaled = gdk_pixbuf_scale_simple (snapshot,
EPHY_THUMBNAIL_WIDTH,
EPHY_THUMBNAIL_HEIGHT,
GDK_INTERP_TILES);
} else {
snapshot = gdk_pixbuf_get_from_surface (surface, 0, 0, orig_width, orig_height);
scaled = gdk_pixbuf_scale_simple (snapshot,
EPHY_THUMBNAIL_WIDTH,
EPHY_THUMBNAIL_HEIGHT,
GDK_INTERP_BILINEAR);
}
g_object_unref (snapshot);
return scaled;
}
typedef struct {
EphySnapshotService *service;
GdkPixbuf *snapshot;
WebKitWebView *web_view;
char *url;
} SnapshotAsyncData;
static SnapshotAsyncData *
snapshot_async_data_new (EphySnapshotService *service,
GdkPixbuf *snapshot,
WebKitWebView *web_view,
const char *url)
{
SnapshotAsyncData *data;
data = g_new0 (SnapshotAsyncData, 1);
data->service = g_object_ref (service);
data->snapshot = snapshot ? g_object_ref (snapshot) : NULL;
data->web_view = web_view;
data->url = g_strdup (url);
if (web_view)
g_object_add_weak_pointer (G_OBJECT (web_view), (gpointer *)&data->web_view);
return data;
}
static SnapshotAsyncData *
snapshot_async_data_copy (SnapshotAsyncData *data)
{
SnapshotAsyncData *copy = snapshot_async_data_new (data->service,
data->snapshot,
data->web_view,
data->url);
return copy;
}
static void
snapshot_async_data_free (SnapshotAsyncData *data)
{
g_clear_object (&data->service);
g_clear_object (&data->snapshot);
if (data->web_view)
g_object_remove_weak_pointer (G_OBJECT (data->web_view), (gpointer *)&data->web_view);
g_free (data->url);
g_free (data);
}
typedef struct {
GHashTable *cache;
char *url;
SnapshotPathCachedData *data;
} CacheData;
static gboolean
idle_cache_snapshot_path (gpointer user_data)
{
CacheData *data = (CacheData *)user_data;
g_hash_table_insert (data->cache, data->url, data->data);
g_hash_table_unref (data->cache);
g_free (data);
return G_SOURCE_REMOVE;
}
static void
cache_snapshot_data_in_idle (EphySnapshotService *service,
const char *url,
const char *path,
EphySnapshotFreshness freshness)
{
CacheData *data;
data = g_new (CacheData, 1);
data->cache = g_hash_table_ref (service->cache);
data->url = g_strdup (url);
data->data = g_new (SnapshotPathCachedData, 1);
data->data->path = g_strdup (path);
data->data->freshness = freshness;
g_idle_add (idle_cache_snapshot_path, data);
}
static void
save_snapshot_thread (GTask *task,
EphySnapshotService *service,
SnapshotAsyncData *data,
GCancellable *cancellable)
{
char *path;
save_thumbnail (data->snapshot, data->url);
path = thumbnail_path (data->url);
cache_snapshot_data_in_idle (service, data->url, path, SNAPSHOT_FRESH);
g_task_return_pointer (task, path, g_free);
}
static void
ephy_snapshot_service_save_snapshot_async (EphySnapshotService *service,
GdkPixbuf *snapshot,
const char *url,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
GTask *task;
g_assert (EPHY_IS_SNAPSHOT_SERVICE (service));
g_assert (GDK_IS_PIXBUF (snapshot));
g_assert (url != NULL);
task = g_task_new (service, cancellable, callback, user_data);
g_task_set_priority (task, G_PRIORITY_LOW);
g_task_set_task_data (task,
snapshot_async_data_new (service, snapshot, NULL, url),
(GDestroyNotify)snapshot_async_data_free);
g_task_run_in_thread (task, (GTaskThreadFunc)save_snapshot_thread);
g_object_unref (task);
}
static char *
ephy_snapshot_service_save_snapshot_finish (EphySnapshotService *service,
GAsyncResult *result,
GError **error)
{
g_assert (g_task_is_valid (result, service));
return g_task_propagate_pointer (G_TASK (result), error);
}
static void
snapshot_saved (EphySnapshotService *service,
GAsyncResult *result,
GTask *task)
{
char *path;
path = ephy_snapshot_service_save_snapshot_finish (service, result, NULL);
g_task_return_pointer (task, path, g_free);
g_object_unref (task);
}
static void
save_snapshot (cairo_surface_t *surface,
GTask *task)
{
SnapshotAsyncData *data = g_task_get_task_data (task);
data->snapshot = ephy_snapshot_service_prepare_snapshot (surface);
ephy_snapshot_service_save_snapshot_async (g_task_get_source_object (task),
data->snapshot,
webkit_web_view_get_uri (data->web_view),
g_task_get_cancellable (task),
(GAsyncReadyCallback)snapshot_saved,
task);
}
static void
on_snapshot_ready (WebKitWebView *web_view,
GAsyncResult *result,
GTask *task)
{
cairo_surface_t *surface;
GError *error = NULL;
surface = webkit_web_view_get_snapshot_finish (web_view, result, &error);
if (error) {
g_task_return_error (task, error);
g_object_unref (task);
return;
}
save_snapshot (surface, task);
cairo_surface_destroy (surface);
}
static gboolean
retrieve_snapshot_from_web_view (GTask *task)
{
SnapshotAsyncData *data;
data = g_task_get_task_data (task);
if (!data->web_view) {
g_task_return_new_error (task,
EPHY_SNAPSHOT_SERVICE_ERROR,
EPHY_SNAPSHOT_SERVICE_ERROR_WEB_VIEW,
"%s", "Error getting snapshot, web view was destroyed");
g_object_unref (task);
return FALSE;
}
webkit_web_view_get_snapshot (data->web_view,
WEBKIT_SNAPSHOT_REGION_VISIBLE,
WEBKIT_SNAPSHOT_OPTIONS_NONE,
g_task_get_cancellable (task),
(GAsyncReadyCallback)on_snapshot_ready,
task);
return FALSE;
}
static void
webview_destroyed_cb (GtkWidget *web_view,
GTask *task)
{
g_task_return_new_error (task,
EPHY_SNAPSHOT_SERVICE_ERROR,
EPHY_SNAPSHOT_SERVICE_ERROR_WEB_VIEW,
"%s", "Error getting snapshot, web view was destroyed");
g_object_unref (task);
}
static void
webview_load_changed_cb (WebKitWebView *web_view,
WebKitLoadEvent load_event,
GTask *task)
{
if (load_event != WEBKIT_LOAD_FINISHED)
return;
/* Load finished doesn't ensure that we actually have visible content yet,
* so hold a bit before retrieving the snapshot. */
g_idle_add ((GSourceFunc)retrieve_snapshot_from_web_view, task);
/* Some pages might end up causing this condition to happen twice, so remove
* the handler in order to avoid calling the above idle function twice. */
g_signal_handlers_disconnect_by_func (web_view, webview_load_changed_cb, task);
g_signal_handlers_disconnect_by_func (web_view, webview_destroyed_cb, task);
}
static gboolean
webview_load_failed_cb (WebKitWebView *web_view,
WebKitLoadEvent load_event,
const char failing_uri,
GError *error,
GTask *task)
{
g_signal_handlers_disconnect_by_func (web_view, webview_load_changed_cb, task);
g_signal_handlers_disconnect_by_func (web_view, webview_load_failed_cb, task);
g_signal_handlers_disconnect_by_func (web_view, webview_destroyed_cb, task);
g_task_return_new_error (task,
EPHY_SNAPSHOT_SERVICE_ERROR,
EPHY_SNAPSHOT_SERVICE_ERROR_WEB_VIEW,
"Error getting snapshot, web view failed to load: %s",
error->message);
g_object_unref (task);
return TRUE;
}
static gboolean
ephy_snapshot_service_take_from_webview (GTask *task)
{
SnapshotAsyncData *data;
data = g_task_get_task_data (task);
if (!data->web_view) {
g_task_return_new_error (task,
EPHY_SNAPSHOT_SERVICE_ERROR,
EPHY_SNAPSHOT_SERVICE_ERROR_WEB_VIEW,
"%s", "Error getting snapshot, web view was destroyed");
g_object_unref (task);
return FALSE;
}
if (webkit_web_view_get_estimated_load_progress (WEBKIT_WEB_VIEW (data->web_view)) == 1.0)
retrieve_snapshot_from_web_view (task);
else {
g_signal_connect_object (data->web_view, "destroy",
G_CALLBACK (webview_destroyed_cb),
task, 0);
g_signal_connect_object (data->web_view, "load-changed",
G_CALLBACK (webview_load_changed_cb),
task, 0);
g_signal_connect_object (data->web_view, "load-failed",
G_CALLBACK (webview_load_failed_cb),
task, 0);
}
return FALSE;
}
GQuark
ephy_snapshot_service_error_quark (void)
{
return g_quark_from_static_string ("ephy-snapshot-service-error-quark");
}
/**
* ephy_snapshot_service_get_default:
*
* Gets the default instance of #EphySnapshotService.
*
* Returns: a #EphySnapshotService
**/
EphySnapshotService *
ephy_snapshot_service_get_default (void)
{
static EphySnapshotService *service = NULL;
if (service == NULL)
service = g_object_new (EPHY_TYPE_SNAPSHOT_SERVICE, NULL);
return service;
}
const char *
ephy_snapshot_service_lookup_cached_snapshot_path (EphySnapshotService *service,
const char *url)
{
SnapshotPathCachedData *data;
g_assert (EPHY_IS_SNAPSHOT_SERVICE (service));
data = g_hash_table_lookup (service->cache, url);
return data == NULL ? NULL : data->path;
}
static EphySnapshotFreshness
ephy_snapshot_service_lookup_snapshot_freshness (EphySnapshotService *service,
const char *url)
{
SnapshotPathCachedData *data;
data = g_hash_table_lookup (service->cache, url);
return data == NULL ? SNAPSHOT_STALE : data->freshness;
}
static void
get_snapshot_path_for_url_thread (GTask *task,
EphySnapshotService *service,
SnapshotAsyncData *data,
GCancellable *cancellable)
{
char *path;
path = thumbnail_path (data->url);
if (!validate_thumbnail_path (path, data->url)) {
g_task_return_new_error (task,
EPHY_SNAPSHOT_SERVICE_ERROR,
EPHY_SNAPSHOT_SERVICE_ERROR_NOT_FOUND,
"Snapshot for url \"%s\" not found in disk cache", data->url);
g_free (path);
return;
}
cache_snapshot_data_in_idle (service, data->url, path, SNAPSHOT_STALE);
g_task_return_pointer (task, path, g_free);
}
void
ephy_snapshot_service_get_snapshot_path_for_url_async (EphySnapshotService *service,
const char *url,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
GTask *task;
const char *path;
g_assert (EPHY_IS_SNAPSHOT_SERVICE (service));
g_assert (url != NULL);
task = g_task_new (service, cancellable, callback, user_data);
path = ephy_snapshot_service_lookup_cached_snapshot_path (service, url);
if (path) {
g_task_return_pointer (task, g_strdup (path), g_free);
g_object_unref (task);
return;
}
g_task_set_priority (task, G_PRIORITY_LOW);
g_task_set_task_data (task,
snapshot_async_data_new (service, NULL, NULL, url),
(GDestroyNotify)snapshot_async_data_free);
g_task_run_in_thread (task, (GTaskThreadFunc)get_snapshot_path_for_url_thread);
g_object_unref (task);
}
static void
take_fresh_snapshot_in_background_if_stale (EphySnapshotService *service,
SnapshotAsyncData *data)
{
GTask *task;
/* We schedule a new snapshot now, which will complete eventually. It won't be
* used now. This is just to ensure we get a newer snapshot in the future. */
if (ephy_snapshot_service_lookup_snapshot_freshness (service, data->url) == SNAPSHOT_STALE) {
task = g_task_new (service, NULL, NULL, NULL);
g_task_set_task_data (task,
data,
(GDestroyNotify)snapshot_async_data_free);
ephy_snapshot_service_take_from_webview (task);
}
}
char *
ephy_snapshot_service_get_snapshot_path_for_url_finish (EphySnapshotService *service,
GAsyncResult *result,
GError **error)
{
g_assert (g_task_is_valid (result, service));
return g_task_propagate_pointer (G_TASK (result), error);
}
static void
got_snapshot_path_for_url (EphySnapshotService *service,
GAsyncResult *result,
GTask *task)
{
#ifndef __clang_analyzer__
SnapshotAsyncData *data = g_task_get_task_data (task);
char *path;
path = ephy_snapshot_service_get_snapshot_path_for_url_finish (service, result, NULL);
if (path) {
take_fresh_snapshot_in_background_if_stale (service, snapshot_async_data_copy (data));
g_task_return_pointer (task, path, g_free);
g_object_unref (task);
} else {
ephy_snapshot_service_take_from_webview (task);
}
#endif
}
void
ephy_snapshot_service_get_snapshot_path_async (EphySnapshotService *service,
WebKitWebView *web_view,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
GTask *task;
const char *uri;
const char *path;
g_assert (EPHY_IS_SNAPSHOT_SERVICE (service));
g_assert (WEBKIT_IS_WEB_VIEW (web_view));
g_assert (webkit_web_view_get_uri (web_view));
task = g_task_new (service, cancellable, callback, user_data);
uri = webkit_web_view_get_uri (web_view);
path = ephy_snapshot_service_lookup_cached_snapshot_path (service, uri);
if (path) {
take_fresh_snapshot_in_background_if_stale (service,
snapshot_async_data_new (service, NULL, web_view, uri));
g_task_return_pointer (task, g_strdup (path), g_free);
g_object_unref (task);
} else {
g_task_set_task_data (task,
snapshot_async_data_new (service, NULL, web_view, uri),
(GDestroyNotify)snapshot_async_data_free);
ephy_snapshot_service_get_snapshot_path_for_url_async (service,
uri,
cancellable,
(GAsyncReadyCallback)got_snapshot_path_for_url,
task);
}
}
char *
ephy_snapshot_service_get_snapshot_path_finish (EphySnapshotService *service,
GAsyncResult *result,
GError **error)
{
g_assert (g_task_is_valid (result, service));
return g_task_propagate_pointer (G_TASK (result), error);
}
static void
got_snapshot_path_to_delete_cb (EphySnapshotService *service,
GAsyncResult *result,
gpointer user_data)
{
char *path;
path = ephy_snapshot_service_get_snapshot_path_for_url_finish (service, result, NULL);
if (path)
unlink (path);
g_free (path);
g_object_unref (service);
}
void
ephy_snapshot_service_delete_snapshot_for_url (EphySnapshotService *service,
const char *url)
{
ephy_snapshot_service_get_snapshot_path_for_url_async (g_object_ref (service),
url,
NULL,
(GAsyncReadyCallback)got_snapshot_path_to_delete_cb,
NULL);
}
void
ephy_snapshot_service_delete_all_snapshots (EphySnapshotService *service)
{
GError *error = NULL;
char *dir;
dir = thumbnail_directory ();
ephy_file_delete_dir_recursively (dir, &error);
if (error) {
g_warning ("Failed to delete thumbnail directory: %s", error->message);
g_error_free (error);
}
g_free (dir);
}