[CMake] Properly test if compiler supports compiler flags
[WebKit-https.git] / Tools / TestWebKitAPI / Tests / WebKitGLib / WebExtensionTest.cpp
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 Library General Public
6  * License as published by the Free Software Foundation; either
7  * version 2 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
22 #include <JavaScriptCore/JSContextRef.h>
23 #include <JavaScriptCore/JSRetainPtr.h>
24 #include <gio/gio.h>
25 #if USE(GSTREAMER)
26 #include <gst/gst.h>
27 #endif
28 #include <stdlib.h>
29 #include <string.h>
30 #include <wtf/Deque.h>
31 #include <wtf/ProcessID.h>
32 #include <wtf/glib/GRefPtr.h>
33 #include <wtf/glib/GUniquePtr.h>
34 #include <wtf/text/CString.h>
35
36 #if PLATFORM(GTK)
37 #include <webkit2/webkit-web-extension.h>
38 #elif PLATFORM(WPE)
39 #include <wpe/webkit-web-extension.h>
40 #endif
41
42 static const char introspectionXML[] =
43     "<node>"
44     " <interface name='org.webkit.gtk.WebExtensionTest'>"
45     "  <method name='GetTitle'>"
46     "   <arg type='t' name='pageID' direction='in'/>"
47     "   <arg type='s' name='title' direction='out'/>"
48     "  </method>"
49     "  <method name='AbortProcess'>"
50     "  </method>"
51     "  <method name='RunJavaScriptInIsolatedWorld'>"
52     "   <arg type='t' name='pageID' direction='in'/>"
53     "   <arg type='s' name='script' direction='in'/>"
54     "  </method>"
55     "  <method name='GetProcessIdentifier'>"
56     "   <arg type='u' name='identifier' direction='out'/>"
57     "  </method>"
58     "  <method name='RemoveAVPluginsFromGSTRegistry'>"
59     "  </method>"
60     "  <signal name='DocumentLoaded'/>"
61     "  <signal name='FormControlsAssociated'>"
62     "   <arg type='s' name='formIds' direction='out'/>"
63     "  </signal>"
64     "  <signal name='URIChanged'>"
65     "   <arg type='s' name='uri' direction='out'/>"
66     "  </signal>"
67     " </interface>"
68     "</node>";
69
70
71 typedef enum {
72     DocumentLoadedSignal,
73     URIChangedSignal,
74     FormControlsAssociatedSignal,
75 } DelayedSignalType;
76
77 struct DelayedSignal {
78     DelayedSignal(DelayedSignalType type)
79         : type(type)
80     {
81     }
82
83     DelayedSignal(DelayedSignalType type, const char* str)
84         : type(type)
85         , str(str)
86     {
87     }
88
89     DelayedSignalType type;
90     CString str;
91 };
92
93 Deque<DelayedSignal> delayedSignalsQueue;
94
95 static void emitDocumentLoaded(GDBusConnection* connection)
96 {
97     bool ok = g_dbus_connection_emit_signal(
98         connection,
99         0,
100         "/org/webkit/gtk/WebExtensionTest",
101         "org.webkit.gtk.WebExtensionTest",
102         "DocumentLoaded",
103         0,
104         0);
105     g_assert(ok);
106 }
107
108 static void documentLoadedCallback(WebKitWebPage* webPage, WebKitWebExtension* extension)
109 {
110 #if PLATFORM(GTK)
111     WebKitDOMDocument* document = webkit_web_page_get_dom_document(webPage);
112     GRefPtr<WebKitDOMDOMWindow> window = adoptGRef(webkit_dom_document_get_default_view(document));
113     webkit_dom_dom_window_webkit_message_handlers_post_message(window.get(), "dom", "DocumentLoaded");
114 #endif
115
116     gpointer data = g_object_get_data(G_OBJECT(extension), "dbus-connection");
117     if (data)
118         emitDocumentLoaded(G_DBUS_CONNECTION(data));
119     else
120         delayedSignalsQueue.append(DelayedSignal(DocumentLoadedSignal));
121 }
122
123 static void emitURIChanged(GDBusConnection* connection, const char* uri)
124 {
125     bool ok = g_dbus_connection_emit_signal(
126         connection,
127         0,
128         "/org/webkit/gtk/WebExtensionTest",
129         "org.webkit.gtk.WebExtensionTest",
130         "URIChanged",
131         g_variant_new("(s)", uri),
132         0);
133     g_assert(ok);
134 }
135
136 static void uriChangedCallback(WebKitWebPage* webPage, GParamSpec* pspec, WebKitWebExtension* extension)
137 {
138     gpointer data = g_object_get_data(G_OBJECT(extension), "dbus-connection");
139     if (data)
140         emitURIChanged(G_DBUS_CONNECTION(data), webkit_web_page_get_uri(webPage));
141     else
142         delayedSignalsQueue.append(DelayedSignal(URIChangedSignal, webkit_web_page_get_uri(webPage)));
143 }
144
145 static gboolean sendRequestCallback(WebKitWebPage*, WebKitURIRequest* request, WebKitURIResponse* redirectResponse, gpointer)
146 {
147     gboolean returnValue = FALSE;
148     const char* requestURI = webkit_uri_request_get_uri(request);
149     g_assert(requestURI);
150
151     if (const char* suffix = g_strrstr(requestURI, "/remove-this/javascript.js")) {
152         GUniquePtr<char> prefix(g_strndup(requestURI, strlen(requestURI) - strlen(suffix)));
153         GUniquePtr<char> newURI(g_strdup_printf("%s/javascript.js", prefix.get()));
154         webkit_uri_request_set_uri(request, newURI.get());
155     } else if (const char* suffix = g_strrstr(requestURI, "/remove-this/javascript-after-redirection.js")) {
156         // Redirected from /redirected.js, redirectResponse should be nullptr.
157         g_assert(WEBKIT_IS_URI_RESPONSE(redirectResponse));
158         g_assert(g_str_has_suffix(webkit_uri_response_get_uri(redirectResponse), "/redirected.js"));
159
160         GUniquePtr<char> prefix(g_strndup(requestURI, strlen(requestURI) - strlen(suffix)));
161         GUniquePtr<char> newURI(g_strdup_printf("%s/javascript-after-redirection.js", prefix.get()));
162         webkit_uri_request_set_uri(request, newURI.get());
163     } else if (g_str_has_suffix(requestURI, "/redirected.js")) {
164         // Original request, redirectResponse should be nullptr.
165         g_assert(!redirectResponse);
166     } else if (g_str_has_suffix(requestURI, "/add-do-not-track-header")) {
167         SoupMessageHeaders* headers = webkit_uri_request_get_http_headers(request);
168         g_assert(headers);
169         soup_message_headers_append(headers, "DNT", "1");
170     } else if (g_str_has_suffix(requestURI, "/normal-change-request")) {
171         GUniquePtr<char> prefix(g_strndup(requestURI, strlen(requestURI) - strlen("/normal-change-request")));
172         GUniquePtr<char> newURI(g_strdup_printf("%s/request-changed%s", prefix.get(), redirectResponse ? "-on-redirect" : ""));
173         webkit_uri_request_set_uri(request, newURI.get());
174     } else if (g_str_has_suffix(requestURI, "/http-get-method")) {
175         g_assert_cmpstr(webkit_uri_request_get_http_method(request), ==, "GET");
176         g_assert(webkit_uri_request_get_http_method(request) == SOUP_METHOD_GET);
177     } else if (g_str_has_suffix(requestURI, "/http-post-method")) {
178         g_assert_cmpstr(webkit_uri_request_get_http_method(request), ==, "POST");
179         g_assert(webkit_uri_request_get_http_method(request) == SOUP_METHOD_POST);
180         returnValue = TRUE;
181     } else if (g_str_has_suffix(requestURI, "/cancel-this.js"))
182         returnValue = TRUE;
183
184     return returnValue;
185 }
186
187 // FIXME: figure out what to do with WebKitWebHitTestResult in WPE.
188 #if PLATFORM(GTK)
189 static GVariant* serializeContextMenu(WebKitContextMenu* menu)
190 {
191     GVariantBuilder builder;
192     g_variant_builder_init(&builder, G_VARIANT_TYPE_ARRAY);
193     GList* items = webkit_context_menu_get_items(menu);
194     for (GList* it = items; it; it = g_list_next(it))
195         g_variant_builder_add(&builder, "u", webkit_context_menu_item_get_stock_action(WEBKIT_CONTEXT_MENU_ITEM(it->data)));
196     return g_variant_builder_end(&builder);
197 }
198
199 static GVariant* serializeNode(WebKitDOMNode* node)
200 {
201     GVariantBuilder builder;
202     g_variant_builder_init(&builder, G_VARIANT_TYPE_ARRAY);
203     g_variant_builder_add(&builder, "{sv}", "Name", g_variant_new_take_string(webkit_dom_node_get_node_name(node)));
204     g_variant_builder_add(&builder, "{sv}", "Type", g_variant_new_uint32(webkit_dom_node_get_node_type(node)));
205     g_variant_builder_add(&builder, "{sv}", "Contents", g_variant_new_take_string(webkit_dom_node_get_text_content(node)));
206     WebKitDOMNode* parent = webkit_dom_node_get_parent_node(node);
207     g_variant_builder_add(&builder, "{sv}", "Parent", parent ? g_variant_new_take_string(webkit_dom_node_get_node_name(parent)) : g_variant_new_string("ROOT"));
208     return g_variant_builder_end(&builder);
209 }
210
211 static gboolean contextMenuCallback(WebKitWebPage* page, WebKitContextMenu* menu, WebKitWebHitTestResult* hitTestResult, gpointer)
212 {
213     const char* pageURI = webkit_web_page_get_uri(page);
214     if (!g_strcmp0(pageURI, "ContextMenuTestDefault")) {
215         webkit_context_menu_set_user_data(menu, serializeContextMenu(menu));
216         return FALSE;
217     }
218
219     if (!g_strcmp0(pageURI, "ContextMenuTestCustom")) {
220         // Remove Back and Forward, and add Inspector action.
221         webkit_context_menu_remove(menu, webkit_context_menu_first(menu));
222         webkit_context_menu_remove(menu, webkit_context_menu_first(menu));
223         webkit_context_menu_append(menu, webkit_context_menu_item_new_separator());
224         webkit_context_menu_append(menu, webkit_context_menu_item_new_from_stock_action(WEBKIT_CONTEXT_MENU_ACTION_INSPECT_ELEMENT));
225         webkit_context_menu_set_user_data(menu, serializeContextMenu(menu));
226         return TRUE;
227     }
228
229     if (!g_strcmp0(pageURI, "ContextMenuTestClear")) {
230         webkit_context_menu_remove_all(menu);
231         return TRUE;
232     }
233
234     if (!g_strcmp0(pageURI, "ContextMenuTestNode")) {
235         WebKitDOMNode* node = webkit_web_hit_test_result_get_node(hitTestResult);
236         g_assert(WEBKIT_DOM_IS_NODE(node));
237         webkit_context_menu_set_user_data(menu, serializeNode(node));
238         return TRUE;
239     }
240
241     return FALSE;
242 }
243 #endif // PLATFORM(GTK)
244
245 static void consoleMessageSentCallback(WebKitWebPage* webPage, WebKitConsoleMessage* consoleMessage)
246 {
247     g_assert(consoleMessage);
248     GRefPtr<GVariant> variant = g_variant_new("(uusus)", webkit_console_message_get_source(consoleMessage),
249         webkit_console_message_get_level(consoleMessage), webkit_console_message_get_text(consoleMessage),
250         webkit_console_message_get_line(consoleMessage), webkit_console_message_get_source_id(consoleMessage));
251     GUniquePtr<char> messageString(g_variant_print(variant.get(), FALSE));
252 #if PLATFORM(GTK)
253     GRefPtr<WebKitDOMDOMWindow> window = adoptGRef(webkit_dom_document_get_default_view(webkit_web_page_get_dom_document(webPage)));
254     g_assert(WEBKIT_DOM_IS_DOM_WINDOW(window.get()));
255     webkit_dom_dom_window_webkit_message_handlers_post_message(window.get(), "console", messageString.get());
256 #else
257     GUniquePtr<char> escapedMessageString(static_cast<char*>(g_malloc(strlen(messageString.get()) * 2 + 1)));
258     char* src = messageString.get();
259     char* dest = escapedMessageString.get();
260     while (*src) {
261         if (*src == '"') {
262             *dest++ = '\\';
263             *dest++ = '"';
264         } else
265             *dest++ = *src;
266         ++src;
267     }
268     *dest = '\0';
269     GUniquePtr<char> script(g_strdup_printf("window.webkit.messageHandlers.console.postMessage(\"%s\");", escapedMessageString.get()));
270     JSGlobalContextRef jsContext = webkit_frame_get_javascript_global_context(webkit_web_page_get_main_frame(webPage));
271     JSRetainPtr<JSStringRef> jsScript(Adopt, JSStringCreateWithUTF8CString(script.get()));
272     JSEvaluateScript(jsContext, jsScript.get(), nullptr, nullptr, 1, nullptr);
273 #endif
274 }
275
276 #if PLATFORM(GTK)
277 static void emitFormControlsAssociated(GDBusConnection* connection, const char* formIds)
278 {
279     bool ok = g_dbus_connection_emit_signal(
280         connection,
281         nullptr,
282         "/org/webkit/gtk/WebExtensionTest",
283         "org.webkit.gtk.WebExtensionTest",
284         "FormControlsAssociated",
285         g_variant_new("(s)", formIds),
286         nullptr);
287     g_assert(ok);
288 }
289
290 static void formControlsAssociatedCallback(WebKitWebPage* webPage, GPtrArray* formElements, WebKitWebExtension* extension)
291 {
292     GString* formIdsBuilder = g_string_new(nullptr);
293     for (guint i = 0; i < formElements->len; ++i) {
294         g_assert(WEBKIT_DOM_IS_ELEMENT(g_ptr_array_index(formElements, i)));
295         auto domElement = WEBKIT_DOM_ELEMENT(g_ptr_array_index(formElements, i));
296         GUniquePtr<char> elementID(webkit_dom_element_get_id(domElement));
297         if (elementID)
298             g_string_append(formIdsBuilder, elementID.get());
299     }
300     if (!formIdsBuilder->len) {
301         g_string_free(formIdsBuilder, TRUE);
302         return;
303     }
304     GUniquePtr<char> formIds(g_string_free(formIdsBuilder, FALSE));
305     gpointer data = g_object_get_data(G_OBJECT(extension), "dbus-connection");
306     if (data)
307         emitFormControlsAssociated(G_DBUS_CONNECTION(data), formIds.get());
308     else
309         delayedSignalsQueue.append(DelayedSignal(FormControlsAssociatedSignal, formIds.get()));
310 }
311 #endif
312
313 static void pageCreatedCallback(WebKitWebExtension* extension, WebKitWebPage* webPage, gpointer)
314 {
315     g_signal_connect(webPage, "document-loaded", G_CALLBACK(documentLoadedCallback), extension);
316     g_signal_connect(webPage, "notify::uri", G_CALLBACK(uriChangedCallback), extension);
317     g_signal_connect(webPage, "send-request", G_CALLBACK(sendRequestCallback), nullptr);
318     g_signal_connect(webPage, "console-message-sent", G_CALLBACK(consoleMessageSentCallback), nullptr);
319 #if PLATFORM(GTK)
320     g_signal_connect(webPage, "context-menu", G_CALLBACK(contextMenuCallback), nullptr);
321     g_signal_connect(webPage, "form-controls-associated", G_CALLBACK(formControlsAssociatedCallback), extension);
322 #endif
323 }
324
325 static JSValueRef echoCallback(JSContextRef jsContext, JSObjectRef, JSObjectRef, size_t argumentCount, const JSValueRef arguments[], JSValueRef*)
326 {
327     if (argumentCount <= 0)
328         return JSValueMakeUndefined(jsContext);
329
330     JSRetainPtr<JSStringRef> string(Adopt, JSValueToStringCopy(jsContext, arguments[0], 0));
331     return JSValueMakeString(jsContext, string.get());
332 }
333
334 static void windowObjectCleared(WebKitScriptWorld* world, WebKitWebPage* page, WebKitFrame* frame, gpointer)
335 {
336     JSGlobalContextRef jsContext = webkit_frame_get_javascript_context_for_script_world(frame, world);
337     g_assert(jsContext);
338     JSObjectRef globalObject = JSContextGetGlobalObject(jsContext);
339     g_assert(globalObject);
340
341     JSRetainPtr<JSStringRef> functionName(Adopt, JSStringCreateWithUTF8CString("echo"));
342     JSObjectRef function = JSObjectMakeFunctionWithCallback(jsContext, functionName.get(), echoCallback);
343     JSObjectSetProperty(jsContext, globalObject, functionName.get(), function, kJSPropertyAttributeDontDelete | kJSPropertyAttributeReadOnly, 0);
344 }
345
346 static WebKitWebPage* getWebPage(WebKitWebExtension* extension, uint64_t pageID, GDBusMethodInvocation* invocation)
347 {
348     WebKitWebPage* page = webkit_web_extension_get_page(extension, pageID);
349     if (!page) {
350         g_dbus_method_invocation_return_error(
351             invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
352             "Invalid page ID: %" G_GUINT64_FORMAT, pageID);
353         return 0;
354     }
355
356     g_assert_cmpuint(webkit_web_page_get_id(page), ==, pageID);
357     return page;
358 }
359
360 static void methodCallCallback(GDBusConnection* connection, const char* sender, const char* objectPath, const char* interfaceName, const char* methodName, GVariant* parameters, GDBusMethodInvocation* invocation, gpointer userData)
361 {
362     if (g_strcmp0(interfaceName, "org.webkit.gtk.WebExtensionTest"))
363         return;
364
365     if (!g_strcmp0(methodName, "GetTitle")) {
366         uint64_t pageID;
367         g_variant_get(parameters, "(t)", &pageID);
368         WebKitWebPage* page = getWebPage(WEBKIT_WEB_EXTENSION(userData), pageID, invocation);
369         if (!page)
370             return;
371
372 #if PLATFORM(GTK)
373         WebKitDOMDocument* document = webkit_web_page_get_dom_document(page);
374         GUniquePtr<char> title(webkit_dom_document_get_title(document));
375         g_dbus_method_invocation_return_value(invocation, g_variant_new("(s)", title.get()));
376 #elif PLATFORM(WPE)
377         // FIXME: Use JSC API to get the title from JavaScript.
378         g_dbus_method_invocation_return_value(invocation, g_variant_new("(s)", ""));
379 #endif
380     } else if (!g_strcmp0(methodName, "RunJavaScriptInIsolatedWorld")) {
381         uint64_t pageID;
382         const char* script;
383         g_variant_get(parameters, "(t&s)", &pageID, &script);
384         WebKitWebPage* page = getWebPage(WEBKIT_WEB_EXTENSION(userData), pageID, invocation);
385         if (!page)
386             return;
387
388         GRefPtr<WebKitScriptWorld> world = adoptGRef(webkit_script_world_new());
389         g_assert(webkit_script_world_get_default() != world.get());
390         WebKitFrame* frame = webkit_web_page_get_main_frame(page);
391         JSGlobalContextRef jsContext = webkit_frame_get_javascript_context_for_script_world(frame, world.get());
392         JSRetainPtr<JSStringRef> jsScript(Adopt, JSStringCreateWithUTF8CString(script));
393         JSEvaluateScript(jsContext, jsScript.get(), 0, 0, 0, 0);
394         g_dbus_method_invocation_return_value(invocation, 0);
395     } else if (!g_strcmp0(methodName, "AbortProcess")) {
396         abort();
397     } else if (!g_strcmp0(methodName, "GetProcessIdentifier")) {
398         g_dbus_method_invocation_return_value(invocation,
399             g_variant_new("(u)", static_cast<guint32>(getCurrentProcessID())));
400     } else if (!g_strcmp0(methodName, "RemoveAVPluginsFromGSTRegistry")) {
401 #if USE(GSTREAMER)
402         gst_init(nullptr, nullptr);
403         static const char* avPlugins[] = { "libav", "omx", "vaapi", nullptr };
404         GstRegistry* registry = gst_registry_get();
405         for (unsigned i = 0; avPlugins[i]; ++i) {
406             if (GstPlugin* plugin = gst_registry_find_plugin(registry, avPlugins[i])) {
407                 gst_registry_remove_plugin(registry, plugin);
408                 gst_object_unref(plugin);
409             }
410         }
411 #endif
412         g_dbus_method_invocation_return_value(invocation, nullptr);
413     }
414 }
415
416 static const GDBusInterfaceVTable interfaceVirtualTable = {
417     methodCallCallback, 0, 0, { 0, }
418 };
419
420 static void busAcquiredCallback(GDBusConnection* connection, const char* name, gpointer userData)
421 {
422     static GDBusNodeInfo* introspectionData = 0;
423     if (!introspectionData)
424         introspectionData = g_dbus_node_info_new_for_xml(introspectionXML, 0);
425
426     GUniqueOutPtr<GError> error;
427     unsigned registrationID = g_dbus_connection_register_object(
428         connection,
429         "/org/webkit/gtk/WebExtensionTest",
430         introspectionData->interfaces[0],
431         &interfaceVirtualTable,
432         g_object_ref(userData),
433         static_cast<GDestroyNotify>(g_object_unref),
434         &error.outPtr());
435     if (!registrationID)
436         g_warning("Failed to register object: %s\n", error->message);
437
438     g_object_set_data(G_OBJECT(userData), "dbus-connection", connection);
439     while (delayedSignalsQueue.size()) {
440         DelayedSignal delayedSignal = delayedSignalsQueue.takeFirst();
441         switch (delayedSignal.type) {
442         case DocumentLoadedSignal:
443             emitDocumentLoaded(connection);
444             break;
445         case URIChangedSignal:
446             emitURIChanged(connection, delayedSignal.str.data());
447             break;
448         case FormControlsAssociatedSignal:
449 #if PLATFORM(GTK)
450             emitFormControlsAssociated(connection, delayedSignal.str.data());
451 #elif PLATFORM(WPE)
452             g_assert_not_reached();
453 #endif
454             break;
455         }
456     }
457 }
458
459 static void registerGResource(void)
460 {
461     GUniquePtr<char> resourcesPath(g_build_filename(WEBKIT_TEST_RESOURCES_DIR, "webkitglib-tests-resources.gresource", nullptr));
462     GResource* resource = g_resource_load(resourcesPath.get(), nullptr);
463     g_assert(resource);
464
465     g_resources_register(resource);
466     g_resource_unref(resource);
467 }
468
469 extern "C" void webkit_web_extension_initialize_with_user_data(WebKitWebExtension* extension, GVariant* userData)
470 {
471     g_signal_connect(extension, "page-created", G_CALLBACK(pageCreatedCallback), extension);
472     g_signal_connect(webkit_script_world_get_default(), "window-object-cleared", G_CALLBACK(windowObjectCleared), 0);
473
474     registerGResource();
475
476     g_assert(userData);
477     g_assert(g_variant_is_of_type(userData, G_VARIANT_TYPE_UINT32));
478     GUniquePtr<char> busName(g_strdup_printf("org.webkit.gtk.WebExtensionTest%u", g_variant_get_uint32(userData)));
479     g_bus_own_name(
480         G_BUS_TYPE_SESSION,
481         busName.get(),
482         G_BUS_NAME_OWNER_FLAGS_NONE,
483         busAcquiredCallback,
484         0, 0,
485         g_object_ref(extension),
486         static_cast<GDestroyNotify>(g_object_unref));
487 }