1/*
2 * Copyright (C) 2018-2019 Igalia S.L.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23 * THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26#include "config.h"
27#include "WebKitUserContentFilterStore.h"
28
29#include "APIContentRuleList.h"
30#include "APIContentRuleListStore.h"
31#include "WebKitError.h"
32#include "WebKitUserContent.h"
33#include "WebKitUserContentPrivate.h"
34#include <WebCore/ContentExtensionError.h>
35#include <glib/gi18n-lib.h>
36#include <wtf/CompletionHandler.h>
37#include <wtf/FileSystem.h>
38#include <wtf/RefPtr.h>
39#include <wtf/glib/GRefPtr.h>
40#include <wtf/glib/GUniquePtr.h>
41#include <wtf/glib/WTFGType.h>
42
43/**
44 * SECTION: WebKitUserContentFilterStore
45 * @Short_description: Handles storage of user content filters on disk.
46 * @Title: WebKitUserContentFilterStore
47 *
48 * The WebKitUserContentFilterStore provides the means to import and save
49 * [JSON rule sets](https://webkit.org/blog/3476/content-blockers-first-look/),
50 * which can be loaded later in an efficient manner. Once filters are stored,
51 * the #WebKitUserContentFilter objects which represent them can be added to
52 * a #WebKitUserContentManager with webkit_user_content_manager_add_filter().
53 *
54 * JSON rule sets are imported using webkit_user_content_filter_store_save() and stored
55 * on disk in an implementation defined format. The contents of a filter store must be
56 * managed using the #WebKitUserContentFilterStore: a list of all the stored filters
57 * can be obtained with webkit_user_content_filter_store_fetch_identifiers(),
58 * webkit_user_content_filter_store_load() can be used to retrieve a previously saved
59 * filter, and removed from the store with webkit_user_content_filter_store_remove().
60 *
61 * Since: 2.24
62 */
63
64enum {
65 PROP_0,
66
67 PROP_PATH,
68};
69
70static inline GError* toGError(WebKitUserContentFilterError code, const std::error_code error)
71{
72 ASSERT(error);
73 ASSERT(error.category() == WebCore::ContentExtensions::contentExtensionErrorCategory());
74 return g_error_new_literal(WEBKIT_USER_CONTENT_FILTER_ERROR, code, error.message().c_str());
75}
76
77struct _WebKitUserContentFilterStorePrivate {
78 GUniquePtr<char> storagePath;
79 RefPtr<API::ContentRuleListStore> store;
80};
81
82WEBKIT_DEFINE_TYPE(WebKitUserContentFilterStore, webkit_user_content_filter_store, G_TYPE_OBJECT)
83
84static void webkitUserContentFilterStoreGetProperty(GObject* object, guint propID, GValue* value, GParamSpec* paramSpec)
85{
86 WebKitUserContentFilterStore* store = WEBKIT_USER_CONTENT_FILTER_STORE(object);
87
88 switch (propID) {
89 case PROP_PATH:
90 g_value_set_string(value, webkit_user_content_filter_store_get_path(store));
91 break;
92 default:
93 G_OBJECT_WARN_INVALID_PROPERTY_ID(object, propID, paramSpec);
94 }
95}
96
97static void webkitUserContentFilterStoreSetProperty(GObject* object, guint propID, const GValue* value, GParamSpec* paramSpec)
98{
99 WebKitUserContentFilterStore* store = WEBKIT_USER_CONTENT_FILTER_STORE(object);
100
101 switch (propID) {
102 case PROP_PATH:
103 store->priv->storagePath.reset(g_value_dup_string(value));
104 break;
105 default:
106 G_OBJECT_WARN_INVALID_PROPERTY_ID(object, propID, paramSpec);
107 }
108}
109
110static void webkitUserContentFilterStoreConstructed(GObject* object)
111{
112 G_OBJECT_CLASS(webkit_user_content_filter_store_parent_class)->constructed(object);
113
114 WebKitUserContentFilterStore* store = WEBKIT_USER_CONTENT_FILTER_STORE(object);
115 store->priv->store = adoptRef(new API::ContentRuleListStore(FileSystem::stringFromFileSystemRepresentation(store->priv->storagePath.get()), false));
116}
117
118static void webkit_user_content_filter_store_class_init(WebKitUserContentFilterStoreClass* storeClass)
119{
120 GObjectClass* gObjectClass = G_OBJECT_CLASS(storeClass);
121
122 gObjectClass->get_property = webkitUserContentFilterStoreGetProperty;
123 gObjectClass->set_property = webkitUserContentFilterStoreSetProperty;
124 gObjectClass->constructed = webkitUserContentFilterStoreConstructed;
125
126 /**
127 * WebKitUserContentFilterStore:path:
128 *
129 * The directory used for filter storage. This path is used as the base
130 * directory where user content filters are stored on disk.
131 *
132 * Since: 2.24
133 */
134 g_object_class_install_property(
135 gObjectClass,
136 PROP_PATH,
137 g_param_spec_string(
138 "path",
139 _("Storage directory path"),
140 _("The directory where user content filters are stored"),
141 nullptr,
142 static_cast<GParamFlags>(WEBKIT_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY)));
143}
144
145/**
146 * webkit_user_content_filter_store_new:
147 * @storage_path: path where data for filters will be stored on disk
148 *
149 * Create a new #WebKitUserContentFilterStore to manipulate filters stored at @storage_path.
150 * The path must point to a local filesystem, and will be created if needed.
151 *
152 * Returns: (transfer full): a newly created #WebKitUserContentFilterStore
153 *
154 * Since: 2.24
155 */
156WebKitUserContentFilterStore* webkit_user_content_filter_store_new(const gchar* storagePath)
157{
158 g_return_val_if_fail(storagePath, nullptr);
159 return WEBKIT_USER_CONTENT_FILTER_STORE(g_object_new(WEBKIT_TYPE_USER_CONTENT_FILTER_STORE, "path", storagePath, nullptr));
160}
161
162/**
163 * webkit_user_content_filter_store_get_path:
164 * @store: a #WebKitUserContentFilterStore
165 *
166 * Returns: (transfer none): The storage path for user content filters.
167 *
168 * Since: 2.24
169 */
170const char* webkit_user_content_filter_store_get_path(WebKitUserContentFilterStore* store)
171{
172 g_return_val_if_fail(WEBKIT_IS_USER_CONTENT_FILTER_STORE(store), nullptr);
173 return store->priv->storagePath.get();
174}
175
176static void webkitUserContentFilterStoreSaveBytes(GRefPtr<GTask>&& task, String&& identifier, GRefPtr<GBytes>&& source)
177{
178 size_t sourceSize;
179 const char* sourceData = static_cast<const char*>(g_bytes_get_data(source.get(), &sourceSize));
180 if (!sourceSize) {
181 g_task_return_error(task.get(), g_error_new_literal(WEBKIT_USER_CONTENT_FILTER_ERROR, WEBKIT_USER_CONTENT_FILTER_ERROR_INVALID_SOURCE, "Source JSON rule set cannot be empty"));
182 return;
183 }
184
185 auto* store = WEBKIT_USER_CONTENT_FILTER_STORE(g_task_get_source_object(task.get()));
186 store->priv->store->compileContentRuleList(identifier, String::fromUTF8(sourceData, sourceSize), [task = WTFMove(task)](RefPtr<API::ContentRuleList> contentRuleList, std::error_code error) {
187 if (g_task_return_error_if_cancelled(task.get()))
188 return;
189
190 if (error)
191 g_task_return_error(task.get(), toGError(WEBKIT_USER_CONTENT_FILTER_ERROR_INVALID_SOURCE, error));
192 else
193 g_task_return_pointer(task.get(), webkitUserContentFilterCreate(WTFMove(contentRuleList)), reinterpret_cast<GDestroyNotify>(webkit_user_content_filter_unref));
194 });
195}
196
197/**
198 * webkit_user_content_filter_store_save:
199 * @store: a #WebKitUserContentFilterStore
200 * @identifier: a string used to identify the saved filter
201 * @source: #GBytes containing the rule set in JSON format
202 * @cancellable: (allow-none): a #GCancellable or %NULL to ignore
203 * @callback: (scope async): a #GAsyncReadyCallback to call when saving is completed
204 * @user_data: (closure): the data to pass to the callback function
205 *
206 * Asynchronously save a content filter from a source rule set in the
207 * [WebKit content extesions JSON format](https://webkit.org/blog/3476/content-blockers-first-look/).
208 *
209 * The @identifier can be used afterwards to refer to the filter when using
210 * webkit_user_content_filter_store_remove() and webkit_user_content_filter_store_load().
211 * When the @identifier has been used in the past, the new filter source will replace
212 * the one saved beforehand for the same identifier.
213 *
214 * When the operation is finished, @callback will be invoked, which then can use
215 * webkit_user_content_filter_store_save_finish() to obtain the resulting filter.
216 *
217 * Since: 2.24
218 */
219void webkit_user_content_filter_store_save(WebKitUserContentFilterStore* store, const gchar* identifier, GBytes* source, GCancellable* cancellable, GAsyncReadyCallback callback, gpointer userData)
220{
221 g_return_if_fail(WEBKIT_IS_USER_CONTENT_FILTER_STORE(store));
222 g_return_if_fail(identifier);
223 g_return_if_fail(source);
224 g_return_if_fail(callback);
225
226 GRefPtr<GTask> task = adoptGRef(g_task_new(store, cancellable, callback, userData));
227 webkitUserContentFilterStoreSaveBytes(WTFMove(task), String::fromUTF8(identifier), GRefPtr<GBytes>(source));
228}
229
230/**
231 * webkit_user_content_filter_store_save_finish:
232 * @store: a #WebKitUserContentFilterStore
233 * @result: a #GAsyncResult
234 * @error: return location for error or %NULL to ignore
235 *
236 * Finishes an asynchronous filter save previously started with
237 * webkit_user_content_filter_store_save().
238 *
239 * Returns: (transfer full): a #WebKitUserContentFilter, or %NULL if saving failed
240 *
241 * Since: 2.24
242 */
243WebKitUserContentFilter* webkit_user_content_filter_store_save_finish(WebKitUserContentFilterStore* store, GAsyncResult* result, GError** error)
244{
245 g_return_val_if_fail(WEBKIT_IS_USER_CONTENT_FILTER_STORE(store), nullptr);
246 g_return_val_if_fail(result, nullptr);
247 return static_cast<WebKitUserContentFilter*>(g_task_propagate_pointer(G_TASK(result), error));
248}
249
250struct SaveTaskData {
251 String identifier;
252};
253WEBKIT_DEFINE_ASYNC_DATA_STRUCT(SaveTaskData)
254
255/**
256 * webkit_user_content_filter_store_save_from_file:
257 * @store: a #WebKitUserContentFilterStore
258 * @identifier: a string used to identify the saved filter
259 * @file: a #GFile containing the rule set in JSON format
260 * @cancellable: (allow-none): a #GCancellable or %NULL to ignore
261 * @callback: (scope async): a #GAsyncReadyCallback to call when saving is completed
262 * @user_data: (closure): the data to pass to the callback function
263 *
264 * Asynchronously save a content filter from the contents of a file, which must be
265 * native to the platform, as checked by g_file_is_native(). See
266 * webkit_user_content_filter_store_save() for more details.
267 *
268 * When the operation is finished, @callback will be invoked, which then can use
269 * webkit_user_content_filter_store_save_finish() to obtain the resulting filter.
270 *
271 * Since: 2.24
272 */
273void webkit_user_content_filter_store_save_from_file(WebKitUserContentFilterStore* store, const gchar* identifier, GFile* file, GCancellable* cancellable, GAsyncReadyCallback callback, gpointer userData)
274{
275 g_return_if_fail(WEBKIT_IS_USER_CONTENT_FILTER_STORE(store));
276 g_return_if_fail(identifier);
277 g_return_if_fail(G_IS_FILE(file));
278 g_return_if_fail(callback);
279
280 GRefPtr<GTask> task = adoptGRef(g_task_new(store, cancellable, callback, userData));
281
282 // Try mapping the file in memory first, and fall-back to reading the contents if that fails.
283 if (g_file_is_native(file)) {
284 GUniquePtr<char> filePath(g_file_get_path(file));
285 GRefPtr<GMappedFile> mappedFile = adoptGRef(g_mapped_file_new(filePath.get(), FALSE, nullptr));
286 if (mappedFile) {
287 GRefPtr<GBytes> source = adoptGRef(g_mapped_file_get_bytes(mappedFile.get()));
288 webkitUserContentFilterStoreSaveBytes(WTFMove(task), String::fromUTF8(identifier), WTFMove(source));
289 return;
290 }
291 }
292
293 // Pass the identifier as task data to be used in the completion callback once the contents have been loaded.
294 SaveTaskData* data = createSaveTaskData();
295 data->identifier = String::fromUTF8(identifier);
296 g_task_set_task_data(task.get(), data, reinterpret_cast<GDestroyNotify>(destroySaveTaskData));
297
298 g_file_load_contents_async(file, cancellable, [](GObject* sourceObject, GAsyncResult* result, void* userData) {
299 GRefPtr<GTask> task = adoptGRef(G_TASK(userData));
300 if (g_task_return_error_if_cancelled(task.get()))
301 return;
302
303 char* sourceData;
304 size_t sourceSize;
305 GUniqueOutPtr<GError> error;
306 if (g_file_load_contents_finish(G_FILE(sourceObject), result, &sourceData, &sourceSize, nullptr, &error.outPtr())) {
307 SaveTaskData* data = static_cast<SaveTaskData*>(g_task_get_task_data(task.get()));
308 webkitUserContentFilterStoreSaveBytes(WTFMove(task), WTFMove(data->identifier), GRefPtr<GBytes>(g_bytes_new_take(sourceData, sourceSize)));
309 } else
310 g_task_return_error(task.get(), error.release().release());
311 }, task.leakRef());
312}
313
314/**
315 * webkit_user_content_filter_store_save_from_file_finish:
316 * @store: a #WebKitUserContentFilterStore
317 * @result: a #GAsyncResult
318 * @error: return location for error or %NULL to ignore
319 *
320 * Finishes and asynchronous filter save previously started with
321 * webkit_user_content_filter_store_save_from_file().
322 *
323 * Returns: (transfer full): a #WebKitUserContentFilter, or %NULL if saving failed.
324 *
325 * Since: 2.24
326 */
327WebKitUserContentFilter* webkit_user_content_filter_store_save_from_file_finish(WebKitUserContentFilterStore* store, GAsyncResult* result, GError** error)
328{
329 g_return_val_if_fail(WEBKIT_IS_USER_CONTENT_FILTER_STORE(store), nullptr);
330 g_return_val_if_fail(result, nullptr);
331 return static_cast<WebKitUserContentFilter*>(g_task_propagate_pointer(G_TASK(result), error));
332}
333
334/**
335 * webkit_user_content_filter_store_remove:
336 * @store: a #WebKitUserContentFilterStore
337 * @identifier: a filter identifier
338 * @cancellable: (allow-none): a #GCancellable or %NULL to ignore
339 * @callback: (scope async): a #GAsyncReadyCallback to call when the removal is completed
340 * @user_data: (closure): the data to pass to the callback function
341 *
342 * Asynchronously remove a content filter given its @identifier.
343 *
344 * When the operation is finished, @callback will be invoked, which then can use
345 * webkit_user_content_filter_store_remove_finish() to check whether the removal was
346 * successful.
347 *
348 * Since: 2.24
349 */
350void webkit_user_content_filter_store_remove(WebKitUserContentFilterStore* store, const gchar* identifier, GCancellable* cancellable, GAsyncReadyCallback callback, gpointer userData)
351{
352 g_return_if_fail(WEBKIT_IS_USER_CONTENT_FILTER_STORE(store));
353 g_return_if_fail(identifier);
354 g_return_if_fail(callback);
355
356 GRefPtr<GTask> task = adoptGRef(g_task_new(store, cancellable, callback, userData));
357 store->priv->store->removeContentRuleList(String::fromUTF8(identifier), [task = WTFMove(task)](std::error_code error) {
358 if (g_task_return_error_if_cancelled(task.get()))
359 return;
360
361 if (error) {
362 ASSERT(static_cast<API::ContentRuleListStore::Error>(error.value()) == API::ContentRuleListStore::Error::RemoveFailed);
363 g_task_return_error(task.get(), toGError(WEBKIT_USER_CONTENT_FILTER_ERROR_NOT_FOUND, error));
364 } else
365 g_task_return_boolean(task.get(), TRUE);
366 });
367}
368
369/**
370 * webkit_user_content_filter_store_remove_finish:
371 * @store: a #WebKitUserContentFilterStore
372 * @result: a #GAsyncResult
373 * @error: return location for error or %NULL to ignore
374 *
375 * Finishes an asynchronous filter removal previously started with
376 * webkit_user_content_filter_store_remove().
377 *
378 * Returns: whether the removal was successful
379 *
380 * Since: 2.24
381 */
382gboolean webkit_user_content_filter_store_remove_finish(WebKitUserContentFilterStore* store, GAsyncResult* result, GError** error)
383{
384 g_return_val_if_fail(WEBKIT_IS_USER_CONTENT_FILTER_STORE(store), FALSE);
385 g_return_val_if_fail(result, FALSE);
386 return g_task_propagate_boolean(G_TASK(result), error);
387}
388
389/**
390 * webkit_user_content_filter_store_load:
391 * @store: a #WebKitUserContentFilterStore
392 * @identifier: a filter identifier
393 * @cancellable: (allow-none): a #GCancellable or %NULL to ignore
394 * @callback: (scope async): a #GAsyncReadyCallback to call when the load is completed
395 * @user_data: (closure): the data to pass to the callback function
396 *
397 * Asynchronously load a content filter given its @identifier. The filter must have been
398 * previously stored using webkit_user_content_filter_store_save().
399 *
400 * When the operation is finished, @callback will be invoked, which then can use
401 * webkit_user_content_filter_store_load_finish() to obtain the resulting filter.
402 *
403 * Since: 2.24
404 */
405void webkit_user_content_filter_store_load(WebKitUserContentFilterStore* store, const gchar* identifier, GCancellable* cancellable, GAsyncReadyCallback callback, gpointer userData)
406{
407 g_return_if_fail(WEBKIT_IS_USER_CONTENT_FILTER_STORE(store));
408 g_return_if_fail(identifier);
409 g_return_if_fail(callback);
410
411 GRefPtr<GTask> task = adoptGRef(g_task_new(store, cancellable, callback, userData));
412 store->priv->store->lookupContentRuleList(String::fromUTF8(identifier), [task = WTFMove(task)](RefPtr<API::ContentRuleList> contentRuleList, std::error_code error) {
413 if (g_task_return_error_if_cancelled(task.get()))
414 return;
415
416 if (error) {
417 ASSERT(static_cast<API::ContentRuleListStore::Error>(error.value()) == API::ContentRuleListStore::Error::LookupFailed
418 || static_cast<API::ContentRuleListStore::Error>(error.value()) == API::ContentRuleListStore::Error::VersionMismatch);
419 g_task_return_error(task.get(), toGError(WEBKIT_USER_CONTENT_FILTER_ERROR_NOT_FOUND, error));
420 } else
421 g_task_return_pointer(task.get(), webkitUserContentFilterCreate(WTFMove(contentRuleList)), reinterpret_cast<GDestroyNotify>(webkit_user_content_filter_unref));
422 });
423}
424
425/**
426 * webkit_user_content_filter_store_load_finish:
427 * @store: a #WebKitUserContentFilterStore
428 * @result: a #GAsyncResult
429 * @error: return location for error or %NULL to ignore
430 *
431 * Finishes an asynchronous filter load previously started with
432 * webkit_user_content_filter_store_load().
433 *
434 * Returns: (transfer full): a #WebKitUserContentFilter, or %NULL if the load failed
435 *
436 * Since: 2.24
437 */
438WebKitUserContentFilter* webkit_user_content_filter_store_load_finish(WebKitUserContentFilterStore* store, GAsyncResult* result, GError** error)
439{
440 g_return_val_if_fail(WEBKIT_IS_USER_CONTENT_FILTER_STORE(store), nullptr);
441 g_return_val_if_fail(result, nullptr);
442 return static_cast<WebKitUserContentFilter*>(g_task_propagate_pointer(G_TASK(result), error));
443}
444
445/**
446 * webkit_user_content_filter_store_fetch_identifiers:
447 * @store: a #WebKitUserContentFilterStore
448 * @cancellable: (allow-none): a #GCancellable or %NULL to ignore
449 * @callback: (scope async): a #GAsyncReadyCallback to call when the removal is completed
450 * @user_data: (closure): the data to pass to the callback function
451 *
452 * Asynchronously retrieve a list of the identifiers for all the stored filters.
453 *
454 * When the operation is finished, @callback will be invoked, which then can use
455 * webkit_user_content_filter_store_fetch_identifiers_finish() to obtain the list of
456 * filter identifiers.
457 *
458 * Since: 2.24
459 */
460void webkit_user_content_filter_store_fetch_identifiers(WebKitUserContentFilterStore* store, GCancellable* cancellable, GAsyncReadyCallback callback, gpointer userData)
461{
462 g_return_if_fail(WEBKIT_IS_USER_CONTENT_FILTER_STORE(store));
463 g_return_if_fail(callback);
464
465 GRefPtr<GTask> task = adoptGRef(g_task_new(store, cancellable, callback, userData));
466 store->priv->store->getAvailableContentRuleListIdentifiers([task = WTFMove(task)](WTF::Vector<WTF::String> identifiers) {
467 if (g_task_return_error_if_cancelled(task.get()))
468 return;
469
470 GStrv result = static_cast<GStrv>(g_new0(gchar*, identifiers.size() + 1));
471 for (size_t i = 0; i < identifiers.size(); ++i)
472 result[i] = g_strdup(identifiers[i].utf8().data());
473 g_task_return_pointer(task.get(), result, reinterpret_cast<GDestroyNotify>(g_strfreev));
474 });
475}
476
477/**
478 * webkit_user_content_filter_store_fetch_identifiers_finish:
479 * @store: a #WebKitUserContentFilterStore
480 * @result: a #GAsyncResult
481 *
482 * Finishes an asynchronous fetch of the list of identifiers for the stored filters previously
483 * started with webkit_user_content_filter_store_fetch_identifiers().
484 *
485 * Returns: (transfer full) (array zero-terminated=1) (element-type utf8): a %NULL-terminated list of filter identifiers.
486 *
487 * Since: 2.24
488 */
489gchar** webkit_user_content_filter_store_fetch_identifiers_finish(WebKitUserContentFilterStore* store, GAsyncResult* result)
490{
491 g_return_val_if_fail(WEBKIT_IS_USER_CONTENT_FILTER_STORE(store), nullptr);
492 g_return_val_if_fail(result, nullptr);
493 return static_cast<gchar**>(g_task_propagate_pointer(G_TASK(result), nullptr));
494}
495