blob: 8eb5179fbf2cdb916acc8a35e411d16471fd11d8 [file] [log] [blame]
/*
* Copyright (C) 2012 Igalia S.L.
*
* 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.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public License
* along with this library; see the file COPYING.LIB. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
#include "config.h"
#include "WebKitWebExtension.h"
#include "APIDictionary.h"
#include "APIInjectedBundleBundleClient.h"
#include "APIString.h"
#include "WebKitUserMessagePrivate.h"
#include "WebKitWebExtensionPrivate.h"
#include "WebKitWebPagePrivate.h"
#include "WebProcess.h"
#include "WebProcessProxyMessages.h"
#include <WebCore/GCController.h>
#include <glib/gi18n-lib.h>
#include <wtf/HashMap.h>
#include <wtf/glib/GRefPtr.h>
#include <wtf/glib/WTFGType.h>
using namespace WebKit;
/**
* SECTION: WebKitWebExtension
* @Short_description: Represents a WebExtension of the WebProcess
* @Title: WebKitWebExtension
*
* WebKitWebExtension is a loadable module for the WebProcess. It allows you to execute code in the
* WebProcess and being able to use the DOM API, to change any request or to inject custom
* JavaScript code, for example.
*
* To create a WebKitWebExtension you should write a module with an initialization function that could
* be either webkit_web_extension_initialize() with prototype #WebKitWebExtensionInitializeFunction or
* webkit_web_extension_initialize_with_user_data() with prototype #WebKitWebExtensionInitializeWithUserDataFunction.
* This function has to be public and it has to use the #G_MODULE_EXPORT macro. It is called when the
* web process is initialized.
*
* <informalexample><programlisting>
* static void
* web_page_created_callback (WebKitWebExtension *extension,
* WebKitWebPage *web_page,
* gpointer user_data)
* {
* g_print ("Page %d created for %s\n",
* webkit_web_page_get_id (web_page),
* webkit_web_page_get_uri (web_page));
* }
*
* G_MODULE_EXPORT void
* webkit_web_extension_initialize (WebKitWebExtension *extension)
* {
* g_signal_connect (extension, "page-created",
* G_CALLBACK (web_page_created_callback),
* NULL);
* }
* </programlisting></informalexample>
*
* The previous piece of code shows a trivial example of an extension that notifies when
* a #WebKitWebPage is created.
*
* WebKit has to know where it can find the created WebKitWebExtension. To do so you
* should use the webkit_web_context_set_web_extensions_directory() function. The signal
* #WebKitWebContext::initialize-web-extensions is the recommended place to call it.
*
* To provide the initialization data used by the webkit_web_extension_initialize_with_user_data()
* function, you have to call webkit_web_context_set_web_extensions_initialization_user_data() with
* the desired data as parameter. You can see an example of this in the following piece of code:
*
* <informalexample><programlisting>
* #define WEB_EXTENSIONS_DIRECTORY /<!-- -->* ... *<!-- -->/
*
* static void
* initialize_web_extensions (WebKitWebContext *context,
* gpointer user_data)
* {
* /<!-- -->* Web Extensions get a different ID for each Web Process *<!-- -->/
* static guint32 unique_id = 0;
*
* webkit_web_context_set_web_extensions_directory (
* context, WEB_EXTENSIONS_DIRECTORY);
* webkit_web_context_set_web_extensions_initialization_user_data (
* context, g_variant_new_uint32 (unique_id++));
* }
*
* int main (int argc, char **argv)
* {
* g_signal_connect (webkit_web_context_get_default (),
* "initialize-web-extensions",
* G_CALLBACK (initialize_web_extensions),
* NULL);
*
* GtkWidget *view = webkit_web_view_new ();
*
* /<!-- -->* ... *<!-- -->/
* }
* </programlisting></informalexample>
*/
enum {
PAGE_CREATED,
USER_MESSAGE_RECEIVED,
LAST_SIGNAL
};
typedef HashMap<WebPage*, GRefPtr<WebKitWebPage> > WebPageMap;
struct _WebKitWebExtensionPrivate {
WebPageMap pages;
#if ENABLE(DEVELOPER_MODE)
bool garbageCollectOnPageDestroy;
#endif
};
static guint signals[LAST_SIGNAL] = { 0, };
WEBKIT_DEFINE_TYPE(WebKitWebExtension, webkit_web_extension, G_TYPE_OBJECT)
static void webkit_web_extension_class_init(WebKitWebExtensionClass* klass)
{
/**
* WebKitWebExtension::page-created:
* @extension: the #WebKitWebExtension on which the signal is emitted
* @web_page: the #WebKitWebPage created
*
* This signal is emitted when a new #WebKitWebPage is created in
* the Web Process.
*/
signals[PAGE_CREATED] = g_signal_new(
"page-created",
G_TYPE_FROM_CLASS(klass),
G_SIGNAL_RUN_LAST,
0, 0, 0,
g_cclosure_marshal_VOID__OBJECT,
G_TYPE_NONE, 1,
WEBKIT_TYPE_WEB_PAGE);
/**
* WebKitWebExtension::user-message-received:
* @extension: the #WebKitWebExtension on which the signal is emitted
* @message: the #WebKitUserMessage received
*
* This signal is emitted when a #WebKitUserMessage is received from the
* #WebKitWebContext corresponding to @extension. Messages sent by #WebKitWebContext
* are always broadcasted to all #WebKitWebExtension<!-- -->s and they can't be
* replied to. Calling webkit_user_message_send_reply() will do nothing.
*
* Since: 2.28
*/
signals[USER_MESSAGE_RECEIVED] = g_signal_new(
"user-message-received",
G_TYPE_FROM_CLASS(klass),
G_SIGNAL_RUN_LAST,
0,
nullptr, nullptr,
g_cclosure_marshal_generic,
G_TYPE_NONE, 1,
WEBKIT_TYPE_USER_MESSAGE);
}
class WebExtensionInjectedBundleClient final : public API::InjectedBundle::Client {
public:
explicit WebExtensionInjectedBundleClient(WebKitWebExtension* extension)
: m_extension(extension)
{
}
private:
void didCreatePage(InjectedBundle&, WebPage& page) override
{
GRefPtr<WebKitWebPage> webPage = adoptGRef(webkitWebPageCreate(&page));
m_extension->priv->pages.add(&page, webPage);
g_signal_emit(m_extension, signals[PAGE_CREATED], 0, webPage.get());
}
void willDestroyPage(InjectedBundle&, WebPage& page) override
{
m_extension->priv->pages.remove(&page);
#if ENABLE(DEVELOPER_MODE)
if (m_extension->priv->garbageCollectOnPageDestroy)
WebCore::GCController::singleton().garbageCollectNow();
#endif
}
void didReceiveMessage(InjectedBundle&, const String& messageName, API::Object* messageBody) override
{
ASSERT(messageBody->type() == API::Object::Type::Dictionary);
API::Dictionary& message = *static_cast<API::Dictionary*>(messageBody);
if (messageName == String::fromUTF8("PrefetchDNS")) {
API::String* hostname = static_cast<API::String*>(message.get(String::fromUTF8("Hostname")));
WebProcess::singleton().prefetchDNS(hostname->string());
} else
ASSERT_NOT_REACHED();
}
void didReceiveMessageToPage(InjectedBundle&, WebPage& page, const String& messageName, API::Object* messageBody) override
{
ASSERT(messageBody->type() == API::Object::Type::Dictionary);
if (auto* webPage = m_extension->priv->pages.get(&page))
webkitWebPageDidReceiveMessage(webPage, messageName, *static_cast<API::Dictionary*>(messageBody));
}
WebKitWebExtension* m_extension;
};
WebKitWebExtension* webkitWebExtensionCreate(InjectedBundle* bundle)
{
WebKitWebExtension* extension = WEBKIT_WEB_EXTENSION(g_object_new(WEBKIT_TYPE_WEB_EXTENSION, NULL));
bundle->setClient(makeUnique<WebExtensionInjectedBundleClient>(extension));
return extension;
}
void webkitWebExtensionDidReceiveUserMessage(WebKitWebExtension* extension, UserMessage&& message)
{
// Sink the floating ref.
GRefPtr<WebKitUserMessage> userMessage = webkitUserMessageCreate(WTFMove(message), [](UserMessage&&) { });
g_signal_emit(extension, signals[USER_MESSAGE_RECEIVED], 0, userMessage.get());
}
void webkitWebExtensionSetGarbageCollectOnPageDestroy(WebKitWebExtension* extension)
{
#if ENABLE(DEVELOPER_MODE)
extension->priv->garbageCollectOnPageDestroy = true;
#endif
}
/**
* webkit_web_extension_get_page:
* @extension: a #WebKitWebExtension
* @page_id: the identifier of the #WebKitWebPage to get
*
* Get the web page of the given @page_id.
*
* Returns: (transfer none): the #WebKitWebPage for the given @page_id, or %NULL if the
* identifier doesn't correspond to an existing web page.
*/
WebKitWebPage* webkit_web_extension_get_page(WebKitWebExtension* extension, guint64 pageID)
{
g_return_val_if_fail(WEBKIT_IS_WEB_EXTENSION(extension), 0);
WebKitWebExtensionPrivate* priv = extension->priv;
WebPageMap::const_iterator end = priv->pages.end();
for (WebPageMap::const_iterator it = priv->pages.begin(); it != end; ++it)
if (it->key->identifier().toUInt64() == pageID)
return it->value.get();
return 0;
}
/**
* webkit_web_extension_send_message_to_context:
* @extension: a #WebKitWebExtension
* @message: a #WebKitUserMessage
* @cancellable: (nullable): a #GCancellable or %NULL to ignore
* @callback: (scope async): (nullable): A #GAsyncReadyCallback to call when the request is satisfied or %NULL
* @user_data: (closure): the data to pass to callback function
*
* Send @message to the #WebKitWebContext corresponding to @extension. If @message is floating, it's consumed.
*
* If you don't expect any reply, or you simply want to ignore it, you can pass %NULL as @calback.
* When the operation is finished, @callback will be called. You can then call
* webkit_web_extension_send_message_to_context_finish() to get the message reply.
*
* Since: 2.28
*/
void webkit_web_extension_send_message_to_context(WebKitWebExtension* extension, WebKitUserMessage* message, GCancellable* cancellable, GAsyncReadyCallback callback, gpointer userData)
{
g_return_if_fail(WEBKIT_IS_WEB_EXTENSION(extension));
g_return_if_fail(WEBKIT_IS_USER_MESSAGE(message));
// We sink the reference in case of being floating.
GRefPtr<WebKitUserMessage> adoptedMessage = message;
if (!callback) {
WebProcess::singleton().parentProcessConnection()->send(Messages::WebProcessProxy::SendMessageToWebContext(webkitUserMessageGetMessage(message)), 0);
return;
}
GRefPtr<GTask> task = adoptGRef(g_task_new(extension, cancellable, callback, userData));
CompletionHandler<void(UserMessage&&)> completionHandler = [task = WTFMove(task)](UserMessage&& replyMessage) {
switch (replyMessage.type) {
case UserMessage::Type::Null:
g_task_return_new_error(task.get(), G_IO_ERROR, G_IO_ERROR_CANCELLED, _("Operation was cancelled"));
break;
case UserMessage::Type::Message:
g_task_return_pointer(task.get(), g_object_ref_sink(webkitUserMessageCreate(WTFMove(replyMessage))), static_cast<GDestroyNotify>(g_object_unref));
break;
case UserMessage::Type::Error:
g_task_return_new_error(task.get(), WEBKIT_USER_MESSAGE_ERROR, replyMessage.errorCode, _("Message %s was not handled"), replyMessage.name.data());
break;
}
};
WebProcess::singleton().parentProcessConnection()->sendWithAsyncReply(Messages::WebProcessProxy::SendMessageToWebContextWithReply(webkitUserMessageGetMessage(message)), WTFMove(completionHandler));
}
/**
* webkit_web_extension_send_message_to_context_finish:
* @extension: a #WebKitWebExtension
* @result: a #GAsyncResult
* @error: return location for error or %NULL to ignor
*
* Finish an asynchronous operation started with webkit_web_extension_send_message_to_context().
*
* Returns: (transfer full): a #WebKitUserMessage with the reply or %NULL in case of error.
*
* Since: 2.28
*/
WebKitUserMessage* webkit_web_extension_send_message_to_context_finish(WebKitWebExtension* extension, GAsyncResult* result, GError** error)
{
g_return_val_if_fail(WEBKIT_IS_WEB_EXTENSION(extension), nullptr);
g_return_val_if_fail(g_task_is_valid(result, extension), nullptr);
return WEBKIT_USER_MESSAGE(g_task_propagate_pointer(G_TASK(result), error));
}