/* * Copyright (C) 2016 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. */ #if defined(HAVE_CONFIG_H) && HAVE_CONFIG_H && defined(BUILDING_WITH_CMAKE) #include "cmakeconfig.h" #endif #include "BrowserTab.h" #include "BrowserSearchBar.h" #include "BrowserWindow.h" #include enum { PROP_0, PROP_VIEW }; struct _BrowserTab { GtkBox parent; WebKitWebView *webView; BrowserSearchBar *searchBar; GtkWidget *statusLabel; gboolean wasSearchingWhenEnteredFullscreen; gboolean inspectorIsVisible; GtkWidget *fullScreenMessageLabel; guint fullScreenMessageLabelId; /* Tab Title */ GtkWidget *titleBox; GtkWidget *titleLabel; GtkWidget *titleSpinner; GtkWidget *titleCloseButton; }; struct _BrowserTabClass { GtkBoxClass parent; }; G_DEFINE_TYPE(BrowserTab, browser_tab, GTK_TYPE_BOX) static void titleChanged(WebKitWebView *webView, GParamSpec *pspec, BrowserTab *tab) { const char *title = webkit_web_view_get_title(webView); if (title && *title) gtk_label_set_text(GTK_LABEL(tab->titleLabel), title); } static void isLoadingChanged(WebKitWebView *webView, GParamSpec *paramSpec, BrowserTab *tab) { if (webkit_web_view_is_loading(webView)) { gtk_spinner_start(GTK_SPINNER(tab->titleSpinner)); gtk_widget_show(tab->titleSpinner); } else { gtk_spinner_stop(GTK_SPINNER(tab->titleSpinner)); gtk_widget_hide(tab->titleSpinner); } } static gboolean decidePolicy(WebKitWebView *webView, WebKitPolicyDecision *decision, WebKitPolicyDecisionType decisionType, BrowserTab *tab) { if (decisionType != WEBKIT_POLICY_DECISION_TYPE_RESPONSE) return FALSE; WebKitResponsePolicyDecision *responseDecision = WEBKIT_RESPONSE_POLICY_DECISION(decision); if (webkit_response_policy_decision_is_mime_type_supported(responseDecision)) return FALSE; WebKitWebResource *mainResource = webkit_web_view_get_main_resource(webView); WebKitURIRequest *request = webkit_response_policy_decision_get_request(responseDecision); const char *requestURI = webkit_uri_request_get_uri(request); if (g_strcmp0(webkit_web_resource_get_uri(mainResource), requestURI)) return FALSE; webkit_policy_decision_download(decision); return TRUE; } static void removeChildIfInfoBar(GtkWidget *child, GtkContainer *tab) { if (GTK_IS_INFO_BAR(child)) gtk_container_remove(tab, child); } static void loadChanged(WebKitWebView *webView, WebKitLoadEvent loadEvent, BrowserTab *tab) { if (loadEvent != WEBKIT_LOAD_STARTED) return; gtk_container_foreach(GTK_CONTAINER(tab), (GtkCallback)removeChildIfInfoBar, tab); } static GtkWidget *createInfoBarQuestionMessage(const char *title, const char *text) { GtkWidget *dialog = gtk_info_bar_new_with_buttons("No", GTK_RESPONSE_NO, "Yes", GTK_RESPONSE_YES, NULL); gtk_info_bar_set_message_type(GTK_INFO_BAR(dialog), GTK_MESSAGE_QUESTION); GtkWidget *contentBox = gtk_info_bar_get_content_area(GTK_INFO_BAR(dialog)); gtk_orientable_set_orientation(GTK_ORIENTABLE(contentBox), GTK_ORIENTATION_VERTICAL); gtk_box_set_spacing(GTK_BOX(contentBox), 0); GtkWidget *label = gtk_label_new(NULL); gchar *markup = g_strdup_printf("%s", title); gtk_label_set_markup(GTK_LABEL(label), markup); g_free(markup); gtk_label_set_line_wrap(GTK_LABEL(label), TRUE); gtk_label_set_selectable(GTK_LABEL(label), TRUE); gtk_misc_set_alignment(GTK_MISC(label), 0., 0.5); gtk_box_pack_start(GTK_BOX(contentBox), label, FALSE, FALSE, 2); gtk_widget_show(label); label = gtk_label_new(text); gtk_label_set_line_wrap(GTK_LABEL(label), TRUE); gtk_label_set_selectable(GTK_LABEL(label), TRUE); gtk_misc_set_alignment(GTK_MISC(label), 0., 0.5); gtk_box_pack_start(GTK_BOX(contentBox), label, FALSE, FALSE, 0); gtk_widget_show(label); return dialog; } static void tlsErrorsDialogResponse(GtkWidget *dialog, gint response, BrowserTab *tab) { if (response == GTK_RESPONSE_YES) { const char *failingURI = (const char *)g_object_get_data(G_OBJECT(dialog), "failingURI"); GTlsCertificate *certificate = (GTlsCertificate *)g_object_get_data(G_OBJECT(dialog), "certificate"); SoupURI *uri = soup_uri_new(failingURI); webkit_web_context_allow_tls_certificate_for_host(webkit_web_view_get_context(tab->webView), certificate, uri->host); soup_uri_free(uri); webkit_web_view_load_uri(tab->webView, failingURI); } gtk_widget_destroy(dialog); } static gboolean loadFailedWithTLSerrors(WebKitWebView *webView, const char *failingURI, GTlsCertificate *certificate, GTlsCertificateFlags errors, BrowserTab *tab) { gchar *text = g_strdup_printf("Failed to load %s: Do you want to continue ignoring the TLS errors?", failingURI); GtkWidget *dialog = createInfoBarQuestionMessage("Invalid TLS Certificate", text); g_free(text); g_object_set_data_full(G_OBJECT(dialog), "failingURI", g_strdup(failingURI), g_free); g_object_set_data_full(G_OBJECT(dialog), "certificate", g_object_ref(certificate), g_object_unref); g_signal_connect(dialog, "response", G_CALLBACK(tlsErrorsDialogResponse), tab); gtk_box_pack_start(GTK_BOX(tab), dialog, FALSE, FALSE, 0); gtk_box_reorder_child(GTK_BOX(tab), dialog, 0); gtk_widget_show(dialog); return TRUE; } static void permissionRequestDialogResponse(GtkWidget *dialog, gint response, WebKitPermissionRequest *request) { switch (response) { case GTK_RESPONSE_YES: webkit_permission_request_allow(request); break; default: webkit_permission_request_deny(request); break; } gtk_widget_destroy(dialog); g_object_unref(request); } static gboolean decidePermissionRequest(WebKitWebView *webView, WebKitPermissionRequest *request, BrowserTab *tab) { const gchar *title = NULL; gchar *text = NULL; if (WEBKIT_IS_GEOLOCATION_PERMISSION_REQUEST(request)) { title = "Geolocation request"; text = g_strdup("Allow geolocation request?"); } else if (WEBKIT_IS_NOTIFICATION_PERMISSION_REQUEST(request)) { title = "Notification request"; text = g_strdup("Allow notifications request?"); } else if (WEBKIT_IS_USER_MEDIA_PERMISSION_REQUEST(request)) { title = "UserMedia request"; gboolean is_for_audio_device = webkit_user_media_permission_is_for_audio_device(WEBKIT_USER_MEDIA_PERMISSION_REQUEST(request)); gboolean is_for_video_device = webkit_user_media_permission_is_for_video_device(WEBKIT_USER_MEDIA_PERMISSION_REQUEST(request)); const char *mediaType = NULL; if (is_for_audio_device) { if (is_for_video_device) mediaType = "audio/video"; else mediaType = "audio"; } else if (is_for_video_device) mediaType = "video"; text = g_strdup_printf("Allow access to %s device?", mediaType); } else if (WEBKIT_IS_INSTALL_MISSING_MEDIA_PLUGINS_PERMISSION_REQUEST(request)) { title = "Media plugin missing request"; text = g_strdup_printf("The media backend was unable to find a plugin to play the requested media:\n%s.\nAllow to search and install the missing plugin?", webkit_install_missing_media_plugins_permission_request_get_description(WEBKIT_INSTALL_MISSING_MEDIA_PLUGINS_PERMISSION_REQUEST(request))); } else return FALSE; GtkWidget *dialog = createInfoBarQuestionMessage(title, text); g_free(text); g_signal_connect(dialog, "response", G_CALLBACK(permissionRequestDialogResponse), g_object_ref(request)); gtk_box_pack_start(GTK_BOX(tab), dialog, FALSE, FALSE, 0); gtk_box_reorder_child(GTK_BOX(tab), dialog, 0); gtk_widget_show(dialog); return TRUE; } #if GTK_CHECK_VERSION(3, 12, 0) static void colorChooserRGBAChanged(GtkColorChooser *colorChooser, GParamSpec *paramSpec, WebKitColorChooserRequest *request) { GdkRGBA rgba; gtk_color_chooser_get_rgba(colorChooser, &rgba); webkit_color_chooser_request_set_rgba(request, &rgba); } static void popoverColorClosed(GtkWidget *popover, WebKitColorChooserRequest *request) { webkit_color_chooser_request_finish(request); } static void colorChooserRequestFinished(WebKitColorChooserRequest *request, GtkWidget *popover) { g_object_unref(request); gtk_widget_destroy(popover); } static gboolean runColorChooserCallback(WebKitWebView *webView, WebKitColorChooserRequest *request, BrowserTab *tab) { GtkWidget *popover = gtk_popover_new(GTK_WIDGET(webView)); GdkRectangle rectangle; webkit_color_chooser_request_get_element_rectangle(request, &rectangle); gtk_popover_set_pointing_to(GTK_POPOVER(popover), &rectangle); GtkWidget *colorChooser = gtk_color_chooser_widget_new(); GdkRGBA rgba; webkit_color_chooser_request_get_rgba(request, &rgba); gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(colorChooser), &rgba); g_signal_connect(colorChooser, "notify::rgba", G_CALLBACK(colorChooserRGBAChanged), request); gtk_container_add(GTK_CONTAINER(popover), colorChooser); gtk_widget_show(colorChooser); g_object_ref(request); g_signal_connect_object(popover, "hide", G_CALLBACK(popoverColorClosed), request, 0); g_signal_connect_object(request, "finished", G_CALLBACK(colorChooserRequestFinished), popover, 0); gtk_widget_show(popover); return TRUE; } #endif /* GTK_CHECK_VERSION(3, 12, 0) */ static gboolean inspectorOpenedInWindow(WebKitWebInspector *inspector, BrowserTab *tab) { tab->inspectorIsVisible = TRUE; return FALSE; } static gboolean inspectorClosed(WebKitWebInspector *inspector, BrowserTab *tab) { tab->inspectorIsVisible = FALSE; return FALSE; } static void browserTabSetProperty(GObject *object, guint propId, const GValue *value, GParamSpec *pspec) { BrowserTab *tab = BROWSER_TAB(object); switch (propId) { case PROP_VIEW: tab->webView = g_value_get_object(value); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, propId, pspec); } } static void browserTabFinalize(GObject *gObject) { BrowserTab *tab = BROWSER_TAB(gObject); if (tab->fullScreenMessageLabelId) g_source_remove(tab->fullScreenMessageLabelId); G_OBJECT_CLASS(browser_tab_parent_class)->finalize(gObject); } static void browser_tab_init(BrowserTab *tab) { gtk_orientable_set_orientation(GTK_ORIENTABLE(tab), GTK_ORIENTATION_VERTICAL); } static void browserTabConstructed(GObject *gObject) { BrowserTab *tab = BROWSER_TAB(gObject); G_OBJECT_CLASS(browser_tab_parent_class)->constructed(gObject); tab->searchBar = BROWSER_SEARCH_BAR(browser_search_bar_new(tab->webView)); gtk_box_pack_start(GTK_BOX(tab), GTK_WIDGET(tab->searchBar), FALSE, FALSE, 0); GtkWidget *overlay = gtk_overlay_new(); gtk_box_pack_start(GTK_BOX(tab), overlay, TRUE, TRUE, 0); gtk_widget_show(overlay); tab->statusLabel = gtk_label_new(NULL); gtk_widget_set_halign(tab->statusLabel, GTK_ALIGN_START); gtk_widget_set_valign(tab->statusLabel, GTK_ALIGN_END); gtk_widget_set_margin_left(tab->statusLabel, 1); gtk_widget_set_margin_right(tab->statusLabel, 1); gtk_widget_set_margin_top(tab->statusLabel, 1); gtk_widget_set_margin_bottom(tab->statusLabel, 1); gtk_overlay_add_overlay(GTK_OVERLAY(overlay), tab->statusLabel); tab->fullScreenMessageLabel = gtk_label_new(NULL); gtk_widget_set_halign(tab->fullScreenMessageLabel, GTK_ALIGN_CENTER); gtk_widget_set_valign(tab->fullScreenMessageLabel, GTK_ALIGN_CENTER); gtk_widget_set_no_show_all(tab->fullScreenMessageLabel, TRUE); gtk_overlay_add_overlay(GTK_OVERLAY(overlay), tab->fullScreenMessageLabel); gtk_container_add(GTK_CONTAINER(overlay), GTK_WIDGET(tab->webView)); gtk_widget_show(GTK_WIDGET(tab->webView)); tab->titleBox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4); GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6); gtk_widget_set_halign(hbox, GTK_ALIGN_CENTER); tab->titleSpinner = gtk_spinner_new(); gtk_box_pack_start(GTK_BOX(hbox), tab->titleSpinner, FALSE, FALSE, 0); tab->titleLabel = gtk_label_new(NULL); gtk_label_set_ellipsize(GTK_LABEL(tab->titleLabel), PANGO_ELLIPSIZE_END); gtk_label_set_single_line_mode(GTK_LABEL(tab->titleLabel), TRUE); gtk_misc_set_padding(GTK_MISC(tab->titleLabel), 0, 0); gtk_box_pack_start(GTK_BOX(hbox), tab->titleLabel, FALSE, FALSE, 0); gtk_widget_show(tab->titleLabel); gtk_box_pack_start(GTK_BOX(tab->titleBox), hbox, TRUE, TRUE, 0); gtk_widget_show(hbox); tab->titleCloseButton = gtk_button_new(); g_signal_connect_swapped(tab->titleCloseButton, "clicked", G_CALLBACK(gtk_widget_destroy), tab); gtk_button_set_relief(GTK_BUTTON(tab->titleCloseButton), GTK_RELIEF_NONE); gtk_button_set_focus_on_click(GTK_BUTTON(tab->titleCloseButton), FALSE); GtkWidget *image = gtk_image_new_from_icon_name("window-close-symbolic", GTK_ICON_SIZE_MENU); gtk_container_add(GTK_CONTAINER(tab->titleCloseButton), image); gtk_widget_show(image); gtk_box_pack_start(GTK_BOX(tab->titleBox), tab->titleCloseButton, FALSE, FALSE, 0); gtk_widget_show(tab->titleCloseButton); g_signal_connect(tab->webView, "notify::title", G_CALLBACK(titleChanged), tab); g_signal_connect(tab->webView, "notify::is-loading", G_CALLBACK(isLoadingChanged), tab); g_signal_connect(tab->webView, "decide-policy", G_CALLBACK(decidePolicy), tab); g_signal_connect(tab->webView, "load-changed", G_CALLBACK(loadChanged), tab); g_signal_connect(tab->webView, "load-failed-with-tls-errors", G_CALLBACK(loadFailedWithTLSerrors), tab); g_signal_connect(tab->webView, "permission-request", G_CALLBACK(decidePermissionRequest), tab); #if GTK_CHECK_VERSION(3, 12, 0) g_signal_connect(tab->webView, "run-color-chooser", G_CALLBACK(runColorChooserCallback), tab); #endif WebKitWebInspector *inspector = webkit_web_view_get_inspector(tab->webView); g_signal_connect(inspector, "open-window", G_CALLBACK(inspectorOpenedInWindow), tab); g_signal_connect(inspector, "closed", G_CALLBACK(inspectorClosed), tab); if (webkit_web_view_is_editable(tab->webView)) webkit_web_view_load_html(tab->webView, "", "file:///"); } static void browser_tab_class_init(BrowserTabClass *klass) { GObjectClass *gobjectClass = G_OBJECT_CLASS(klass); gobjectClass->constructed = browserTabConstructed; gobjectClass->set_property = browserTabSetProperty; gobjectClass->finalize = browserTabFinalize; g_object_class_install_property( gobjectClass, PROP_VIEW, g_param_spec_object( "view", "View", "The web view of this tab", WEBKIT_TYPE_WEB_VIEW, G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY)); } static char *getInternalURI(const char *uri) { /* Internally we use minibrowser-about: as about: prefix is ignored by WebKit. */ if (g_str_has_prefix(uri, "about:") && !g_str_equal(uri, "about:blank")) return g_strconcat(BROWSER_ABOUT_SCHEME, uri + strlen ("about"), NULL); return g_strdup(uri); } /* Public API. */ GtkWidget *browser_tab_new(WebKitWebView *view) { g_return_val_if_fail(WEBKIT_IS_WEB_VIEW(view), NULL); return GTK_WIDGET(g_object_new(BROWSER_TYPE_TAB, "view", view, NULL)); } WebKitWebView *browser_tab_get_web_view(BrowserTab *tab) { g_return_val_if_fail(BROWSER_IS_TAB(tab), NULL); return tab->webView; } void browser_tab_load_uri(BrowserTab *tab, const char *uri) { g_return_if_fail(BROWSER_IS_TAB(tab)); g_return_if_fail(uri); if (!g_str_has_prefix(uri, "javascript:")) { char *internalURI = getInternalURI(uri); webkit_web_view_load_uri(tab->webView, internalURI); g_free(internalURI); return; } webkit_web_view_run_javascript(tab->webView, strstr(uri, "javascript:"), NULL, NULL, NULL); } GtkWidget *browser_tab_get_title_widget(BrowserTab *tab) { g_return_val_if_fail(BROWSER_IS_TAB(tab), NULL); return tab->titleBox; } void browser_tab_set_status_text(BrowserTab *tab, const char *text) { g_return_if_fail(BROWSER_IS_TAB(tab)); gtk_label_set_text(GTK_LABEL(tab->statusLabel), text); gtk_widget_set_visible(tab->statusLabel, !!text); } void browser_tab_toggle_inspector(BrowserTab *tab) { g_return_if_fail(BROWSER_IS_TAB(tab)); WebKitWebInspector *inspector = webkit_web_view_get_inspector(tab->webView); if (!tab->inspectorIsVisible) { webkit_web_inspector_show(inspector); tab->inspectorIsVisible = TRUE; } else webkit_web_inspector_close(inspector); } void browser_tab_start_search(BrowserTab *tab) { g_return_if_fail(BROWSER_IS_TAB(tab)); if (!gtk_widget_get_visible(GTK_WIDGET(tab->searchBar))) browser_search_bar_open(tab->searchBar); } void browser_tab_stop_search(BrowserTab *tab) { g_return_if_fail(BROWSER_IS_TAB(tab)); if (gtk_widget_get_visible(GTK_WIDGET(tab->searchBar))) browser_search_bar_close(tab->searchBar); } void browser_tab_add_accelerators(BrowserTab *tab, GtkAccelGroup *accelGroup) { g_return_if_fail(BROWSER_IS_TAB(tab)); g_return_if_fail(GTK_IS_ACCEL_GROUP(accelGroup)); browser_search_bar_add_accelerators(tab->searchBar, accelGroup); } static gboolean fullScreenMessageTimeoutCallback(BrowserTab *tab) { gtk_widget_hide(tab->fullScreenMessageLabel); tab->fullScreenMessageLabelId = 0; return FALSE; } void browser_tab_enter_fullscreen(BrowserTab *tab) { g_return_if_fail(BROWSER_IS_TAB(tab)); const gchar *titleOrURI = webkit_web_view_get_title(tab->webView); if (!titleOrURI || !titleOrURI[0]) titleOrURI = webkit_web_view_get_uri(tab->webView); gchar *message = g_strdup_printf("%s is now full screen. Press ESC or f to exit.", titleOrURI); gtk_label_set_text(GTK_LABEL(tab->fullScreenMessageLabel), message); g_free(message); gtk_widget_show(tab->fullScreenMessageLabel); tab->fullScreenMessageLabelId = g_timeout_add_seconds(2, (GSourceFunc)fullScreenMessageTimeoutCallback, tab); g_source_set_name_by_id(tab->fullScreenMessageLabelId, "[WebKit] fullScreenMessageTimeoutCallback"); tab->wasSearchingWhenEnteredFullscreen = gtk_widget_get_visible(GTK_WIDGET(tab->searchBar)); browser_tab_stop_search(tab); } void browser_tab_leave_fullscreen(BrowserTab *tab) { g_return_if_fail(BROWSER_IS_TAB(tab)); if (tab->fullScreenMessageLabelId) { g_source_remove(tab->fullScreenMessageLabelId); tab->fullScreenMessageLabelId = 0; } gtk_widget_hide(tab->fullScreenMessageLabel); if (tab->wasSearchingWhenEnteredFullscreen) { /* Opening the search bar steals the focus. Usually, we want * this but not when coming back from fullscreen. */ GtkWindow *window = GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(tab))); GtkWidget *focusWidget = gtk_window_get_focus(window); browser_tab_start_search(tab); gtk_window_set_focus(window, focusWidget); } }