midori/extensions/web-cache.c
Christian Dywan d73b471ec4 Implement sokoke_register_privacy_item facility
The new function allows registering callbacks which clear data,
with a name and label so they show up in the Clear Private Data
dialogue and the chosen items are saved.

The old enumeration still keeps history, trash and the flag
whether to clear when Midori quits.
2010-10-17 22:48:32 +02:00

486 lines
15 KiB
C

/*
Copyright (C) 2009 Christian Dywan <christian@twotoasts.de>
Copyright (C) 2009 Alexander Butenko <a.butenka@gmail.com>
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>
#include <stdlib.h>
#if HAVE_UNISTD_H
#include <unistd.h>
#endif
#define MAXLENGTH 1024 * 1024
static gchar*
web_cache_get_cache_dir (void)
{
static gchar* cache_dir = NULL;
if (!cache_dir)
cache_dir = g_build_filename (g_get_user_cache_dir (),
PACKAGE_NAME, "web", NULL);
return cache_dir;
}
static gchar*
web_cache_get_cached_path (MidoriExtension* extension,
const gchar* uri)
{
gchar* checksum;
gchar* folder;
gchar* sub_path;
gchar* encoded;
gchar* ext;
gchar* cached_filename;
gchar* cached_path;
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,
web_cache_get_cache_dir (), folder, NULL);
katze_mkdir_with_parents (sub_path, 0700);
g_free (folder);
encoded = soup_uri_encode (uri, "/");
ext = g_strdup (g_strrstr (encoded, "."));
/* Make sure ext isn't becoming too long */
if (ext && ext[0] && ext[1] && ext[2] && ext[3] && ext[4])
ext[4] = '\0';
cached_filename = g_strdup_printf ("%s%s", checksum, ext ? ext : "");
g_free (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_save_headers (SoupMessage* msg,
gchar* filename)
{
gchar* dsc_filename = g_strdup_printf ("%s.dsc.tmp", filename);
SoupMessageHeaders* hdrs = msg->response_headers;
SoupMessageHeadersIter iter;
const gchar* name, *value;
FILE* dscfd;
soup_message_headers_iter_init (&iter, hdrs);
dscfd = g_fopen (dsc_filename, "w");
g_free (dsc_filename);
if (!dscfd)
return FALSE;
while (soup_message_headers_iter_next (&iter, &name, &value))
g_fprintf (dscfd, "%s: %s\n", name, value);
fclose (dscfd);
return TRUE;
}
GHashTable*
web_cache_get_headers (gchar* filename)
{
GHashTable* headers;
FILE* file;
gchar* dsc_filename;
gchar line[128];
if (!filename)
return NULL;
/* use g_access() instead of g_file_test for better performance */
if (g_access (filename, F_OK) != 0)
return NULL;
dsc_filename = g_strdup_printf ("%s.dsc", filename);
headers = g_hash_table_new_full (g_str_hash, g_str_equal,
(GDestroyNotify)g_free,
(GDestroyNotify)g_free);
if (!(file = g_fopen (dsc_filename, "r")))
{
g_hash_table_destroy (headers);
g_free (dsc_filename);
return NULL;
}
while (fgets (line, 128, file))
{
gchar** data;
if (line == NULL)
continue;
g_strchomp (line);
data = g_strsplit (line, ":", 2);
if (data[0] && data[1])
g_hash_table_insert (headers, g_strdup (data[0]),
g_strdup (g_strchug (data[1])));
g_strfreev (data);
}
fclose (file);
g_free (dsc_filename);
return headers;
}
static GFile*
web_cache_tmp_prepare (gchar* filename)
{
GFile *file;
gchar* tmp_filename = g_strdup_printf ("%s.tmp", filename);
if (g_access (tmp_filename, F_OK) == 0)
{
g_free (tmp_filename);
return NULL;
}
file = g_file_new_for_path (tmp_filename);
g_free (tmp_filename);
return file;
}
static void
web_cache_set_content_type (SoupMessage* msg,
SoupBuffer* buffer)
{
#if WEBKIT_CHECK_VERSION (1, 1, 15)
gchar* sniffed_type;
SoupContentSniffer* sniffer = soup_content_sniffer_new ();
if ((sniffed_type = soup_content_sniffer_sniff (sniffer, msg, buffer, NULL)))
{
g_signal_emit_by_name (msg, "content-sniffed", sniffed_type, NULL);
g_free (sniffed_type);
}
else
{
const gchar* content_type = soup_message_headers_get_one (
msg->response_headers, "Content-Type");
g_signal_emit_by_name (msg, "content-sniffed", content_type, NULL);
}
#endif
}
static void
web_cache_message_finished_cb (SoupMessage* msg,
GOutputStream* stream)
{
gchar* headers;
gchar* tmp_headers;
gchar* tmp_data;
gchar* filename;
filename = g_object_get_data (G_OBJECT (stream), "filename");
headers = g_strdup_printf ("%s.dsc", filename);
tmp_headers = g_strdup_printf ("%s.dsc.tmp", filename);
tmp_data = g_strdup_printf ("%s.tmp", filename);
g_output_stream_close (stream, NULL, NULL);
if (msg->status_code == SOUP_STATUS_OK)
{
g_rename (tmp_data, filename);
g_rename (tmp_headers, headers);
}
else
{
g_unlink (tmp_data);
g_unlink (tmp_headers);
}
g_object_unref (stream);
g_free (headers);
g_free (tmp_headers);
g_free (tmp_data);
}
static void web_cache_pause_message (SoupMessage* msg)
{
SoupSession* session;
session = g_object_get_data (G_OBJECT (msg), "session");
soup_session_pause_message (session, msg);
}
static void web_cache_unpause_message (SoupMessage* msg)
{
SoupSession* session;
session = g_object_get_data (G_OBJECT (msg), "session");
soup_session_unpause_message (session, msg);
}
static void
web_cache_message_got_chunk_cb (SoupMessage* msg,
SoupBuffer* chunk,
GOutputStream* stream)
{
if (!chunk->data || !chunk->length)
return;
/* FIXME g_output_stream_write_async (stream, chunk->data, chunk->length,
G_PRIORITY_DEFAULT, NULL, NULL, (gpointer)chunk->length); */
g_output_stream_write (stream, chunk->data, chunk->length, NULL, NULL);
}
static void
web_cache_message_rewrite_async_cb (GFile *file,
GAsyncResult* res,
SoupMessage* msg)
{
SoupBuffer *buffer;
char *data;
gsize length;
GError *error = NULL;
if (g_file_load_contents_finish (file, res, &data, &length, NULL, &error))
{
buffer = soup_buffer_new (SOUP_MEMORY_TEMPORARY, data, length);
web_cache_set_content_type (msg, buffer);
soup_message_body_append_buffer (msg->response_body, buffer);
/* FIXME? */
web_cache_unpause_message (msg);
g_signal_emit_by_name (msg, "got-chunk", buffer, NULL);
soup_buffer_free (buffer);
g_free (data);
soup_message_got_body (msg);
soup_message_finished (msg);
}
g_object_unref (file);
g_object_unref (msg);
}
static void
web_cache_message_rewrite (SoupMessage* msg,
gchar* filename)
{
GHashTableIter iter;
gpointer key, value;
GFile *file;
GHashTable* cache_headers = web_cache_get_headers (filename);
if (!cache_headers)
return;
soup_message_set_status (msg, SOUP_STATUS_OK);
g_hash_table_iter_init (&iter, cache_headers);
while (g_hash_table_iter_next (&iter, &key, &value))
soup_message_headers_replace (msg->response_headers, key, value);
g_signal_emit_by_name (msg, "got-headers", NULL);
g_hash_table_destroy (cache_headers);
/* FIXME? It seems libsoup already said "goodbye" by the time
the asynchronous function is starting to send data */
web_cache_pause_message (msg);
file = g_file_new_for_path (filename);
g_free (filename);
g_object_ref (msg);
g_file_load_contents_async (file, NULL,
(GAsyncReadyCallback)web_cache_message_rewrite_async_cb, msg);
}
static void
web_cache_mesage_got_headers_cb (SoupMessage* msg,
gchar* filename)
{
const gchar* nocache;
SoupMessageHeaders *hdrs = msg->response_headers;
const char* cl;
/* Skip files downloaded by the user */
if (g_object_get_data (G_OBJECT (msg), "midori-web-cache-download"))
return;
/* Skip big files */
cl = soup_message_headers_get_one (hdrs, "Content-Length");
if (cl && atoi (cl) > MAXLENGTH)
return;
nocache = soup_message_headers_get_one (hdrs, "Pragma");
if (!nocache)
nocache = soup_message_headers_get_one (hdrs, "Cache-Control");
if (nocache && g_regex_match_simple ("no-cache|no-store", nocache,
G_REGEX_CASELESS, G_REGEX_MATCH_NOTEMPTY))
{
return;
}
if (msg->status_code == SOUP_STATUS_NOT_MODIFIED)
{
/* g_debug ("loading from cache: %s", filename); */
g_signal_handlers_disconnect_by_func (msg,
web_cache_mesage_got_headers_cb, filename);
web_cache_message_rewrite (msg, filename);
}
else if (msg->status_code == SOUP_STATUS_OK)
{
GFile* file;
GOutputStream* ostream;
/* g_debug ("updating cache: %s", filename); */
if (!(file = web_cache_tmp_prepare (filename)))
return;
if (!web_cache_save_headers (msg, filename))
return;
#if GLIB_CHECK_VERSION (2, 20, 0)
ostream = (GOutputStream*)g_file_append_to (file,
G_FILE_CREATE_PRIVATE | G_FILE_CREATE_REPLACE_DESTINATION, NULL, NULL);
#else
g_unlink (filename);
ostream = (GOutputStream*)g_file_append_to (file,
G_FILE_CREATE_PRIVATE, NULL, NULL);
#endif
g_object_unref (file);
if (!ostream)
return;
g_object_set_data_full (G_OBJECT (ostream), "filename",
filename, (GDestroyNotify)g_free);
g_signal_connect (msg, "got-chunk",
G_CALLBACK (web_cache_message_got_chunk_cb), ostream);
g_signal_connect (msg, "finished",
G_CALLBACK (web_cache_message_finished_cb), ostream);
}
}
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_to_string (soup_uri, FALSE);
if (uri && g_str_has_prefix (uri, "http") && !g_strcmp0 (msg->method, "GET"))
{
gchar* filename = web_cache_get_cached_path (extension, uri);
GHashTable* cache_headers;
gchar* etag;
gchar* last_modified;
cache_headers = web_cache_get_headers (filename);
if (cache_headers)
{
etag = g_hash_table_lookup (cache_headers, "ETag");
last_modified = g_hash_table_lookup (cache_headers, "Last-Modified");
if (etag)
soup_message_headers_replace (msg->request_headers,
"If-None-Match", etag);
if (last_modified)
soup_message_headers_replace (msg->request_headers,
"If-Modified-Since", last_modified);
g_hash_table_destroy (cache_headers);
}
g_object_set_data (G_OBJECT (msg), "session", session);
g_signal_connect (msg, "got-headers",
G_CALLBACK (web_cache_mesage_got_headers_cb), filename);
}
g_free (uri);
}
#if WEBKIT_CHECK_VERSION (1, 1, 3)
static void
web_cache_add_download_cb (MidoriBrowser* browser,
WebKitDownload* download,
MidoriExtension* extension)
{
WebKitNetworkRequest* request = webkit_download_get_network_request (download);
SoupMessage* msg = webkit_network_request_get_message (request);
if (msg)
g_object_set_data (G_OBJECT (msg), "midori-web-cache-download",
(gpointer)0xdeadbeef);
}
#endif
static void
web_cache_deactivate_cb (MidoriExtension* extension,
MidoriBrowser* browser);
static void
web_cache_app_add_browser_cb (MidoriApp* app,
MidoriBrowser* browser,
MidoriExtension* extension)
{
#if WEBKIT_CHECK_VERSION (1, 1, 3)
g_signal_connect (browser, "add-download",
G_CALLBACK (web_cache_add_download_cb), extension);
#endif
g_signal_connect (extension, "deactivate",
G_CALLBACK (web_cache_deactivate_cb), browser);
}
static void
web_cache_deactivate_cb (MidoriExtension* extension,
MidoriBrowser* browser)
{
MidoriApp* app = midori_extension_get_app (extension);
SoupSession* session = webkit_get_default_session ();
g_signal_handlers_disconnect_by_func (
session, web_cache_session_request_queued_cb, 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);
#if WEBKIT_CHECK_VERSION (1, 1, 3)
g_signal_handlers_disconnect_by_func (
browser, web_cache_add_download_cb, extension);
#endif
}
static void
web_cache_activate_cb (MidoriExtension* extension,
MidoriApp* app)
{
KatzeArray* browsers;
MidoriBrowser* browser;
SoupSession* session = webkit_get_default_session ();
katze_mkdir_with_parents (web_cache_get_cache_dir (), 0700);
g_signal_connect (session, "request-queued",
G_CALLBACK (web_cache_session_request_queued_cb), extension);
browsers = katze_object_get_object (app, "browsers");
KATZE_ARRAY_FOREACH_ITEM (browser, browsers)
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);
}
static void
web_cache_clear_cache_cb (void)
{
sokoke_remove_path (web_cache_get_cache_dir (), TRUE);
}
MidoriExtension*
extension_init (void)
{
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);
g_signal_connect (extension, "activate",
G_CALLBACK (web_cache_activate_cb), NULL);
sokoke_register_privacy_item ("web-cache", _("Web Cache"),
G_CALLBACK (web_cache_clear_cache_cb));
return extension;
}