1/*
2 * Copyright (C) 2012, 2017 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 "IconDatabase.h"
24#include "WebKitFaviconDatabasePrivate.h"
25#include "WebPreferences.h"
26#include <WebCore/Image.h>
27#include <WebCore/IntSize.h>
28#include <WebCore/RefPtrCairo.h>
29#include <WebCore/SharedBuffer.h>
30#include <glib/gi18n-lib.h>
31#include <wtf/FileSystem.h>
32#include <wtf/RunLoop.h>
33#include <wtf/SetForScope.h>
34#include <wtf/glib/GRefPtr.h>
35#include <wtf/glib/GUniquePtr.h>
36#include <wtf/glib/WTFGType.h>
37#include <wtf/text/CString.h>
38#include <wtf/text/StringHash.h>
39
40using namespace WebKit;
41using namespace WebCore;
42
43/**
44 * SECTION: WebKitFaviconDatabase
45 * @Short_description: A WebKit favicon database
46 * @Title: WebKitFaviconDatabase
47 *
48 * #WebKitFaviconDatabase provides access to the icons associated with
49 * web sites.
50 *
51 * WebKit will automatically look for available icons in &lt;link&gt;
52 * elements on opened pages as well as an existing favicon.ico and
53 * load the images found into a memory cache if possible. That cache
54 * is frozen to an on-disk database for persistence.
55 *
56 * If #WebKitSettings:enable-private-browsing is %TRUE, new icons
57 * won't be added to the on-disk database and no existing icons will
58 * be deleted from it. Nevertheless, WebKit will still store them in
59 * the in-memory cache during the current execution.
60 *
61 */
62
63enum {
64 FAVICON_CHANGED,
65
66 LAST_SIGNAL
67};
68
69static guint signals[LAST_SIGNAL] = { 0, };
70
71typedef Vector<GRefPtr<GTask> > PendingIconRequestVector;
72typedef HashMap<String, PendingIconRequestVector*> PendingIconRequestMap;
73
74struct _WebKitFaviconDatabasePrivate {
75 std::unique_ptr<IconDatabase> iconDatabase;
76 Vector<std::pair<String, Function<void(bool)>>> pendingLoadDecisions;
77 PendingIconRequestMap pendingIconRequests;
78 HashMap<String, String> pageURLToIconURLMap;
79 bool isURLImportCompleted;
80 bool isSettingIcon;
81};
82
83WEBKIT_DEFINE_TYPE(WebKitFaviconDatabase, webkit_favicon_database, G_TYPE_OBJECT)
84
85static void webkitFaviconDatabaseDispose(GObject* object)
86{
87 WebKitFaviconDatabase* database = WEBKIT_FAVICON_DATABASE(object);
88
89 if (webkitFaviconDatabaseIsOpen(database))
90 database->priv->iconDatabase->close();
91
92 G_OBJECT_CLASS(webkit_favicon_database_parent_class)->dispose(object);
93}
94
95static void webkit_favicon_database_class_init(WebKitFaviconDatabaseClass* faviconDatabaseClass)
96{
97 GObjectClass* gObjectClass = G_OBJECT_CLASS(faviconDatabaseClass);
98 gObjectClass->dispose = webkitFaviconDatabaseDispose;
99
100 /**
101 * WebKitFaviconDatabase::favicon-changed:
102 * @database: the object on which the signal is emitted
103 * @page_uri: the URI of the Web page containing the icon
104 * @favicon_uri: the URI of the favicon
105 *
106 * This signal is emitted when the favicon URI of @page_uri has
107 * been changed to @favicon_uri in the database. You can connect
108 * to this signal and call webkit_favicon_database_get_favicon()
109 * to get the favicon. If you are interested in the favicon of a
110 * #WebKitWebView it's easier to use the #WebKitWebView:favicon
111 * property. See webkit_web_view_get_favicon() for more details.
112 */
113 signals[FAVICON_CHANGED] = g_signal_new(
114 "favicon-changed",
115 G_TYPE_FROM_CLASS(faviconDatabaseClass),
116 G_SIGNAL_RUN_LAST,
117 0, nullptr, nullptr,
118 g_cclosure_marshal_generic,
119 G_TYPE_NONE, 2,
120 G_TYPE_STRING,
121 G_TYPE_STRING);
122}
123
124#if PLATFORM(GTK)
125struct GetFaviconSurfaceAsyncData {
126 ~GetFaviconSurfaceAsyncData()
127 {
128 if (shouldReleaseIconForPageURL)
129 faviconDatabase->priv->iconDatabase->releaseIconForPageURL(pageURL);
130 }
131
132 GRefPtr<WebKitFaviconDatabase> faviconDatabase;
133 String pageURL;
134 RefPtr<cairo_surface_t> icon;
135 GRefPtr<GCancellable> cancellable;
136 bool shouldReleaseIconForPageURL;
137};
138WEBKIT_DEFINE_ASYNC_DATA_STRUCT(GetFaviconSurfaceAsyncData)
139
140static RefPtr<cairo_surface_t> getIconSurfaceSynchronously(WebKitFaviconDatabase* database, const String& pageURL, GError** error)
141{
142 ASSERT(RunLoop::isMain());
143
144 // The exact size we pass is irrelevant to the iconDatabase code.
145 // We must pass something greater than 0x0 to get an icon.
146 auto iconData = database->priv->iconDatabase->synchronousIconForPageURL(pageURL, WebCore::IntSize(1, 1));
147 if (iconData.second == IconDatabase::IsKnownIcon::No) {
148 g_set_error(error, WEBKIT_FAVICON_DATABASE_ERROR, WEBKIT_FAVICON_DATABASE_ERROR_FAVICON_UNKNOWN, _("Unknown favicon for page %s"), pageURL.utf8().data());
149 return nullptr;
150 }
151
152 if (!iconData.first) {
153 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());
154 return nullptr;
155 }
156
157 return iconData.first;
158}
159
160static void deletePendingIconRequests(WebKitFaviconDatabase* database, PendingIconRequestVector* requests, const String& pageURL)
161{
162 database->priv->pendingIconRequests.remove(pageURL);
163 delete requests;
164}
165
166static void processPendingIconsForPageURL(WebKitFaviconDatabase* database, const String& pageURL)
167{
168 PendingIconRequestVector* pendingIconRequests = database->priv->pendingIconRequests.get(pageURL);
169 if (!pendingIconRequests)
170 return;
171
172 GUniqueOutPtr<GError> error;
173 RefPtr<cairo_surface_t> icon = getIconSurfaceSynchronously(database, pageURL, &error.outPtr());
174
175 for (size_t i = 0; i < pendingIconRequests->size(); ++i) {
176 GTask* task = pendingIconRequests->at(i).get();
177 if (error)
178 g_task_return_error(task, error.release().release());
179 else {
180 GetFaviconSurfaceAsyncData* data = static_cast<GetFaviconSurfaceAsyncData*>(g_task_get_task_data(task));
181 data->icon = icon;
182 data->shouldReleaseIconForPageURL = false;
183 g_task_return_boolean(task, TRUE);
184 }
185 }
186 deletePendingIconRequests(database, pendingIconRequests, pageURL);
187}
188#endif
189
190static void webkitFaviconDatabaseSetIconURLForPageURL(WebKitFaviconDatabase* database, const String& iconURL, const String& pageURL)
191{
192 WebKitFaviconDatabasePrivate* priv = database->priv;
193 if (!priv->isURLImportCompleted)
194 return;
195
196 if (pageURL.isEmpty())
197 return;
198
199 const String& currentIconURL = priv->pageURLToIconURLMap.get(pageURL);
200 if (iconURL == currentIconURL)
201 return;
202
203 priv->pageURLToIconURLMap.set(pageURL, iconURL);
204 if (priv->isSettingIcon)
205 return;
206
207 g_signal_emit(database, signals[FAVICON_CHANGED], 0, pageURL.utf8().data(), iconURL.utf8().data());
208}
209
210class WebKitIconDatabaseClient final : public IconDatabaseClient {
211public:
212 explicit WebKitIconDatabaseClient(WebKitFaviconDatabase* database)
213 : m_database(database)
214 {
215 }
216
217private:
218 void didImportIconURLForPageURL(const String& pageURL) override
219 {
220 String iconURL = m_database->priv->iconDatabase->synchronousIconURLForPageURL(pageURL);
221 webkitFaviconDatabaseSetIconURLForPageURL(m_database, iconURL, pageURL);
222 }
223
224 void didChangeIconForPageURL(const String& pageURL) override
225 {
226 if (m_database->priv->isSettingIcon)
227 return;
228 String iconURL = m_database->priv->iconDatabase->synchronousIconURLForPageURL(pageURL);
229 webkitFaviconDatabaseSetIconURLForPageURL(m_database, iconURL, pageURL);
230 }
231
232 void didImportIconDataForPageURL(const String& pageURL) override
233 {
234#if PLATFORM(GTK)
235 processPendingIconsForPageURL(m_database, pageURL);
236#endif
237 String iconURL = m_database->priv->iconDatabase->synchronousIconURLForPageURL(pageURL);
238 webkitFaviconDatabaseSetIconURLForPageURL(m_database, iconURL, pageURL);
239 }
240
241 void didFinishURLImport() override
242 {
243 WebKitFaviconDatabasePrivate* priv = m_database->priv;
244
245 while (!priv->pendingLoadDecisions.isEmpty()) {
246 auto iconURLAndCallback = priv->pendingLoadDecisions.takeLast();
247 auto decision = priv->iconDatabase->synchronousLoadDecisionForIconURL(iconURLAndCallback.first);
248 // Decisions should never be unknown after the inital import is complete.
249 ASSERT(decision != IconDatabase::IconLoadDecision::Unknown);
250 iconURLAndCallback.second(decision == IconDatabase::IconLoadDecision::Yes);
251 }
252
253 priv->isURLImportCompleted = true;
254 }
255
256 WebKitFaviconDatabase* m_database;
257};
258
259WebKitFaviconDatabase* webkitFaviconDatabaseCreate()
260{
261 return WEBKIT_FAVICON_DATABASE(g_object_new(WEBKIT_TYPE_FAVICON_DATABASE, nullptr));
262}
263
264void webkitFaviconDatabaseOpen(WebKitFaviconDatabase* database, const String& path)
265{
266 if (webkitFaviconDatabaseIsOpen(database))
267 return;
268
269 WebKitFaviconDatabasePrivate* priv = database->priv;
270 priv->iconDatabase = std::make_unique<IconDatabase>();
271 priv->iconDatabase->setClient(std::make_unique<WebKitIconDatabaseClient>(database));
272 IconDatabase::delayDatabaseCleanup();
273 priv->iconDatabase->setEnabled(true);
274 priv->iconDatabase->setPrivateBrowsingEnabled(WebPreferences::anyPagesAreUsingPrivateBrowsing());
275
276 if (!priv->iconDatabase->open(FileSystem::directoryName(path), FileSystem::pathGetFileName(path))) {
277 priv->iconDatabase = nullptr;
278 IconDatabase::allowDatabaseCleanup();
279 }
280}
281
282bool webkitFaviconDatabaseIsOpen(WebKitFaviconDatabase* database)
283{
284 return database->priv->iconDatabase && database->priv->iconDatabase->isOpen();
285}
286
287void webkitFaviconDatabaseSetPrivateBrowsingEnabled(WebKitFaviconDatabase* database, bool enabled)
288{
289 if (database->priv->iconDatabase)
290 database->priv->iconDatabase->setPrivateBrowsingEnabled(enabled);
291}
292
293#if PLATFORM(GTK)
294void webkitFaviconDatabaseGetLoadDecisionForIcon(WebKitFaviconDatabase* database, const LinkIcon& icon, const String& pageURL, Function<void(bool)>&& completionHandler)
295{
296 if (!webkitFaviconDatabaseIsOpen(database)) {
297 completionHandler(false);
298 return;
299 }
300
301 WebKitFaviconDatabasePrivate* priv = database->priv;
302 auto decision = priv->iconDatabase->synchronousLoadDecisionForIconURL(icon.url.string());
303 switch (decision) {
304 case IconDatabase::IconLoadDecision::Unknown:
305 priv->pendingLoadDecisions.append(std::make_pair(icon.url.string(), WTFMove(completionHandler)));
306 priv->iconDatabase->setIconURLForPageURL(icon.url.string(), pageURL);
307 break;
308 case IconDatabase::IconLoadDecision::No:
309 priv->iconDatabase->setIconURLForPageURL(icon.url.string(), pageURL);
310 completionHandler(false);
311 break;
312 case IconDatabase::IconLoadDecision::Yes:
313 completionHandler(true);
314 break;
315 }
316}
317
318void webkitFaviconDatabaseSetIconForPageURL(WebKitFaviconDatabase* database, const LinkIcon& icon, API::Data& iconData, const String& pageURL)
319{
320 if (!webkitFaviconDatabaseIsOpen(database))
321 return;
322
323 if (pageURL.isEmpty())
324 return;
325
326 WebKitFaviconDatabasePrivate* priv = database->priv;
327 SetForScope<bool> change(priv->isSettingIcon, true);
328 priv->iconDatabase->setIconURLForPageURL(icon.url.string(), pageURL);
329 priv->iconDatabase->setIconDataForIconURL(SharedBuffer::create(iconData.bytes(), iconData.size()), icon.url.string());
330 webkitFaviconDatabaseSetIconURLForPageURL(database, icon.url.string(), pageURL);
331 g_signal_emit(database, signals[FAVICON_CHANGED], 0, pageURL.utf8().data(), icon.url.string().utf8().data());
332 processPendingIconsForPageURL(database, pageURL);
333}
334
335static PendingIconRequestVector* getOrCreatePendingIconRequests(WebKitFaviconDatabase* database, const String& pageURL)
336{
337 PendingIconRequestVector* icons = database->priv->pendingIconRequests.get(pageURL);
338 if (!icons) {
339 icons = new PendingIconRequestVector;
340 database->priv->pendingIconRequests.set(pageURL, icons);
341 }
342
343 return icons;
344}
345#endif
346
347GQuark webkit_favicon_database_error_quark(void)
348{
349 return g_quark_from_static_string("WebKitFaviconDatabaseError");
350}
351
352#if PLATFORM(GTK)
353/**
354 * webkit_favicon_database_get_favicon:
355 * @database: a #WebKitFaviconDatabase
356 * @page_uri: URI of the page for which we want to retrieve the favicon
357 * @cancellable: (allow-none): A #GCancellable or %NULL.
358 * @callback: (scope async): A #GAsyncReadyCallback to call when the request is
359 * satisfied or %NULL if you don't care about the result.
360 * @user_data: (closure): The data to pass to @callback.
361 *
362 * Asynchronously obtains a #cairo_surface_t of the favicon for the
363 * given page URI. It returns the cached icon if it's in the database
364 * asynchronously waiting for the icon to be read from the database.
365 *
366 * This is an asynchronous method. When the operation is finished, callback will
367 * be invoked. You can then call webkit_favicon_database_get_favicon_finish()
368 * to get the result of the operation.
369 *
370 * You must call webkit_web_context_set_favicon_database_directory() for
371 * the #WebKitWebContext associated with this #WebKitFaviconDatabase
372 * before attempting to use this function; otherwise,
373 * webkit_favicon_database_get_favicon_finish() will return
374 * %WEBKIT_FAVICON_DATABASE_ERROR_NOT_INITIALIZED.
375 */
376void webkit_favicon_database_get_favicon(WebKitFaviconDatabase* database, const gchar* pageURI, GCancellable* cancellable, GAsyncReadyCallback callback, gpointer userData)
377{
378 g_return_if_fail(WEBKIT_IS_FAVICON_DATABASE(database));
379 g_return_if_fail(pageURI);
380
381 if (!webkitFaviconDatabaseIsOpen(database)) {
382 g_task_report_new_error(database, callback, userData, 0,
383 WEBKIT_FAVICON_DATABASE_ERROR, WEBKIT_FAVICON_DATABASE_ERROR_NOT_INITIALIZED, _("Favicons database not initialized yet"));
384 return;
385 }
386
387 if (g_str_has_prefix(pageURI, "about:")) {
388 g_task_report_new_error(database, callback, userData, 0,
389 WEBKIT_FAVICON_DATABASE_ERROR, WEBKIT_FAVICON_DATABASE_ERROR_FAVICON_NOT_FOUND, _("Page %s does not have a favicon"), pageURI);
390 return;
391 }
392
393 GRefPtr<GTask> task = adoptGRef(g_task_new(database, cancellable, callback, userData));
394
395 GetFaviconSurfaceAsyncData* data = createGetFaviconSurfaceAsyncData();
396 data->faviconDatabase = database;
397 data->pageURL = String::fromUTF8(pageURI);
398 g_task_set_task_data(task.get(), data, reinterpret_cast<GDestroyNotify>(destroyGetFaviconSurfaceAsyncData));
399
400 WebKitFaviconDatabasePrivate* priv = database->priv;
401 priv->iconDatabase->retainIconForPageURL(data->pageURL);
402
403 // We ask for the icon directly. If we don't get the icon data now,
404 // we'll be notified later (even if the database is still importing icons).
405 GUniqueOutPtr<GError> error;
406 data->icon = getIconSurfaceSynchronously(database, data->pageURL, &error.outPtr());
407 if (data->icon) {
408 g_task_return_boolean(task.get(), TRUE);
409 return;
410 }
411
412 // At this point we still don't know whether we will get a valid icon for pageURL.
413 data->shouldReleaseIconForPageURL = true;
414
415 if (g_error_matches(error.get(), WEBKIT_FAVICON_DATABASE_ERROR, WEBKIT_FAVICON_DATABASE_ERROR_FAVICON_NOT_FOUND)) {
416 g_task_return_error(task.get(), error.release().release());
417 return;
418 }
419
420 // If there's not a valid icon, but there's an iconURL registered,
421 // or it's still not registered but the import process hasn't
422 // finished yet, we need to wait for iconDataReadyForPage to be
423 // called before making and informed decision.
424 String iconURLForPageURL = priv->iconDatabase->synchronousIconURLForPageURL(data->pageURL);
425 if (!iconURLForPageURL.isEmpty() || !priv->isURLImportCompleted) {
426 PendingIconRequestVector* iconRequests = getOrCreatePendingIconRequests(database, data->pageURL);
427 ASSERT(iconRequests);
428 iconRequests->append(task);
429 return;
430 }
431
432 g_task_return_new_error(task.get(), WEBKIT_FAVICON_DATABASE_ERROR, WEBKIT_FAVICON_DATABASE_ERROR_FAVICON_UNKNOWN,
433 _("Unknown favicon for page %s"), pageURI);
434}
435
436/**
437 * webkit_favicon_database_get_favicon_finish:
438 * @database: a #WebKitFaviconDatabase
439 * @result: A #GAsyncResult obtained from the #GAsyncReadyCallback passed to webkit_favicon_database_get_favicon()
440 * @error: (allow-none): Return location for error or %NULL.
441 *
442 * Finishes an operation started with webkit_favicon_database_get_favicon().
443 *
444 * Returns: (transfer full): a new reference to a #cairo_surface_t, or
445 * %NULL in case of error.
446 */
447cairo_surface_t* webkit_favicon_database_get_favicon_finish(WebKitFaviconDatabase* database, GAsyncResult* result, GError** error)
448{
449 g_return_val_if_fail(WEBKIT_IS_FAVICON_DATABASE(database), 0);
450 g_return_val_if_fail(g_task_is_valid(result, database), 0);
451
452 GTask* task = G_TASK(result);
453 if (!g_task_propagate_boolean(task, error))
454 return 0;
455
456 GetFaviconSurfaceAsyncData* data = static_cast<GetFaviconSurfaceAsyncData*>(g_task_get_task_data(task));
457 return cairo_surface_reference(data->icon.get());
458}
459#endif
460
461/**
462 * webkit_favicon_database_get_favicon_uri:
463 * @database: a #WebKitFaviconDatabase
464 * @page_uri: URI of the page containing the icon
465 *
466 * Obtains the URI of the favicon for the given @page_uri.
467 *
468 * Returns: a newly allocated URI for the favicon, or %NULL if the
469 * database doesn't have a favicon for @page_uri.
470 */
471gchar* webkit_favicon_database_get_favicon_uri(WebKitFaviconDatabase* database, const gchar* pageURL)
472{
473 g_return_val_if_fail(WEBKIT_IS_FAVICON_DATABASE(database), nullptr);
474 g_return_val_if_fail(pageURL, nullptr);
475 ASSERT(RunLoop::isMain());
476
477 if (!webkitFaviconDatabaseIsOpen(database))
478 return nullptr;
479
480 String iconURLForPageURL = database->priv->iconDatabase->synchronousIconURLForPageURL(String::fromUTF8(pageURL));
481 if (iconURLForPageURL.isEmpty())
482 return nullptr;
483
484 return g_strdup(iconURLForPageURL.utf8().data());
485}
486
487/**
488 * webkit_favicon_database_clear:
489 * @database: a #WebKitFaviconDatabase
490 *
491 * Clears all icons from the database.
492 */
493void webkit_favicon_database_clear(WebKitFaviconDatabase* database)
494{
495 g_return_if_fail(WEBKIT_IS_FAVICON_DATABASE(database));
496
497 if (!webkitFaviconDatabaseIsOpen(database))
498 return;
499
500 database->priv->iconDatabase->removeAllIcons();
501}
502