| /******************************************************************************* |
| * Copyright (c) 2010, 2019 IBM Corporation and others. |
| * |
| * This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License 2.0 |
| * which accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * Contributors: |
| * IBM Corporation - initial API and implementation |
| * Red Hat Inc. - generification |
| *******************************************************************************/ |
| package org.eclipse.swt.browser; |
| |
| |
| import java.io.*; |
| import java.lang.reflect.*; |
| import java.net.*; |
| import java.nio.charset.*; |
| import java.time.*; |
| import java.util.*; |
| import java.util.concurrent.atomic.*; |
| import java.util.function.*; |
| |
| import org.eclipse.swt.*; |
| import org.eclipse.swt.graphics.*; |
| import org.eclipse.swt.internal.*; |
| import org.eclipse.swt.internal.gtk.*; |
| import org.eclipse.swt.internal.gtk3.*; |
| import org.eclipse.swt.internal.webkit.*; |
| import org.eclipse.swt.internal.webkit.GdkRectangle; |
| import org.eclipse.swt.layout.*; |
| import org.eclipse.swt.widgets.*; |
| |
| /** |
| * VERSIONS: |
| * Versioning for webkit is somewhat confusing because it's trying to incorporate webkit, gtk and (various linux distribution) versions. |
| * The way they version webkitGTK is different from webkit. |
| * WebkitGTK: |
| * 2.5 is webkit2. [2.4-..) is Gtk3. |
| * Further, linux distributions might refer to webkit2 bindings linked against gtk3 differently. |
| * E.g on Fedora: |
| * webkitgtk4 = webkit2 / Gtk3 |
| * webkit2gtk3 = WebKit2/ Gtk3 |
| * |
| * Webkit2 loading: |
| * - Dynamic bindings are auto generated and linked when the @dynamic keyword is used in WebKitGTK.java |
| * Unlike in OS.java, you don't have to add any code saying what lib the dynamic method is linked to. It's auto-linked to webkit lib by default. |
| * - At no point should you have hard-compiled code, because this will cause crashes on older machines without webkit2. |
| * (the exception is the webextension, because it runs as a separate process and is only loaded dynamically). |
| * - Try to keep all of your logic in Java and avoid writing custom C-code. (I went down this pit). Because if you |
| * use native code, then you have to write dynamic native code (get function pointers, cast types etc.. big pain in the ass). |
| * (Webextension is again an exception). |
| * - Don't try to add webkit2 include flags to pkg-config, as this will tie the swt-glue code to specific webkit versions. Thou shall not do this. |
| * (webextension is an exception). |
| * |
| * Webextension: |
| * - On Webkit2, a webextension is used to provide browserfunction/javascript callback functionality. (See the whole WebkitGDBus.java business). |
| * - I've initially implemented javascript execution by running javascript and then waiting in a display-loop until webkit makes a return call. |
| * I then added a whole bunch of logic to avoid deadlocks. |
| * In retrospec, the better approach would be to send things off via GDBus and let the webextension run the javascript synchronously. |
| * But this would take another 1-2 months of implementation time and wouldn't guarantee dead-lock free behaviour as callbacks could potentailly still |
| * cause deadlocks. It's an interesting thought however.. |
| * - Note, most GDBus tutorials talk about compiling GDBus bindings. But using them dynamically I found is much easier. See this guide: |
| * http://www.cs.grinnell.edu/~rebelsky/Courses/CSC195/2013S/Outlines/ |
| * |
| * |
| * EVENT_HANDLING_DOC: |
| * - On webkit2, signals are implemented via regular gtk mechanism, hook events and pass them along as we receive them. |
| * I haven't found a need to use the dom events, because webkitgtk seems to adequately meet the requirements via regular gtk |
| * events, but maybe I missed something? Who knows. |
| * |
| * setUrl(..) with 'post data' was implemented in a very hacky way, via native Java due to missing webkit2gtk api. |
| * It's the best that could be done at the time, but it could result in strange behavior like some webpages loading in funky ways if post-data is used. |
| * |
| * Some good resources that I found are as following: |
| * - Webkit2 reference: https://webkitgtk.org/reference/webkit2gtk/stable/ |
| * |
| * - My github repository has a lot of snippets to prototype individual features (e.g gdbus, barebone webkit extension, GVariants etc..): |
| * https://github.com/LeoUfimtsev/LeoGtk3 |
| * Be also mindful about snippets found in org.eclipse.swt.gtk.linux.x86_64 -> snippets -> widget.browser. |
| * |
| * - To understand GDBus, consider reading this guide: |
| * http://www.cs.grinnell.edu/~rebelsky/Courses/CSC195/2013S/Outlines/ |
| * And then see the relevant reference I made in WebkitGDBus.java. |
| * Note, DBus is not the same as GDBus. GDBus is an implementation of the DBus protocol (with it's own quirks). |
| * |
| * - This is a good starting point for webkit2 extension reading: |
| * https://blogs.igalia.com/carlosgc/2013/09/10/webkit2gtk-web-process-extensions/ |
| * |
| * [April 2018] |
| * Note on WebKitContext: |
| * We only use a single webcontext, so WebKitGTK.webkit_web_context_get_default() works well for getting this when |
| * needed. |
| * |
| * |
| * |
| * ~May the force be with you. |
| */ |
| class WebKit extends WebBrowser { |
| long webView; |
| long pageId; |
| |
| int failureCount, lastKeyCode, lastCharCode; |
| |
| boolean ignoreDispose; |
| boolean tlsError; |
| long tlsErrorCertificate; |
| String tlsErrorUriString; |
| URI tlsErrorUri; |
| String tlsErrorType; |
| |
| boolean firstLoad = true; |
| |
| /** |
| * Timeout used for javascript execution / deadlock detection. |
| * Loosely based on the 10s limit commonly found in browsers. |
| * (Except for SWT browser we use 3s as chunks of the UI is blocked). |
| * https://www.nczonline.net/blog/2009/01/05/what-determines-that-a-script-is-long-running/ |
| * https://stackoverflow.com/questions/3030024/maximum-execution-time-for-javascript |
| */ |
| static final int ASYNC_EXEC_TIMEOUT_MS = 10000; |
| |
| /** Workaround for bug 522733 */ |
| static boolean bug522733FirstInstanceCreated = false; |
| |
| /** Part of workaround in Bug 527738. Prevent old request overring newer request */ |
| static AtomicInteger w2_bug527738LastRequestCounter = new AtomicInteger(); |
| |
| /** |
| * Webkit2: In a few situations, evaluate() should not wait for it's asynchronous callback to finish. |
| * This is to avoid deadlocks, see Bug 512001.<br> |
| * 0 means evaluate should wait for callback. <br> |
| * >0 means evaluate should not block. In this case 'null' is returned. This condition is rare. <br> |
| * |
| * <p>Note: This has to be *static*. |
| * Webkit2 seems to share one event queue, as such two webkit2 instances can interfere with each other. |
| * An example of this interfering is when you open a link in a javadoc hover. The new webkit2 in the new tab |
| * interferes with the old instance in the hoverbox. |
| * As such, any locks should apply to all webkit2 instances.</p> |
| */ |
| private static int nonBlockingEvaluate = 0; |
| |
| static Map<LONG, Integer> webKitDownloadStatus = new HashMap<> (); |
| |
| static final String ABOUT_BLANK = "about:blank"; //$NON-NLS-1$ |
| static final String CLASSNAME_EXTERNAL = "External"; //$NON-NLS-1$ |
| static final String FUNCTIONNAME_CALLJAVA = "callJava"; //$NON-NLS-1$ |
| static final String HEADER_CONTENTTYPE = "content-type"; //$NON-NLS-1$ |
| static final String MIMETYPE_FORMURLENCODED = "application/x-www-form-urlencoded"; //$NON-NLS-1$ |
| static final String OBJECTNAME_EXTERNAL = "external"; //$NON-NLS-1$ |
| static final String PROPERTY_LENGTH = "length"; //$NON-NLS-1$ |
| static final String PROPERTY_PROXYHOST = "network.proxy_host"; //$NON-NLS-1$ |
| static final String PROPERTY_PROXYPORT = "network.proxy_port"; //$NON-NLS-1$ |
| static final String PROTOCOL_FILE = "file://"; //$NON-NLS-1$ |
| static final String PROTOCOL_HTTP = "http://"; //$NON-NLS-1$ |
| static final String URI_FILEROOT = "file:///"; //$NON-NLS-1$ |
| static final String USER_AGENT = "user-agent"; //$NON-NLS-1$ |
| static final int MAX_PORT = 65535; |
| static final int MAX_PROGRESS = 100; |
| static final int[] MIN_VERSION = {1, 2, 0}; |
| static final int SENTINEL_KEYPRESS = -1; |
| static final char SEPARATOR_FILE = File.separatorChar; |
| static final int STOP_PROPOGATE = 1; |
| |
| static final String DOMEVENT_DRAGSTART = "dragstart"; //$NON-NLS-1$ |
| static final String DOMEVENT_KEYDOWN = "keydown"; //$NON-NLS-1$ |
| static final String DOMEVENT_KEYPRESS = "keypress"; //$NON-NLS-1$ |
| static final String DOMEVENT_KEYUP = "keyup"; //$NON-NLS-1$ |
| static final String DOMEVENT_MOUSEDOWN = "mousedown"; //$NON-NLS-1$ |
| static final String DOMEVENT_MOUSEUP = "mouseup"; //$NON-NLS-1$ |
| static final String DOMEVENT_MOUSEMOVE = "mousemove"; //$NON-NLS-1$ |
| static final String DOMEVENT_MOUSEOUT = "mouseout"; //$NON-NLS-1$ |
| static final String DOMEVENT_MOUSEOVER = "mouseover"; //$NON-NLS-1$ |
| static final String DOMEVENT_MOUSEWHEEL = "mousewheel"; //$NON-NLS-1$ |
| |
| /* WebKit signal data */ |
| static final int NOTIFY_PROGRESS = 1; |
| static final int NOTIFY_TITLE = 2; |
| static final int CREATE_WEB_VIEW = 3; |
| static final int WEB_VIEW_READY = 4; |
| static final int CLOSE_WEB_VIEW = 5; |
| static final int LOAD_CHANGED = 6; |
| static final int DECIDE_POLICY = 7; |
| static final int MOUSE_TARGET_CHANGED = 8; |
| static final int CONTEXT_MENU = 9; |
| static final int AUTHENTICATE = 10; |
| static final int DECIDE_DESTINATION = 11; |
| static final int FAILED = 12; |
| static final int FINISHED = 13; |
| static final int DOWNLOAD_STARTED = 14; |
| static final int WIDGET_EVENT = 15; // Used for events like keyboard/mouse input. See Bug 528549 and Bug 533833. |
| static final int LOAD_FAILED_TLS = 16; |
| |
| static final String KEY_CHECK_SUBWINDOW = "org.eclipse.swt.internal.control.checksubwindow"; //$NON-NLS-1$ |
| |
| static final String SWT_WEBKITGTK_VERSION = "org.eclipse.swt.internal.webkitgtk.version"; //$NON-NLS-1$ |
| |
| /* the following Callbacks are never freed */ |
| static Callback Proc2, Proc3, Proc4, Proc5; |
| |
| /** Process key/mouse events from javascript. */ |
| static Callback JSDOMEventProc; |
| |
| /** Flag indicating whether TLS errors (like self-signed certificates) are to be ignored. */ |
| static final boolean ignoreTls; |
| |
| static { |
| Proc2 = new Callback (WebKit.class, "Proc", 2); //$NON-NLS-1$ |
| Proc3 = new Callback (WebKit.class, "Proc", 3); //$NON-NLS-1$ |
| Proc4 = new Callback (WebKit.class, "Proc", 4); //$NON-NLS-1$ |
| Proc5 = new Callback (WebKit.class, "Proc", 5); //$NON-NLS-1$ |
| new Webkit2AsyncToSync(); |
| |
| WebKitExtension.init(); |
| |
| JSDOMEventProc = new Callback (WebKit.class, "JSDOMEventProc", 3); //$NON-NLS-1$ |
| |
| NativeClearSessions = () -> { |
| if (!WebKitGTK.LibraryLoaded) return; |
| if (WebKitGTK.webkit_get_minor_version() >= 16) { |
| long context = WebKitGTK.webkit_web_context_get_default(); |
| long manager = WebKitGTK.webkit_web_context_get_website_data_manager (context); |
| WebKitGTK.webkit_website_data_manager_clear(manager, WebKitGTK.WEBKIT_WEBSITE_DATA_COOKIES, 0, 0, 0, 0); |
| } else { |
| System.err.println("SWT WebKit: clear sessions only supported on WebKitGtk version 2.16 and above. " |
| + "Your version is: " + internalGetWebKitVersionStr()); |
| } |
| }; |
| |
| NativeGetCookie = () -> { |
| if (!WebKitGTK.LibraryLoaded) return; |
| if (WebKitGTK.webkit_get_minor_version() >= 20) { |
| CookieValue = Webkit2AsyncToSync.getCookie(CookieUrl, CookieName); |
| } else { |
| System.err.println("SWT WebKit: getCookie() only supported on WebKitGTK version 2.20 and above. " |
| + "Your version is: " + internalGetWebKitVersionStr()); |
| } |
| }; |
| |
| NativeSetCookie = () -> { |
| if (!WebKitGTK.LibraryLoaded) return; |
| if (WebKitGTK.webkit_get_minor_version() >= 20) { |
| CookieResult = Webkit2AsyncToSync.setCookie(CookieUrl, CookieValue); |
| } else { |
| System.err.println("SWT WebKit: setCookie() only supported on WebKitGTK version 2.20 and above. " |
| + "Your version is: " + internalGetWebKitVersionStr()); |
| } |
| }; |
| |
| if (NativePendingCookies != null) { |
| SetPendingCookies (NativePendingCookies); |
| NativePendingCookies = null; |
| } |
| ignoreTls = "true".equals(System.getProperty("org.eclipse.swt.internal.webkitgtk.ignoretlserrors")); |
| } |
| |
| @Override |
| public void createFunction(BrowserFunction function) { |
| if (!WebkitGDBus.initialized) { |
| System.err.println("SWT webkit: WebkitGDBus and/or Webkit2Extension not loaded, BrowserFunction will not work." + |
| "Tried to create "+ function.name); |
| return; |
| } |
| super.createFunction(function); |
| String url = this.getUrl().isEmpty() ? "nullURL" : this.getUrl(); |
| /* |
| * If the proxy to the extension has not yet been loaded, store the BrowserFunction page ID, |
| * function string, and URL in a HashMap. Once the proxy to the extension is loaded, these |
| * functions will be sent to and registered in the extension. |
| */ |
| if (!WebkitGDBus.connectionToExtensionCreated) { |
| WebkitGDBus.functionsPending = true; |
| ArrayList<ArrayList<String>> list = new ArrayList<>(); |
| ArrayList<String> functionAndUrl = new ArrayList<>(); |
| functionAndUrl.add(0, function.functionString); |
| functionAndUrl.add(1, url); |
| list.add(functionAndUrl); |
| ArrayList<ArrayList<String>> existing = WebkitGDBus.pendingBrowserFunctions.putIfAbsent(this.pageId, list); |
| if (existing != null) { |
| existing.add(functionAndUrl); |
| } |
| } else { |
| // If the proxy to the extension is already loaded, register the function in the extension via DBus |
| boolean successful = webkit_extension_modify_function(this.pageId, function.functionString, url, "register"); |
| if (!successful) { |
| System.err.println("SWT webkit: failure registering BrowserFunction " + function.name); |
| } |
| } |
| } |
| |
| @Override |
| public void destroyFunction (BrowserFunction function) { |
| // Only deregister functions if the proxy to the extension has been loaded |
| if (WebkitGDBus.connectionToExtensionCreated) { |
| String url = this.getUrl().isEmpty() ? "nullURL" : this.getUrl(); |
| boolean successful = webkit_extension_modify_function(this.pageId, function.functionString, url, "deregister"); |
| if (!successful) { |
| System.err.println("SWT webkit: failure deregistering BrowserFunction from extension " + function.name); |
| } |
| } |
| super.destroyFunction(function); |
| } |
| |
| private static String getInternalErrorMsg () { |
| String reportErrMsg = "Please report this issue *with steps to reproduce* via:\n" |
| + " https://bugs.eclipse.org/bugs/enter_bug.cgi?" |
| + "alias=&assigned_to=platform-swt-inbox%40eclipse.org&attach_text=&blocked=&bug_file_loc=http%3A%2F%2F&bug_severity=normal" |
| + "&bug_status=NEW&comment=&component=SWT&contenttypeentry=&contenttypemethod=autodetect&contenttypeselection=text%2Fplain" |
| + "&data=&defined_groups=1&dependson=&description=&flag_type-1=X&flag_type-11=X&flag_type-12=X&flag_type-13=X&flag_type-14=X" |
| + "&flag_type-15=X&flag_type-16=X&flag_type-2=X&flag_type-4=X&flag_type-6=X&flag_type-7=X&flag_type-8=X&form_name=enter_bug" |
| + "&keywords=&maketemplate=Remember%20values%20as%20bookmarkable%20template&op_sys=Linux&product=Platform&qa_contact=" |
| + "&rep_platform=PC&requestee_type-1=&requestee_type-2=&short_desc=webkit2_BrowserProblem"; |
| |
| return reportErrMsg + "\nFor bug report, please atatch this stack trace:\n" + getStackTrace(); |
| } |
| |
| |
| private static String getStackTrace() { |
| // Get a stacktrace. Note, this doesn't actually throw anything, we just get the stacktrace. |
| StringWriter sw = new StringWriter(); |
| new Throwable("").printStackTrace(new PrintWriter(sw)); |
| return sw.toString(); |
| } |
| |
| /** |
| * This class deals with the WebKit extension. |
| * |
| * Extension is separately loaded and deals Javascript callbacks to Java. |
| * Extension is needed so that Javascript can receive a return value from Java |
| * (for which currently there is no api in WebkitGtk 2.18) |
| */ |
| static class WebKitExtension { |
| /** Note, if updating this, you need to change it also in webkitgtk_extension.c */ |
| private static final String javaScriptFunctionName = "webkit2callJava"; // $NON-NLS-1$ |
| private static final String webkitWebExtensionIdentifier = "webkitWebExtensionIdentifier"; // $NON-NLS-1$ |
| private static Callback initializeWebExtensions_callback; |
| |
| /** GDBusServer returned by WebkitGDBus */ |
| private static long dBusServer = 0; |
| |
| /** |
| * Don't continue initialization if something failed. This allows Browser to carryout some functionality |
| * even if the webextension failed to load. |
| */ |
| private static boolean loadFailed; |
| |
| static String getJavaScriptFunctionName() { |
| return javaScriptFunctionName; |
| } |
| static String getWebExtensionIdentifier() { |
| return webkitWebExtensionIdentifier; |
| } |
| static String getJavaScriptFunctionDeclaration(long webView) { |
| return "if (!window.callJava) {\n" |
| + " window.callJava = function callJava(index, token, args) {\n" |
| + " return " + javaScriptFunctionName + "('" + String.valueOf(webView) + "', index, token, args);\n" |
| + " }\n" |
| + "};\n"; |
| } |
| |
| static void init() { |
| /* |
| * Initialize GDBus before the extension, as the extension initialization callback at the C level |
| * sends data back to SWT via GDBus. Failure to load GDBus here will result in crashes. |
| * See bug 536141. |
| */ |
| dBusServer = gdbus_init(); |
| if (dBusServer == 0) { |
| System.err.println("SWT WebKit: error initializing DBus server, dBusServer == 0"); |
| } |
| initializeWebExtensions_callback = new Callback(WebKitExtension.class, "initializeWebExtensions_callback", void.class, new Type [] {long.class, long.class}); |
| if (WebKitGTK.webkit_get_minor_version() >= 4) { // Callback exists only since 2.04 |
| OS.g_signal_connect (WebKitGTK.webkit_web_context_get_default(), WebKitGTK.initialize_web_extensions, initializeWebExtensions_callback.getAddress(), 0); |
| } |
| } |
| |
| /** |
| * GDbus initialization can cause performance slow downs. So we int GDBus in lazy way. |
| * It can be initialized upon first use of BrowserFunction. |
| */ |
| static long gdbus_init() { |
| if (WebKitGTK.webkit_get_minor_version() < 4) { |
| System.err.println("SWT Webkit: Warning, You are using an old version of webkitgtk. (pre 2.4)" |
| + " BrowserFunction functionality will not be avaliable"); |
| return 0; |
| } |
| |
| |
| if (!loadFailed) { |
| return WebkitGDBus.init(); |
| } else { |
| return 0; |
| } |
| } |
| |
| /** |
| * This callback is called to initialize webextension. |
| * It is the optimum place to set extension directory and set initialization user data. |
| * |
| * I've experimented with loading webextension later (to see if we can get performance gains), |
| * but found breakage. Webkitgtk doc says it should be loaded as early as possible and specifically best |
| * to do it in this calllback. |
| * |
| * See documenation: WebKitWebExtension (Description) |
| */ |
| @SuppressWarnings("unused") // Only called directly from C |
| private static void initializeWebExtensions_callback (long WebKitWebContext, long user_data) { |
| // 1) GDBus: |
| // Normally we'd first initialize gdbus channel. But gdbus makes Browser slower and isn't always needed. |
| // So WebkitGDBus is lazy-initialized, although it can be initialized here if gdbus is ever needed |
| // for more than BrowserFunction, like: |
| // WebkitGDBus.init(String.valueOf(uniqueID)); |
| // Also consider only loading gdbus if the extension initialized properly. |
| |
| // 2) Load Webkit Extension: |
| // Webkit extensions should be in their own directory. |
| String swtVersion = Library.getVersionString(); |
| File extension; |
| try { |
| extension = Library.findResource("webkitextensions" + swtVersion ,"swt-webkit2extension", true); |
| if (extension == null){ |
| throw new UnsatisfiedLinkError("SWT Webkit could not find it's webextension"); |
| } |
| } catch (UnsatisfiedLinkError e) { |
| System.err.println("SWT Webkit.java Error: Could not find webkit extension. BrowserFunction functionality will not be available. \n" |
| + "(swt version: " + swtVersion + ")" + WebKitGTK.swtWebkitGlueCodeVersion + WebKitGTK.swtWebkitGlueCodeVersionInfo); |
| int [] vers = internalGetWebkitVersion(); |
| System.err.println(String.format("WebKit2Gtk version %s.%s.%s", vers[0], vers[1], vers[2])); |
| System.err.println(getInternalErrorMsg()); |
| loadFailed = true; |
| return; |
| } |
| |
| String extensionsFolder = extension.getParent(); |
| /* Dev note: |
| * As per |
| * - WebkitSrc: WebKitExtensionManager.cpp, |
| * - IRC discussion with annulen |
| * you cannot load the webextension GModule directly, (webkitgtk 2.18). You can only specify directory and user data. |
| * So we need to treat this '.so' in a special way. |
| * (as a note, the webprocess would have to load the gmodule). |
| */ |
| WebKitGTK.webkit_web_context_set_web_extensions_directory(WebKitGTK.webkit_web_context_get_default(), Converter.wcsToMbcs (extensionsFolder, true)); |
| long clientAddress = OS.g_dbus_server_get_client_address(dBusServer); |
| String clientAddressJava = Converter.cCharPtrToJavaString(clientAddress, false); |
| long gvariantUserData = OS.g_variant_new_string(clientAddress); |
| WebKitGTK.webkit_web_context_set_web_extensions_initialization_user_data(WebKitGTK.webkit_web_context_get_default(), gvariantUserData); |
| } |
| |
| /** |
| * @param cb_args Raw callback arguments by function. |
| */ |
| static Object webkit2callJavaCallback(Object [] cb_args) { |
| assert cb_args.length == 4; |
| Object returnValue = null; |
| Long webViewLocal = (Double.valueOf((String) cb_args[0])).longValue(); |
| Browser browser = FindBrowser((long ) webViewLocal.longValue()); |
| Integer functionIndex = ((Double) cb_args[1]).intValue(); |
| String token = (String) cb_args[2]; |
| |
| BrowserFunction function = browser.webBrowser.functions.get(functionIndex); |
| if (function == null) { |
| System.err.println("SWT Webkit Error: Failed to find function with index: " + functionIndex); |
| return null; |
| } |
| if (!function.token.equals(token)) { |
| System.err.println("SWT Webkit Error: token mismatch for function with index: " + functionIndex); |
| return null; |
| } |
| try { |
| // Call user code. Exceptions can occur. |
| nonBlockingEvaluate++; |
| Object [] user_args = (Object []) cb_args[3]; |
| returnValue = function.function(user_args); |
| } catch (Exception e ) { |
| // - Something went wrong in user code. |
| System.err.println("SWT Webkit: Exception occured in user code of function: " + function.name); |
| returnValue = WebBrowser.CreateErrorString (e.getLocalizedMessage ()); |
| } finally { |
| nonBlockingEvaluate--; |
| } |
| return returnValue; |
| } |
| } |
| |
| @Override |
| String getJavaCallDeclaration() { |
| return WebKitExtension.getJavaScriptFunctionDeclaration(webView); |
| } |
| |
| /** |
| * Gets the webkit version, within an <code>int[3]</code> array with |
| * <code>{major, minor, micro}</code> version |
| */ |
| private static int[] internalGetWebkitVersion(){ |
| int [] vers = new int[3]; |
| vers[0] = WebKitGTK.webkit_get_major_version (); |
| vers[1] = WebKitGTK.webkit_get_minor_version (); |
| vers[2] = WebKitGTK.webkit_get_micro_version (); |
| return vers; |
| } |
| |
| private static String internalGetWebKitVersionStr () { |
| int [] vers = internalGetWebkitVersion(); |
| return String.valueOf(vers[0]) + "." + String.valueOf(vers[1]) + "." + String.valueOf(vers[2]); |
| } |
| |
| |
| static String getString (long strPtr) { |
| int length = C.strlen (strPtr); |
| byte [] buffer = new byte [length]; |
| C.memmove (buffer, strPtr, length); |
| return new String (Converter.mbcsToWcs (buffer)); |
| } |
| |
| static Browser FindBrowser (long webView) { |
| if (webView == 0) return null; |
| long parent = GTK.gtk_widget_get_parent (webView); |
| return (Browser)Display.getCurrent ().findWidget (parent); |
| } |
| |
| static boolean IsInstalled () { |
| if (GTK.GTK4) return false; |
| if (!WebKitGTK.LibraryLoaded) return false; |
| // TODO webkit_check_version() should take care of the following, but for some |
| // reason this symbol is missing from the latest build. If it is present in |
| // Linux distro-provided builds then replace the following with this call. |
| int [] vers = internalGetWebkitVersion(); |
| int major = vers[0], minor = vers[1], micro = vers[2]; |
| return major > MIN_VERSION[0] || |
| (major == MIN_VERSION[0] && minor > MIN_VERSION[1]) || |
| (major == MIN_VERSION[0] && minor == MIN_VERSION[1] && micro >= MIN_VERSION[2]); |
| } |
| |
| static long JSDOMEventProc (long arg0, long event, long user_data) { |
| if (user_data == WIDGET_EVENT) { |
| /* |
| * Only consider using GDK events to create SWT events to send if JS is disabled |
| * in one or more WebKit instances (indicates that this instance may not be |
| * receiving events from the DOM). This check is done up-front for performance. |
| */ |
| final Browser browser = FindBrowser (arg0); |
| if (browser != null && user_data == WIDGET_EVENT){ |
| /* this instance does need to use the GDK event to create an SWT event to send */ |
| switch (GDK.GDK_EVENT_TYPE (event)) { |
| case GDK.GDK_KEY_PRESS: { |
| if (browser.isFocusControl ()) { |
| int [] key = new int [1]; |
| int [] state = new int[1]; |
| if (GTK.GTK4) { |
| key[0] = GDK.gdk_key_event_get_keyval(event); |
| state[0] = GDK.gdk_event_get_modifier_state(event); |
| } else { |
| GDK.gdk_event_get_keyval(event, key); |
| GDK.gdk_event_get_state(event, state); |
| } |
| |
| switch (key[0]) { |
| case GDK.GDK_ISO_Left_Tab: |
| case GDK.GDK_Tab: { |
| if ((state[0] & (GDK.GDK_CONTROL_MASK | GDK.GDK_MOD1_MASK)) == 0) { |
| browser.getDisplay ().asyncExec (() -> { |
| if (browser.isDisposed ()) return; |
| if (browser.getDisplay ().getFocusControl () == null) { |
| int traversal = (state[0] & GDK.GDK_SHIFT_MASK) != 0 ? SWT.TRAVERSE_TAB_PREVIOUS : SWT.TRAVERSE_TAB_NEXT; |
| browser.traverse (traversal); |
| } |
| }); |
| } |
| break; |
| } |
| case GDK.GDK_Escape: { |
| Event keyEvent = new Event (); |
| keyEvent.widget = browser; |
| keyEvent.type = SWT.KeyDown; |
| keyEvent.keyCode = keyEvent.character = SWT.ESC; |
| if ((state[0] & GDK.GDK_MOD1_MASK) != 0) keyEvent.stateMask |= SWT.ALT; |
| if ((state[0] & GDK.GDK_SHIFT_MASK) != 0) keyEvent.stateMask |= SWT.SHIFT; |
| if ((state[0]& GDK.GDK_CONTROL_MASK) != 0) keyEvent.stateMask |= SWT.CONTROL; |
| try { // to avoid deadlocks, evaluate() should not block during listener. See Bug 512001 |
| // I.e, evaluate() can be called and script will be executed, but no return value will be provided. |
| nonBlockingEvaluate++; |
| browser.webBrowser.sendKeyEvent (keyEvent); |
| } catch (Exception e) { |
| throw e; |
| } finally { |
| nonBlockingEvaluate--; |
| } |
| return 1; |
| } |
| } |
| } |
| break; |
| } |
| } |
| if (browser != null) { |
| GTK3.gtk_widget_event (browser.handle, event); |
| } |
| } |
| return 0; |
| } |
| return 0; |
| } |
| |
| static long Proc (long handle, long user_data) { |
| long webView = handle; |
| |
| if (user_data == FINISHED) { |
| // Special case, callback from WebKitDownload instead of webview. |
| long webKitDownload = handle; |
| return webkit_download_finished(webKitDownload); |
| } |
| |
| Browser browser = FindBrowser (webView); |
| if (browser == null) return 0; |
| WebKit webkit = (WebKit)browser.webBrowser; |
| return webkit.webViewProc (handle, user_data); |
| } |
| |
| static long Proc (long handle, long arg0, long user_data) { |
| // As a note, don't use instance checks like 'G_TYPE_CHECK_INSTANCE_TYPE ' |
| // to determine difference between webview and webcontext as these |
| // don't seem to work reliably for all clients. For some clients they always return true. |
| // Instead use user_data. |
| |
| { // Deal with Special cases where callback comes not from webview. Handle is not a webview. |
| |
| if (user_data == DOWNLOAD_STARTED) { |
| // This callback comes from WebKitWebContext as oppose to the WebView. So handle is WebContext not Webview. |
| // user_function (WebKitWebContext *context, WebKitDownload *download, gpointer user_data) |
| long webKitDownload = arg0; |
| webkit_download_started(webKitDownload); |
| return 0; |
| } |
| |
| if (user_data == DECIDE_DESTINATION) { |
| // This callback comes from WebKitDownload, so handle is WebKitDownload not webview. |
| // gboolean user_function (WebKitDownload *download, gchar *suggested_filename, gpointer user_data) |
| long webKitDownload = handle; |
| long suggested_filename = arg0; |
| return webkit_download_decide_destination(webKitDownload,suggested_filename); |
| } |
| |
| if (user_data == FAILED) { |
| // void user_function (WebKitDownload *download, GError *error, gpointer user_data) |
| long webKitDownload = handle; |
| return webkit_download_failed(webKitDownload); |
| } |
| } |
| |
| { // Callbacks connected with a WebView. |
| assert handle != 0 : "Webview shouldn't be null here"; |
| long webView = handle; |
| Browser browser = FindBrowser (webView); |
| if (browser == null) return 0; |
| WebKit webkit = (WebKit)browser.webBrowser; |
| return webkit.webViewProc (webView, arg0, user_data); |
| } |
| } |
| |
| static long Proc (long handle, long arg0, long arg1, long user_data) { |
| Browser browser = FindBrowser (handle); |
| if (browser == null) return 0; |
| WebKit webkit = (WebKit)browser.webBrowser; |
| return webkit.webViewProc (handle, arg0, arg1, user_data); |
| } |
| |
| static long Proc (long handle, long arg0, long arg1, long arg2, long user_data) { |
| long webView = handle; |
| Browser browser = FindBrowser (webView); |
| if (browser == null) return 0; |
| WebKit webkit = (WebKit)browser.webBrowser; |
| |
| return webkit.webViewProc (handle, arg0, arg1, arg2, user_data); |
| } |
| |
| /** |
| * gboolean user_function (WebKitWebView *web_view, WebKitAuthenticationRequest *request, gpointer user_data) |
| * - https://webkitgtk.org/reference/webkit2gtk/stable/WebKitWebView.html#WebKitWebView-authenticate |
| */ |
| long webkit_authenticate (long web_view, long request){ |
| |
| /* authentication challenges are currently the only notification received from the session */ |
| if (!WebKitGTK.webkit_authentication_request_is_retry(request)) { |
| failureCount = 0; |
| } else { |
| if (++failureCount >= 3) return 0; |
| } |
| |
| String location = getUrl(); |
| |
| for (int i = 0; i < authenticationListeners.length; i++) { |
| AuthenticationEvent event = new AuthenticationEvent (browser); |
| event.location = location; |
| |
| try { // to avoid deadlocks, evaluate() should not block during authentication listener. See Bug 512001 |
| // I.e, evaluate() can be called and script will be executed, but no return value will be provided. |
| nonBlockingEvaluate++; |
| authenticationListeners[i].authenticate (event); |
| } catch (Exception e) { |
| throw e; |
| } finally { |
| nonBlockingEvaluate--; |
| } |
| |
| if (!event.doit) { |
| WebKitGTK.webkit_authentication_request_cancel (request); |
| return 0; |
| } |
| if (event.user != null && event.password != null) { |
| byte[] userBytes = Converter.wcsToMbcs (event.user, true); |
| byte[] passwordBytes = Converter.wcsToMbcs (event.password, true); |
| long credentials = WebKitGTK.webkit_credential_new (userBytes, passwordBytes, WebKitGTK.WEBKIT_CREDENTIAL_PERSISTENCE_NONE); |
| WebKitGTK.webkit_authentication_request_authenticate(request, credentials); |
| WebKitGTK.webkit_credential_free(credentials); |
| return 0; |
| } |
| } |
| return 0; |
| } |
| |
| long webViewProc (long handle, long user_data) { |
| switch ((int)user_data) { |
| case CLOSE_WEB_VIEW: return webkit_close_web_view (handle); |
| case WEB_VIEW_READY: return webkit_web_view_ready (handle); |
| default: return 0; |
| } |
| } |
| |
| long webViewProc (long handle, long arg0, long user_data) { |
| switch ((int)user_data) { |
| case CREATE_WEB_VIEW: return webkit_create_web_view (handle, arg0); |
| case LOAD_CHANGED: return webkit_load_changed (handle, (int) arg0, user_data); |
| case NOTIFY_PROGRESS: return webkit_notify_progress (handle, arg0); |
| case NOTIFY_TITLE: return webkit_notify_title (handle, arg0); |
| case AUTHENTICATE: return webkit_authenticate (handle, arg0); |
| default: return 0; |
| } |
| } |
| |
| long webViewProc (long handle, long arg0, long arg1, long user_data) { |
| switch ((int)user_data) { |
| case MOUSE_TARGET_CHANGED: return webkit_mouse_target_changed (handle, arg0, arg1); // Webkit2 only. |
| case DECIDE_POLICY: return webkit_decide_policy(handle, arg0, (int)arg1, user_data); |
| default: return 0; |
| } |
| } |
| |
| long webViewProc (long handle, long arg0, long arg1, long arg2, long user_data) { |
| switch ((int)user_data) { |
| case CONTEXT_MENU: return webkit_context_menu(handle, arg0, arg1, arg2); |
| case LOAD_FAILED_TLS: return webkit_load_failed_tls(handle, arg0, arg1, arg2); |
| default: return 0; |
| } |
| } |
| |
| @Override |
| public void create (Composite parent, int style) { |
| int [] vers = internalGetWebkitVersion(); |
| System.setProperty(SWT_WEBKITGTK_VERSION, |
| String.format("%s.%s.%s", vers[0], vers[1], vers[2])); // $NON-NLS-1$ |
| if (Device.DEBUG) { |
| System.out.println(String.format("WebKit version %s.%s.%s", vers[0], vers[1], vers[2])); //$NON-NLS-1$ |
| } |
| /* |
| * Set this Browser instance to Webki2AsyncToSync in order for cookie |
| * functionality to work. See bug 522181. |
| */ |
| Webkit2AsyncToSync.setCookieBrowser(browser); |
| |
| |
| Composite parentShell = parent.getParent(); |
| Browser parentBrowser = null; |
| if (parentShell != null) { |
| Control[] children = parentShell.getChildren(); |
| for (int i = 0; i < children.length; i++) { |
| if (children[i] instanceof Browser) { |
| parentBrowser = (Browser) children[i]; |
| break; |
| } |
| } |
| } |
| |
| if (parentBrowser == null) { |
| webView = WebKitGTK.webkit_web_view_new(); |
| } else { |
| webView = WebKitGTK.webkit_web_view_new_with_related_view(((WebKit)parentBrowser.webBrowser).webView); |
| } |
| |
| // Bug 522733 Webkit2 workaround for crash |
| // As of Webkitgtk 2.18, webkitgtk2 crashes if the first instance of webview is not referenced when JVM shuts down. |
| // There is a exit handler that tries to dereference the first instance [which if not referenced] |
| // leads to a crash. This workaround would benefit from deeper investigation (find root cause etc...). |
| // [edit] Bug 530678. Note, it seems that as of Webkit2.18, webkit auto-disposes itself if parent get's disposed. |
| // While not directly related, see onDispose() for how to deal with disposal of this. |
| if (!bug522733FirstInstanceCreated && vers[0] == 2 && vers[1] >= 18) { |
| bug522733FirstInstanceCreated = true; |
| OS.g_object_ref(webView); |
| } |
| if (ignoreTls) { |
| WebKitGTK.webkit_web_context_set_tls_errors_policy(WebKitGTK.webkit_web_view_get_context(webView), |
| WebKitGTK.WEBKIT_TLS_ERRORS_POLICY_IGNORE); |
| System.out.println("***WARNING: WebKitGTK is configured to ignore TLS errors via -Dorg.eclipse.swt.internal.webkitgtk.ignoretlserrors=true ."); |
| System.out.println("***WARNING: Please use for development purposes only!"); |
| } |
| |
| // Webkit2 Signal Documentation: https://webkitgtk.org/reference/webkit2gtk/stable/WebKitWebView.html#WebKitWebView--title |
| GTK3.gtk_container_add (browser.handle, webView); |
| OS.g_signal_connect (webView, WebKitGTK.close, Proc2.getAddress (), CLOSE_WEB_VIEW); |
| OS.g_signal_connect (webView, WebKitGTK.ready_to_show, Proc2.getAddress (), WEB_VIEW_READY); |
| OS.g_signal_connect (webView, WebKitGTK.decide_policy, Proc4.getAddress (), DECIDE_POLICY); |
| OS.g_signal_connect (webView, WebKitGTK.mouse_target_changed, Proc4.getAddress (), MOUSE_TARGET_CHANGED); |
| OS.g_signal_connect (webView, WebKitGTK.context_menu, Proc5.getAddress (), CONTEXT_MENU); |
| OS.g_signal_connect (webView, WebKitGTK.load_failed_with_tls_errors, Proc5.getAddress (), LOAD_FAILED_TLS); |
| |
| // GtkWidget* user_function (WebKitWebView *web_view, WebKitNavigationAction *navigation_action, gpointer user_data) |
| OS.g_signal_connect (webView, WebKitGTK.create, Proc3.getAddress (), CREATE_WEB_VIEW); |
| //void user_function (WebKitWebView *web_view, WebKitLoadEvent load_event, gpointer user_data) |
| OS.g_signal_connect (webView, WebKitGTK.load_changed, Proc3.getAddress (), LOAD_CHANGED); |
| // Property change: of 'estimated-load-progress' args: webview, pspec |
| OS.g_signal_connect (webView, WebKitGTK.notify_estimated_load_progress, Proc3.getAddress (), NOTIFY_PROGRESS); |
| |
| // gboolean user_function (WebKitWebView *web_view, WebKitAuthenticationRequest *request, gpointer user_data) |
| OS.g_signal_connect (webView, WebKitGTK.authenticate, Proc3.getAddress (), AUTHENTICATE); |
| |
| // (!) Note this one's a 'webContext' signal, not webview. See: |
| // https://webkitgtk.org/reference/webkit2gtk/stable/WebKitWebContext.html#WebKitWebContext-download-started |
| OS.g_signal_connect (WebKitGTK.webkit_web_context_get_default(), WebKitGTK.download_started, Proc3.getAddress (), DOWNLOAD_STARTED); |
| |
| GTK.gtk_widget_show (webView); |
| GTK.gtk_widget_show (browser.handle); |
| |
| // Webview 'title' property |
| OS.g_signal_connect (webView, WebKitGTK.notify_title, Proc3.getAddress (), NOTIFY_TITLE); |
| |
| OS.g_signal_connect (webView, OS.button_press_event, JSDOMEventProc.getAddress (), WIDGET_EVENT); |
| OS.g_signal_connect (webView, OS.button_release_event, JSDOMEventProc.getAddress (), WIDGET_EVENT); |
| OS.g_signal_connect (webView, OS.focus_in_event, JSDOMEventProc.getAddress (), WIDGET_EVENT); |
| OS.g_signal_connect (webView, OS.focus_out_event, JSDOMEventProc.getAddress (), WIDGET_EVENT); |
| // if connecting any other special gtk event to webkit, add SWT.* to w2_passThroughSwtEvents above. |
| |
| this.pageId = WebKitGTK.webkit_web_view_get_page_id (webView); |
| |
| OS.g_signal_connect (webView, OS.key_press_event, JSDOMEventProc.getAddress (), WIDGET_EVENT); |
| OS.g_signal_connect (webView, OS.key_release_event, JSDOMEventProc.getAddress (), WIDGET_EVENT); |
| OS.g_signal_connect (webView, OS.scroll_event, JSDOMEventProc.getAddress (), WIDGET_EVENT); |
| OS.g_signal_connect (webView, OS.motion_notify_event, JSDOMEventProc.getAddress (), WIDGET_EVENT); |
| |
| byte[] utfBytes = Converter.wcsToMbcs ("UTF-8", true); // $NON-NLS-1$ |
| |
| long settings = WebKitGTK.webkit_web_view_get_settings (webView); |
| OS.g_object_set (settings, WebKitGTK.javascript_can_open_windows_automatically, 1, 0); |
| OS.g_object_set (settings, WebKitGTK.enable_webgl, 1, 0); |
| OS.g_object_set (settings, WebKitGTK.enable_developer_extras, 1, 0); |
| |
| OS.g_object_set (settings, WebKitGTK.default_charset, utfBytes, 0); |
| if (WebKitGTK.webkit_get_minor_version() >= 14) { |
| OS.g_object_set (settings, WebKitGTK.allow_universal_access_from_file_urls, 1, 0); |
| if (WebKitGTK.webkit_get_minor_version() >= 24) { |
| OS.g_object_set (settings, WebKitGTK.enable_back_forward_navigation_gestures, 1, 0); |
| } |
| } else { |
| System.err.println("SWT WEBKIT: Warning, you are using Webkitgtk below version 2.14. Your version is: " |
| + "Your version is: " + internalGetWebKitVersionStr() |
| + "\nJavascript execution limited to same origin due to unimplemented feature of this version."); |
| } |
| |
| Listener listener = event -> { |
| switch (event.type) { |
| case SWT.Dispose: { |
| /* make this handler run after other dispose listeners */ |
| if (ignoreDispose) { |
| ignoreDispose = false; |
| break; |
| } |
| ignoreDispose = true; |
| browser.notifyListeners (event.type, event); |
| event.type = SWT.NONE; |
| onDispose (event); |
| break; |
| } |
| case SWT.FocusIn: { |
| if (webView != 0) |
| GTK.gtk_widget_grab_focus (webView); |
| break; |
| } |
| case SWT.Resize: { |
| onResize (event); |
| break; |
| } |
| } |
| }; |
| browser.addListener (SWT.Dispose, listener); |
| browser.addListener (SWT.FocusIn, listener); |
| browser.addListener (SWT.KeyDown, listener); |
| browser.addListener (SWT.Resize, listener); |
| |
| /* |
| * Bug in WebKitGTK. MouseOver/MouseLeave events are not consistently sent from |
| * the DOM when the mouse enters and exits the browser control, see |
| * https://bugs.webkit.org/show_bug.cgi?id=35246. As a workaround for sending |
| * MouseEnter/MouseExit events, swt's default mouse enter/exit mechanism is used, |
| * but in order to do this the Browser's default sub-window check behavior must |
| * be changed. |
| */ |
| browser.setData (KEY_CHECK_SUBWINDOW, Boolean.FALSE); |
| |
| /* |
| * Bug in WebKitGTK. In WebKitGTK 1.10.x a crash can occur if an |
| * attempt is made to show a browser before a size has been set on |
| * it. The workaround is to temporarily give it a size that forces |
| * the native resize events to fire. |
| */ |
| int major = vers[0], minor = vers[1]; |
| if (major == 1 && minor >= 10) { |
| Rectangle minSize = browser.computeTrim (0, 0, 2, 2); |
| Point size = browser.getSize (); |
| size.x += minSize.width; size.y += minSize.height; |
| browser.setSize (size); |
| size.x -= minSize.width; size.y -= minSize.height; |
| browser.setSize (size); |
| } |
| } |
| |
| @Override |
| public boolean back () { |
| if (WebKitGTK.webkit_web_view_can_go_back (webView) == 0) return false; |
| WebKitGTK.webkit_web_view_go_back (webView); |
| return true; |
| } |
| |
| @Override |
| public boolean close () { |
| return close (true); |
| } |
| |
| // Developer note: |
| // @return true = leads to disposal. In Browser.java, user is told widget is disposed. Ex in Snippe326 close button is grayed out. |
| // false = blocks disposal. In Browser.java, user is told widget was not disposed. |
| // See Snippet326. |
| boolean close (boolean showPrompters) { |
| // don't execute any JavaScript if it's disabled or requested to get disabled |
| // we need to check jsEnabledOnNextPage here because jsEnabled is updated asynchronously |
| // and may not reflect the proper state (bug 571746 and bug 567881) |
| if (!jsEnabled || !jsEnabledOnNextPage) return true; |
| |
| String message1 = Compatibility.getMessage("SWT_OnBeforeUnload_Message1"); // $NON-NLS-1$ |
| String message2 = Compatibility.getMessage("SWT_OnBeforeUnload_Message2"); // $NON-NLS-1$ |
| String functionName = EXECUTE_ID + "CLOSE"; // $NON-NLS-1$ |
| StringBuilder buffer = new StringBuilder ("function "); // $NON-NLS-1$ |
| buffer.append (functionName); |
| buffer.append ("(win) {\n"); // $NON-NLS-1$ |
| buffer.append ("var fn = win.onbeforeunload; if (fn != null) {try {var str = fn(); "); // $NON-NLS-1$ |
| if (showPrompters) { |
| buffer.append ("if (str != null) { "); // $NON-NLS-1$ |
| buffer.append ("var result = confirm('"); // $NON-NLS-1$ |
| buffer.append (message1); |
| buffer.append ("\\n\\n'+str+'\\n\\n"); // $NON-NLS-1$ |
| buffer.append (message2); |
| buffer.append ("');"); // $NON-NLS-1$ |
| buffer.append ("if (!result) return false;}"); // $NON-NLS-1$ |
| } |
| buffer.append ("} catch (e) {}}"); // $NON-NLS-1$ |
| buffer.append ("try {for (var i = 0; i < win.frames.length; i++) {var result = "); // $NON-NLS-1$ |
| buffer.append (functionName); |
| buffer.append ("(win.frames[i]); if (!result) return false;}} catch (e) {} return true;"); // $NON-NLS-1$ |
| buffer.append ("\n};"); // $NON-NLS-1$ |
| nonBlockingExecute (buffer.toString ()); |
| |
| Boolean result; |
| /* |
| * Sometimes if a disposal is already underway (ex parent shell disposed), then |
| * Javascript execution can throw. We have to account for that. |
| */ |
| try { |
| result = (Boolean)evaluate ("return " + functionName +"(window);"); // $NON-NLS-1$ // $NON-NLS-2$ |
| if (result == null) return true; // Default to assume that webkit is disposed and allow disposal of Browser. |
| } catch (SWTException e) { |
| return true; // Permit browser to be disposed if javascript execution failed. |
| } |
| return result.booleanValue (); |
| } |
| |
| |
| private boolean isJavascriptEnabled() { |
| // If you try to run Javascript while Javascript is turned off, then an exception is thrown. |
| return webkit_settings_get(WebKitGTK.enable_javascript) != 0; |
| } |
| |
| @Override |
| void nonBlockingExecute(String script) { |
| try { |
| nonBlockingEvaluate++; |
| execute(script); |
| } finally { |
| nonBlockingEvaluate--; |
| } |
| } |
| |
| /** |
| * Modifies a BrowserFunction in the web extension. This method can be used to register/deregister BrowserFunctions |
| * in the web extension, so that those BrowserFunctions are executed upon triggering of the object_cleared callback (in |
| * the extension, not in Java). |
| * |
| * This function will return true if: the operation succeeds synchronously, or if the synchronous call timed out and an |
| * asynchronous call was performed instead. All other cases will return false. |
| * |
| * Supported actions: "register" and "deregister" |
| * |
| * @param pageId the page ID of the WebKit instance/web page |
| * @param function the function string |
| * @param url the URL |
| * @param action the action being performed on the function, which will be used to form the DBus method name. |
| * @return true if the action succeeded (or was performed asynchronously), false if it failed |
| */ |
| private boolean webkit_extension_modify_function (long pageId, String function, String url, String action){ |
| long args[] = { OS.g_variant_new_uint64(pageId), |
| OS.g_variant_new_string (Converter.javaStringToCString(function)), |
| OS.g_variant_new_string (Converter.javaStringToCString(url))}; |
| final long argsTuple = OS.g_variant_new_tuple(args, args.length); |
| if (argsTuple == 0) return false; |
| String dbusMethodName = "webkitgtk_extension_" + action + "_function"; |
| Object returnVal = WebkitGDBus.callExtensionSync(argsTuple, dbusMethodName); |
| if (returnVal instanceof Boolean) { |
| return (Boolean) returnVal; |
| } else if (returnVal instanceof String) { |
| String returnString = (String) returnVal; |
| /* |
| * Call the extension asynchronously if a synchronous call times out. |
| * Note: this is a pretty rare case, and usually only happens when running test cases. |
| * See bug 536141. |
| */ |
| if ("timeout".equals(returnString)) { |
| return WebkitGDBus.callExtensionAsync(argsTuple, dbusMethodName); |
| } |
| } |
| return false; |
| } |
| |
| @Override |
| public boolean execute (String script) { |
| if (!isJavascriptEnabled()) { |
| System.err.println("SWT Webkit Warning: Attempting to execute javascript when javascript is dissabled." |
| + "Execution has no effect. Script:\n" + script); |
| return false; |
| } |
| try { |
| Webkit2AsyncToSync.runjavascript(script, this.browser, webView); |
| } catch (SWTException e) { |
| return false; |
| } |
| return true; |
| } |
| |
| /** |
| * Webkit2 introduces async api. However SWT has sync execution model. This class it to convert async api to sync. |
| * |
| * Be careful about using these methods in synchronous callbacks from webkit, as those can cause deadlocks. (See inner javadocs). |
| * |
| * The mechanism generates an ID for each callback and waits for that callback to complete. |
| */ |
| private static class Webkit2AsyncToSync { |
| /** We need a way to associate a Browser instance with this class for cookie functionality */ |
| private static Browser cookieBrowser; |
| private static Callback runjavascript_callback; |
| private static Callback getText_callback; |
| private static Callback setCookie_callback; |
| private static Callback getCookie_callback; |
| |
| static { |
| runjavascript_callback = new Callback(Webkit2AsyncToSync.class, "runjavascript_callback", void.class, new Type[] {long.class, long.class, long.class}); |
| getText_callback = new Callback(Webkit2AsyncToSync.class, "getText_callback", void.class, new Type[] {long.class, long.class, long.class}); |
| setCookie_callback = new Callback(Webkit2AsyncToSync.class, "setCookie_callback", void.class, new Type[] {long.class, long.class, long.class}); |
| getCookie_callback = new Callback(Webkit2AsyncToSync.class, "getCookie_callback", void.class, new Type[] {long.class, long.class, long.class}); |
| } |
| |
| /** Object used to return data from callback to original call */ |
| private static class Webkit2AsyncReturnObj { |
| boolean callbackFinished = false; |
| Object returnValue = null; // As note, if browser is disposed during excution, null is returned. |
| |
| /** 0=no error. >0 means error. **/ |
| int errorNum = 0; |
| String errorMsg; |
| |
| /** Set to true if call timed out. Not set by javascript execution itself */ |
| boolean swtAsyncTimeout; |
| } |
| |
| /** |
| * Every callback is tagged with a unique ID. |
| * The ID is used for the callback to find the object via which data is returned |
| * and allow the original call to finish. |
| * |
| * Note: The reason each callback is tagged with an ID is because two(or more) subsequent |
| * evaluate() calls can be started before the first callback comes back. |
| * As such, there would be ambiguity as to which call a callback belongs to, which in turn causes deadlocks. |
| * This is typically seen when a webkit2 signal (e.g closeListener) makes a call to evaluate(), |
| * when the closeListener was triggered by evaluate("window.close()"). |
| * An example test case where this is seen is: |
| * org.eclipse.swt.tests.junit.Test_org_eclipse_swt_browser_Browser.test_execute_and_closeListener() |
| */ |
| private static class CallBackMap { |
| private static HashMap<Integer, Webkit2AsyncReturnObj> callbackMap = new HashMap<>(); |
| |
| static int putObject(Webkit2AsyncReturnObj obj) { |
| int id = getNextId(); |
| callbackMap.put(id, obj); |
| return id; |
| } |
| static Webkit2AsyncReturnObj getObj(int id) { |
| return callbackMap.get(id); |
| } |
| static void removeObject(int id) { |
| callbackMap.remove(id); |
| removeId(id); |
| } |
| |
| // Mechanism to generate unique ID's |
| private static int nextCallbackId = 1; |
| private static HashSet<Integer> usedCallbackIds = new HashSet<>(); |
| static int getNextId() { |
| int value = 0; |
| boolean unique = false; |
| while (unique == false) { |
| value = nextCallbackId; |
| unique = !usedCallbackIds.contains(value); |
| if (nextCallbackId != Integer.MAX_VALUE) |
| nextCallbackId++; |
| else |
| nextCallbackId = 1; |
| } |
| usedCallbackIds.add(value); |
| return value; |
| } |
| private static void removeId(int id) { |
| usedCallbackIds.remove(id); |
| } |
| } |
| |
| static Object evaluate (String script, Browser browser, long webView) { |
| // /* Wrap script around a temporary function for backwards compatibility, |
| // * user can specify 'return', which may not be at the beginning of the script. |
| // * Valid scripts: |
| // * 'hi' |
| // * return 'hi' |
| // * var x = 1; return 'hi' |
| // */ |
| String swtUniqueExecFunc = "SWTWebkit2TempFunc" + CallBackMap.getNextId() + "()"; |
| String wrappedScript = "function " + swtUniqueExecFunc +"{" + script + "}; " + swtUniqueExecFunc; |
| return runjavascript(wrappedScript, browser, webView); |
| |
| } |
| |
| /** |
| * Run javascript, wait for a return value. |
| * |
| * Developer note: |
| * Be EXTRA careful with this method, it can cause deadlocks in situations where |
| * javascript is executed in a callback that provides a return value to webkit. |
| * In otherwords, if webkit does a sync callback (one that requires a return value), |
| * then running javascript will lead to a deadlock because webkit will not execute |
| * the javascript until it's sync callback finished. |
| * As a note, SWT's callback mechanism hard-codes 'long' return even when a callback |
| * is actually 'void'. So reference webkit callback signature documentation and not |
| * SWT implementation. |
| * |
| * If in doubt, you should use nonBlockingExecute() where possible :-). |
| * |
| * TODO_SOMEDAY: |
| * - Instead of async js execution and waiting for return value, it might be |
| * better to use gdbus, connect to webextension and execute JS synchronously. |
| * See: https://blogs.igalia.com/carlosgc/2013/09/10/webkit2gtk-web-process-extensions/ |
| * 'Extending JavaScript' |
| * Pros: |
| * - less likely deadlocks would occur due to developer error/not being careful. |
| * - js execution can work in synchronous callbacks from webkit. |
| * Cons: |
| * - High implementation cost/complexity. |
| * - Unexpected errors/behaviour due to GDBus timeouts. |
| * Proof of concept: |
| * https://git.eclipse.org/r/#/c/23416/16/bundles/org.eclipse.swt/Eclipse+SWT+WebKit/gtk/library/webkit_extension.c |
| * > 'webkit_extension_execute_script' |
| * Tennative structure: |
| * - Webextension should create gdbus server, make & communicate UniqueID (pid) to main proc |
| * - main proc should make a note of webextension's name+uniqueID |
| * - implement mechanism for packaging Java objects into gvariants, (see WebkitGDBus.java), |
| * - call webextension over gdbus, parse return value. |
| * |
| */ |
| static Object runjavascript(String script, Browser browser, long webView) { |
| if (nonBlockingEvaluate > 0) { |
| // Execute script, but do not wait for async call to complete. (assume it does). Bug 512001. |
| WebKitGTK.webkit_web_view_run_javascript(webView, Converter.wcsToMbcs(script, true), 0, 0, 0); |
| return null; |
| } else { |
| // Callback logic: Initiate an async callback and wait for it to finish. |
| // The callback comes back in runjavascript_callback(..) below. |
| Consumer <Integer> asyncFunc = (callbackId) -> { |
| WebKitGTK.webkit_web_view_run_javascript(webView, Converter.wcsToMbcs(script, true), 0, runjavascript_callback.getAddress(), callbackId); |
| }; |
| |
| Webkit2AsyncReturnObj retObj = execAsyncAndWaitForReturn(browser, asyncFunc, " The following javascript was executed:\n" + script +"\n\n"); |
| |
| if (retObj.swtAsyncTimeout) { |
| return null; |
| } else if (retObj.errorNum != 0) { |
| throw new SWTException(retObj.errorNum, retObj.errorMsg +"\nScript that was evaluated:\n" + script); |
| } else { |
| // This is also the implicit case where browser was disposed while javascript was executing. It returns null. |
| return retObj.returnValue; |
| } |
| } |
| } |
| |
| @SuppressWarnings("unused") // Only called directly from C (from javascript). |
| private static void runjavascript_callback (long GObject_source, long GAsyncResult, long user_data) { |
| int callbackId = (int) user_data; |
| Webkit2AsyncReturnObj retObj = CallBackMap.getObj(callbackId); |
| |
| if (retObj != null) { // retObj can be null if there was a timeout. |
| long [] gerror = new long [1]; // GError ** |
| long js_result = WebKitGTK.webkit_web_view_run_javascript_finish(GObject_source, GAsyncResult, gerror); |
| if (js_result == 0) { |
| long errMsg = OS.g_error_get_message(gerror[0]); |
| String msg = Converter.cCharPtrToJavaString(errMsg, false); |
| OS.g_error_free(gerror[0]); |
| |
| retObj.errorNum = SWT.ERROR_FAILED_EVALUATE; |
| retObj.errorMsg = msg != null ? msg : ""; |
| } else { |
| long context = WebKitGTK.webkit_javascript_result_get_global_context (js_result); |
| long value = WebKitGTK.webkit_javascript_result_get_value (js_result); |
| |
| try { |
| retObj.returnValue = convertToJava(context, value); |
| } catch (IllegalArgumentException ex) { |
| retObj.errorNum = SWT.ERROR_INVALID_RETURN_VALUE; |
| retObj.errorMsg = "Type of return value not is not valid. For supported types see: Browser.evaluate() JavaDoc"; |
| } |
| WebKitGTK.webkit_javascript_result_unref (js_result); |
| } |
| retObj.callbackFinished = true; |
| } |
| Display.getCurrent().wake(); |
| } |
| |
| static String getText(Browser browser, long webView) { |
| long WebKitWebResource = WebKitGTK.webkit_web_view_get_main_resource(webView); |
| if (WebKitWebResource == 0) { // No page yet loaded. |
| return ""; |
| } |
| if (nonBlockingEvaluate > 0) { |
| System.err.println("SWT Webkit Warning: getText() called inside a synchronous callback, which can lead to a deadlock.\n" |
| + "Avoid using getText in OpenWindowListener, Authentication listener and when webkit is about to change to a new page\n" |
| + "Return value is empty string '' instead of actual text"); |
| return ""; |
| } |
| |
| Consumer<Integer> asyncFunc = (callbackId) -> WebKitGTK.webkit_web_resource_get_data(WebKitWebResource, 0, getText_callback.getAddress(), callbackId); |
| Webkit2AsyncReturnObj retObj = execAsyncAndWaitForReturn(browser, asyncFunc, " getText() was called"); |
| |
| if (retObj.swtAsyncTimeout) |
| return "SWT WEBKIT TIMEOUT ERROR"; |
| else |
| return (String) retObj.returnValue; |
| } |
| |
| @SuppressWarnings("unused") // Callback only called only by C directly |
| private static void getText_callback(long WebResource, long GAsyncResult, long user_data) { |
| int callbackId = (int) user_data; |
| Webkit2AsyncReturnObj retObj = CallBackMap.getObj(callbackId); |
| |
| long [] gsize_len = new long [1]; |
| long [] gerrorRes = new long [1]; // GError ** |
| long guchar_data = WebKitGTK.webkit_web_resource_get_data_finish(WebResource, GAsyncResult, gsize_len, gerrorRes); |
| if (gerrorRes[0] != 0 || guchar_data == 0) { |
| OS.g_error_free(gerrorRes[0]); |
| retObj.returnValue = (String) ""; |
| } else { |
| int len = (int) gsize_len[0]; |
| byte[] buffer = new byte [len]; |
| C.memmove (buffer, guchar_data, len); |
| String text = Converter.byteToStringViaHeuristic(buffer); |
| retObj.returnValue = text; |
| } |
| |
| retObj.callbackFinished = true; |
| Display.getCurrent().wake(); |
| } |
| |
| /** |
| * Associates a Browser instance with this class, mainly so we can get its Display |
| * and check for disposal. |
| * @param toSet the Browser instance to set |
| */ |
| static void setCookieBrowser (Browser toSet) { |
| if (toSet != null) cookieBrowser = toSet; |
| } |
| |
| static boolean setCookie(String cookieUrl, String cookieValue) { |
| long context = WebKitGTK.webkit_web_context_get_default(); |
| long cookieManager = WebKitGTK.webkit_web_context_get_cookie_manager(context); |
| byte[] bytes = Converter.wcsToMbcs (cookieUrl, true); |
| long uri = WebKitGTK.soup_uri_new (bytes); |
| if (uri == 0) { |
| System.err.println("SWT WebKit: SoupURI == 0 when setting cookie"); |
| return false; |
| } |
| bytes = Converter.wcsToMbcs (cookieValue, true); |
| long soupCookie = WebKitGTK.soup_cookie_parse (bytes, uri); |
| |
| if (nonBlockingEvaluate > 0) { |
| System.err.println("SWT Webkit: setCookie() called inside a synchronous callback, which can lead to a deadlock.\n" |
| + "Return value is false."); |
| return false; |
| } |
| |
| Consumer<Integer> asyncFunc = (callbackID) -> WebKitGTK.webkit_cookie_manager_add_cookie(cookieManager, soupCookie, 0, |
| setCookie_callback.getAddress(), callbackID); |
| Webkit2AsyncReturnObj retObj = execAsyncAndWaitForReturn(cookieBrowser, asyncFunc, " setCookie() was called"); |
| |
| WebKitGTK.soup_uri_free (uri); |
| |
| if (retObj.swtAsyncTimeout) { |
| return false; |
| } else { |
| return (Boolean) retObj.returnValue; |
| } |
| } |
| |
| @SuppressWarnings("unused") // Callback only called only by C directly |
| private static void setCookie_callback(long cookieManager, long result, long user_data) { |
| int callbackID = (int) user_data; |
| Webkit2AsyncReturnObj retObj = CallBackMap.getObj(callbackID); |
| |
| long [] error = new long [1]; |
| retObj.returnValue = WebKitGTK.webkit_cookie_manager_add_cookie_finish(cookieManager, result, error); |
| |
| if (error[0] != 0) { |
| long errorMessageC = OS.g_error_get_message(error[0]); |
| String errorMessageStr = Converter.cCharPtrToJavaString(errorMessageC, false); |
| System.err.println("SWT WebKit: error setting cookie: " + errorMessageStr); |
| OS.g_error_free(error[0]); |
| } |
| |
| retObj.callbackFinished = true; |
| Display.getCurrent().wake(); |
| |
| } |
| |
| static String getCookie(String cookieUrl, String cookieName) { |
| long context = WebKitGTK.webkit_web_context_get_default(); |
| long cookieManager = WebKitGTK.webkit_web_context_get_cookie_manager(context); |
| byte[] uri = Converter.wcsToMbcs (cookieUrl, true); |
| if (nonBlockingEvaluate > 0) { |
| System.err.println("SWT Webkit: getCookie() called inside a synchronous callback, which can lead to a deadlock.\n" |
| + "Return value is an empty string '' instead of actual cookie value."); |
| return ""; |
| } |
| |
| /* |
| * We package the cookie name and callbackID into a GVariant which can be passed to the callback. |
| * The callbackID is necessary so we can find our way back to the correct Browser instance, and |
| * the cookie name is necessary as the field could have been modified by the time the callback |
| * triggers. |
| */ |
| Consumer<Integer> asyncFunc = (callbackID) -> WebKitGTK.webkit_cookie_manager_get_cookies(cookieManager, uri, 0, |
| getCookie_callback.getAddress(), WebkitGDBus.convertJavaToGVariant(new Object [] {cookieName, callbackID})); |
| Webkit2AsyncReturnObj retObj = execAsyncAndWaitForReturn(cookieBrowser, asyncFunc, " getCookie() was called"); |
| |
| if (retObj.swtAsyncTimeout) { |
| return "SWT WEBKIT TIMEOUT ERROR"; |
| } else { |
| return (String) retObj.returnValue; |
| } |
| } |
| |
| @SuppressWarnings("unused") // Callback only called only by C directly |
| private static void getCookie_callback(long cookieManager, long result, long user_data) { |
| Object resultObject = WebkitGDBus.convertGVariantToJava(user_data); |
| |
| // We are expecting a GVariant tuple, anything else means something went wrong |
| if (resultObject instanceof Object []) { |
| // Unpack callback ID and cookie name |
| Object [] nameAndId = (Object []) resultObject; |
| String cookieName = (String) nameAndId[0]; |
| int callbackId = ((Number) nameAndId[1]).intValue(); |
| Webkit2AsyncReturnObj retObj = CallBackMap.getObj(callbackId); |
| |
| // Get GSList of cookies |
| long [] error = new long [1]; |
| long cookieList = WebKitGTK.webkit_cookie_manager_get_cookies_finish(cookieManager, result, error); |
| if (error[0] != 0) { |
| long errorMessageC = OS.g_error_get_message(error[0]); |
| String errorMessageStr = Converter.cCharPtrToJavaString(errorMessageC, false); |
| System.err.println("SWT WebKit: error getting cookie: " + errorMessageStr); |
| OS.g_error_free(error[0]); |
| retObj.returnValue = (String) ""; |
| } |
| |
| int length = OS.g_slist_length (cookieList); |
| long current = cookieList; |
| for (int i = 0; i < length; i++) { |
| long soupCookie = OS.g_slist_data (current); |
| long soupName = WebKitGTK.soup_cookie_get_name(soupCookie); |
| String soupNameStr = Converter.cCharPtrToJavaString(soupName, false); |
| if (soupNameStr != null && soupNameStr.equals(cookieName)) { |
| long soupValue = WebKitGTK.soup_cookie_get_value(soupCookie); |
| retObj.returnValue = Converter.cCharPtrToJavaString(soupValue, false); |
| break; |
| } |
| current = OS.g_slist_next (current); |
| } |
| OS.g_slist_free (cookieList); |
| |
| retObj.callbackFinished = true; |
| Display.getCurrent().wake(); |
| } else { |
| System.err.println("SWT WebKit: something went wrong unpacking GVariant tuple for getCookie_callback"); |
| } |
| } |
| |
| /** |
| * You should check 'retObj.swtAsyncTimeout' after making a call to this. |
| */ |
| private static Webkit2AsyncReturnObj execAsyncAndWaitForReturn(Browser browser, Consumer<Integer> asyncFunc, String additionalErrorInfo) { |
| Webkit2AsyncReturnObj retObj = new Webkit2AsyncReturnObj(); |
| int callbackId = CallBackMap.putObject(retObj); |
| asyncFunc.accept(callbackId); |
| final Instant timeOut = Instant.now().plusMillis(ASYNC_EXEC_TIMEOUT_MS); |
| while (!browser.isDisposed()) { |
| if (retObj.callbackFinished) |
| break; |
| else if (Instant.now().isAfter(timeOut)) { |
| System.err.println("SWT call to Webkit timed out after " + ASYNC_EXEC_TIMEOUT_MS |
| + "ms. No return value will be provided.\n" |
| + "Possible reasons:\n" |
| + "1) Problem: Your javascript needs more than " + ASYNC_EXEC_TIMEOUT_MS +"ms to execute.\n" |
| + " Solution: Don't run such javascript, it blocks Eclipse's UI. SWT currently allows such code to complete, but this error is thrown \n" |
| + " and the return value of execute()/evalute() will be false/null.\n\n" |
| + "2) However, if you believe that your application should execute as expected (in under" + ASYNC_EXEC_TIMEOUT_MS + " ms),\n" |
| + " then it might be a deadlock in SWT/Browser/webkit2 logic.\n" |
| + " I.e, it might be a bug in SWT (e.g this does not occur on Windows/Cocoa, but occurs on Linux). If you believe it to be a bug in SWT, then\n" |
| + getInternalErrorMsg() |
| + "\n Additional information about the error is as following:\n" |
| + additionalErrorInfo); |
| retObj.swtAsyncTimeout = true; |
| break; |
| } |
| else { |
| if (GTK.GTK4) { |
| OS.g_main_context_iteration (0, true); |
| } else { |
| GTK3.gtk_main_iteration_do (true); |
| } |
| } |
| } |
| CallBackMap.removeObject(callbackId); |
| return retObj; |
| } |
| } |
| |
| @Override |
| public Object evaluate (String script) throws SWTException { |
| if ("".equals(script)) { |
| return null; // A litte optimization. Sometimes evaluate() is called with a generated script, where the generated script is sometimes empty. |
| } |
| if (!isJavascriptEnabled()) { |
| return null; |
| } |
| return Webkit2AsyncToSync.evaluate(script, this.browser, webView); |
| } |
| |
| @Override |
| public boolean forward () { |
| if (webView == 0) { |
| assert false; |
| System.err.println("SWT Webkit: forward() called after widget disposed. Should not have happened.\n" + getInternalErrorMsg()); |
| return false; // Disposed. |
| } |
| if (WebKitGTK.webkit_web_view_can_go_forward (webView) == 0) return false; |
| WebKitGTK.webkit_web_view_go_forward (webView); |
| return true; |
| } |
| |
| @Override |
| public String getBrowserType () { |
| return "webkit"; //$NON-NLS-1$ |
| } |
| |
| @Override |
| public String getText () { |
| return Webkit2AsyncToSync.getText(browser, webView); |
| } |
| |
| @Override |
| public String getUrl () { |
| if (webView == 0) { |
| assert false; |
| System.err.println("SWT Webkit: getUrl() called after widget disposed. Should not have happened.\n" + getInternalErrorMsg()); |
| return null; // Disposed. |
| } |
| long uri = WebKitGTK.webkit_web_view_get_uri (webView); |
| |
| /* WebKit auto-navigates to about:blank at startup */ |
| if (uri == 0) return ABOUT_BLANK; |
| |
| int length = C.strlen (uri); |
| byte[] bytes = new byte[length]; |
| C.memmove (bytes, uri, length); |
| |
| String url = new String (Converter.mbcsToWcs (bytes)); |
| /* |
| * If the URI indicates that the page is being rendered from memory |
| * (via setText()) then set it to about:blank to be consistent with IE. |
| */ |
| if (url.equals (URI_FILEROOT)) { |
| url = ABOUT_BLANK; |
| } else { |
| length = URI_FILEROOT.length (); |
| if (url.startsWith (URI_FILEROOT) && url.charAt (length) == '#') { |
| url = ABOUT_BLANK + url.substring (length); |
| } |
| } |
| return url; |
| } |
| |
| boolean handleDOMEvent (long event, int type) { |
| /* |
| * This method handles JS events that are received through the DOM |
| * listener API that was introduced in WebKitGTK 1.4. |
| */ |
| String typeString = null; |
| boolean isMouseEvent = false; |
| switch (type) { |
| case SWT.DragDetect: { |
| typeString = "dragstart"; //$NON-NLS-1$ |
| isMouseEvent = true; |
| break; |
| } |
| case SWT.MouseDown: { |
| typeString = "mousedown"; //$NON-NLS-1$ |
| isMouseEvent = true; |
| break; |
| } |
| case SWT.MouseMove: { |
| typeString = "mousemove"; //$NON-NLS-1$ |
| isMouseEvent = true; |
| break; |
| } |
| case SWT.MouseUp: { |
| typeString = "mouseup"; //$NON-NLS-1$ |
| isMouseEvent = true; |
| break; |
| } |
| case SWT.MouseWheel: { |
| typeString = "mousewheel"; //$NON-NLS-1$ |
| isMouseEvent = true; |
| break; |
| } |
| case SWT.KeyDown: { |
| typeString = "keydown"; //$NON-NLS-1$ |
| break; |
| } |
| case SWT.KeyUp: { |
| typeString = "keyup"; //$NON-NLS-1$ |
| break; |
| } |
| case SENTINEL_KEYPRESS: { |
| typeString = "keypress"; //$NON-NLS-1$ |
| break; |
| } |
| } |
| |
| if (isMouseEvent) { |
| int screenX = (int)WebKitGTK.webkit_dom_mouse_event_get_screen_x (event); |
| int screenY = (int)WebKitGTK.webkit_dom_mouse_event_get_screen_y (event); |
| int button = (int)WebKitGTK.webkit_dom_mouse_event_get_button (event) + 1; |
| boolean altKey = WebKitGTK.webkit_dom_mouse_event_get_alt_key (event) != 0; |
| boolean ctrlKey = WebKitGTK.webkit_dom_mouse_event_get_ctrl_key (event) != 0; |
| boolean shiftKey = WebKitGTK.webkit_dom_mouse_event_get_shift_key (event) != 0; |
| boolean metaKey = WebKitGTK.webkit_dom_mouse_event_get_meta_key (event) != 0; |
| int detail = (int)WebKitGTK.webkit_dom_ui_event_get_detail (event); |
| boolean hasRelatedTarget = false; //WebKitGTK.webkit_dom_mouse_event_get_related_target (event) != 0; |
| return handleMouseEvent(typeString, screenX, screenY, detail, button, altKey, ctrlKey, shiftKey, metaKey, hasRelatedTarget); |
| } |
| |
| /* key event */ |
| int keyEventState = 0; |
| long eventPtr = GTK3.gtk_get_current_event (); |
| if (eventPtr != 0) { |
| int eventType = GDK.gdk_event_get_event_type(eventPtr); |
| int [] state = new int[1]; |
| if (GTK.GTK4) { |
| state[0] = GDK.gdk_event_get_modifier_state(eventPtr); |
| } else { |
| GDK.gdk_event_get_state(eventPtr, state); |
| } |
| switch (eventType) { |
| case GDK.GDK_KEY_PRESS: |
| case GDK.GDK_KEY_RELEASE: |
| keyEventState = state[0]; |
| break; |
| } |
| if (GTK.GTK4) { |
| OS.g_object_unref(eventPtr); |
| } else { |
| GDK.gdk_event_free (eventPtr); |
| } |
| } |
| int keyCode = (int)WebKitGTK.webkit_dom_ui_event_get_key_code (event); |
| int charCode = (int)WebKitGTK.webkit_dom_ui_event_get_char_code (event); |
| boolean altKey = (keyEventState & GDK.GDK_MOD1_MASK) != 0; |
| boolean ctrlKey = (keyEventState & GDK.GDK_CONTROL_MASK) != 0; |
| boolean shiftKey = (keyEventState & GDK.GDK_SHIFT_MASK) != 0; |
| return handleKeyEvent(typeString, keyCode, charCode, altKey, ctrlKey, shiftKey, false); |
| } |
| |
| boolean handleEventFromFunction (Object[] arguments) { |
| /* |
| * Prior to WebKitGTK 1.4 there was no API for hooking DOM listeners. |
| * As a workaround, eventFunction was introduced to capture JS events |
| * and report them back to the java side. This method handles these |
| * events by extracting their arguments and passing them to the |
| * handleKeyEvent()/handleMouseEvent() event handler methods. |
| */ |
| |
| /* |
| * The arguments for key events are: |
| * argument 0: type (String) |
| * argument 1: keyCode (Double) |
| * argument 2: charCode (Double) |
| * argument 3: altKey (Boolean) |
| * argument 4: ctrlKey (Boolean) |
| * argument 5: shiftKey (Boolean) |
| * argument 6: metaKey (Boolean) |
| * returns doit |
| * |
| * The arguments for mouse events are: |
| * argument 0: type (String) |
| * argument 1: screenX (Double) |
| * argument 2: screenY (Double) |
| * argument 3: detail (Double) |
| * argument 4: button (Double) |
| * argument 5: altKey (Boolean) |
| * argument 6: ctrlKey (Boolean) |
| * argument 7: shiftKey (Boolean) |
| * argument 8: metaKey (Boolean) |
| * argument 9: hasRelatedTarget (Boolean) |
| * returns doit |
| */ |
| |
| String type = (String)arguments[0]; |
| if (type.equals (DOMEVENT_KEYDOWN) || type.equals (DOMEVENT_KEYPRESS) || type.equals (DOMEVENT_KEYUP)) { |
| return handleKeyEvent( |
| type, |
| ((Double)arguments[1]).intValue (), |
| ((Double)arguments[2]).intValue (), |
| ((Boolean)arguments[3]).booleanValue (), |
| ((Boolean)arguments[4]).booleanValue (), |
| ((Boolean)arguments[5]).booleanValue (), |
| ((Boolean)arguments[6]).booleanValue ()); |
| } |
| |
| return handleMouseEvent( |
| type, |
| ((Double)arguments[1]).intValue (), |
| ((Double)arguments[2]).intValue (), |
| ((Double)arguments[3]).intValue (), |
| (arguments[4] != null ? ((Double)arguments[4]).intValue () : 0) + 1, |
| ((Boolean)arguments[5]).booleanValue (), |
| ((Boolean)arguments[6]).booleanValue (), |
| ((Boolean)arguments[7]).booleanValue (), |
| ((Boolean)arguments[8]).booleanValue (), |
| ((Boolean)arguments[9]).booleanValue ()); |
| } |
| |
| boolean handleKeyEvent (String type, int keyCode, int charCode, boolean altKey, boolean ctrlKey, boolean shiftKey, boolean metaKey) { |
| if (type.equals (DOMEVENT_KEYDOWN)) { |
| keyCode = translateKey (keyCode); |
| lastKeyCode = keyCode; |
| switch (keyCode) { |
| case SWT.SHIFT: |
| case SWT.CONTROL: |
| case SWT.ALT: |
| case SWT.CAPS_LOCK: |
| case SWT.NUM_LOCK: |
| case SWT.SCROLL_LOCK: |
| case SWT.COMMAND: |
| case SWT.ESC: |
| case SWT.TAB: |
| case SWT.PAUSE: |
| case SWT.BS: |
| case SWT.INSERT: |
| case SWT.DEL: |
| case SWT.HOME: |
| case SWT.END: |
| case SWT.PAGE_UP: |
| case SWT.PAGE_DOWN: |
| case SWT.ARROW_DOWN: |
| case SWT.ARROW_UP: |
| case SWT.ARROW_LEFT: |
| case SWT.ARROW_RIGHT: |
| case SWT.F1: |
| case SWT.F2: |
| case SWT.F3: |
| case SWT.F4: |
| case SWT.F5: |
| case SWT.F6: |
| case SWT.F7: |
| case SWT.F8: |
| case SWT.F9: |
| case SWT.F10: |
| case SWT.F11: |
| case SWT.F12: { |
| /* keypress events will not be received for these keys, so send KeyDowns for them now */ |
| |
| Event keyEvent = new Event (); |
| keyEvent.widget = browser; |
| keyEvent.type = type.equals (DOMEVENT_KEYDOWN) ? SWT.KeyDown : SWT.KeyUp; |
| keyEvent.keyCode = keyCode; |
| switch (keyCode) { |
| case SWT.BS: keyEvent.character = SWT.BS; break; |
| case SWT.DEL: keyEvent.character = SWT.DEL; break; |
| case SWT.ESC: keyEvent.character = SWT.ESC; break; |
| case SWT.TAB: keyEvent.character = SWT.TAB; break; |
| } |
| lastCharCode = keyEvent.character; |
| keyEvent.stateMask = (altKey ? SWT.ALT : 0) | (ctrlKey ? SWT.CTRL : 0) | (shiftKey ? SWT.SHIFT : 0) | (metaKey ? SWT.COMMAND : 0); |
| keyEvent.stateMask &= ~keyCode; /* remove current keydown if it's a state key */ |
| final int stateMask = keyEvent.stateMask; |
| if (!sendKeyEvent (keyEvent) || browser.isDisposed ()) return false; |
| |
| if (browser.isFocusControl ()) { |
| if (keyCode == SWT.TAB && (stateMask & (SWT.CTRL | SWT.ALT)) == 0) { |
| browser.getDisplay ().asyncExec (() -> { |
| if (browser.isDisposed ()) return; |
| if (browser.getDisplay ().getFocusControl () == null) { |
| int traversal = (stateMask & SWT.SHIFT) != 0 ? SWT.TRAVERSE_TAB_PREVIOUS : SWT.TRAVERSE_TAB_NEXT; |
| browser.traverse (traversal); |
| } |
| }); |
| } |
| } |
| break; |
| } |
| } |
| return true; |
| } |
| |
| if (type.equals (DOMEVENT_KEYPRESS)) { |
| /* |
| * if keydown could not determine a keycode for this key then it's a |
| * key for which key events are not sent (eg.- the Windows key) |
| */ |
| if (lastKeyCode == 0) return true; |
| |
| lastCharCode = charCode; |
| if (ctrlKey && (0 <= lastCharCode && lastCharCode <= 0x7F)) { |
| if ('a' <= lastCharCode && lastCharCode <= 'z') lastCharCode -= 'a' - 'A'; |
| if (64 <= lastCharCode && lastCharCode <= 95) lastCharCode -= 64; |
| } |
| |
| Event keyEvent = new Event (); |
| keyEvent.widget = browser; |
| keyEvent.type = SWT.KeyDown; |
| keyEvent.keyCode = lastKeyCode; |
| keyEvent.character = (char)lastCharCode; |
| keyEvent.stateMask = (altKey ? SWT.ALT : 0) | (ctrlKey ? SWT.CTRL : 0) | (shiftKey ? SWT.SHIFT : 0) | (metaKey ? SWT.COMMAND : 0); |
| return sendKeyEvent (keyEvent) && !browser.isDisposed (); |
| } |
| |
| /* keyup */ |
| |
| keyCode = translateKey (keyCode); |
| if (keyCode == 0) { |
| /* indicates a key for which key events are not sent */ |
| return true; |
| } |
| if (keyCode != lastKeyCode) { |
| /* keyup does not correspond to the last keydown */ |
| lastKeyCode = keyCode; |
| lastCharCode = 0; |
| } |
| |
| Event keyEvent = new Event (); |
| keyEvent.widget = browser; |
| keyEvent.type = SWT.KeyUp; |
| keyEvent.keyCode = lastKeyCode; |
| keyEvent.character = (char)lastCharCode; |
| keyEvent.stateMask = (altKey ? SWT.ALT : 0) | (ctrlKey ? SWT.CTRL : 0) | (shiftKey ? SWT.SHIFT : 0) | (metaKey ? SWT.COMMAND : 0); |
| switch (lastKeyCode) { |
| case SWT.SHIFT: |
| case SWT.CONTROL: |
| case SWT.ALT: |
| case SWT.COMMAND: { |
| keyEvent.stateMask |= lastKeyCode; |
| } |
| } |
| browser.notifyListeners (keyEvent.type, keyEvent); |
| lastKeyCode = lastCharCode = 0; |
| return keyEvent.doit && !browser.isDisposed (); |
| } |
| |
| boolean handleMouseEvent (String type, int screenX, int screenY, int detail, int button, boolean altKey, boolean ctrlKey, boolean shiftKey, boolean metaKey, boolean hasRelatedTarget) { |
| /* |
| * MouseOver and MouseOut events are fired any time the mouse enters or exits |
| * any element within the Browser. To ensure that SWT events are only |
| * fired for mouse movements into or out of the Browser, do not fire an |
| * event if there is a related target element. |
| */ |
| |
| /* |
| * The following is intentionally commented because MouseOver and MouseOut events |
| * are not being hooked until https://bugs.webkit.org/show_bug.cgi?id=35246 is fixed. |
| */ |
| //if (type.equals (DOMEVENT_MOUSEOVER) || type.equals (DOMEVENT_MOUSEOUT)) { |
| // if (((Boolean)arguments[9]).booleanValue ()) return true; |
| //} |
| |
| /* |
| * The position of mouse events is received in screen-relative coordinates |
| * in order to handle pages with frames, since frames express their event |
| * coordinates relative to themselves rather than relative to their top- |
| * level page. Convert screen-relative coordinates to be browser-relative. |
| */ |
| Point position = new Point (screenX, screenY); |
| position = browser.getDisplay ().map (null, browser, position); |
| |
| Event mouseEvent = new Event (); |
| mouseEvent.widget = browser; |
| mouseEvent.x = position.x; |
| mouseEvent.y = position.y; |
| int mask = (altKey ? SWT.ALT : 0) | (ctrlKey ? SWT.CTRL : 0) | (shiftKey ? SWT.SHIFT : 0) | (metaKey ? SWT.COMMAND : 0); |
| mouseEvent.stateMask = mask; |
| |
| if (type.equals (DOMEVENT_MOUSEDOWN)) { |
| mouseEvent.type = SWT.MouseDown; |
| mouseEvent.count = detail; |
| mouseEvent.button = button; |
| browser.notifyListeners (mouseEvent.type, mouseEvent); |
| if (browser.isDisposed ()) return true; |
| if (detail == 2) { |
| mouseEvent = new Event (); |
| mouseEvent.type = SWT.MouseDoubleClick; |
| mouseEvent.widget = browser; |
| mouseEvent.x = position.x; |
| mouseEvent.y = position.y; |
| mouseEvent.stateMask = mask; |
| mouseEvent.count = detail; |
| mouseEvent.button = button; |
| browser.notifyListeners (mouseEvent.type, mouseEvent); |
| } |
| return true; |
| } |
| |
| if (type.equals (DOMEVENT_MOUSEUP)) { |
| mouseEvent.type = SWT.MouseUp; |
| mouseEvent.count = detail; |
| mouseEvent.button = button; |
| } else if (type.equals (DOMEVENT_MOUSEMOVE)) { |
| mouseEvent.type = SWT.MouseMove; |
| } else if (type.equals (DOMEVENT_MOUSEWHEEL)) { |
| mouseEvent.type = SWT.MouseWheel; |
| mouseEvent.count = detail; |
| |
| /* |
| * The following is intentionally commented because MouseOver and MouseOut events |
| * are not being hooked until https://bugs.webkit.org/show_bug.cgi?id=35246 is fixed. |
| */ |
| //} else if (type.equals (DOMEVENT_MOUSEOVER)) { |
| // mouseEvent.type = SWT.MouseEnter; |
| //} else if (type.equals (DOMEVENT_MOUSEOUT)) { |
| // mouseEvent.type = SWT.MouseExit; |
| |
| } else if (type.equals (DOMEVENT_DRAGSTART)) { |
| mouseEvent.type = SWT.DragDetect; |
| mouseEvent.button = button; |
| switch (mouseEvent.button) { |
| case 1: mouseEvent.stateMask |= SWT.BUTTON1; break; |
| case 2: mouseEvent.stateMask |= SWT.BUTTON2; break; |
| case 3: mouseEvent.stateMask |= SWT.BUTTON3; break; |
| case 4: mouseEvent.stateMask |= SWT.BUTTON4; break; |
| case 5: mouseEvent.stateMask |= SWT.BUTTON5; break; |
| } |
| } |
| |
| browser.notifyListeners (mouseEvent.type, mouseEvent); |
| return true; |
| } |
| |
| long handleLoadCommitted (long uri, boolean top) { |
| int length = C.strlen (uri); |
| byte[] bytes = new byte[length]; |
| C.memmove (bytes, uri, length); |
| String url = new String (Converter.mbcsToWcs (bytes)); |
| /* |
| * If the URI indicates that the page is being rendered from memory |
| * (via setText()) then set it to about:blank to be consistent with IE. |
| */ |
| if (url.equals (URI_FILEROOT)) { |
| url = ABOUT_BLANK; |
| } else { |
| length = URI_FILEROOT.length (); |
| if (url.startsWith (URI_FILEROOT) && url.charAt (length) == '#') { |
| url = ABOUT_BLANK + url.substring (length); |
| } |
| } |
| |
| LocationEvent event = new LocationEvent (browser); |
| event.display = browser.getDisplay (); |
| event.widget = browser; |
| event.location = url; |
| event.top = top; |
| Runnable fireLocationChanged = () -> { |
| if (browser.isDisposed ()) return; |
| for (int i = 0; i < locationListeners.length; i++) { |
| locationListeners[i].changed (event); |
| } |
| }; |
| browser.getDisplay().asyncExec(fireLocationChanged); |
| return 0; |
| } |
| |
| /** |
| * This method is reached by: |
| * Webkit2: WebKitWebView load-changed signal |
| * - void user_function (WebKitWebView *web_view, WebKitLoadEvent load_event, gpointer user_data) |
| * - https://webkitgtk.org/reference/webkit2gtk/stable/WebKitWebView.html#WebKitWebView-load-changed |
| * - Note: As there is no return value, safe to fire asynchronously. |
| */ |
| private void fireProgressCompletedEvent(){ |
| Runnable fireProgressEvents = () -> { |
| if (browser.isDisposed() || progressListeners == null) return; |
| ProgressEvent progress = new ProgressEvent (browser); |
| progress.display = browser.getDisplay (); |
| progress.widget = browser; |
| progress.current = MAX_PROGRESS; |
| progress.total = MAX_PROGRESS; |
| for (int i = 0; i < progressListeners.length; i++) { |
| progressListeners[i].completed (progress); |
| } |
| }; |
| browser.getDisplay().asyncExec(fireProgressEvents); |
| } |
| |
| @Override |
| public boolean isBackEnabled () { |
| if (webView == 0) |
| return false; //disposed. |
| return WebKitGTK.webkit_web_view_can_go_back (webView) != 0; |
| } |
| |
| @Override |
| public boolean isForwardEnabled () { |
| return WebKitGTK.webkit_web_view_can_go_forward (webView) != 0; |
| } |
| |
| void onDispose (Event e) { |
| /* Browser could have been disposed by one of the Dispose listeners */ |
| if (!browser.isDisposed()) { |
| /* invoke onbeforeunload handlers */ |
| if (!browser.isClosing) { |
| close (false); |
| } |
| } |
| |
| for (BrowserFunction function : functions.values()) { |
| function.dispose(false); |
| } |
| functions = null; |
| |
| if (WebKitGTK.webkit_get_minor_version() >= 18) { |
| // Bug 530678. |
| // * As of Webkit 2.18, (it seems) webkitGtk auto-disposes itself when the parent is disposed. |
| // * This can cause a deadlock inside Webkit process if WebkitGTK widget's parent is disposed during a callback. |
| // This is because webkit process is waiting for it's callback to finish which never completes |
| // because parent's disposal also disposed webkitGTK widget. (Note Webkit process vs WebkitGtk widget). |
| // * To break the deadlock, we unparent webkitGtk temporarily and unref (dispose) it later after callback is done. |
| // |
| // If you change dispose logic, to check that you haven't introduced memory leaks, test via: |
| // org.eclipse.swt.tests.junit.memoryleak.Test_Memory_Leak.test_Browser() |
| OS.g_object_ref (webView); |
| GTK3.gtk_container_remove (GTK.gtk_widget_get_parent (webView), webView); |
| long webViewTempRef = webView; |
| Display.getDefault().asyncExec(() -> { |
| OS.g_object_unref(webViewTempRef); |
| }); |
| webView = 0; |
| } |
| } |
| |
| void onResize (Event e) { |
| Rectangle rect = DPIUtil.autoScaleUp(browser.getClientArea ()); |
| if (webView == 0) |
| return; |
| GTK.gtk_widget_set_size_request (webView, rect.width, rect.height); |
| } |
| |
| void openDownloadWindow (final long webkitDownload, final String suggested_filename) { |
| final Shell shell = new Shell (); |
| String msg = Compatibility.getMessage ("SWT_FileDownload"); //$NON-NLS-1$ |
| shell.setText (msg); |
| GridLayout gridLayout = new GridLayout (); |
| gridLayout.marginHeight = 15; |
| gridLayout.marginWidth = 15; |
| gridLayout.verticalSpacing = 20; |
| shell.setLayout (gridLayout); |
| |
| String nameString = suggested_filename; |
| |
| long request = WebKitGTK.webkit_download_get_request(webkitDownload); |
| long url = WebKitGTK.webkit_uri_request_get_uri(request); |
| |
| int length = C.strlen (url); |
| byte[] bytes = new byte[length]; |
| C.memmove (bytes, url, length); |
| String urlString = new String (Converter.mbcsToWcs (bytes)); |
| msg = Compatibility.getMessage ("SWT_Download_Location", new Object[] {nameString, urlString}); //$NON-NLS-1$ |
| Label nameLabel = new Label (shell, SWT.WRAP); |
| nameLabel.setText (msg); |
| GridData data = new GridData (); |
| Monitor monitor = browser.getMonitor (); |
| int maxWidth = monitor.getBounds ().width / 2; |
| int width = nameLabel.computeSize (SWT.DEFAULT, SWT.DEFAULT).x; |
| data.widthHint = Math.min (width, maxWidth); |
| data.horizontalAlignment = GridData.FILL; |
| data.grabExcessHorizontalSpace = true; |
| nameLabel.setLayoutData (data); |
| |
| final Label statusLabel = new Label (shell, SWT.NONE); |
| statusLabel.setText (Compatibility.getMessage ("SWT_Download_Started")); //$NON-NLS-1$ |
| data = new GridData (GridData.FILL_BOTH); |
| statusLabel.setLayoutData (data); |
| |
| final Button cancel = new Button (shell, SWT.PUSH); |
| cancel.setText (Compatibility.getMessage ("SWT_Cancel")); //$NON-NLS-1$ |
| data = new GridData (); |
| data.horizontalAlignment = GridData.CENTER; |
| cancel.setLayoutData (data); |
| final Listener cancelListener = event -> { |
| webKitDownloadStatus.put(new LONG(webkitDownload), WebKitGTK.WEBKIT_DOWNLOAD_STATUS_CANCELLED); |
| WebKitGTK.webkit_download_cancel (webkitDownload); |
| }; |
| cancel.addListener (SWT.Selection, cancelListener); |
| |
| OS.g_object_ref (webkitDownload); |
| final Display display = browser.getDisplay (); |
| final int INTERVAL = 500; |
| display.timerExec (INTERVAL, new Runnable () { |
| @Override |
| public void run () { |
| int status = webKitDownloadStatus.containsKey(new LONG(webkitDownload)) ? webKitDownloadStatus.get(new LONG(webkitDownload)) : 0; |
| if (shell.isDisposed () || status == WebKitGTK.WEBKIT_DOWNLOAD_STATUS_FINISHED || status == WebKitGTK.WEBKIT_DOWNLOAD_STATUS_CANCELLED) { |
| shell.dispose (); |
| display.timerExec (-1, this); |
| OS.g_object_unref (webkitDownload); |
| webKitDownloadStatus.remove(new LONG(webkitDownload)); |
| return; |
| } |
| if (status == WebKitGTK.WEBKIT_DOWNLOAD_STATUS_ERROR) { |
| statusLabel.setText (Compatibility.getMessage ("SWT_Download_Error")); //$NON-NLS-1$ |
| display.timerExec (-1, this); |
| OS.g_object_unref (webkitDownload); |
| cancel.removeListener (SWT.Selection, cancelListener); |
| cancel.addListener (SWT.Selection, event -> shell.dispose ()); |
| webKitDownloadStatus.remove(new LONG(webkitDownload)); |
| return; |
| } |
| |
| long current = WebKitGTK.webkit_download_get_received_data_length(webkitDownload) / 1024L; |
| long response = WebKitGTK.webkit_download_get_response(webkitDownload); |
| long total = WebKitGTK.webkit_uri_response_get_content_length(response) / 1024L; |
| String message = Compatibility.getMessage ("SWT_Download_Status", new Object[] {current, total}); //$NON-NLS-1$ |
| statusLabel.setText (message); |
| display.timerExec (INTERVAL, this); |
| } |
| }); |
| |
| shell.pack (); |
| shell.open (); |
| } |
| |
| @Override |
| public void refresh () { |
| if (webView == 0) |
| return; //disposed. |
| WebKitGTK.webkit_web_view_reload (webView); |
| } |
| |
| @Override |
| public boolean setText (String html, boolean trusted) { |
| /* convert the String containing HTML to an array of bytes with UTF-8 data */ |
| byte[] html_bytes = (html + '\0').getBytes (StandardCharsets.UTF_8); //$NON-NLS-1$ |
| |
| w2_bug527738LastRequestCounter.incrementAndGet(); |
| byte[] uriBytes; |
| if (!trusted) { |
| uriBytes = Converter.wcsToMbcs (ABOUT_BLANK, true); |
| } else { |
| uriBytes = Converter.wcsToMbcs (URI_FILEROOT, true); |
| } |
| WebKitGTK.webkit_web_view_load_html (webView, html_bytes, uriBytes); |
| |
| return true; |
| } |
| |
| @Override |
| public boolean setUrl (String url, String postData, String[] headers) { |
| w2_bug527738LastRequestCounter.incrementAndGet(); |
| |
| if (webView == 0) |
| return false; // disposed. |
| |
| /* |
| * WebKitGTK attempts to open the exact url string that is passed to it and |
| * will not infer a protocol if it's not specified. Detect the case of an |
| * invalid URL string and try to fix it by prepending an appropriate protocol. |
| */ |
| try { |
| new URL(url); |
| } catch (MalformedURLException e) { |
| String testUrl = null; |
| if (url.charAt (0) == SEPARATOR_FILE) { |
| /* appears to be a local file */ |
| testUrl = PROTOCOL_FILE + url; |
| } else { |
| testUrl = PROTOCOL_HTTP + url; |
| } |
| try { |
| new URL (testUrl); |
| url = testUrl; /* adding the protocol made the url valid */ |
| } catch (MalformedURLException e2) { |
| /* adding the protocol did not make the url valid, so do nothing */ |
| } |
| } |
| |
| /* |
| * Feature of WebKit. The user-agent header value cannot be overridden |
| * by changing it in the resource request. The workaround is to detect |
| * here whether the user-agent is being overridden, and if so, temporarily |
| * set the value on the WebView when initiating the load request and then |
| * remove it afterwards. |
| */ |
| long settings = WebKitGTK.webkit_web_view_get_settings (webView); |
| if (headers != null) { |
| for (int i = 0; i < headers.length; i++) { |
| String current = headers[i]; |
| if (current != null) { |
| int index = current.indexOf (':'); |
| if (index != -1) { |
| String key = current.substring (0, index).trim (); |
| String value = current.substring (index + 1).trim (); |
| if (key.length () > 0 && value.length () > 0) { |
| if (key.equalsIgnoreCase (USER_AGENT)) { |
| byte[] bytes = Converter.wcsToMbcs (value, true); |
| OS.g_object_set (settings, WebKitGTK.user_agent, bytes, 0); |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| byte[] uriBytes = Converter.wcsToMbcs (url, true); |
| |
| if (postData==null && headers != null) { |
| long request = WebKitGTK.webkit_uri_request_new (uriBytes); |
| long requestHeaders = WebKitGTK.webkit_uri_request_get_http_headers (request); |
| if (requestHeaders != 0) { |
| addRequestHeaders(requestHeaders, headers); |
| } |
| WebKitGTK.webkit_web_view_load_request (webView, request); |
| OS.g_object_set (settings, WebKitGTK.user_agent, 0, 0); |
| return true; |
| } |
| |
| // Bug 527738 |
| // Webkit2 doesn't have api to set url with data. (2.18). While we wait for them to implement, |
| // this workaround uses java to query a server and then manually populate webkit with content. |
| // This should be version guarded and replaced with proper functions once webkit2 has implemented api. |
| if (postData != null) { |
| final String base_url = url; |
| |
| // Use Webkit User-Agent |
| long [] user_agent_str_ptr = new long [1]; |
| OS.g_object_get (settings, WebKitGTK.user_agent, user_agent_str_ptr, 0); |
| final String userAgent = Converter.cCharPtrToJavaString(user_agent_str_ptr[0], true); |
| final int lastRequest = w2_bug527738LastRequestCounter.incrementAndGet(); // Webkit 2 only |
| Thread send_request = new Thread(() -> { |
| String html = null; |
| String mime_type = null; |
| String encoding_type = null; |
| try { |
| URL base = new URL(base_url); |
| URLConnection url_conn = base.openConnection(); |
| if (url_conn instanceof HttpURLConnection) { |
| HttpURLConnection conn = (HttpURLConnection) url_conn; |
| |
| { // Configure connection. |
| conn.setRequestMethod("POST"); //$NON-NLS-1$ |
| |
| // Use Webkit Accept |
| conn.setRequestProperty( "Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"); //$NON-NLS-1$ $NON-NLS-2$ |
| |
| conn.setRequestProperty("User-Agent", userAgent); //$NON-NLS-1$ |
| conn.setDoOutput(true); // because default value is false |
| |
| // Set headers |
| if (headers != null) { |
| for (String header : headers) { |
| int index = header.indexOf(':'); |
| if (index > 0) { |
| String key = header.substring(0, index).trim(); |
| String value = header.substring(index + 1).trim(); |
| conn.setRequestProperty(key, value); |
| } |
| } |
| } |
| } |
| |
| { // Query server |
| try (OutputStream out = conn.getOutputStream()) { |
| out.write(postData.getBytes()); |
| } |
| |
| StringBuilder response = new StringBuilder(); |
| try (BufferedReader buff = new BufferedReader(new InputStreamReader(conn.getInputStream()))) { |
| char [] cbuff = new char[4096]; |
| while (buff.read(cbuff, 0, cbuff.length) > 0) { |
| response.append(new String(cbuff)); |
| Arrays.fill(cbuff, '\0'); |
| } |
| } |
| html = response.toString(); |
| } |
| |
| { // Extract result meta data |
| // Get Media Type from Content-Type |
| String content_type = conn.getContentType(); |
| int paramaterSeparatorIndex = content_type.indexOf(';'); |
| mime_type = paramaterSeparatorIndex > 0 ? content_type.substring(0, paramaterSeparatorIndex) : content_type; |
| |
| // Get Encoding if defined |
| if (content_type.indexOf(';') > 0) { |
| String [] attrs = content_type.split(";"); |
| for (String attr : attrs) { |
| int i = attr.indexOf('='); |
| if (i > 0) { |
| String key = attr.substring(0, i).trim(); |
| String value = attr.substring(i + 1).trim(); |
| if ("charset".equalsIgnoreCase(key)) { //$NON-NLS-1$ |
| encoding_type = value; |
| } |
| } |
| } |
| } |
| } |
| } |
| } catch (IOException e) { // MalformedURLException is an IOException also. |
| html = e.getMessage(); |
| } finally { |
| if (html != null && lastRequest == w2_bug527738LastRequestCounter.get()) { |
| final String final_html = html; |
| final String final_mime_type = mime_type; |
| final String final_encoding_type = encoding_type; |
| Display.getDefault().syncExec(() -> { |
| byte [] html_bytes = Converter.wcsToMbcs(final_html, false); |
| byte [] mime_type_bytes = final_mime_type != null ? Converter.javaStringToCString(final_mime_type) : Converter.javaStringToCString("text/plain"); |
| byte [] encoding_bytes = final_encoding_type != null ? Converter.wcsToMbcs(final_encoding_type, true) : new byte [] {0}; |
| long gByte = OS.g_bytes_new(html_bytes, html_bytes.length); |
| WebKitGTK.webkit_web_view_load_bytes (webView, gByte, mime_type_bytes, encoding_bytes, uriBytes); |
| OS.g_bytes_unref (gByte); // as per glib/tests/keyfile:test_bytes().. |
| OS.g_object_set (settings, WebKitGTK.user_agent, 0, 0); |
| }); |
| } |
| } |
| }); |
| send_request.start(); |
| } else { |
| WebKitGTK.webkit_web_view_load_uri (webView, uriBytes); |
| } |
| |
| if (postData == null) { |
| OS.g_object_set (settings, WebKitGTK.user_agent, 0, 0); |
| } |
| return true; |
| } |
| |
| @Override |
| public void stop () { |
| WebKitGTK.webkit_web_view_stop_loading (webView); |
| } |
| |
| /** |
| * WebKitWebView 'close' signal |
| * void user_function (WebKitWebView *web_view, gpointer user_data); // observe *no* return value. |
| * https://webkitgtk.org/reference/webkit2gtk/stable/WebKitWebView.html#WebKitWebView-close |
| */ |
| long webkit_close_web_view (long web_view) { |
| WindowEvent newEvent = new WindowEvent (browser); |
| newEvent.display = browser.getDisplay (); |
| newEvent.widget = browser; |
| Runnable fireCloseWindowListeners = () -> { |
| if (browser.isDisposed()) return; |
| for (int i = 0; i < closeWindowListeners.length; i++) { |
| closeWindowListeners[i].close (newEvent); |
| } |
| browser.dispose (); |
| }; |
| // On WebKit2 this signal doesn't expect a return value. |
| // As such, we can safley execute the SWT listeners later to avoid deadlocks. See bug 512001 |
| browser.getDisplay().asyncExec(fireCloseWindowListeners); |
| return 0; |
| } |
| |
| long webkit_create_web_view (long web_view, long frame) { |
| WindowEvent newEvent = new WindowEvent (browser); |
| newEvent.display = browser.getDisplay (); |
| newEvent.widget = browser; |
| newEvent.required = true; |
| Runnable fireOpenWindowListeners = () -> { |
| if (openWindowListeners != null) { |
| for (int i = 0; i < openWindowListeners.length; i++) { |
| openWindowListeners[i].open (newEvent); |
| } |
| } |
| }; |
| try { |
| nonBlockingEvaluate++; // running evaluate() inside openWindowListener and waiting for return leads to deadlock. Bug 512001 |
| fireOpenWindowListeners.run();// Permit evaluate()/execute() to execute scripts in listener, but do not provide return value. |
| } catch (Exception e) { |
| throw e; // rethrow execption if thrown, but decrement counter first. |
| } finally { |
| nonBlockingEvaluate--; |
| } |
| Browser browser = null; |
| if (newEvent.browser != null && newEvent.browser.webBrowser instanceof WebKit) { |
| browser = newEvent.browser; |
| } |
| if (browser != null && !browser.isDisposed ()) { |
| return ((WebKit)browser.webBrowser).webView; |
| } |
| return 0; |
| } |
| |
| static long webkit_download_started(long webKitDownload) { |
| OS.g_signal_connect(webKitDownload, WebKitGTK.decide_destination, Proc3.getAddress(), DECIDE_DESTINATION); |
| OS.g_signal_connect(webKitDownload, WebKitGTK.failed, Proc3.getAddress(), FAILED); |
| OS.g_signal_connect(webKitDownload, WebKitGTK.finished, Proc2.getAddress(), FINISHED); |
| return 1; |
| } |
| |
| |
| static long webkit_download_decide_destination(long webKitDownload, long suggested_filename) { |
| final String fileName = getString(suggested_filename); |
| long webView = WebKitGTK.webkit_download_get_web_view(webKitDownload); |
| if (webView != 0) { |
| Browser browser = FindBrowser (webView); |
| if (browser == null || browser.isDisposed() || browser.isClosing) return 0; |
| |
| FileDialog dialog = new FileDialog (browser.getShell (), SWT.SAVE); |
| dialog.setFileName (fileName); |
| String title = Compatibility.getMessage ("SWT_FileDownload"); //$NON-NLS-1$ |
| dialog.setText (title); |
| String path = dialog.open (); |
| if (path != null) { |
| path = URI_FILEROOT + path; |
| byte[] uriBytes = Converter.wcsToMbcs (path, true); |
| |
| if (WebKitGTK.webkit_get_minor_version() >= 6) { |
| WebKitGTK.webkit_download_set_allow_overwrite (webKitDownload, true); |
| } |
| WebKitGTK.webkit_download_set_destination (webKitDownload, uriBytes); |
| ((WebKit)browser.webBrowser).openDownloadWindow(webKitDownload, fileName); |
| } |
| } |
| return 0; |
| } |
| |
| static long webkit_download_finished(long download) { |
| // A failed signal may have been recorded prior. The finish signal is now being called. |
| if (!webKitDownloadStatus.containsKey(new LONG(download))) { |
| webKitDownloadStatus.put(new LONG(download), WebKitGTK.WEBKIT_DOWNLOAD_STATUS_FINISHED); |
| } |
| return 0; |
| } |
| |
| static long webkit_download_failed(long download) { |
| // A cancel may have been issued resulting in this signal call. Preserve the original cause. |
| if (!webKitDownloadStatus.containsKey(new LONG(download))) { |
| webKitDownloadStatus.put(new LONG(download), WebKitGTK.WEBKIT_DOWNLOAD_STATUS_ERROR); |
| } |
| return 0; |
| } |
| |
| /** |
| * WebkitWebView mouse-target-changed |
| * - void user_function (WebKitWebView *web_view, WebKitHitTestResult *hit_test_result, guint modifiers, gpointer user_data) |
| * - https://webkitgtk.org/reference/webkit2gtk/stable/WebKitWebView.html#WebKitWebView-mouse-target-changed |
| * */ |
| long webkit_mouse_target_changed (long web_view, long hit_test_result, long modifiers) { |
| if (WebKitGTK.webkit_hit_test_result_context_is_link(hit_test_result)){ |
| long uri = WebKitGTK.webkit_hit_test_result_get_link_uri(hit_test_result); |
| long title = WebKitGTK.webkit_hit_test_result_get_link_title(hit_test_result); |
| return webkit_hovering_over_link(web_view, title, uri); |
| } |
| |
| return 0; |
| } |
| |
| /** |
| * Webkit2: WebkitWebView mouse-target-change |
| * - Normally this signal is called for many different events, e.g hoveing over an image. |
| * But in our case, in webkit_mouse_target_changed() we filter out everything except mouse_over_link events. |
| * |
| * Since there is no return value, it is safe to run asynchronously. |
| */ |
| long webkit_hovering_over_link (long web_view, long title, long uri) { |
| if (uri != 0) { |
| int length = C.strlen (uri); |
| byte[] bytes = new byte[length]; |
| C.memmove (bytes, uri, length); |
| String text = new String (Converter.mbcsToWcs (bytes)); |
| StatusTextEvent event = new StatusTextEvent (browser); |
| event.display = browser.getDisplay (); |
| event.widget = browser; |
| event.text = text; |
| Runnable fireStatusTextListener = () -> { |
| if (browser.isDisposed() || statusTextListeners == null) return; |
| for (int i = 0; i < statusTextListeners.length; i++) { |
| statusTextListeners[i].changed (event); |
| } |
| }; |
| browser.getDisplay().asyncExec(fireStatusTextListener); |
| } |
| return 0; |
| } |
| |
| long webkit_decide_policy (long web_view, long decision, int decision_type, long user_data) { |
| switch (decision_type) { |
| case WebKitGTK.WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION: |
| long request = WebKitGTK. webkit_navigation_policy_decision_get_request(decision); |
| if (request == 0){ |
| return 0; |
| } |
| long uri = WebKitGTK.webkit_uri_request_get_uri (request); |
| String url = getString(uri); |
| /* |
| * If the URI indicates that the page is being rendered from memory |
| * (via setText()) then set it to about:blank to be consistent with IE. |
| */ |
| if (url.equals (URI_FILEROOT)) { |
| url = ABOUT_BLANK; |
| } else { |
| int length = URI_FILEROOT.length (); |
| if (url.startsWith (URI_FILEROOT) && url.charAt (length) == '#') { |
| url = ABOUT_BLANK + url.substring (length); |
| } |
| } |
| |
| LocationEvent newEvent = new LocationEvent (browser); |
| newEvent.display = browser.getDisplay (); |
| newEvent.widget = browser; |
| newEvent.location = url; |
| newEvent.doit = true; |
| |
| try { |
| nonBlockingEvaluate++; |
| if (locationListeners != null) { |
| for (int i = 0; i < locationListeners.length; i++) { |
| locationListeners[i].changing (newEvent); |
| } |
| } |
| } catch (Exception e) { |
| throw e; |
| } finally { |
| nonBlockingEvaluate--; |
| } |
| |
| if (newEvent.doit && !browser.isDisposed ()) { |
| if (jsEnabled != jsEnabledOnNextPage) { |
| jsEnabled = jsEnabledOnNextPage; |
| webkit_settings_set(WebKitGTK.enable_javascript, jsEnabled ? 1 : 0); |
| } |
| } |
| if(!newEvent.doit){ |
| WebKitGTK.webkit_policy_decision_ignore (decision); |
| } |
| break; |
| case WebKitGTK.WEBKIT_POLICY_DECISION_TYPE_NEW_WINDOW_ACTION: |
| break; |
| case WebKitGTK.WEBKIT_POLICY_DECISION_TYPE_RESPONSE: |
| long response = WebKitGTK.webkit_response_policy_decision_get_response(decision); |
| long mime_type = WebKitGTK.webkit_uri_response_get_mime_type(response); |
| boolean canShow = WebKitGTK.webkit_web_view_can_show_mime_type (webView, mime_type) != 0; |
| if (!canShow) { |
| WebKitGTK.webkit_policy_decision_download (decision); |
| return 1; |
| } |
| break; |
| default: |
| /* Making no decision results in webkit_policy_decision_use(). */ |
| return 0; |
| } |
| return 0; |
| } |
| |
| long webkit_load_changed (long web_view, int status, long user_data) { |
| switch (status) { |
| case WebKitGTK.WEBKIT2_LOAD_COMMITTED: { |
| long uri = WebKitGTK.webkit_web_view_get_uri (webView); |
| return handleLoadCommitted (uri, true); |
| } |
| case WebKitGTK.WEBKIT2_LOAD_FINISHED: { |
| if (firstLoad) { |
| GtkAllocation allocation = new GtkAllocation (); |
| GTK.gtk_widget_get_allocation(browser.handle, allocation); |
| GTK.gtk_widget_size_allocate(browser.handle, allocation); |
| firstLoad = false; |
| } |
| |
| fireProgressCompletedEvent(); |
| |
| /* |
| * If there is a pending TLS error, handle it by prompting the user for input. |
| * This is done by popping up a message box and asking if the user would like |
| * ignore warnings for this host. Clicking yes will do so, clicking no will |
| * load the previous page. |
| * |
| * Not applicable if the ignoreTls flag has been set. See bug 531341. |
| */ |
| if (tlsError && !ignoreTls) { |
| tlsError = false; |
| String javaHost = tlsErrorUri.getHost(); |
| MessageBox prompt = new MessageBox (browser.getShell(), SWT.YES | SWT.NO); |
| prompt.setText(SWT.getMessage("SWT_InvalidCert_Title")); |
| String specific = tlsErrorType.isEmpty() ? "\n\n" : "\n\n" + tlsErrorType + "\n\n"; |
| String message = SWT.getMessage("SWT_InvalidCert_Message", new Object[] {javaHost}) + |
| specific + SWT.getMessage("SWT_InvalidCert_Connect"); |
| prompt.setMessage(message); |
| int result = prompt.open(); |
| if (result == SWT.YES) { |
| long webkitcontext = WebKitGTK.webkit_web_view_get_context(web_view); |
| if (javaHost != null) { |
| byte [] host = Converter.javaStringToCString(javaHost); |
| WebKitGTK.webkit_web_context_allow_tls_certificate_for_host(webkitcontext, tlsErrorCertificate, host); |
| WebKitGTK.webkit_web_view_reload (web_view); |
| } else { |
| System.err.println("***ERROR: Unable to parse host from URI!"); |
| } |
| } else { |
| back(); |
| } |
| // De-reference Webkit certificate so it can be freed |
| if (tlsErrorCertificate != 0) { |
| OS.g_object_unref (tlsErrorCertificate); |
| tlsErrorCertificate = 0; |
| } |
| } |
| |
| return 0; |
| } |
| } |
| return 0; |
| } |
| |
| /** |
| * Called in cases where a web page failed to load due to TLS errors |
| * (self-signed certificates, as an example). |
| */ |
| long webkit_load_failed_tls (long web_view, long failing_uri, long certificate, long error) { |
| if (!ignoreTls) { |
| // Set tlsError flag so that the user can be prompted once this "bad" page has finished loading |
| tlsError = true; |
| OS.g_object_ref(certificate); |
| tlsErrorCertificate = certificate; |
| convertUri (failing_uri); |
| switch ((int)error) { |
| case WebKitGTK.G_TLS_CERTIFICATE_UNKNOWN_CA: { |
| tlsErrorType = SWT.getMessage("SWT_InvalidCert_UnknownCA"); |
| break; |
| } |
| case WebKitGTK.G_TLS_CERTIFICATE_BAD_IDENTITY: { |
| tlsErrorType = SWT.getMessage("SWT_InvalidCert_BadIdentity"); |
| break; |
| } |
| case WebKitGTK.G_TLS_CERTIFICATE_NOT_ACTIVATED: { |
| tlsErrorType = SWT.getMessage("SWT_InvalidCert_NotActivated"); |
| break; |
| } |
| case WebKitGTK.G_TLS_CERTIFICATE_EXPIRED: { |
| tlsErrorType = SWT.getMessage("SWT_InvalidCert_Expired"); |
| break; |
| } |
| case WebKitGTK.G_TLS_CERTIFICATE_REVOKED: { |
| tlsErrorType = SWT.getMessage("SWT_InvalidCert_Revoked"); |
| break; |
| } |
| case WebKitGTK.G_TLS_CERTIFICATE_INSECURE: { |
| tlsErrorType = SWT.getMessage("SWT_InvalidCert_Insecure"); |
| break; |
| } |
| case WebKitGTK.G_TLS_CERTIFICATE_GENERIC_ERROR: { |
| tlsErrorType = SWT.getMessage("SWT_InvalidCert_GenericError"); |
| break; |
| } |
| case WebKitGTK.G_TLS_CERTIFICATE_VALIDATE_ALL: { |
| tlsErrorType = SWT.getMessage("SWT_InvalidCert_ValidateAll"); |
| break; |
| } |
| default: { |
| tlsErrorType = SWT.getMessage("SWT_InvalidCert_GenericError"); |
| break; |
| } |
| } |
| } |
| return 0; |
| } |
| |
| /** |
| * Converts a WebKit URI into a Java URI object. |
| * |
| * @param webkitUri a long pointing to the URI in C string form (gchar *) |
| * @throws URISyntaxException if the string violates RFC 2396, or is otherwise |
| * malformed |
| */ |
| void convertUri (long webkitUri) { |
| try { |
| tlsErrorUriString = Converter.cCharPtrToJavaString(webkitUri, false); |
| tlsErrorUri = new URI (tlsErrorUriString); |
| } catch (URISyntaxException e) { |
| System.err.println("***ERROR: Malformed URI from WebKit!"); |
| return; |
| } |
| } |
| |
| /** |
| * Triggered by a change in property. (both gdouble[0,1]) |
| * Webkit2: WebkitWebview notify::estimated-load-progress |
| * https://webkitgtk.org/reference/webkit2gtk/stable/WebKitWebView.html#WebKitWebView--estimated-load-progress |
| * |
| * No return value required. Thus safe to run asynchronously. |
| */ |
| long webkit_notify_progress (long web_view, long pspec) { |
| ProgressEvent event = new ProgressEvent (browser); |
| event.display = browser.getDisplay (); |
| event.widget = browser; |
| double progress = 0; |
| progress = WebKitGTK.webkit_web_view_get_estimated_load_progress (webView); |
| event.current = (int) (progress * MAX_PROGRESS); |
| event.total = MAX_PROGRESS; |
| Runnable fireProgressChangedEvents = () -> { |
| if (browser.isDisposed() || progressListeners == null) return; |
| for (int i = 0; i < progressListeners.length; i++) { |
| progressListeners[i].changed (event); |
| } |
| }; |
| browser.getDisplay().asyncExec(fireProgressChangedEvents); |
| return 0; |
| } |
| |
| /** |
| * Triggerd by webkit's 'notify::title' signal and forwarded to this function. |
| * The signal doesn't have documentation (2.15.4), but is mentioned here: |
| * https://webkitgtk.org/reference/webkit2gtk/stable/WebKitWebView.html#webkit-web-view-get-title |
| * |
| * It doesn't look it would require a return value, so running in asyncExec should be fine. |
| */ |
| long webkit_notify_title (long web_view, long pspec) { |
| long title = WebKitGTK.webkit_web_view_get_title (webView); |
| String titleString; |
| if (title == 0) { |
| titleString = ""; //$NON-NLS-1$ |
| } else { |
| int length = C.strlen (title); |
| byte[] bytes = new byte[length]; |
| C.memmove (bytes, title, length); |
| titleString = new String (Converter.mbcsToWcs (bytes)); |
| } |
| TitleEvent event = new TitleEvent (browser); |
| event.display = browser.getDisplay (); |
| event.widget = browser; |
| event.title = titleString; |
| Runnable fireTitleListener = () -> { |
| for (int i = 0; i < titleListeners.length; i++) { |
| titleListeners[i].changed (event); |
| } |
| }; |
| browser.getDisplay().asyncExec(fireTitleListener); |
| return 0; |
| } |
| |
| long webkit_context_menu (long web_view, long context_menu, long eventXXX, long hit_test_result) { |
| Point pt = browser.getDisplay ().getCursorLocation (); // might break on Wayland? Wouldn't hurt to verify. |
| Event event = new Event (); |
| event.x = pt.x; |
| event.y = pt.y; |
| browser.notifyListeners (SWT.MenuDetect, event); |
| if (!event.doit) { |
| // Do not display the menu |
| return 1; |
| } |
| |
| Menu menu = browser.getMenu (); |
| if (menu != null && !menu.isDisposed ()) { |
| if (pt.x != event.x || pt.y != event.y) { |
| menu.setLocation (event.x, event.y); |
| } |
| menu.setVisible (true); |
| // Do not display the webkit menu |
| return 1; |
| } |
| return 0; |
| } |
| |
| private void addRequestHeaders(long requestHeaders, String[] headers){ |
| for (int i = 0; i < headers.length; i++) { |
| String current = headers[i]; |
| if (current != null) { |
| int index = current.indexOf (':'); |
| if (index != -1) { |
| String key = current.substring (0, index).trim (); |
| String value = current.substring (index + 1).trim (); |
| if (key.length () > 0 && value.length () > 0) { |
| byte[] nameBytes = Converter.wcsToMbcs (key, true); |
| byte[] valueBytes = Converter.wcsToMbcs (value, true); |
| WebKitGTK.soup_message_headers_append (requestHeaders, nameBytes, valueBytes); |
| } |
| } |
| } |
| } |
| |
| } |
| |
| /** |
| * Emitted after "create" on the newly created WebKitWebView when it should be displayed to the user. |
| * Webkit2 signal: ready-to-show |
| * https://webkitgtk.org/reference/webkitgtk/unstable/webkitgtk-webkitwebview.html#WebKitWebView-web-view-ready |
| * Note in webkit2, no return value has to be provided in callback. |
| */ |
| long webkit_web_view_ready (long web_view) { |
| WindowEvent newEvent = new WindowEvent (browser); |
| newEvent.display = browser.getDisplay (); |
| newEvent.widget = browser; |
| |
| long properties = WebKitGTK.webkit_web_view_get_window_properties(webView); |
| newEvent.addressBar = webkit_settings_get(properties, WebKitGTK.locationbar_visible) != 0; |
| newEvent.menuBar = webkit_settings_get(properties, WebKitGTK.menubar_visible) != 0; |
| newEvent.statusBar = webkit_settings_get(properties, WebKitGTK.statusbar_visible) != 0; |
| newEvent.toolBar = webkit_settings_get(properties, WebKitGTK.toolbar_visible) != 0; |
| |
| GdkRectangle rect = new GdkRectangle(); |
| WebKitGTK.webkit_window_properties_get_geometry(properties, rect); |
| newEvent.location = new Point(Math.max(0, rect.x),Math.max(0, rect.y)); |
| |
| int width = rect.width; |
| int height = rect.height; |
| if (height == 100 && width == 100) { |
| // On Webkit2, if no height/width is specified, then minimum (which is 100) is allocated to popus. |
| // This makes popups very small. |
| // For better cross-platform consistency (Win/Cocoa/Gtk), we give more reasonable defaults (2/3 the size of a screen). |
| Rectangle primaryMonitorBounds = browser.getDisplay ().getPrimaryMonitor().getBounds(); |
| height = (int) (primaryMonitorBounds.height * 0.66); |
| width = (int) (primaryMonitorBounds.width * 0.66); |
| } |
| newEvent.size = new Point(width, height); |
| |
| Runnable fireVisibilityListeners = () -> { |
| if (browser.isDisposed()) return; |
| for (int i = 0; i < visibilityWindowListeners.length; i++) { |
| visibilityWindowListeners[i].show (newEvent); |
| } |
| }; |
| // Postpone execution of listener, to avoid deadlocks in case evaluate() is |
| // called in the listener while another signal is being handled. See bug 512001. |
| // evaluate() can safely be called in this listener with no adverse effects. |
| browser.getDisplay().asyncExec(fireVisibilityListeners); |
| return 0; |
| } |
| |
| /** |
| * @return An integer value for the property is returned. For boolean settings, 0 indicates false, |
| * 1 indicates true. -1= is error. |
| */ |
| private int webkit_settings_get(byte [] property) { |
| if (webView == 0) { // already disposed. |
| return -1; // error. |
| } |
| long settings = WebKitGTK.webkit_web_view_get_settings (webView); |
| return webkit_settings_get(settings, property); |
| } |
| |
| /** @return An integer value for the property is returned. For boolean settings, 0 indicates false, 1 indicates true */ |
| private int webkit_settings_get(long settings, byte[] property) { |
| int[] result = new int[1]; |
| OS.g_object_get (settings, property, result, 0); |
| return result[0]; |
| } |
| |
| private void webkit_settings_set(byte [] property, int value) { |
| if (webView == 0) { // already disposed. |
| return; |
| } |
| long settings = WebKitGTK.webkit_web_view_get_settings (webView); |
| OS.g_object_set(settings, property, value, 0); |
| } |
| |
| long convertToJS (long ctx, Object value) { |
| if (value == null) { |
| return WebKitGTK.JSValueMakeUndefined (ctx); |
| } |
| if (value instanceof String) { |
| byte[] bytes = ((String)value + '\0').getBytes (StandardCharsets.UTF_8); //$NON-NLS-1$ |
| long stringRef = WebKitGTK.JSStringCreateWithUTF8CString (bytes); |
| long result = WebKitGTK.JSValueMakeString (ctx, stringRef); |
| WebKitGTK.JSStringRelease (stringRef); |
| return result; |
| } |
| if (value instanceof Boolean) { |
| return WebKitGTK.JSValueMakeBoolean (ctx, ((Boolean)value).booleanValue () ? 1 : 0); |
| } |
| if (value instanceof Number) { |
| return WebKitGTK.JSValueMakeNumber (ctx, ((Number)value).doubleValue ()); |
| } |
| if (value instanceof Object[]) { |
| Object[] arrayValue = (Object[]) value; |
| int length = arrayValue.length; |
| long [] arguments = new long [length]; |
| for (int i = 0; i < length; i++) { |
| Object javaObject = arrayValue[i]; |
| long jsObject = convertToJS (ctx, javaObject); |
| arguments[i] = jsObject; |
| } |
| return WebKitGTK.JSObjectMakeArray (ctx, length, arguments, null); |
| } |
| SWT.error (SWT.ERROR_INVALID_RETURN_VALUE); |
| return 0; |
| } |
| |
| static Object convertToJava (long ctx, long value) { |
| int type = WebKitGTK.JSValueGetType (ctx, value); |
| switch (type) { |
| case WebKitGTK.kJSTypeBoolean: { |
| int result = (int)WebKitGTK.JSValueToNumber (ctx, value, null); |
| return result != 0; |
| } |
| case WebKitGTK.kJSTypeNumber: { |
| double result = WebKitGTK.JSValueToNumber (ctx, value, null); |
| return Double.valueOf(result); |
| } |
| case WebKitGTK.kJSTypeString: { |
| long string = WebKitGTK.JSValueToStringCopy (ctx, value, null); |
| if (string == 0) return ""; //$NON-NLS-1$ |
| long length = WebKitGTK.JSStringGetMaximumUTF8CStringSize (string); |
| byte[] bytes = new byte[(int)length]; |
| length = WebKitGTK.JSStringGetUTF8CString (string, bytes, length); |
| WebKitGTK.JSStringRelease (string); |
| /* length-1 is needed below to exclude the terminator character */ |
| return new String (bytes, 0, (int)length - 1, StandardCharsets.UTF_8); |
| } |
| case WebKitGTK.kJSTypeNull: |
| // FALL THROUGH |
| case WebKitGTK.kJSTypeUndefined: return null; |
| case WebKitGTK.kJSTypeObject: { |
| byte[] bytes = (PROPERTY_LENGTH + '\0').getBytes (StandardCharsets.UTF_8); //$NON-NLS-1$ |
| long propertyName = WebKitGTK.JSStringCreateWithUTF8CString (bytes); |
| long valuePtr = WebKitGTK.JSObjectGetProperty (ctx, value, propertyName, null); |
| WebKitGTK.JSStringRelease (propertyName); |
| type = WebKitGTK.JSValueGetType (ctx, valuePtr); |
| if (type == WebKitGTK.kJSTypeNumber) { |
| int length = (int)WebKitGTK.JSValueToNumber (ctx, valuePtr, null); |
| Object[] result = new Object[length]; |
| for (int i = 0; i < length; i++) { |
| long current = WebKitGTK.JSObjectGetPropertyAtIndex (ctx, value, i, null); |
| if (current != 0) { |
| result[i] = convertToJava (ctx, current); |
| } |
| } |
| return result; |
| } |
| } |
| } |
| SWT.error (SWT.ERROR_INVALID_ARGUMENT); |
| return null; |
| } |
| |
| } |