1/*
2 * Copyright (C) 2012 Igalia S.L.
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 2,1 of the License, or (at your option) any later version.
8 *
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12 * Library General Public License for more details.
13 *
14 * You should have received a copy of the GNU Library General Public License
15 * along with this library; see the file COPYING.LIB.  If not, write to
16 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
17 * Boston, MA 02110-1301, USA.
18 */
19
20#include "config.h"
21#include "WebKitFaviconDatabase.h"
22
23#include "WebKitFaviconDatabasePrivate.h"
24#include "WebKitMarshal.h"
25#include "WebKitPrivate.h"
26#include <WebCore/FileSystem.h>
27#include <WebCore/Image.h>
28#include <WebCore/IntSize.h>
29#include <WebCore/RefPtrCairo.h>
30#include <glib/gi18n-lib.h>
31#include <wtf/RunLoop.h>
32#include <wtf/gobject/GRefPtr.h>
33#include <wtf/gobject/GUniquePtr.h>
34#include <wtf/text/CString.h>
35
36using namespace WebKit;
37using namespace WebCore;
38
39/**
40 * SECTION: WebKitFaviconDatabase
41 * @Short_description: A WebKit favicon database
42 * @Title: WebKitFaviconDatabase
43 *
44 * #WebKitFaviconDatabase provides access to the icons associated with
45 * web sites.
46 *
47 * WebKit will automatically look for available icons in &lt;link&gt;
48 * elements on opened pages as well as an existing favicon.ico and
49 * load the images found into a memory cache if possible. That cache
50 * is frozen to an on-disk database for persistence.
51 *
52 * If #WebKitSettings:enable-private-browsing is %TRUE, new icons
53 * won't be added to the on-disk database and no existing icons will
54 * be deleted from it. Nevertheless, WebKit will still store them in
55 * the in-memory cache during the current execution.
56 *
57 */
58
59enum {
60    FAVICON_CHANGED,
61
62    LAST_SIGNAL
63};
64
65static guint signals[LAST_SIGNAL] = { 0, };
66
67typedef Vector<GRefPtr<GTask> > PendingIconRequestVector;
68typedef HashMap<String, PendingIconRequestVector*> PendingIconRequestMap;
69
70struct _WebKitFaviconDatabasePrivate {
71    RefPtr<WebIconDatabase> iconDatabase;
72    PendingIconRequestMap pendingIconRequests;
73    HashMap<String, String> pageURLToIconURLMap;
74};
75
76WEBKIT_DEFINE_TYPE(WebKitFaviconDatabase, webkit_favicon_database, G_TYPE_OBJECT)
77
78static void webkitFaviconDatabaseDispose(GObject* object)
79{
80    WebKitFaviconDatabase* database = WEBKIT_FAVICON_DATABASE(object);
81
82    WebKitFaviconDatabasePrivate* priv = database->priv;
83    if (priv->iconDatabase->isOpen())
84        priv->iconDatabase->close();
85
86    G_OBJECT_CLASS(webkit_favicon_database_parent_class)->dispose(object);
87}
88
89static void webkit_favicon_database_class_init(WebKitFaviconDatabaseClass* faviconDatabaseClass)
90{
91    GObjectClass* gObjectClass = G_OBJECT_CLASS(faviconDatabaseClass);
92    gObjectClass->dispose = webkitFaviconDatabaseDispose;
93
94    /**
95     * WebKitFaviconDatabase::favicon-changed:
96     * @database: the object on which the signal is emitted
97     * @page_uri: the URI of the Web page containing the icon
98     * @favicon_uri: the URI of the favicon
99     *
100     * This signal is emitted when the favicon URI of @page_uri has
101     * been changed to @favicon_uri in the database. You can connect
102     * to this signal and call webkit_favicon_database_get_favicon()
103     * to get the favicon. If you are interested in the favicon of a
104     * #WebKitWebView it's easier to use the #WebKitWebView:favicon
105     * property. See webkit_web_view_get_favicon() for more details.
106     */
107    signals[FAVICON_CHANGED] =
108        g_signal_new(
109            "favicon-changed",
110            G_TYPE_FROM_CLASS(faviconDatabaseClass),
111            G_SIGNAL_RUN_LAST,
112            0, 0, 0,
113            webkit_marshal_VOID__STRING_STRING,
114            G_TYPE_NONE, 2,
115            G_TYPE_STRING,
116            G_TYPE_STRING);
117}
118
119struct GetFaviconSurfaceAsyncData {
120    ~GetFaviconSurfaceAsyncData()
121    {
122        if (shouldReleaseIconForPageURL)
123            faviconDatabase->priv->iconDatabase->releaseIconForPageURL(pageURL);
124    }
125
126    GRefPtr<WebKitFaviconDatabase> faviconDatabase;
127    String pageURL;
128    RefPtr<cairo_surface_t> icon;
129    GRefPtr<GCancellable> cancellable;
130    bool shouldReleaseIconForPageURL;
131};
132WEBKIT_DEFINE_ASYNC_DATA_STRUCT(GetFaviconSurfaceAsyncData)
133
134static PassRefPtr<cairo_surface_t> getIconSurfaceSynchronously(WebKitFaviconDatabase* database, const String& pageURL, GError** error)
135{
136    ASSERT(RunLoop::isMain());
137
138    // The exact size we pass is irrelevant to the iconDatabase code.
139    // We must pass something greater than 0x0 to get an icon.
140    WebCore::Image* iconImage = database->priv->iconDatabase->imageForPageURL(pageURL, WebCore::IntSize(1, 1));
141    if (!iconImage) {
142        g_set_error(error, WEBKIT_FAVICON_DATABASE_ERROR, WEBKIT_FAVICON_DATABASE_ERROR_FAVICON_UNKNOWN, _("Unknown favicon for page %s"), pageURL.utf8().data());
143        return 0;
144    }
145
146    RefPtr<cairo_surface_t> surface = iconImage->nativeImageForCurrentFrame();
147    if (!surface) {
148        g_set_error(error, WEBKIT_FAVICON_DATABASE_ERROR, WEBKIT_FAVICON_DATABASE_ERROR_FAVICON_NOT_FOUND, _("Page %s does not have a favicon"), pageURL.utf8().data());
149        return 0;
150    }
151
152    return surface.release();
153}
154
155static void deletePendingIconRequests(WebKitFaviconDatabase* database, PendingIconRequestVector* requests, const String& pageURL)
156{
157    database->priv->pendingIconRequests.remove(pageURL);
158    delete requests;
159}
160
161static void processPendingIconsForPageURL(WebKitFaviconDatabase* database, const String& pageURL)
162{
163    PendingIconRequestVector* pendingIconRequests = database->priv->pendingIconRequests.get(pageURL);
164    if (!pendingIconRequests)
165        return;
166
167    GUniqueOutPtr<GError> error;
168    RefPtr<cairo_surface_t> icon = getIconSurfaceSynchronously(database, pageURL, &error.outPtr());
169
170    for (size_t i = 0; i < pendingIconRequests->size(); ++i) {
171        GTask* task = pendingIconRequests->at(i).get();
172        if (error)
173            g_task_return_error(task, error.release().release());
174        else {
175            GetFaviconSurfaceAsyncData* data = static_cast<GetFaviconSurfaceAsyncData*>(g_task_get_task_data(task));
176            data->icon = icon;
177            data->shouldReleaseIconForPageURL = false;
178            g_task_return_boolean(task, TRUE);
179        }
180    }
181    deletePendingIconRequests(database, pendingIconRequests, pageURL);
182}
183
184static void didChangeIconForPageURLCallback(WKIconDatabaseRef, WKURLRef wkPageURL, const void* clientInfo)
185{
186    WebKitFaviconDatabase* database = WEBKIT_FAVICON_DATABASE(clientInfo);
187    if (!database->priv->iconDatabase->isUrlImportCompleted())
188        return;
189
190    // Wait until there's an icon record in the database for this page URL.
191    String pageURL = toImpl(wkPageURL)->string();
192    WebCore::Image* iconImage = database->priv->iconDatabase->imageForPageURL(pageURL, WebCore::IntSize(1, 1));
193    if (!iconImage || iconImage->isNull())
194        return;
195
196    String currentIconURL;
197    database->priv->iconDatabase->synchronousIconURLForPageURL(pageURL, currentIconURL);
198    const String& iconURL = database->priv->pageURLToIconURLMap.get(pageURL);
199    if (iconURL == currentIconURL)
200        return;
201
202    database->priv->pageURLToIconURLMap.set(pageURL, currentIconURL);
203    g_signal_emit(database, signals[FAVICON_CHANGED], 0, pageURL.utf8().data(), currentIconURL.utf8().data());
204}
205
206static void iconDataReadyForPageURLCallback(WKIconDatabaseRef, WKURLRef wkPageURL, const void* clientInfo)
207{
208    ASSERT(RunLoop::isMain());
209    processPendingIconsForPageURL(WEBKIT_FAVICON_DATABASE(clientInfo), toImpl(wkPageURL)->string());
210}
211
212WebKitFaviconDatabase* webkitFaviconDatabaseCreate(WebIconDatabase* iconDatabase)
213{
214    WebKitFaviconDatabase* faviconDatabase = WEBKIT_FAVICON_DATABASE(g_object_new(WEBKIT_TYPE_FAVICON_DATABASE, NULL));
215    faviconDatabase->priv->iconDatabase = iconDatabase;
216
217    WKIconDatabaseClientV1 wkIconDatabaseClient = {
218        {
219            1, // version
220            faviconDatabase, // clientInfo
221        },
222        didChangeIconForPageURLCallback,
223        0, // didRemoveAllIconsCallback
224        iconDataReadyForPageURLCallback,
225    };
226    WKIconDatabaseSetIconDatabaseClient(toAPI(iconDatabase), &wkIconDatabaseClient.base);
227    return faviconDatabase;
228}
229
230static PendingIconRequestVector* getOrCreatePendingIconRequests(WebKitFaviconDatabase* database, const String& pageURL)
231{
232    PendingIconRequestVector* icons = database->priv->pendingIconRequests.get(pageURL);
233    if (!icons) {
234        icons = new PendingIconRequestVector;
235        database->priv->pendingIconRequests.set(pageURL, icons);
236    }
237
238    return icons;
239}
240
241GQuark webkit_favicon_database_error_quark(void)
242{
243    return g_quark_from_static_string("WebKitFaviconDatabaseError");
244}
245
246/**
247 * webkit_favicon_database_get_favicon:
248 * @database: a #WebKitFaviconDatabase
249 * @page_uri: URI of the page for which we want to retrieve the favicon
250 * @cancellable: (allow-none): A #GCancellable or %NULL.
251 * @callback: (scope async): A #GAsyncReadyCallback to call when the request is
252 *            satisfied or %NULL if you don't care about the result.
253 * @user_data: (closure): The data to pass to @callback.
254 *
255 * Asynchronously obtains a #cairo_surface_t of the favicon for the
256 * given page URI. It returns the cached icon if it's in the database
257 * asynchronously waiting for the icon to be read from the database.
258 *
259 * This is an asynchronous method. When the operation is finished, callback will
260 * be invoked. You can then call webkit_favicon_database_get_favicon_finish()
261 * to get the result of the operation.
262 */
263void webkit_favicon_database_get_favicon(WebKitFaviconDatabase* database, const gchar* pageURI, GCancellable* cancellable, GAsyncReadyCallback callback, gpointer userData)
264{
265    g_return_if_fail(WEBKIT_IS_FAVICON_DATABASE(database));
266    g_return_if_fail(pageURI);
267
268    WebKitFaviconDatabasePrivate* priv = database->priv;
269    WebIconDatabase* iconDatabaseImpl = priv->iconDatabase.get();
270    if (!iconDatabaseImpl->isOpen()) {
271        g_task_report_new_error(database, callback, userData, 0,
272            WEBKIT_FAVICON_DATABASE_ERROR, WEBKIT_FAVICON_DATABASE_ERROR_NOT_INITIALIZED, _("Favicons database not initialized yet"));
273        return;
274    }
275
276    if (g_str_has_prefix(pageURI, "about:")) {
277        g_task_report_new_error(database, callback, userData, 0,
278            WEBKIT_FAVICON_DATABASE_ERROR, WEBKIT_FAVICON_DATABASE_ERROR_FAVICON_NOT_FOUND, _("Page %s does not have a favicon"), pageURI);
279        return;
280    }
281
282    GRefPtr<GTask> task = adoptGRef(g_task_new(database, cancellable, callback, userData));
283
284    GetFaviconSurfaceAsyncData* data = createGetFaviconSurfaceAsyncData();
285    data->faviconDatabase = database;
286    data->pageURL = String::fromUTF8(pageURI);
287    g_task_set_task_data(task.get(), data, reinterpret_cast<GDestroyNotify>(destroyGetFaviconSurfaceAsyncData));
288
289    priv->iconDatabase->retainIconForPageURL(data->pageURL);
290
291    // We ask for the icon directly. If we don't get the icon data now,
292    // we'll be notified later (even if the database is still importing icons).
293    GUniqueOutPtr<GError> error;
294    data->icon = getIconSurfaceSynchronously(database, data->pageURL, &error.outPtr());
295    if (data->icon) {
296        g_task_return_boolean(task.get(), TRUE);
297        return;
298    }
299
300    // At this point we still don't know whether we will get a valid icon for pageURL.
301    data->shouldReleaseIconForPageURL = true;
302
303    if (g_error_matches(error.get(), WEBKIT_FAVICON_DATABASE_ERROR, WEBKIT_FAVICON_DATABASE_ERROR_FAVICON_NOT_FOUND)) {
304        g_task_return_error(task.get(), error.release().release());
305        return;
306    }
307
308    // If there's not a valid icon, but there's an iconURL registered,
309    // or it's still not registered but the import process hasn't
310    // finished yet, we need to wait for iconDataReadyForPage to be
311    // called before making and informed decision.
312    String iconURLForPageURL;
313    iconDatabaseImpl->synchronousIconURLForPageURL(data->pageURL, iconURLForPageURL);
314    if (!iconURLForPageURL.isEmpty() || !iconDatabaseImpl->isUrlImportCompleted()) {
315        PendingIconRequestVector* iconRequests = getOrCreatePendingIconRequests(database, data->pageURL);
316        ASSERT(iconRequests);
317        iconRequests->append(task);
318        return;
319    }
320
321    g_task_return_new_error(task.get(), WEBKIT_FAVICON_DATABASE_ERROR, WEBKIT_FAVICON_DATABASE_ERROR_FAVICON_UNKNOWN,
322        _("Unknown favicon for page %s"), pageURI);
323}
324
325/**
326 * webkit_favicon_database_get_favicon_finish:
327 * @database: a #WebKitFaviconDatabase
328 * @result: A #GAsyncResult obtained from the #GAsyncReadyCallback passed to webkit_favicon_database_get_favicon()
329 * @error: (allow-none): Return location for error or %NULL.
330 *
331 * Finishes an operation started with webkit_favicon_database_get_favicon().
332 *
333 * Returns: (transfer full): a new reference to a #cairo_surface_t, or
334 * %NULL in case of error.
335 */
336cairo_surface_t* webkit_favicon_database_get_favicon_finish(WebKitFaviconDatabase* database, GAsyncResult* result, GError** error)
337{
338    g_return_val_if_fail(WEBKIT_IS_FAVICON_DATABASE(database), 0);
339    g_return_val_if_fail(g_task_is_valid(result, database), 0);
340
341    GTask* task = G_TASK(result);
342    if (!g_task_propagate_boolean(task, error))
343        return 0;
344
345    GetFaviconSurfaceAsyncData* data = static_cast<GetFaviconSurfaceAsyncData*>(g_task_get_task_data(task));
346    return cairo_surface_reference(data->icon.get());
347}
348
349/**
350 * webkit_favicon_database_get_favicon_uri:
351 * @database: a #WebKitFaviconDatabase
352 * @page_uri: URI of the page containing the icon
353 *
354 * Obtains the URI of the favicon for the given @page_uri.
355 *
356 * Returns: a newly allocated URI for the favicon, or %NULL if the
357 * database doesn't have a favicon for @page_uri.
358 */
359gchar* webkit_favicon_database_get_favicon_uri(WebKitFaviconDatabase* database, const gchar* pageURL)
360{
361    g_return_val_if_fail(WEBKIT_IS_FAVICON_DATABASE(database), 0);
362    g_return_val_if_fail(pageURL, 0);
363    ASSERT(RunLoop::isMain());
364
365    String iconURLForPageURL;
366    database->priv->iconDatabase->synchronousIconURLForPageURL(String::fromUTF8(pageURL), iconURLForPageURL);
367    if (iconURLForPageURL.isEmpty())
368        return 0;
369
370    return g_strdup(iconURLForPageURL.utf8().data());
371}
372
373/**
374 * webkit_favicon_database_clear:
375 * @database: a #WebKitFaviconDatabase
376 *
377 * Clears all icons from the database.
378 */
379void webkit_favicon_database_clear(WebKitFaviconDatabase* database)
380{
381    g_return_if_fail(WEBKIT_IS_FAVICON_DATABASE(database));
382
383    database->priv->iconDatabase->removeAllIcons();
384}
385