WebKitUserContentFilterStore.cpp   [plain text]


/*
 * Copyright (C) 2018-2019 Igalia S.L.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "config.h"
#include "WebKitUserContentFilterStore.h"

#include "APIContentRuleList.h"
#include "APIContentRuleListStore.h"
#include "WebKitError.h"
#include "WebKitUserContent.h"
#include "WebKitUserContentPrivate.h"
#include <WebCore/ContentExtensionError.h>
#include <glib/gi18n-lib.h>
#include <wtf/CompletionHandler.h>
#include <wtf/FileSystem.h>
#include <wtf/RefPtr.h>
#include <wtf/glib/GRefPtr.h>
#include <wtf/glib/GUniquePtr.h>
#include <wtf/glib/WTFGType.h>

/**
 * SECTION: WebKitUserContentFilterStore
 * @Short_description: Handles storage of user content filters on disk.
 * @Title: WebKitUserContentFilterStore
 *
 * The WebKitUserContentFilterStore provides the means to import and save
 * [JSON rule sets](https://webkit.org/blog/3476/content-blockers-first-look/),
 * which can be loaded later in an efficient manner. Once filters are stored,
 * the #WebKitUserContentFilter objects which represent them can be added to
 * a #WebKitUserContentManager with webkit_user_content_manager_add_filter().
 *
 * JSON rule sets are imported using webkit_user_content_filter_store_save() and stored
 * on disk in an implementation defined format. The contents of a filter store must be
 * managed using the #WebKitUserContentFilterStore: a list of all the stored filters
 * can be obtained with webkit_user_content_filter_store_fetch_identifiers(),
 * webkit_user_content_filter_store_load() can be used to retrieve a previously saved
 * filter, and removed from the store with webkit_user_content_filter_store_remove().
 *
 * Since: 2.24
 */

enum {
    PROP_0,

    PROP_PATH,
};

static inline GError* toGError(WebKitUserContentFilterError code, const std::error_code error)
{
    ASSERT(error);
    ASSERT(error.category() == WebCore::ContentExtensions::contentExtensionErrorCategory());
    return g_error_new_literal(WEBKIT_USER_CONTENT_FILTER_ERROR, code, error.message().c_str());
}

struct _WebKitUserContentFilterStorePrivate {
    GUniquePtr<char> storagePath;
    RefPtr<API::ContentRuleListStore> store;
};

WEBKIT_DEFINE_TYPE(WebKitUserContentFilterStore, webkit_user_content_filter_store, G_TYPE_OBJECT)

static void webkitUserContentFilterStoreGetProperty(GObject* object, guint propID, GValue* value, GParamSpec* paramSpec)
{
    WebKitUserContentFilterStore* store = WEBKIT_USER_CONTENT_FILTER_STORE(object);

    switch (propID) {
    case PROP_PATH:
        g_value_set_string(value, webkit_user_content_filter_store_get_path(store));
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, propID, paramSpec);
    }
}

static void webkitUserContentFilterStoreSetProperty(GObject* object, guint propID, const GValue* value, GParamSpec* paramSpec)
{
    WebKitUserContentFilterStore* store = WEBKIT_USER_CONTENT_FILTER_STORE(object);

    switch (propID) {
    case PROP_PATH:
        store->priv->storagePath.reset(g_value_dup_string(value));
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, propID, paramSpec);
    }
}

static void webkitUserContentFilterStoreConstructed(GObject* object)
{
    G_OBJECT_CLASS(webkit_user_content_filter_store_parent_class)->constructed(object);

    WebKitUserContentFilterStore* store = WEBKIT_USER_CONTENT_FILTER_STORE(object);
    store->priv->store = adoptRef(new API::ContentRuleListStore(FileSystem::stringFromFileSystemRepresentation(store->priv->storagePath.get()), false));
}

static void webkit_user_content_filter_store_class_init(WebKitUserContentFilterStoreClass* storeClass)
{
    GObjectClass* gObjectClass = G_OBJECT_CLASS(storeClass);

    gObjectClass->get_property = webkitUserContentFilterStoreGetProperty;
    gObjectClass->set_property = webkitUserContentFilterStoreSetProperty;
    gObjectClass->constructed = webkitUserContentFilterStoreConstructed;

    /**
     * WebKitUserContentFilterStore:path:
     *
     * The directory used for filter storage. This path is used as the base
     * directory where user content filters are stored on disk.
     *
     * Since: 2.24
     */
    g_object_class_install_property(
        gObjectClass,
        PROP_PATH,
        g_param_spec_string(
            "path",
            _("Storage directory path"),
            _("The directory where user content filters are stored"),
            nullptr,
            static_cast<GParamFlags>(WEBKIT_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY)));
}

/**
 * webkit_user_content_filter_store_new:
 * @storage_path: path where data for filters will be stored on disk
 *
 * Create a new #WebKitUserContentFilterStore to manipulate filters stored at @storage_path.
 * The path must point to a local filesystem, and will be created if needed.
 *
 * Returns: (transfer full): a newly created #WebKitUserContentFilterStore
 *
 * Since: 2.24
 */
WebKitUserContentFilterStore* webkit_user_content_filter_store_new(const gchar* storagePath)
{
    g_return_val_if_fail(storagePath, nullptr);
    return WEBKIT_USER_CONTENT_FILTER_STORE(g_object_new(WEBKIT_TYPE_USER_CONTENT_FILTER_STORE, "path", storagePath, nullptr));
}

/**
 * webkit_user_content_filter_store_get_path:
 * @store: a #WebKitUserContentFilterStore
 *
 * Returns: (transfer none): The storage path for user content filters.
 *
 * Since: 2.24
 */
const char* webkit_user_content_filter_store_get_path(WebKitUserContentFilterStore* store)
{
    g_return_val_if_fail(WEBKIT_IS_USER_CONTENT_FILTER_STORE(store), nullptr);
    return store->priv->storagePath.get();
}

static void webkitUserContentFilterStoreSaveBytes(GRefPtr<GTask>&& task, String&& identifier, GRefPtr<GBytes>&& source)
{
    size_t sourceSize;
    const char* sourceData = static_cast<const char*>(g_bytes_get_data(source.get(), &sourceSize));
    if (!sourceSize) {
        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"));
        return;
    }

    auto* store = WEBKIT_USER_CONTENT_FILTER_STORE(g_task_get_source_object(task.get()));
    store->priv->store->compileContentRuleList(identifier, String::fromUTF8(sourceData, sourceSize), [task = WTFMove(task)](RefPtr<API::ContentRuleList> contentRuleList, std::error_code error) {
        if (g_task_return_error_if_cancelled(task.get()))
            return;

        if (error)
            g_task_return_error(task.get(), toGError(WEBKIT_USER_CONTENT_FILTER_ERROR_INVALID_SOURCE, error));
        else
            g_task_return_pointer(task.get(), webkitUserContentFilterCreate(WTFMove(contentRuleList)), reinterpret_cast<GDestroyNotify>(webkit_user_content_filter_unref));
    });
}

/**
 * webkit_user_content_filter_store_save:
 * @store: a #WebKitUserContentFilterStore
 * @identifier: a string used to identify the saved filter
 * @source: #GBytes containing the rule set in JSON format
 * @cancellable: (allow-none): a #GCancellable or %NULL to ignore
 * @callback: (scope async): a #GAsyncReadyCallback to call when saving is completed
 * @user_data: (closure): the data to pass to the callback function
 *
 * Asynchronously save a content filter from a source rule set in the
 * [WebKit content extesions JSON format](https://webkit.org/blog/3476/content-blockers-first-look/).
 *
 * The @identifier can be used afterwards to refer to the filter when using
 * webkit_user_content_filter_store_remove() and webkit_user_content_filter_store_load().
 * When the @identifier has been used in the past, the new filter source will replace
 * the one saved beforehand for the same identifier.
 *
 * When the operation is finished, @callback will be invoked, which then can use
 * webkit_user_content_filter_store_save_finish() to obtain the resulting filter.
 *
 * Since: 2.24
 */
void webkit_user_content_filter_store_save(WebKitUserContentFilterStore* store, const gchar* identifier, GBytes* source, GCancellable* cancellable, GAsyncReadyCallback callback, gpointer userData)
{
    g_return_if_fail(WEBKIT_IS_USER_CONTENT_FILTER_STORE(store));
    g_return_if_fail(identifier);
    g_return_if_fail(source);
    g_return_if_fail(callback);

    GRefPtr<GTask> task = adoptGRef(g_task_new(store, cancellable, callback, userData));
    webkitUserContentFilterStoreSaveBytes(WTFMove(task), String::fromUTF8(identifier), GRefPtr<GBytes>(source));
}

/**
 * webkit_user_content_filter_store_save_finish:
 * @store: a #WebKitUserContentFilterStore
 * @result: a #GAsyncResult
 * @error: return location for error or %NULL to ignore
 *
 * Finishes an asynchronous filter save previously started with
 * webkit_user_content_filter_store_save().
 *
 * Returns: (transfer full): a #WebKitUserContentFilter, or %NULL if saving failed
 *
 * Since: 2.24
 */
WebKitUserContentFilter* webkit_user_content_filter_store_save_finish(WebKitUserContentFilterStore* store, GAsyncResult* result, GError** error)
{
    g_return_val_if_fail(WEBKIT_IS_USER_CONTENT_FILTER_STORE(store), nullptr);
    g_return_val_if_fail(result, nullptr);
    return static_cast<WebKitUserContentFilter*>(g_task_propagate_pointer(G_TASK(result), error));
}

struct SaveTaskData {
    String identifier;
};
WEBKIT_DEFINE_ASYNC_DATA_STRUCT(SaveTaskData)

/**
 * webkit_user_content_filter_store_save_from_file:
 * @store: a #WebKitUserContentFilterStore
 * @identifier: a string used to identify the saved filter
 * @file: a #GFile containing the rule set in JSON format
 * @cancellable: (allow-none): a #GCancellable or %NULL to ignore
 * @callback: (scope async): a #GAsyncReadyCallback to call when saving is completed
 * @user_data: (closure): the data to pass to the callback function
 *
 * Asynchronously save a content filter from the contents of a file, which must be
 * native to the platform, as checked by g_file_is_native(). See
 * webkit_user_content_filter_store_save() for more details.
 *
 * When the operation is finished, @callback will be invoked, which then can use
 * webkit_user_content_filter_store_save_finish() to obtain the resulting filter.
 *
 * Since: 2.24
 */
void webkit_user_content_filter_store_save_from_file(WebKitUserContentFilterStore* store, const gchar* identifier, GFile* file, GCancellable* cancellable, GAsyncReadyCallback callback, gpointer userData)
{
    g_return_if_fail(WEBKIT_IS_USER_CONTENT_FILTER_STORE(store));
    g_return_if_fail(identifier);
    g_return_if_fail(G_IS_FILE(file));
    g_return_if_fail(callback);

    GRefPtr<GTask> task = adoptGRef(g_task_new(store, cancellable, callback, userData));

    // Try mapping the file in memory first, and fall-back to reading the contents if that fails.
    if (g_file_is_native(file)) {
        GUniquePtr<char> filePath(g_file_get_path(file));
        GRefPtr<GMappedFile> mappedFile = adoptGRef(g_mapped_file_new(filePath.get(), FALSE, nullptr));
        if (mappedFile) {
            GRefPtr<GBytes> source = adoptGRef(g_mapped_file_get_bytes(mappedFile.get()));
            webkitUserContentFilterStoreSaveBytes(WTFMove(task), String::fromUTF8(identifier), WTFMove(source));
            return;
        }
    }

    // Pass the identifier as task data to be used in the completion callback once the contents have been loaded.
    SaveTaskData* data = createSaveTaskData();
    data->identifier = String::fromUTF8(identifier);
    g_task_set_task_data(task.get(), data, reinterpret_cast<GDestroyNotify>(destroySaveTaskData));

    g_file_load_contents_async(file, cancellable, [](GObject* sourceObject, GAsyncResult* result, void* userData) {
        GRefPtr<GTask> task = adoptGRef(G_TASK(userData));
        if (g_task_return_error_if_cancelled(task.get()))
            return;

        char* sourceData;
        size_t sourceSize;
        GUniqueOutPtr<GError> error;
        if (g_file_load_contents_finish(G_FILE(sourceObject), result, &sourceData, &sourceSize, nullptr, &error.outPtr())) {
            SaveTaskData* data = static_cast<SaveTaskData*>(g_task_get_task_data(task.get()));
            webkitUserContentFilterStoreSaveBytes(WTFMove(task), WTFMove(data->identifier), GRefPtr<GBytes>(g_bytes_new_take(sourceData, sourceSize)));
        } else
            g_task_return_error(task.get(), error.release());
    }, task.leakRef());
}

/**
 * webkit_user_content_filter_store_save_from_file_finish:
 * @store: a #WebKitUserContentFilterStore
 * @result: a #GAsyncResult
 * @error: return location for error or %NULL to ignore
 *
 * Finishes and asynchronous filter save previously started with
 * webkit_user_content_filter_store_save_from_file().
 *
 * Returns: (transfer full): a #WebKitUserContentFilter, or %NULL if saving failed.
 *
 * Since: 2.24
 */
WebKitUserContentFilter* webkit_user_content_filter_store_save_from_file_finish(WebKitUserContentFilterStore* store, GAsyncResult* result, GError** error)
{
    g_return_val_if_fail(WEBKIT_IS_USER_CONTENT_FILTER_STORE(store), nullptr);
    g_return_val_if_fail(result, nullptr);
    return static_cast<WebKitUserContentFilter*>(g_task_propagate_pointer(G_TASK(result), error));
}

/**
 * webkit_user_content_filter_store_remove:
 * @store: a #WebKitUserContentFilterStore
 * @identifier: a filter identifier
 * @cancellable: (allow-none): a #GCancellable or %NULL to ignore
 * @callback: (scope async): a #GAsyncReadyCallback to call when the removal is completed
 * @user_data: (closure): the data to pass to the callback function
 *
 * Asynchronously remove a content filter given its @identifier.
 *
 * When the operation is finished, @callback will be invoked, which then can use
 * webkit_user_content_filter_store_remove_finish() to check whether the removal was
 * successful.
 *
 * Since: 2.24
 */
void webkit_user_content_filter_store_remove(WebKitUserContentFilterStore* store, const gchar* identifier, GCancellable* cancellable, GAsyncReadyCallback callback, gpointer userData)
{
    g_return_if_fail(WEBKIT_IS_USER_CONTENT_FILTER_STORE(store));
    g_return_if_fail(identifier);
    g_return_if_fail(callback);

    GRefPtr<GTask> task = adoptGRef(g_task_new(store, cancellable, callback, userData));
    store->priv->store->removeContentRuleList(String::fromUTF8(identifier), [task = WTFMove(task)](std::error_code error) {
        if (g_task_return_error_if_cancelled(task.get()))
            return;

        if (error) {
            ASSERT(static_cast<API::ContentRuleListStore::Error>(error.value()) == API::ContentRuleListStore::Error::RemoveFailed);
            g_task_return_error(task.get(), toGError(WEBKIT_USER_CONTENT_FILTER_ERROR_NOT_FOUND, error));
        } else
            g_task_return_boolean(task.get(), TRUE);
    });
}

/**
 * webkit_user_content_filter_store_remove_finish:
 * @store: a #WebKitUserContentFilterStore
 * @result: a #GAsyncResult
 * @error: return location for error or %NULL to ignore
 *
 * Finishes an asynchronous filter removal previously started with
 * webkit_user_content_filter_store_remove().
 *
 * Returns: whether the removal was successful
 *
 * Since: 2.24
 */
gboolean webkit_user_content_filter_store_remove_finish(WebKitUserContentFilterStore* store, GAsyncResult* result, GError** error)
{
    g_return_val_if_fail(WEBKIT_IS_USER_CONTENT_FILTER_STORE(store), FALSE);
    g_return_val_if_fail(result, FALSE);
    return g_task_propagate_boolean(G_TASK(result), error);
}

/**
 * webkit_user_content_filter_store_load:
 * @store: a #WebKitUserContentFilterStore
 * @identifier: a filter identifier
 * @cancellable: (allow-none): a #GCancellable or %NULL to ignore
 * @callback: (scope async): a #GAsyncReadyCallback to call when the load is completed
 * @user_data: (closure): the data to pass to the callback function
 *
 * Asynchronously load a content filter given its @identifier. The filter must have been
 * previously stored using webkit_user_content_filter_store_save().
 *
 * When the operation is finished, @callback will be invoked, which then can use
 * webkit_user_content_filter_store_load_finish() to obtain the resulting filter.
 *
 * Since: 2.24
 */
void webkit_user_content_filter_store_load(WebKitUserContentFilterStore* store, const gchar* identifier, GCancellable* cancellable, GAsyncReadyCallback callback, gpointer userData)
{
    g_return_if_fail(WEBKIT_IS_USER_CONTENT_FILTER_STORE(store));
    g_return_if_fail(identifier);
    g_return_if_fail(callback);

    GRefPtr<GTask> task = adoptGRef(g_task_new(store, cancellable, callback, userData));
    store->priv->store->lookupContentRuleList(String::fromUTF8(identifier), [task = WTFMove(task)](RefPtr<API::ContentRuleList> contentRuleList, std::error_code error) {
        if (g_task_return_error_if_cancelled(task.get()))
            return;

        if (error) {
            ASSERT(static_cast<API::ContentRuleListStore::Error>(error.value()) == API::ContentRuleListStore::Error::LookupFailed
                || static_cast<API::ContentRuleListStore::Error>(error.value()) == API::ContentRuleListStore::Error::VersionMismatch);
            g_task_return_error(task.get(), toGError(WEBKIT_USER_CONTENT_FILTER_ERROR_NOT_FOUND, error));
        } else
            g_task_return_pointer(task.get(), webkitUserContentFilterCreate(WTFMove(contentRuleList)), reinterpret_cast<GDestroyNotify>(webkit_user_content_filter_unref));
    });
}

/**
 * webkit_user_content_filter_store_load_finish:
 * @store: a #WebKitUserContentFilterStore
 * @result: a #GAsyncResult
 * @error: return location for error or %NULL to ignore
 *
 * Finishes an asynchronous filter load previously started with
 * webkit_user_content_filter_store_load().
 *
 * Returns: (transfer full): a #WebKitUserContentFilter, or %NULL if the load failed
 *
 * Since: 2.24
 */
WebKitUserContentFilter* webkit_user_content_filter_store_load_finish(WebKitUserContentFilterStore* store, GAsyncResult* result, GError** error)
{
    g_return_val_if_fail(WEBKIT_IS_USER_CONTENT_FILTER_STORE(store), nullptr);
    g_return_val_if_fail(result, nullptr);
    return static_cast<WebKitUserContentFilter*>(g_task_propagate_pointer(G_TASK(result), error));
}

/**
 * webkit_user_content_filter_store_fetch_identifiers:
 * @store: a #WebKitUserContentFilterStore
 * @cancellable: (allow-none): a #GCancellable or %NULL to ignore
 * @callback: (scope async): a #GAsyncReadyCallback to call when the removal is completed
 * @user_data: (closure): the data to pass to the callback function
 *
 * Asynchronously retrieve a list of the identifiers for all the stored filters.
 *
 * When the operation is finished, @callback will be invoked, which then can use
 * webkit_user_content_filter_store_fetch_identifiers_finish() to obtain the list of
 * filter identifiers.
 *
 * Since: 2.24
 */
void webkit_user_content_filter_store_fetch_identifiers(WebKitUserContentFilterStore* store, GCancellable* cancellable, GAsyncReadyCallback callback, gpointer userData)
{
    g_return_if_fail(WEBKIT_IS_USER_CONTENT_FILTER_STORE(store));
    g_return_if_fail(callback);

    GRefPtr<GTask> task = adoptGRef(g_task_new(store, cancellable, callback, userData));
    store->priv->store->getAvailableContentRuleListIdentifiers([task = WTFMove(task)](WTF::Vector<WTF::String> identifiers) {
        if (g_task_return_error_if_cancelled(task.get()))
            return;

        GStrv result = static_cast<GStrv>(g_new0(gchar*, identifiers.size() + 1));
        for (size_t i = 0; i < identifiers.size(); ++i)
            result[i] = g_strdup(identifiers[i].utf8().data());
        g_task_return_pointer(task.get(), result, reinterpret_cast<GDestroyNotify>(g_strfreev));
    });
}

/**
 * webkit_user_content_filter_store_fetch_identifiers_finish:
 * @store: a #WebKitUserContentFilterStore
 * @result: a #GAsyncResult
 *
 * Finishes an asynchronous fetch of the list of identifiers for the stored filters previously
 * started with webkit_user_content_filter_store_fetch_identifiers().
 *
 * Returns: (transfer full) (array zero-terminated=1) (element-type utf8): a %NULL-terminated list of filter identifiers.
 *
 * Since: 2.24
 */
gchar** webkit_user_content_filter_store_fetch_identifiers_finish(WebKitUserContentFilterStore* store, GAsyncResult* result)
{
    g_return_val_if_fail(WEBKIT_IS_USER_CONTENT_FILTER_STORE(store), nullptr);
    g_return_val_if_fail(result, nullptr);
    return static_cast<gchar**>(g_task_propagate_pointer(G_TASK(result), nullptr));
}