midori/extensions/web-cache.c
Christian Dywan 9040d43632 Refactor web cache to circumvent libsoup when needed
At this point libsoup is dependent on a functional network adapter,
which may not be available if no network connection is there or
the connection is faulty. So *if* we have a requested page in the
cache, we load it before libsoup comes into play.

Depending on the WebKitGTK+ version, loading from cache uses an
"alternate" URI so that we don't end up having cache filenames
in the location entry.

Generation of cache filenames is fixed to never include special
characters such as slashes or question marks.

Only non-empty files are actually stored to the cache folder.
2009-10-22 02:20:27 +02:00

399 lines
14 KiB
C

/*
Copyright (C) 2009 Christian Dywan <christian@twotoasts.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.
*/
#include <midori/midori.h>
#include <midori/sokoke.h>
#include "config.h"
#include <glib/gstdio.h>
#if HAVE_UNISTD_H
#include <unistd.h>
#endif
#define HAVE_WEBKIT_RESOURCE_REQUEST WEBKIT_CHECK_VERSION (1, 1, 14)
static gchar*
web_cache_get_cached_path (MidoriExtension* extension,
const gchar* uri)
{
static const gchar* cache_path = NULL;
gchar* checksum;
gchar* folder;
gchar* sub_path;
gchar* encoded;
gchar* ext;
gchar* cached_filename;
gchar* cached_path;
/* cache_path = midori_extension_get_string (extension, "path"); */
if (!cache_path)
cache_path = g_build_filename (g_get_user_cache_dir (),
PACKAGE_NAME, "web", NULL);
checksum = g_compute_checksum_for_string (G_CHECKSUM_MD5, uri, -1);
folder = g_strdup_printf ("%c%c", checksum[0], checksum[1]);
sub_path = g_build_path (G_DIR_SEPARATOR_S, cache_path, folder, NULL);
g_mkdir (sub_path, 0700);
g_free (folder);
encoded = soup_uri_encode (uri, "/");
ext = g_strrstr (encoded, ".");
cached_filename = g_strdup_printf ("%s%s", checksum, ext ? ext : "");
g_free (encoded);
g_free (checksum);
cached_path = g_build_filename (sub_path, cached_filename, NULL);
g_free (cached_filename);
return cached_path;
}
static gboolean
web_cache_replace_frame_uri (MidoriExtension* extension,
const gchar* uri,
WebKitWebFrame* web_frame)
{
gchar* filename;
gboolean handled = FALSE;
filename = web_cache_get_cached_path (extension, uri);
/* g_debug ("cache lookup: %s => %s", uri, filename); */
if (g_file_test (filename, G_FILE_TEST_EXISTS))
{
gchar* data;
g_file_get_contents (filename, &data, NULL, NULL);
webkit_web_frame_load_alternate_string (web_frame, data, NULL, uri);
g_free (data);
handled = TRUE;
}
g_free (filename);
return handled;
}
static gboolean
web_cache_navigation_decision_cb (WebKitWebView* web_view,
WebKitWebFrame* web_frame,
WebKitNetworkRequest* request,
WebKitWebNavigationAction* action,
WebKitWebPolicyDecision* decision,
MidoriExtension* extension)
{
const gchar* uri = webkit_network_request_get_uri (request);
if (!(uri && g_str_has_prefix (uri, "http://")))
return FALSE;
return web_cache_replace_frame_uri (extension, uri, web_frame);
}
#if WEBKIT_CHECK_VERSION (1, 1, 6)
static gboolean
web_cache_load_error_cb (WebKitWebView* web_view,
WebKitWebFrame* web_frame,
const gchar* uri,
GError* error,
MidoriExtension* extension)
{
const gchar* provisional;
if (!(uri && g_str_has_prefix (uri, "http://")))
return FALSE;
return web_cache_replace_frame_uri (extension, uri, web_frame);
}
#endif
#if HAVE_WEBKIT_RESOURCE_REQUEST
static void
web_cache_resource_request_starting_cb (WebKitWebView* web_view,
WebKitWebFrame* web_frame,
WebKitWebResource* web_resource,
WebKitNetworkRequest* request,
WebKitNetworkResponse* response,
MidoriExtension* extension)
{
const gchar* uri;
gchar* filename;
uri = webkit_network_request_get_uri (request);
if (!(uri && g_str_has_prefix (uri, "http://")))
return;
if (!(g_strcmp0 (uri, webkit_web_frame_get_uri (web_frame))
&& g_strcmp0 (webkit_web_data_source_get_unreachable_uri (webkit_web_frame_get_data_source (web_frame)), uri)))
{
web_cache_replace_frame_uri (extension, uri, web_frame);
return;
}
filename = web_cache_get_cached_path (extension, uri);
if (g_file_test (filename, G_FILE_TEST_EXISTS))
{
gchar* file_uri = g_filename_to_uri (filename, NULL, NULL);
webkit_network_request_set_uri (request, file_uri);
g_free (file_uri);
}
g_free (filename);
}
#endif
static void
web_cache_mesage_got_headers_cb (SoupMessage* msg,
MidoriExtension* extension)
{
SoupURI* soup_uri = soup_message_get_uri (msg);
gchar* uri = soup_uri ? soup_uri_to_string (soup_uri, FALSE) : g_strdup ("");
gchar* filename = web_cache_get_cached_path (extension, uri);
gchar* data;
gsize length;
/* g_debug ("cache serve: %s (%s)", uri, filename); */
/* FIXME: Inspect headers and decide whether we need to update the cache */
g_file_get_contents (filename, &data, &length, NULL);
soup_message_set_status (msg, SOUP_STATUS_OK);
/* FIXME: MIME type */
soup_message_set_request (msg, "image/jpeg", SOUP_MEMORY_TAKE, data, length);
g_signal_handlers_disconnect_by_func (msg, web_cache_mesage_got_headers_cb, extension);
soup_session_requeue_message (g_object_get_data (G_OBJECT (msg), "session"), msg);
g_free (filename);
g_free (uri);
}
static void
web_cache_mesage_got_chunk_cb (SoupMessage* msg,
SoupBuffer* chunk,
MidoriExtension* extension)
{
/* Fill the body in manually for later use, even if WebKitGTK+
disables accumulation. We should probably do this differently. */
if (!soup_message_body_get_accumulate (msg->response_body))
soup_message_body_append_buffer (msg->response_body, chunk);
}
static void
web_cache_session_request_queued_cb (SoupSession* session,
SoupMessage* msg,
MidoriExtension* extension)
{
SoupURI* soup_uri = soup_message_get_uri (msg);
gchar* uri = soup_uri ? soup_uri_to_string (soup_uri, FALSE) : g_strdup ("");
if (g_str_has_prefix (uri, "http"))
{
gchar* filename = web_cache_get_cached_path (extension, uri);
/* g_debug ("cache lookup: %d %s => %s", msg->status_code, uri, filename); */
g_object_set_data (G_OBJECT (msg), "session", session);
/* Network is unavailable, so we fallback to cache */
if (msg->status_code == SOUP_STATUS_CANT_RESOLVE)
web_cache_mesage_got_headers_cb (msg, extension);
if (g_file_test (filename, G_FILE_TEST_EXISTS))
g_signal_connect (msg, "got-headers",
G_CALLBACK (web_cache_mesage_got_headers_cb), extension);
else
g_signal_connect (msg, "got-chunk",
G_CALLBACK (web_cache_mesage_got_chunk_cb), extension);
g_free (filename);
}
g_free (uri);
}
static void
web_cache_session_request_unqueued_cb (SoupSession* session,
SoupMessage* msg,
MidoriExtension* extension)
{
SoupURI* soup_uri = soup_message_get_uri (msg);
gchar* uri = soup_uri ? soup_uri_to_string (soup_uri, FALSE) : NULL;
/* g_debug ("request unqueued: %d %s", msg->status_code, uri); */
#if !HAVE_WEBKIT_RESOURCE_REQUEST
/* Network is unavailable, so we fallback to cache */
if (msg->status_code == SOUP_STATUS_CANT_RESOLVE)
web_cache_mesage_got_headers_cb (msg, extension);
else
#endif
/* FIXME: Only store if this wasn't a cached message already */
/* FIXME: Don't store files from the res server */
if (uri && g_str_has_prefix (uri, "http"))
{
SoupMessageHeaders* hdrs = msg->response_headers;
const gchar* mime_type = soup_message_headers_get_content_type (hdrs, NULL);
/* FIXME: Don't store big files */
if (mime_type)
{
gchar* filename = web_cache_get_cached_path (extension, uri);
SoupMessageBody* body = msg->response_body;
SoupBuffer* buffer = NULL;
/* We fed the buffer manually before, so this actually works. */
if (!soup_message_body_get_accumulate (body))
{
soup_message_body_set_accumulate (body, TRUE);
buffer = soup_message_body_flatten (body);
}
/* FIXME: Update sensibly */
if (!g_file_test (filename, G_FILE_TEST_EXISTS))
if (body->length)
{
/* g_debug ("cache store: %s => %s (%d)", uri, filename, body->length); */
GError* error = NULL;
g_file_set_contents (filename, body->data, body->length, &error);
if (error)
{
g_printf ("%s\n", error->message);
g_error_free (error);
}
}
/* else
g_debug ("cache skip empty"); */
if (buffer)
soup_buffer_free (buffer);
}
/* else
g_debug ("cache skip: %s", mime_type); */
}
g_free (uri);
}
static void
web_cache_add_tab_cb (MidoriBrowser* browser,
MidoriView* view,
MidoriExtension* extension)
{
GtkWidget* web_view = gtk_bin_get_child (GTK_BIN (view));
g_signal_connect (web_view, "navigation-policy-decision-requested",
G_CALLBACK (web_cache_navigation_decision_cb), extension);
#if WEBKIT_CHECK_VERSION (1, 1, 6)
g_signal_connect (web_view, "load-error",
G_CALLBACK (web_cache_load_error_cb), extension);
#endif
#if HAVE_WEBKIT_RESOURCE_REQUEST
g_signal_connect (web_view, "resource-request-starting",
G_CALLBACK (web_cache_resource_request_starting_cb), extension);
#endif
}
static void
web_cache_deactivate_cb (MidoriExtension* extension,
MidoriBrowser* browser);
static void
web_cache_add_tab_foreach_cb (MidoriView* view,
MidoriBrowser* browser,
MidoriExtension* extension)
{
web_cache_add_tab_cb (browser, view, extension);
}
static void
web_cache_app_add_browser_cb (MidoriApp* app,
MidoriBrowser* browser,
MidoriExtension* extension)
{
midori_browser_foreach (browser,
(GtkCallback)web_cache_add_tab_foreach_cb, extension);
g_signal_connect (browser, "add-tab",
G_CALLBACK (web_cache_add_tab_cb), extension);
g_signal_connect (extension, "deactivate",
G_CALLBACK (web_cache_deactivate_cb), browser);
}
static void
web_cache_deactivate_tabs (MidoriView* view,
MidoriExtension* extension)
{
GtkWidget* web_view = gtk_bin_get_child (GTK_BIN (view));
MidoriBrowser* browser = midori_browser_get_for_widget (web_view);
g_signal_handlers_disconnect_by_func (
browser, web_cache_add_tab_cb, 0);
#if HAVE_WEBKIT_RESOURCE_REQUEST
g_signal_handlers_disconnect_by_func (
web_view, web_cache_resource_request_starting_cb, extension);
#endif
g_signal_handlers_disconnect_by_func (
webkit_get_default_session (), web_cache_session_request_queued_cb, extension);
}
static void
web_cache_deactivate_cb (MidoriExtension* extension,
MidoriBrowser* browser)
{
MidoriApp* app = midori_extension_get_app (extension);
g_signal_handlers_disconnect_by_func (
extension, web_cache_deactivate_cb, browser);
g_signal_handlers_disconnect_by_func (
app, web_cache_app_add_browser_cb, extension);
midori_browser_foreach (browser, (GtkCallback)web_cache_deactivate_tabs, extension);
}
static void
web_cache_activate_cb (MidoriExtension* extension,
MidoriApp* app)
{
KatzeArray* browsers;
MidoriBrowser* browser;
guint i;
SoupSession* session = webkit_get_default_session ();
g_signal_connect (session, "request-queued",
G_CALLBACK (web_cache_session_request_queued_cb), extension);
g_signal_connect (session, "request-unqueued",
G_CALLBACK (web_cache_session_request_unqueued_cb), extension);
browsers = katze_object_get_object (app, "browsers");
i = 0;
while ((browser = katze_array_get_nth_item (browsers, i++)))
web_cache_app_add_browser_cb (app, browser, extension);
g_signal_connect (app, "add-browser",
G_CALLBACK (web_cache_app_add_browser_cb), extension);
g_object_unref (browsers);
}
MidoriExtension*
extension_init (void)
{
gchar* cache_path = g_build_filename (g_get_user_cache_dir (),
PACKAGE_NAME, "web", NULL);
MidoriExtension* extension = g_object_new (MIDORI_TYPE_EXTENSION,
"name", _("Web Cache"),
"description", _("Cache HTTP communication on disk"),
"version", "0.1",
"authors", "Christian Dywan <christian@twotoasts.de>",
NULL);
midori_extension_install_string (extension, "path", cache_path);
midori_extension_install_integer (extension, "size", 50);
g_free (cache_path);
g_signal_connect (extension, "activate",
G_CALLBACK (web_cache_activate_cb), NULL);
return extension;
}