[Pkg-javascript-commits] [node-webkitgtk] 01/02: Imported Upstream version 0.0.5
Jérémy Lal
kapouer at moszumanska.debian.org
Sat Sep 6 22:00:42 UTC 2014
This is an automated email from the git hooks/post-receive script.
kapouer pushed a commit to branch master
in repository node-webkitgtk.
commit 76db4f96f80b393f49d3e2857bbde1ab8d3434c4
Author: Jérémy Lal <kapouer at melix.org>
Date: Sat Sep 6 22:58:11 2014 +0200
Imported Upstream version 0.0.5
---
.gitignore | 5 +
.npmignore | 5 +
LICENSE | 22 ++
Makefile | 16 ++
README.md | 196 ++++++++++++++++
binding.gyp | 109 +++++++++
css/png.css | 17 ++
package.json | 32 +++
src/DEBUG | 13 ++
src/dbus.h | 8 +
src/runnable.cc | 15 ++
src/runnable.h | 24 ++
src/webextension.cc | 75 ++++++
src/webresponse.cc | 98 ++++++++
src/webresponse.h | 35 +++
src/webview.cc | 643 ++++++++++++++++++++++++++++++++++++++++++++++++++++
src/webview.h | 105 +++++++++
test/cookie.js | 24 ++
test/html.js | 14 ++
test/pdf.js | 26 +++
test/png.js | 28 +++
test/request.js | 21 ++
test/response.js | 20 ++
webkitgtk.js | 305 +++++++++++++++++++++++++
24 files changed, 1856 insertions(+)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..b590928
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+build
+node_modules
+lib/*
+dev/run
+test/shots
diff --git a/.npmignore b/.npmignore
new file mode 100644
index 0000000..f8c6b8b
--- /dev/null
+++ b/.npmignore
@@ -0,0 +1,5 @@
+lib/*
+build/*
+node_modules/*
+test/shots/*
+
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..40b3cea
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,22 @@
+Copyright 2014 Jérémy Lal <kapouer at melix.org>
+
+The MIT license
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..adb6543
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,16 @@
+lib: ./build
+ node-gyp build
+
+./build:
+ node-gyp configure
+
+clean:
+ rm -rf ./build
+ rm -rf ./lib
+ rm -f test.trace
+ rm -f test/shots/*
+
+check: lib
+ mocha
+
+.PHONY: lib
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..673af6b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,196 @@
+node-webkitgtk
+==============
+
+Pilot webkitgtk from Node.js with a simple API.
+
+*DEVELOPMENT VERSION*
+
+
+usage
+-----
+
+```
+var WebKit = require('webkitgtk');
+WebKit(uri, {
+ width: 1024,
+ height: 768,
+ stylesheet: "css/png.css"
+}, function(err, view) {
+ // optional callback
+}).on('load', function(view) {
+ view.png().save('test.png', function(err) {
+ // file saved
+ });
+});
+```
+
+See test/ for more examples.
+
+WebKit is a class whose instances are views.
+`var view = new Webkit()` is the same as `var view = Webkit()`.
+
+`Webkit(uri, [opts], [cb])` is a short-hand for `Webkit().load(uri, opts, cb)`.
+
+
+options
+-------
+
+- username
+- password
+ string, default none
+ HTTP Auth.
+
+- cookies
+ string | [string], default none
+ it preloads the document to be able to set document.cookie, all other
+ requests being disabled, then do the actual load with Cookie header.
+
+- width
+ number, 1024
+- height
+ number, 768
+ the viewport
+
+- allow
+ "all" or "same-origin" or "none" or a RegExp, default "all"
+ allow requests only matching option (except the document request),
+ bypassing 'request' event.
+
+- dialogs
+ boolean, default false
+ allow display of dialogs.
+
+- css
+ string, default none
+ a css string applied as user stylesheet.
+
+- stylesheet
+ string, default none
+ path to some user stylesheet, overrides css option if any.
+
+- display
+ number, default 0
+ the X display needed by gtk X11 backend
+
+- xfb
+ {width: 1024, height: 768, depth: 32}, default false
+ spawn a X backend with these options with number given by 'display',
+ or any higher number available.
+ Requires "headless" module.
+ It is safer to use a daemon monitoring tool for xvfb and just
+ set display option.
+
+
+events
+------
+
+All events are on the WebKit instance.
+
+* ready
+ same as document's DOMContentLoaded event
+ listener(view)
+
+* load
+ same as window's load event
+ listener(view)
+
+* steady
+ when there are no pending requests that have been initiated after
+ load event TODO
+
+* request
+ listener(request, view) where request.uri is read/write.
+ If request.uri is set to null or "", the request is cancelled.
+
+* response
+ listener(response, view)
+ response.uri, response.mime, response.status are read-only.
+ response.data(function(err, buf)) fetches the response data.
+
+
+methods
+-------
+
+* load(uri, [opts], [cb])
+
+* run(sync-script, cb)
+ any synchronous script text or global function
+
+* run(async-script, cb)
+ async-script must be a function that calls its first and only argument,
+ like `function(done) { done(err, str); }`
+
+* run(async-script, event) TODO
+ async-script must be a function that calls its first and only argument,
+ and each call emits the named event on current view object, which can
+ be listened using view.on(event, listener)
+
+* png()
+ takes a png snapshot immediately, returns a stream with an additional
+ .save(filename) short-hand for saving to a file
+
+* html(cb)
+ get documentElement.outerHTML when document is ready
+
+* pdf(filepath, [opts], [cb])
+ print page to file
+ orientation : "landscape" or "portrait", default "portrait"
+ fullpage : boolean, sets margins to 0, default false
+
+* unload
+ like `load('about:blank')`
+
+
+about plugins
+-------------
+
+In webkit2gtk ^2.4.4, if there are plugins in
+/usr/lib/mozilla/plugins
+they could be loaded (but not necessarily enabled on the WebView),
+and that could impact first page load time greatly - especially if
+there's a java plugin.
+Workaround: uninstall the plugin, on my dev machine it was
+/usr/lib/mozilla/plugins/libjavaplugin.so installed by icedtea.
+
+
+use cases
+---------
+
+This module is specifically designed to run 'headless'.
+Patches are welcome for UI uses, though.
+
+* snapshotting service (in combination with 'gm' module)
+
+* print to pdf service (in combination with 'gs' module)
+
+* static web page rendering
+
+* long-running web page as a service with websockets or webrtc
+ communications
+
+
+install
+-------
+
+Linux only.
+
+These libraries and their development files must be available in usual
+locations.
+
+webkit2gtk-3.0 (2.4.x)
+dbus-glib-1
+glib-2.0
+gtk+-3.0
+
+Also usual development tools are needed (pkg-config, gcc, and so on).
+
+On debian, these packages are needed :
+
+libwebkit2gtk-3.0-dev
+libdbus-glib-1-dev
+
+License
+-------
+
+MIT, see LICENSE file
+
diff --git a/binding.gyp b/binding.gyp
new file mode 100644
index 0000000..dffb6f2
--- /dev/null
+++ b/binding.gyp
@@ -0,0 +1,109 @@
+{
+ 'targets': [
+ {
+ 'target_name': 'webkitgtk',
+ 'variables': {
+ 'enable_web_extension%': 'true'
+ },
+ 'conditions': [
+ ['enable_web_extension=="true"', {
+ 'defines': [ 'ENABLE_WEB_EXTENSION' ]
+ }],
+ ['OS=="linux"', {
+ 'sources': [ 'src/webresponse.cc', 'src/runnable.cc', 'src/webview.cc' ],
+ "include_dirs": ["<!(node -e \"require('nan')\")"],
+ 'cflags_cc' : [
+ '<!@(pkg-config gtk+-3.0 --cflags)',
+ '<!@(pkg-config glib-2.0 --cflags)',
+ '<!@(pkg-config webkit2gtk-3.0 --cflags)',
+ '-I/usr/include/gtk-3.0/unix-print'
+ ],
+ 'libraries':[
+ '<!@(pkg-config gtk+-3.0 --libs)',
+ '<!@(pkg-config glib-2.0 --libs)',
+ '<!@(pkg-config webkit2gtk-3.0 --libs)'
+ ],
+ 'ldflags': ['-ldl']
+ }]
+ ]
+ },
+ {
+ 'target_name': 'webextension',
+ 'type': 'shared_library',
+ 'variables': {
+ 'enable_web_extension%': 'true'
+ },
+ 'conditions': [
+ ['enable_web_extension=="true"', {
+ 'defines': [ 'ENABLE_WEB_EXTENSION' ]
+ }],
+ ['OS=="linux"', {
+ 'product_extension': 'so',
+ 'sources': [ 'src/webextension.cc' ],
+ 'cflags': ['-fPIC'],
+ 'cflags_cc' : [
+ '<!@(pkg-config glib-2.0 --cflags)',
+ '<!@(pkg-config webkit2gtk-3.0 --cflags)',
+ '<!@(pkg-config dbus-glib-1 --cflags)'
+ ],
+ 'libraries':[
+ '<!@(pkg-config glib-2.0 --libs)',
+ '<!@(pkg-config webkit2gtk-3.0 --libs)',
+ '<!@(pkg-config dbus-glib-1 --libs)',
+ '-ldl'
+ ]
+ }]
+ ]
+ },
+ {
+ 'target_name': 'mkdirs',
+ 'type': 'none',
+ 'dependencies': [ ],
+ 'conditions': [
+ ['OS=="linux"', {
+ 'actions': [
+ {
+ 'action_name': 'make_dirs',
+ 'inputs': [],
+ 'outputs': [
+ 'lib/ext'
+ ],
+ 'action': ['mkdir', '-p', 'lib/ext']
+ }
+ ]
+ }]
+ ]
+ },
+ {
+ 'target_name': 'action_after_build',
+ 'type': 'none',
+ 'dependencies': [ 'mkdirs', 'webkitgtk', 'webextension' ],
+ 'conditions': [
+ ['OS=="linux"', {
+ 'actions': [
+ {
+ 'action_name': 'move_node',
+ 'inputs': [
+ '<@(PRODUCT_DIR)/webkitgtk.node'
+ ],
+ 'outputs': [
+ 'lib/webkitgtk'
+ ],
+ 'action': ['cp', '<@(PRODUCT_DIR)/webkitgtk.node', 'lib/webkitgtk.node']
+ },
+ {
+ 'action_name': 'move_ext',
+ 'inputs': [
+ '<@(PRODUCT_DIR)/lib.target/webextension.so'
+ ],
+ 'outputs': [
+ 'lib/webextension'
+ ],
+ 'action': ['cp', '<@(PRODUCT_DIR)/lib.target/webextension.so', 'lib/ext/']
+ }
+ ]
+ }]
+ ]
+ }
+ ]
+}
diff --git a/css/png.css b/css/png.css
new file mode 100644
index 0000000..1f241e7
--- /dev/null
+++ b/css/png.css
@@ -0,0 +1,17 @@
+/* hide all scrollbars */
+*::-webkit-scrollbar {
+ width:0px !important;
+ height:0px !important;
+}
+
+/* do not animate as far as possible */
+* {
+ -webkit-transition:none !important;
+ transition:none !important;
+ -webkit-transition-property: none !important;
+ transition-property: none !important;
+ -webkit-transform: none !important;
+ transform: none !important;
+ -webkit-animation: none !important;
+ animation: none !important;
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..27b65ef
--- /dev/null
+++ b/package.json
@@ -0,0 +1,32 @@
+{
+ "name": "webkitgtk",
+ "version": "0.0.5",
+ "description": "Drive webkitgtk from Node.js",
+ "keywords": [
+ "webkit",
+ "binding",
+ "gtk",
+ "phantom",
+ "snapshot",
+ "screenshot",
+ "pdf"
+ ],
+ "main": "webkitgtk.js",
+ "dependencies": {
+ "nan": "^1.3.0"
+ },
+ "optionalDependencies": {
+ "headless": "^0.2.1"
+ },
+ "devDependencies": {
+ "mocha": "^1.21.4",
+ "expect.js": "^0.3.1",
+ "http-digest-auth": "^0.1.3"
+ },
+ "scripts": {
+ "test": "mocha"
+ },
+ "repository": "git at github.com:kapouer/node-webkitgtk.git",
+ "author": "Jérémy Lal <kapouer at melix.org>",
+ "license": "MIT"
+}
diff --git a/src/DEBUG b/src/DEBUG
new file mode 100644
index 0000000..63136e3
--- /dev/null
+++ b/src/DEBUG
@@ -0,0 +1,13 @@
+Create a new view, let the node process open (long setTimeout),
+and then one can:
+
+dbus-send --session --print-reply --type=method_call --dest='org.nodejs.WebKitGtk' '/org/nodejs/WebKitGtk' org.nodejs.WebKitGtk.HandleRequest string:"http://www.test.com/test.js"
+
+to check if the dbus server is running well.
+
+
+Calls to g_print() won't be printed to the terminal, but they can be
+searched in strace output: `strace -f node test.js 2> test.trace`.
+
+To enable/disable compilation of the webextension
+node-gyp configure --enable_web_extension=true
diff --git a/src/dbus.h b/src/dbus.h
new file mode 100644
index 0000000..9bf9cbd
--- /dev/null
+++ b/src/dbus.h
@@ -0,0 +1,8 @@
+#ifndef WEBKITGTK_DBUS_H
+#define WEBKITGTK_DBUS_H
+
+
+#define DBUS_OBJECT_WKGTK "/org/nodejs/WebKitGtk/WebView"
+#define DBUS_INTERFACE_WKGTK "org.nodejs.WebKitGtk.WebView"
+
+#endif
diff --git a/src/runnable.cc b/src/runnable.cc
new file mode 100644
index 0000000..80b673b
--- /dev/null
+++ b/src/runnable.cc
@@ -0,0 +1,15 @@
+#include "runnable.h"
+
+Runnable::Runnable(void* view) {
+ state = 0;
+ sync = false;
+ this->view = view;
+}
+
+Runnable::~Runnable() {
+ delete ticket;
+ delete callback;
+ delete script;
+ delete finish;
+}
+
diff --git a/src/runnable.h b/src/runnable.h
new file mode 100644
index 0000000..64260ef
--- /dev/null
+++ b/src/runnable.h
@@ -0,0 +1,24 @@
+#ifndef WEBKITGTK_RUNNABLE_H
+#define WEBKITGTK_RUNNABLE_H
+
+#include <map>
+#include <nan.h>
+
+class Runnable {
+public:
+ static const int RAN_SCRIPT = 1;
+ static const int RAN_FINISH = 2;
+
+ char* ticket = NULL;
+ NanCallback* callback = NULL;
+ NanUtf8String* script = NULL;
+ NanUtf8String* finish = NULL;
+ bool sync;
+ int state;
+ void* view = NULL;
+
+ Runnable(void*);
+ ~Runnable();
+};
+
+#endif
diff --git a/src/webextension.cc b/src/webextension.cc
new file mode 100644
index 0000000..931464a
--- /dev/null
+++ b/src/webextension.cc
@@ -0,0 +1,75 @@
+#ifdef ENABLE_WEB_EXTENSION
+
+#include <dbus/dbus-glib.h>
+#include <webkit2/webkit-web-extension.h>
+#include <JavaScriptCore/JSContextRef.h>
+#include <JavaScriptCore/JSStringRef.h>
+#include "dbus.h"
+
+static GDBusConnection* connection;
+
+static gboolean web_page_send_request(WebKitWebPage* web_page, WebKitURIRequest* request, WebKitURIResponse* redirected_response, gpointer data) {
+ GError* error = NULL;
+ const char* uri = webkit_uri_request_get_uri(request);
+ GVariant* value = g_dbus_connection_call_sync(connection, NULL, DBUS_OBJECT_WKGTK, DBUS_INTERFACE_WKGTK,
+ "HandleRequest", g_variant_new("(s)", uri), G_VARIANT_TYPE("(s)"), G_DBUS_CALL_FLAGS_NONE, -1, NULL,
+ &error);
+ if (value == NULL) {
+ g_printerr("ERR %s\n", error->message);
+ g_error_free(error);
+ return FALSE;
+ }
+ const gchar* newuri;
+ g_variant_get(value, "(&s)", &newuri);
+ if (newuri != NULL && !g_strcmp0(newuri, "")) {
+ return TRUE;
+ } else if (g_strcmp0(uri, newuri)) {
+ webkit_uri_request_set_uri(request, newuri);
+ }
+ g_variant_unref(value);
+ return FALSE;
+}
+
+
+static void web_page_created_callback(WebKitWebExtension* extension, WebKitWebPage* web_page, gpointer data) {
+ g_signal_connect(web_page, "send-request", G_CALLBACK(web_page_send_request), data);
+}
+
+/*
+static void window_object_cleared_callback(WebKitScriptWorld* world, WebKitWebPage* web_page, WebKitFrame* frame, gpointer user_data) {
+ // JSGlobalContextRef jsContext;
+ // JSObjectRef globalObject;
+//
+ // jsContext = webkit_frame_get_javascript_context_for_script_world(frame, world);
+ // globalObject = JSContextGetGlobalObject(jsContext);
+ // JSEvaluateScript(jsContext, JSStringCreateWithUTF8CString("document.cookie='wont=work'"), NULL, NULL, 1, NULL);
+ // GError* error = NULL;
+ // WebKitDOMDocument* dom = webkit_web_page_get_dom_document(web_page);
+ // webkit_dom_document_set_cookie(dom, "mycookie=myval", &error);
+ // if (error != NULL) g_printerr("Error in dom %s", error->message);
+}
+*/
+
+
+extern "C" {
+ G_MODULE_EXPORT void webkit_web_extension_initialize_with_user_data(WebKitWebExtension* extension, const GVariant* constData) {
+ // constData will be the dbus object number
+ // GVariant* data = g_variant_new_string(g_variant_get_string((GVariant*)constData, NULL));
+ // note that page-created happens before window-object-cleared
+ // g_signal_connect(webkit_script_world_get_default(), "window-object-cleared", G_CALLBACK(window_object_cleared_callback), NULL);
+
+ gchar* address = NULL;
+ g_variant_get((GVariant*)constData, "s", &address);
+
+ g_signal_connect(extension, "page-created", G_CALLBACK(web_page_created_callback), NULL);
+
+ GError* error = NULL;
+ connection = g_dbus_connection_new_for_address_sync(address, G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT, NULL, NULL, &error);
+ if (connection == NULL) {
+ g_printerr("Failed to open connection to bus: %s\n", error->message);
+ g_error_free(error);
+ }
+ }
+}
+
+#endif
diff --git a/src/webresponse.cc b/src/webresponse.cc
new file mode 100644
index 0000000..9d90ec8
--- /dev/null
+++ b/src/webresponse.cc
@@ -0,0 +1,98 @@
+#include "webresponse.h"
+
+
+using namespace v8;
+
+Persistent<FunctionTemplate> WebResponse::constructor;
+
+WebResponse::WebResponse() {}
+
+WebResponse::~WebResponse() {
+ g_object_unref(response);
+ g_object_unref(resource);
+ delete dataCallback;
+}
+
+void WebResponse::Init(Handle<Object> target) {
+ NanScope();
+
+ Local<FunctionTemplate> tpl = NanNew<FunctionTemplate>(WebResponse::New);
+ tpl->InstanceTemplate()->SetInternalFieldCount(1);
+ tpl->SetClassName(NanNew("WebResponse"));
+
+ ATTR(tpl, "uri", get_prop, NULL);
+ ATTR(tpl, "status", get_prop, NULL);
+ ATTR(tpl, "mime", get_prop, NULL);
+
+ NODE_SET_PROTOTYPE_METHOD(tpl, "data", WebResponse::Data);
+
+ target->Set(NanNew("WebResponse"), tpl->GetFunction());
+ NanAssignPersistent(constructor, tpl);
+}
+
+NAN_METHOD(WebResponse::New) {
+ NanScope();
+ WebResponse* self = new WebResponse();
+ self->Wrap(args.This());
+ NanReturnValue(args.This());
+}
+
+NAN_METHOD(WebResponse::Data) {
+ NanScope();
+ WebResponse* self = ObjectWrap::Unwrap<WebResponse>(args.This());
+ if (self->resource == NULL) {
+ NanThrowError("cannot call data(cb) on a response decision");
+ NanReturnUndefined();
+ }
+ self->dataCallback = new NanCallback(args[0].As<Function>());
+
+ webkit_web_resource_get_data(self->resource, NULL, WebResponse::DataFinished, self);
+ NanReturnUndefined();
+}
+
+void WebResponse::DataFinished(GObject* object, GAsyncResult* result, gpointer data) {
+ WebResponse* self = (WebResponse*)data;
+ GError* error = NULL;
+ gsize length;
+ guchar* buf = webkit_web_resource_get_data_finish(self->resource, result, &length, &error);
+ if (buf == NULL) { // if NULL, error is defined
+ Handle<Value> argv[] = {
+ NanError(error->message)
+ };
+ g_error_free(error);
+ self->dataCallback->Call(1, argv);
+ delete self->dataCallback;
+ self->dataCallback = NULL;
+ return;
+ }
+ Handle<Value> argv[] = {
+ NanNull(),
+ NanNewBufferHandle(reinterpret_cast<const char*>(buf), length)
+ };
+ self->dataCallback->Call(2, argv);
+}
+
+NAN_GETTER(WebResponse::get_prop) {
+ NanScope();
+ WebResponse* self = node::ObjectWrap::Unwrap<WebResponse>(args.Holder());
+ std::string propstr = TOSTR(property);
+
+ if (propstr == "uri") {
+ NanReturnValue(NanNew<String>(webkit_uri_response_get_uri(self->response)));
+ } else if (propstr == "mime") {
+ NanReturnValue(NanNew<String>(webkit_uri_response_get_mime_type(self->response)));
+ } else if (propstr == "status") {
+ NanReturnValue(NanNew<Integer>(webkit_uri_response_get_status_code(self->response)));
+ }
+ NanReturnUndefined();
+}
+
+// NAN_SETTER(WebResponse::set_prop) {
+ // NanScope();
+ // WebResponse* self = node::ObjectWrap::Unwrap<WebResponse>(args.Holder());
+ // std::string propstr = TOSTR(property);
+ // if (propstr == "cancel") {
+ // self->cancel = value->BooleanValue();
+ // }
+ // NanThrowError("Cannot a property on response object");
+// }
\ No newline at end of file
diff --git a/src/webresponse.h b/src/webresponse.h
new file mode 100644
index 0000000..35f950a
--- /dev/null
+++ b/src/webresponse.h
@@ -0,0 +1,35 @@
+#ifndef WEBKITGTK_WEBRESPONSE_H
+#define WEBKITGTK_WEBRESPONSE_H
+
+#include <node.h>
+#include <webkit2/webkit2.h>
+#include <nan.h>
+
+#define ATTR(t, name, get, set) t->InstanceTemplate()->SetAccessor(NanNew(name), get, set);
+#define TOSTR(obj) (*String::Utf8Value((obj)->ToString()))
+
+using namespace v8;
+
+class WebResponse : public node::ObjectWrap {
+public:
+ static Persistent<FunctionTemplate> constructor;
+ static void Init(Handle<Object>);
+ static NAN_METHOD(New);
+ static void DataFinished(GObject*, GAsyncResult*, gpointer);
+
+ WebKitURIResponse* response = NULL;
+ WebKitWebResource* resource = NULL;
+
+ WebResponse();
+
+private:
+ ~WebResponse();
+ NanCallback* dataCallback = NULL;
+
+ static NAN_GETTER(get_prop);
+ // static NAN_SETTER(set_prop);
+
+ static NAN_METHOD(Data);
+};
+
+#endif
diff --git a/src/webview.cc b/src/webview.cc
new file mode 100644
index 0000000..43e42b4
--- /dev/null
+++ b/src/webview.cc
@@ -0,0 +1,643 @@
+#include <JavaScriptCore/JSValueRef.h>
+#include <JavaScriptCore/JSStringRef.h>
+#include "webview.h"
+#include "webresponse.h"
+#include "dbus.h"
+
+using namespace v8;
+
+Persistent<Function> WebView::constructor;
+
+static const GDBusInterfaceVTable interface_vtable = {
+ WebView::handle_method_call,
+ NULL,
+ NULL,
+ NULL
+};
+
+WebView::WebView(Handle<Object> opts) {
+ NanAdjustExternalMemory(1000000);
+ gtk_init(0, NULL);
+ state = 0;
+
+ guid = g_dbus_generate_guid();
+ instances.insert(ObjMapPair(guid, this));
+
+ GDBusServerFlags server_flags = G_DBUS_SERVER_FLAGS_NONE;
+ GError* error = NULL;
+ gchar* address = g_strconcat("unix:tmpdir=", g_get_tmp_dir(), "/node-webkitgtk", NULL);
+ this->server = g_dbus_server_new_sync(address, server_flags, guid, NULL, NULL, &error);
+ g_dbus_server_start(this->server);
+
+ if (server == NULL) {
+ g_printerr ("Error creating server at address %s: %s\n", address, error->message);
+ g_error_free(error);
+ NanThrowError("WebKitGtk could not create dbus server");
+ return;
+ }
+ g_signal_connect(this->server, "new-connection", G_CALLBACK(on_new_connection), this);
+
+ WebKitWebContext* context = webkit_web_context_get_default();
+ webkit_web_context_set_process_model(context, WEBKIT_PROCESS_MODEL_MULTIPLE_SECONDARY_PROCESSES);
+ webkit_web_context_set_cache_model(context, WEBKIT_CACHE_MODEL_WEB_BROWSER);
+#ifdef ENABLE_WEB_EXTENSION
+ if (opts->Has(H("webextension"))) {
+ NanUtf8String* wePath = new NanUtf8String(opts->Get(H("webextension")));
+ if (wePath->Size() > 1) {
+ webkit_web_context_set_web_extensions_directory(context, **wePath);
+ g_signal_connect(context, "initialize-web-extensions", G_CALLBACK(WebView::InitExtensions), this);
+ delete wePath;
+ }
+ }
+#endif
+ if (opts->Has(H("requestListener"))) {
+ this->requestCallback = new NanCallback(opts->Get(H("requestListener")).As<Function>());
+ }
+ if (opts->Has(H("responseListener"))) {
+ this->responseCallback = new NanCallback(opts->Get(H("responseListener")).As<Function>());
+ }
+
+ view = WEBKIT_WEB_VIEW(webkit_web_view_new());
+
+ window = gtk_offscreen_window_new();
+ gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
+ gtk_widget_show_all(window);
+
+ g_signal_connect(view, "authenticate", G_CALLBACK(WebView::Authenticate), this);
+ g_signal_connect(view, "load-failed", G_CALLBACK(WebView::Fail), this);
+ g_signal_connect(view, "load-changed", G_CALLBACK(WebView::Change), this);
+ g_signal_connect(view, "resource-load-started", G_CALLBACK(WebView::ResourceLoad), this);
+ g_signal_connect(view, "script-dialog", G_CALLBACK(WebView::ScriptDialog), this);
+ g_signal_connect(view, "notify::title", G_CALLBACK(WebView::TitleChange), this);
+}
+
+void WebView::close() {
+ delete[] cookie;
+ delete[] username;
+ delete[] password;
+ delete[] css;
+
+ if (window != NULL) gtk_widget_destroy(window);
+
+ runnables.clear();
+
+ delete pngCallback;
+ delete pngFilename;
+
+ delete printCallback;
+ delete printUri;
+
+ delete loadCallback;
+ delete requestCallback;
+ delete responseCallback;
+ g_dbus_server_stop(server);
+ g_object_unref(server);
+ instances.erase(guid);
+ g_free(guid);
+}
+
+WebView::~WebView() {
+ close();
+}
+
+void WebView::Init(Handle<Object> exports, Handle<Object> module) {
+ node::AtExit(Exit);
+ const gchar* introspection_xml =
+ "<node>"
+ " <interface name='org.nodejs.WebKitGtk.WebView'>"
+ " <method name='HandleRequest'>"
+ " <arg type='s' name='uri' direction='in'/>"
+ " <arg type='s' name='uri' direction='out'/>"
+ " </method>"
+ " </interface>"
+ "</node>";
+
+ introspection_data = g_dbus_node_info_new_for_xml(introspection_xml, NULL);
+ g_assert(introspection_data != NULL);
+
+ Local<FunctionTemplate> tpl = FunctionTemplate::New(WebView::New);
+ tpl->SetClassName(NanNew("WebView"));
+ tpl->InstanceTemplate()->SetInternalFieldCount(1);
+
+ NODE_SET_PROTOTYPE_METHOD(tpl, "load", WebView::Load);
+ NODE_SET_PROTOTYPE_METHOD(tpl, "loop", WebView::Loop);
+ NODE_SET_PROTOTYPE_METHOD(tpl, "run", WebView::Run);
+ NODE_SET_PROTOTYPE_METHOD(tpl, "png", WebView::Png);
+ NODE_SET_PROTOTYPE_METHOD(tpl, "pdf", WebView::Print);
+
+ ATTR(tpl, "uri", get_prop, NULL);
+
+ constructor = Persistent<Function>::New(tpl->GetFunction());
+ module->Set(NanNew("exports"), constructor);
+
+ WebResponse::Init(exports);
+}
+
+gboolean WebView::Authenticate(WebKitWebView* view, WebKitAuthenticationRequest* request, gpointer data) {
+ WebView* self = (WebView*)data;
+ if (!webkit_authentication_request_is_retry(request)) self->authRetryCount = 0;
+ else self->authRetryCount += 1;
+ if (self->username != NULL && self->password != NULL && self->authRetryCount <= 1) {
+ WebKitCredential* creds = webkit_credential_new(self->username, self->password, WEBKIT_CREDENTIAL_PERSISTENCE_FOR_SESSION);
+ webkit_authentication_request_authenticate(request, creds);
+ webkit_credential_free(creds);
+ } else {
+ webkit_authentication_request_cancel(request);
+ }
+ return TRUE;
+
+}
+
+#ifdef ENABLE_WEB_EXTENSION
+void WebView::InitExtensions(WebKitWebContext* context, gpointer data) {
+ WebView* self = (WebView*)data;
+ GVariant* userData = g_variant_new("s", g_dbus_server_get_client_address(self->server));
+ webkit_web_context_set_web_extensions_initialization_user_data(context, userData);
+}
+#endif
+
+void WebView::ResourceLoad(WebKitWebView* web_view, WebKitWebResource* resource, WebKitURIRequest* request, gpointer data) {
+ WebView* self = (WebView*)data;
+ // if (self->requestCallback != NULL) {
+ // const char* uri = webkit_uri_request_get_uri(request);
+ // Handle<Value> argv[] = {
+ // NanNew(uri)
+ // };
+ // self->requestCallback->Call(1, argv);
+ // }
+ if (self->responseCallback != NULL) {
+ g_signal_connect(resource, "notify::response", G_CALLBACK(WebView::ResourceResponse), self);
+ }
+}
+
+void WebView::ResourceResponse(WebKitWebResource* resource, GParamSpec*, gpointer data) {
+ WebView* self = (WebView*)data;
+ WebKitURIResponse* response = webkit_web_resource_get_response(resource);
+ Handle<Object> obj = WebResponse::constructor->GetFunction()->NewInstance();
+ WebResponse* selfResponse = node::ObjectWrap::Unwrap<WebResponse>(obj);
+ selfResponse->resource = resource;
+ g_object_ref(resource);
+ selfResponse->response = response;
+ g_object_ref(response);
+ int argc = 1;
+ Handle<Value> argv[] = { obj };
+ self->responseCallback->Call(argc, argv);
+}
+
+gboolean WebView::ScriptDialog(WebKitWebView* web_view, WebKitScriptDialog* dialog, gpointer data) {
+ WebView* self = (WebView*)data;
+ if (!self->allowDialogs) return TRUE;
+ else return FALSE;
+}
+
+void WebView::Change(WebKitWebView* web_view, WebKitLoadEvent load_event, gpointer data) {
+ WebView* self = (WebView*)data;
+ switch (load_event) {
+ case WEBKIT_LOAD_STARTED: // 0
+ /* New load, we have now a provisional URI */
+ // provisional_uri = webkit_web_view_get_uri (web_view);
+ /* Here we could start a spinner or update the
+ * location bar with the provisional URI */
+ self->uri = webkit_web_view_get_uri(web_view);
+ break;
+ case WEBKIT_LOAD_REDIRECTED: // 1
+ // redirected_uri = webkit_web_view_get_uri (web_view);
+ self->uri = webkit_web_view_get_uri(web_view);
+ break;
+ case WEBKIT_LOAD_COMMITTED: // 2
+ /* The load is being performed. Current URI is
+ * the final one and it won't change unless a new
+ * load is requested or a navigation within the
+ * same page is performed */
+ self->uri = webkit_web_view_get_uri(web_view);
+ if (self->state == DOCUMENT_LOADING) {
+ self->state = DOCUMENT_LOADED;
+ Handle<Value> argv[] = {};
+ self->loadCallback->Call(0, argv);
+ }
+ break;
+ case WEBKIT_LOAD_FINISHED: // 3
+ /* Load finished, we can now stop the spinner */
+
+ break;
+ }
+}
+
+gboolean WebView::Fail(WebKitWebView* web_view, WebKitLoadEvent load_event, gchar* failing_uri, GError* error, gpointer data) {
+ WebView* self = (WebView*)data;
+ if (load_event <= WEBKIT_LOAD_COMMITTED && self->state == DOCUMENT_LOADING && g_strcmp0(failing_uri, self->uri) == 0) {
+ self->state = DOCUMENT_ERROR;
+ Handle<Value> argv[] = {
+ NanError(error->message)
+ };
+ self->loadCallback->Call(1, argv);
+ return TRUE;
+ } else {
+ return FALSE;
+ }
+}
+
+NAN_METHOD(WebView::New) {
+ NanScope();
+ WebView* self = new WebView(args[0]->ToObject());
+ self->Wrap(args.This());
+ NanReturnValue(args.This());
+}
+
+NAN_METHOD(WebView::Load) {
+ NanScope();
+ WebView* self = ObjectWrap::Unwrap<WebView>(args.This());
+
+ if (!args[2]->IsFunction()) {
+ NanThrowError("load(uri, opts, cb) missing cb argument");
+ NanReturnUndefined();
+ }
+
+ if (self->state == DOCUMENT_LOADING) {
+ Handle<Value> argv[] = {
+ NanError("A document is being loaded")
+ };
+ (new NanCallback(args[2].As<Function>()))->Call(1, argv);
+ NanReturnUndefined();
+ }
+ self->state = DOCUMENT_LOADING;
+ if (args[0]->IsString()) self->uri = **(new NanUtf8String(args[0])); // leaking by design :(
+ if (self->uri == NULL || strlen(self->uri) == 0) {
+ Handle<Value> argv[] = {
+ NanError("Empty uri")
+ };
+ (new NanCallback(args[2].As<Function>()))->Call(1, argv);
+ NanReturnUndefined();
+ }
+
+ Local<Object> opts = args[1]->ToObject();
+
+ if (self->cookie != NULL) self->cookie = NULL;
+ if (opts->Has(H("cookie"))) self->cookie = **(new NanUtf8String(opts->Get(H("cookie"))));
+
+ if (self->username != NULL) delete self->username;
+ if (opts->Has(H("username"))) self->username = **(new NanUtf8String(opts->Get(H("username"))));
+
+ if (self->password != NULL) delete self->password;
+ if (opts->Has(H("password"))) self->password = **(new NanUtf8String(opts->Get(H("password"))));
+
+ WebKitWebViewGroup* group = webkit_web_view_get_group(self->view);
+ webkit_web_view_group_remove_all_user_style_sheets(group);
+ if (opts->Has(H("css"))) webkit_web_view_group_add_user_style_sheet(
+ group,
+ *NanUtf8String(opts->Get(H("css"))),
+ self->uri,
+ NULL, // whitelist
+ NULL, // blacklist
+ WEBKIT_INJECTED_CONTENT_FRAMES_TOP_ONLY
+ );
+
+ gtk_window_set_default_size(GTK_WINDOW(self->window),
+ NanUInt32OptionValue(opts, H("width"), 1024),
+ NanUInt32OptionValue(opts, H("height"), 768)
+ );
+ //gtk_window_resize(GTK_WINDOW(self->window), width, height); // useless
+
+ WebKitSettings* settings = webkit_web_view_get_settings(self->view);
+ g_object_set(settings,
+ "enable-plugins", FALSE,
+ "print-backgrounds", TRUE,
+ "enable-javascript", TRUE,
+ "enable-html5-database", FALSE,
+ "enable-html5-local-storage", FALSE,
+ "enable-java", FALSE,
+ "enable-page-cache", FALSE,
+ "enable-write-console-messages-to-stdout", FALSE,
+ "enable-offline-web-application-cache", FALSE,
+ "auto-load-images", NanBooleanOptionValue(opts, H("images"), true),
+ "zoom-text-only", FALSE,
+ "media-playback-requires-user-gesture", FALSE, // effectively disables media playback ?
+ "user-agent", "Mozilla/5.0", NULL
+ );
+
+ self->allowDialogs = NanBooleanOptionValue(opts, H("dialogs"), false);
+
+ if (self->loadCallback != NULL) delete self->loadCallback;
+ self->loadCallback = new NanCallback(args[2].As<Function>());
+
+ WebKitURIRequest* request = webkit_uri_request_new(self->uri);
+ webkit_web_view_load_request(self->view, request);
+ NanReturnUndefined();
+}
+
+void WebView::RunFinished(GObject* object, GAsyncResult* result, gpointer data) {
+ Runnable* run = (Runnable*)data;
+ JSValueRef value;
+ JSGlobalContextRef context;
+ GError* error = NULL;
+ Local<String> local_str;
+ WebKitJavascriptResult* js_result = webkit_web_view_run_javascript_finish(WEBKIT_WEB_VIEW(object), result, &error);
+ if (js_result == NULL) { // if NULL, error is defined
+ Handle<Value> argv[] = {
+ NanError(error->message)
+ };
+ g_error_free(error);
+ run->callback->Call(1, argv);
+ ((WebView*)run->view)->runnables.erase(run->ticket);
+ delete run;
+ return;
+ }
+ context = webkit_javascript_result_get_global_context(js_result);
+ value = webkit_javascript_result_get_value(js_result);
+ if (JSValueIsString(context, value)) {
+ JSStringRef js_str_value;
+ gchar* str_value;
+ gsize str_length;
+ js_str_value = JSValueToStringCopy(context, value, NULL);
+ str_length = JSStringGetMaximumUTF8CStringSize(js_str_value);
+ str_value = (gchar*)g_malloc(str_length);
+ JSStringGetUTF8CString(js_str_value, str_value, str_length);
+ JSStringRelease(js_str_value);
+ local_str = NanNew<String>(str_value);
+ g_free (str_value);
+ } else {
+ local_str = NanNew<String>("");
+ }
+ webkit_javascript_result_unref(js_result);
+ if (run->state < Runnable::RAN_SCRIPT) {
+ run->state = Runnable::RAN_SCRIPT;
+ if (run->sync == false) {
+ return;
+ }
+ } else {
+ run->state = Runnable::RAN_FINISH;
+ }
+ Handle<Value> argv[] = {
+ NanNull(),
+ local_str
+ };
+ run->callback->Call(2, argv);
+ ((WebView*)run->view)->runnables.erase(run->ticket);
+ delete run;
+}
+
+void WebView::TitleChange(WebKitWebView* web_view, GParamSpec*, gpointer data) {
+ WebView* self = (WebView*)data;
+ const gchar* title = webkit_web_view_get_title(web_view);
+ RunMap::iterator it = self->runnables.find((char*)title);
+ if (it != self->runnables.end()) {
+ Runnable* run = it->second;
+ if (run != NULL && run->sync == false && run->state < Runnable::RAN_FINISH) {
+ webkit_web_view_run_javascript(
+ self->view,
+ **run->finish,
+ NULL,
+ WebView::RunFinished,
+ run
+ );
+ }
+ }
+}
+
+NAN_METHOD(WebView::Run) {
+ // wrapped, retrieve, cb
+ // 1) tell TitleChange we expect something by setting self->scriptAsync
+ // 2) RunSync(wrapped), RunFinished see self->scriptAsync so doesn't call runCallback
+ // 3) TitleChange is notified and calls RunSync(self->scriptAsync), delete self->scriptAsync
+ // 4) RunFinished calls runCallback
+ NanScope();
+ WebView* self = ObjectWrap::Unwrap<WebView>(args.This());
+ Runnable* run = new Runnable(self);
+
+ if (!args[4]->IsFunction()) {
+ NanThrowError("run(ticket, sync, script, finish, cb) missing cb argument");
+ NanReturnUndefined();
+ }
+ if (!args[2]->IsString()) {
+ NanThrowError("run(ticket, sync, script, finish, cb) missing wrapped argument");
+ NanReturnUndefined();
+ }
+ if (!args[1]->IsBoolean()) {
+ NanThrowError("run(ticket, sync, script, finish, cb) missing sync argument");
+ NanReturnUndefined();
+ }
+ if (!args[0]->IsString()) {
+ NanThrowError("run(ticket, sync, script, finish, cb) missing ticket argument");
+ NanReturnUndefined();
+ }
+
+ run->sync = args[1]->BooleanValue();
+
+ if (run->sync == false && !args[3]->IsString()) {
+ NanThrowError("run(ticket, false, script, finish, cb) missing retrieve argument");
+ NanReturnUndefined();
+ }
+ run->ticket = **(new NanUtf8String(args[0]));
+ run->script = new NanUtf8String(args[2]);
+ if (run->sync == false) run->finish = new NanUtf8String(args[3]);
+ run->callback = new NanCallback(args[4].As<Function>());
+ self->runnables.insert(RunMapPair(run->ticket, run));
+
+ // self->scriptCancel = g_cancellable_new();
+ webkit_web_view_run_javascript(
+ self->view,
+ **run->script,
+ NULL /*self->scriptCancel*/,
+ WebView::RunFinished,
+ run
+ );
+
+ NanReturnUndefined();
+}
+
+cairo_status_t WebView::PngWrite(void* closure, const unsigned char* data, unsigned int length) {
+ WebView* self = (WebView*)closure;
+ Handle<Value> argv[] = {
+ NanNull(),
+ NanNewBufferHandle(reinterpret_cast<const char*>(data), length)
+ };
+ self->pngCallback->Call(2, argv);
+ return CAIRO_STATUS_SUCCESS;
+}
+
+void WebView::PngFinished(GObject* object, GAsyncResult* result, gpointer data) {
+ WebView* self = (WebView*)data;
+ GError* error = NULL;
+ cairo_surface_t* surface = webkit_web_view_get_snapshot_finish(self->view, result, &error);
+ cairo_status_t status = cairo_surface_write_to_png_stream(surface, WebView::PngWrite, self);
+ Handle<Value> argv[] = {};
+ if (status == CAIRO_STATUS_SUCCESS) argv[0] = NanNull();
+ else argv[0] = NanError(error->message);
+ self->pngCallback->Call(1, argv);
+ delete self->pngCallback;
+ self->pngCallback = NULL;
+}
+
+NAN_METHOD(WebView::Png) {
+ NanScope();
+ WebView* self = ObjectWrap::Unwrap<WebView>(args.This());
+
+ if (!args[0]->IsFunction()) {
+ NanThrowError("png(cb) missing cb argument");
+ NanReturnUndefined();
+ }
+ self->pngCallback = new NanCallback(args[0].As<Function>());
+ webkit_web_view_get_snapshot(
+ self->view,
+ WEBKIT_SNAPSHOT_REGION_VISIBLE,
+ WEBKIT_SNAPSHOT_OPTIONS_NONE,
+ NULL, // GCancellable
+ WebView::PngFinished,
+ self
+ );
+ NanReturnUndefined();
+}
+
+void WebView::PrintFinished(WebKitPrintOperation* op, gpointer data) {
+ WebView* self = (WebView*)data;
+ if (self->printUri == NULL) return;
+ Handle<Value> argv[] = {};
+ self->printCallback->Call(0, argv);
+ delete self->printCallback;
+ self->printCallback = NULL;
+ delete self->printUri;
+ self->printUri = NULL;
+}
+void WebView::PrintFailed(WebKitPrintOperation* op, gpointer error, gpointer data) {
+ WebView* self = (WebView*)data;
+ Handle<Value> argv[] = {
+ NanError(((GError*)error)->message)
+ };
+ self->printCallback->Call(1, argv);
+ delete self->printCallback;
+ self->printCallback = NULL;
+ delete self->printUri;
+ self->printUri = NULL;
+}
+
+static gboolean find_file_printer(GtkPrinter* printer, char** data) {
+ if (!g_strcmp0(G_OBJECT_TYPE_NAME(gtk_printer_get_backend(printer)), "GtkPrintBackendFile")) {
+ *data = strdup(gtk_printer_get_name(printer));
+ return TRUE;
+ }
+ return FALSE;
+}
+
+NAN_METHOD(WebView::Print) {
+ NanScope();
+ WebView* self = ObjectWrap::Unwrap<WebView>(args.This());
+
+ if (self->printUri != NULL) {
+ NanThrowError("print() can be executed only one at a time");
+ NanReturnUndefined();
+ }
+ if (!args[0]->IsString()) {
+ NanThrowError("print(filename, opts, cb) missing filename argument");
+ NanReturnUndefined();
+ }
+ self->printUri = new NanUtf8String(args[0]);
+ if (!args[2]->IsFunction()) {
+ NanThrowError("print(filename, opts, cb) missing cb argument");
+ NanReturnUndefined();
+ }
+ self->printCallback = new NanCallback(args[2].As<Function>());
+ Local<Object> opts = args[1]->ToObject();
+
+ WebKitPrintOperation* op = webkit_print_operation_new(self->view);
+ // settings
+ GtkPrintSettings* settings = gtk_print_settings_new();
+ GtkPaperSize* paper = gtk_paper_size_new(GTK_PAPER_NAME_A4);
+ GtkPageOrientation orientation = GTK_PAGE_ORIENTATION_PORTRAIT;
+
+ if (opts->Has(H("orientation")) && g_strcmp0(*String::Utf8Value(opts->Get(H("orientation"))->ToString()), "landscape")) {
+ orientation = GTK_PAGE_ORIENTATION_LANDSCAPE;
+ }
+ gtk_print_settings_set_orientation(settings, orientation);
+ gtk_print_settings_set_paper_size(settings, paper);
+ gtk_print_settings_set_quality(settings, GTK_PRINT_QUALITY_HIGH);
+
+ char* printer = NULL;
+ gtk_enumerate_printers((GtkPrinterFunc)find_file_printer, &printer, NULL, TRUE);
+ gtk_print_settings_set_printer(settings, printer);
+ delete printer;
+ gtk_print_settings_set(settings, GTK_PRINT_SETTINGS_OUTPUT_URI, **self->printUri);
+
+ webkit_print_operation_set_print_settings(op, settings);
+
+ // page setup
+ GtkPageSetup* setup = webkit_print_operation_get_page_setup(op);
+ if (NanBooleanOptionValue(opts, H("fullpage"), false)) {
+ gtk_page_setup_set_right_margin(setup, 0, GTK_UNIT_NONE);
+ gtk_page_setup_set_left_margin(setup, 0, GTK_UNIT_NONE);
+ gtk_page_setup_set_top_margin(setup, 0, GTK_UNIT_NONE);
+ gtk_page_setup_set_bottom_margin(setup, 0, GTK_UNIT_NONE);
+ }
+
+ // print
+ g_signal_connect(op, "failed", G_CALLBACK(WebView::PrintFailed), self);
+ g_signal_connect(op, "finished", G_CALLBACK(WebView::PrintFinished), self);
+ webkit_print_operation_print(op);
+ NanReturnUndefined();
+}
+
+NAN_GETTER(WebView::get_prop) {
+ NanScope();
+ WebView* self = ObjectWrap::Unwrap<WebView>(args.This());
+ std::string propstr = TOSTR(property);
+
+ if (propstr == "uri") {
+ NanReturnValue(NanNew<String>(self->uri));
+ } else {
+ NanReturnUndefined();
+ }
+}
+
+NAN_METHOD(WebView::Loop) {
+ NanScope();
+ bool block = FALSE;
+ if (args[0]->IsBoolean()) block = args[0]->BooleanValue();
+ if (block) while (gtk_events_pending()) gtk_main_iteration_do(TRUE);
+ else gtk_main_iteration_do(FALSE);
+ NanReturnUndefined();
+}
+
+gboolean WebView::on_new_connection(GDBusServer* server, GDBusConnection* connection, gpointer data) {
+ g_object_ref(connection);
+ GError* error = NULL;
+ guint registration_id = g_dbus_connection_register_object(connection, DBUS_OBJECT_WKGTK,
+ introspection_data->interfaces[0], &interface_vtable, data, NULL, &error);
+ g_assert(registration_id > 0);
+ return TRUE;
+}
+
+void WebView::handle_method_call(
+GDBusConnection* connection,
+const gchar* sender,
+const gchar* object_path,
+const gchar* interface_name,
+const gchar* method_name,
+GVariant* parameters,
+GDBusMethodInvocation* invocation,
+gpointer data) {
+ WebView* self = (WebView*)data;
+ if (g_strcmp0(method_name, "HandleRequest") == 0) {
+ const gchar* requestUri;
+ g_variant_get(parameters, "(&s)", &requestUri);
+ Handle<Value> argv[] = {
+ NanNew(requestUri)
+ };
+ GVariant* response;
+ Handle<Value> uriVal = self->requestCallback->Call(1, argv);
+ NanUtf8String* uriStr = new NanUtf8String(uriVal->ToString());
+ if (uriVal->IsString()) response = g_variant_new("(s)", **uriStr);
+ else response = g_variant_new("(s)", "");
+ g_dbus_method_invocation_return_value(invocation, response);
+ g_free(uriStr);
+ }
+}
+
+void WebView::Exit(void*) {
+ NanScope();
+ for (ObjMap::iterator it = instances.begin(); it != instances.end(); it++) {
+ it->second->close();
+ }
+ instances.clear();
+}
+
+
+NODE_MODULE(webkitgtk, WebView::Init)
diff --git a/src/webview.h b/src/webview.h
new file mode 100644
index 0000000..c3a852b
--- /dev/null
+++ b/src/webview.h
@@ -0,0 +1,105 @@
+#ifndef WEBKITGTK_WEBVIEW_H
+#define WEBKITGTK_WEBVIEW_H
+
+#include "runnable.h"
+#include <node.h>
+#include <webkit2/webkit2.h>
+#include <nan.h>
+#include <gtk/gtkunixprint.h>
+#include <map>
+
+using namespace v8;
+
+#define H(name) NanNew<String>(name)
+
+struct RunMapComparator {
+ bool operator()(char const *a, char const *b) {
+ return strcmp(a, b) < 0;
+ }
+};
+
+typedef std::map<char*, Runnable*, RunMapComparator> RunMap;
+typedef std::pair<char*, Runnable*> RunMapPair;
+
+static const GDBusNodeInfo* introspection_data;
+
+class WebView : public node::ObjectWrap {
+public:
+ static const int DOCUMENT_UNLOADED = 0;
+ static const int DOCUMENT_LOADING = 1;
+ static const int DOCUMENT_LOADED = 2;
+ static const int DOCUMENT_ERROR = -1;
+
+ static void Init(Handle<Object>, Handle<Object>);
+ static void Exit(void*);
+
+ static gboolean Authenticate(WebKitWebView*, WebKitAuthenticationRequest*, gpointer);
+#ifdef ENABLE_WEB_EXTENSION
+ static void InitExtensions(WebKitWebContext*, gpointer);
+#endif
+ static void ResourceLoad(WebKitWebView*, WebKitWebResource*, WebKitURIRequest*, gpointer);
+ static void ResourceResponse(WebKitWebResource*, GParamSpec*, gpointer);
+ static void Change(WebKitWebView*, WebKitLoadEvent, gpointer);
+ static gboolean Fail(WebKitWebView*, WebKitLoadEvent, gchar*, GError*, gpointer);
+ static void TitleChange(WebKitWebView*, GParamSpec*, gpointer);
+ static gboolean ScriptDialog(WebKitWebView*, WebKitScriptDialog*, gpointer);
+ static void PngFinished(GObject*, GAsyncResult*, gpointer);
+ static cairo_status_t PngWrite(void*, const unsigned char*, unsigned int);
+ static void RunFinished(GObject*, GAsyncResult*, gpointer);
+ static void PrintFinished(WebKitPrintOperation*, gpointer);
+ static void PrintFailed(WebKitPrintOperation*, gpointer, gpointer);
+
+
+ static void handle_method_call(GDBusConnection*, const gchar*, const gchar*,
+ const gchar*, const gchar*, GVariant*, GDBusMethodInvocation*, gpointer);
+ static gboolean on_new_connection(GDBusServer*, GDBusConnection*, gpointer);
+
+private:
+ static v8::Persistent<v8::Function> constructor;
+ WebView(Handle<Object>);
+ ~WebView();
+ void close();
+
+ RunMap runnables;
+
+ gchar* guid;
+ GDBusServer* server;
+
+ int state;
+ int authRetryCount;
+ bool allowDialogs;
+ char* cookie = NULL;
+ char* username = NULL;
+ char* password = NULL;
+ char* css = NULL;
+
+ WebKitWebView* view = NULL;
+ GtkWidget* window = NULL;
+
+ NanCallback* pngCallback = NULL;
+ NanUtf8String* pngFilename = NULL;
+
+ NanCallback* printCallback = NULL;
+ NanUtf8String* printUri = NULL;
+
+ NanCallback* loadCallback = NULL;
+ NanCallback* requestCallback = NULL;
+ NanCallback* responseCallback = NULL;
+
+ const char* uri = NULL;
+
+ static NAN_METHOD(New);
+ static NAN_METHOD(Load);
+ static NAN_METHOD(Loop);
+ static NAN_METHOD(Run);
+ static NAN_METHOD(Png);
+ static NAN_METHOD(Print);
+
+ static NAN_GETTER(get_prop);
+};
+
+typedef std::map<char*, WebView*> ObjMap;
+typedef std::pair<char*, WebView*> ObjMapPair;
+static ObjMap instances;
+
+#endif
diff --git a/test/cookie.js b/test/cookie.js
new file mode 100644
index 0000000..70e2618
--- /dev/null
+++ b/test/cookie.js
@@ -0,0 +1,24 @@
+var WebKit = require('../');
+var expect = require('expect.js');
+var fs = require('fs');
+
+
+describe("cookies option", function suite() {
+ it("should set Cookie HTTP header on second request", function(done) {
+ var count = 0;
+ var cookiestr = "mycookie=myvalue";
+ require('http').createServer(function(req, res) {
+ if (req.url == "/") count++;
+ if (count == 2) expect(req.headers.cookie).to.be(cookiestr);
+ res.write('<html><body><img src="myimg.png"/></body></html>');
+ res.end();
+ }).listen(8008);
+
+ WebKit("http://localhost:8008", {cookies:cookiestr + ";Path=/"}, function(err, view) {
+ expect(err).to.not.be.ok();
+ expect(count).to.be(2);
+ done();
+ });
+ });
+});
+
diff --git a/test/html.js b/test/html.js
new file mode 100644
index 0000000..00f1f4d
--- /dev/null
+++ b/test/html.js
@@ -0,0 +1,14 @@
+var WebKit = require('../');
+var expect = require('expect.js');
+var fs = require('fs');
+
+describe("html method", function suite() {
+ it("should return document html when dom is ready", function(done) {
+ this.timeout(10000);
+ WebKit("http://google.fr").html(function(err, html) {
+ expect(err).to.not.be.ok();
+ expect(html).to.be.ok();
+ done();
+ });
+ });
+});
diff --git a/test/pdf.js b/test/pdf.js
new file mode 100644
index 0000000..ee27abf
--- /dev/null
+++ b/test/pdf.js
@@ -0,0 +1,26 @@
+var WebKit = require('../');
+var expect = require('expect.js');
+var fs = require('fs');
+
+describe("pdf method", function suite() {
+ it("should save a printed pdf to disk", function(done) {
+ this.timeout(10000);
+ WebKit("http://www.neufdeuxtroisa.fr", {
+ width:400, height:1000,
+ stylesheet: __dirname + "/../css/png.css"
+ }, function(err) {
+ expect(err).to.not.be.ok();
+ }).on("load", function(view) {
+ var filepath = __dirname + '/shots/test.pdf';
+ view.pdf(filepath, function(err) {
+ expect(err).to.not.be.ok();
+ fs.stat(filepath, function(err, stat) {
+ expect(stat.size).to.be.above(100000);
+ done();
+ });
+ });
+ });
+ });
+});
+
+
diff --git a/test/png.js b/test/png.js
new file mode 100644
index 0000000..15ce7ec
--- /dev/null
+++ b/test/png.js
@@ -0,0 +1,28 @@
+var WebKit = require('../');
+var expect = require('expect.js');
+var fs = require('fs');
+
+describe("png method", function suite() {
+ it("should save a screenshot to disk", function(done) {
+ this.timeout(10000);
+ WebKit("http://www.neufdeuxtroisa.fr", {
+ display: 98,
+ xfb: true,
+ width:400, height:1000,
+ stylesheet: __dirname + "/../css/png.css"
+ }, function(err) {
+ expect(err).to.not.be.ok();
+ }).on("load", function(view) {
+ var filepath = __dirname + '/shots/test.png';
+ view.png().save(filepath, function(err) {
+ expect(err).to.not.be.ok();
+ fs.stat(filepath, function(err, stat) {
+ expect(stat.size).to.be.above(100000);
+ done();
+ });
+ });
+ });
+ });
+});
+
+
diff --git a/test/request.js b/test/request.js
new file mode 100644
index 0000000..56e0eda
--- /dev/null
+++ b/test/request.js
@@ -0,0 +1,21 @@
+var WebKit = require('../');
+var expect = require('expect.js');
+var fs = require('fs');
+
+describe("changing request uri in listener", function suite() {
+ it("should cancel the request when set to null or empty string", function(done) {
+ this.timeout(10000);
+ var cancelledRequests = 0;
+ WebKit("http://www.selmer.fr").on("request", function(request, view) {
+ if (/\.js$/.test(request.uri)) {
+ cancelledRequests++;
+ request.uri = null;
+ }
+ }).on("response", function(response) {
+ expect(/\.js$/.test(response.uri)).to.not.be(true);
+ }).on("load", function(view) {
+ expect(cancelledRequests).to.be.greaterThan(3);
+ done();
+ });
+ });
+});
diff --git a/test/response.js b/test/response.js
new file mode 100644
index 0000000..8e44987
--- /dev/null
+++ b/test/response.js
@@ -0,0 +1,20 @@
+var WebKit = require('../');
+var expect = require('expect.js');
+var fs = require('fs');
+
+describe("response handler data method", function suite() {
+ it("should get data from the response", function(done) {
+ this.timeout(5000);
+ WebKit("http://www.google.com").on("response", function(response, view) {
+ if (response.uri == "https://www.google.fr/images/nav_logo195.png") {
+ response.data(function(err, data) {
+ expect(data.slice(1, 4).toString()).to.be("PNG");
+ expect(data.length).to.be.greaterThan(15000);
+ done();
+ });
+ }
+ });
+ });
+});
+
+
diff --git a/webkitgtk.js b/webkitgtk.js
new file mode 100644
index 0000000..990a13c
--- /dev/null
+++ b/webkitgtk.js
@@ -0,0 +1,305 @@
+var util = require('util');
+var events = require('events');
+var stream = require('stream');
+var fs = require('fs');
+var path = require('path');
+var url = require('url');
+
+var RUN_SYNC = 0;
+var RUN_ASYNC = 1;
+var RUN_PATH = 2;
+
+function WebKit(uri, opts, cb) {
+ if (!(this instanceof WebKit)) return new WebKit(uri, opts, cb);
+ if (!cb && typeof opts == "function") {
+ cb = opts;
+ opts = {};
+ }
+ if (typeof uri != "string") {
+ opts = uri;
+ uri = null;
+ }
+ opts = opts || {};
+ this.looping = 0;
+ this.ticket = 0;
+ var self = this;
+ this.display(opts.display || 0, opts, function(err, display) {
+ if (err) return cb(err);
+ process.env.DISPLAY = ":" + display;
+ var Bindings = require(__dirname + '/lib/webkitgtk.node');
+ self.webview = new Bindings({
+ webextension: __dirname + '/lib/ext',
+ requestListener: requestDispatcher.bind(self),
+ responseListener: responseDispatcher.bind(self)
+ });
+ if (uri) self.load(uri, opts, cb);
+ });
+}
+util.inherits(WebKit, events.EventEmitter);
+
+WebKit.prototype.display = function(display, opts, cb) {
+ var self = this;
+ fs.exists('/tmp/.X' + display + '-lock', function(exists) {
+ if (exists) return cb(null, display);
+ if (display == 0) return cb("Error - do not spawn xvfb on DISPLAY 0");
+ if (opts.xfb) {
+ console.log("Unsafe xfb option is spawning xvfb...");
+ require('headless')({
+ display: {
+ width: opts.xfb.width || 1024,
+ height: opts.xfb.height || 768,
+ depth: opts.xfb.depth || 32
+ }
+ }, display, function(err, child, display) {
+ cb(err, display);
+ if (!err) process.on('exit', function() {
+ child.kill();
+ });
+ });
+ }
+ });
+};
+
+function Request(uri) {
+ this.uri = uri;
+}
+
+function requestDispatcher(uri) {
+ if (this.preloading && uri != this.webview.uri) {
+ return;
+ }
+ if (this.allow == "none") {
+ if (uri != this.webview.uri) return;
+ } else if (this.allow == "same-origin") {
+ if (url.parse(uri).host != url.parse(this.webview.uri).host) return;
+ } else if (this.allow instanceof RegExp) {
+ if (!this.allow.test(uri)) return;
+ }
+ var req = new Request(uri);
+ this.emit('request', req, this);
+ return req.uri;
+}
+
+function responseDispatcher(webResponse) {
+ if (this.preloading) return;
+ this.emit('response', webResponse, this);
+}
+
+function noop(err) {
+ if (err) console.error(err);
+}
+
+WebKit.prototype.load = function(uri, opts, cb) {
+ if (!cb && typeof opts == "function") {
+ cb = opts;
+ opts = {};
+ } else if (!opts) {
+ opts = {};
+ }
+ if (!cb) cb = noop;
+ this.allow = opts.allow || "all";
+ this.preload(uri, opts, cb);
+ var self = this;
+ (function(next) {
+ if (opts.stylesheet) {
+ fs.readFile(opts.stylesheet, function(err, css) {
+ if (err) console.error(err);
+ if (opts.css) console.error("stylesheet option overwrites css option");
+ if (css) opts.css = css;
+ next();
+ });
+ } else {
+ next();
+ }
+ })(function() {
+ self.loop(true);
+ self.webview.load(uri, opts, function(err) {
+ self.loop(false);
+ if (!self.preloading) cb(err, self);
+ self.run(function(done) {
+ // this function is executed in the window context of the current view - it cannot access local scopes
+ if (/interactive|complete/.test(document.readyState)) done(null, document.readyState);
+ else document.addEventListener('DOMContentLoaded', function() { done(null, "interactive"); }, false);
+ }, function(err, result) {
+ if (err) console.error(err);
+ self.readyState = result;
+ var prefix = self.preloading ? "pre" : "";
+ self.emit(prefix + 'ready', self);
+ if (result == "complete") {
+ self.emit(prefix + 'load', self);
+ } else {
+ self.run(function(done) {
+ if (document.readyState == "complete") done(null, document.readyState);
+ else window.addEventListener('load', function() { done(null, "complete"); }, false);
+ }, function(err, result) {
+ if (err) console.error(err);
+ self.readyState = result;
+ self.emit(prefix + 'load', self);
+ });
+ }
+ });
+ });
+ });
+ this.uri = uri;
+ return this;
+};
+
+WebKit.prototype.unload = function(cb) {
+ this.load('about:blank', cb);
+ delete this.uri;
+ delete this.readyState;
+};
+
+WebKit.prototype.loop = function(start, block) {
+ if (start) {
+ this.looping++;
+ } else if (start === false) {
+ this.looping--;
+ }
+ if (!this.looping) return;
+ var self = this;
+ this.webview.loop(block);
+ if (!self.timeoutId) self.timeoutId = setTimeout(function() {
+ self.timeoutId = null;
+ self.loop(null, block);
+ }, 20);
+};
+
+WebKit.prototype.run = function(script, cb) {
+ if (typeof script == "function") script = script.toString();
+ cb = cb || noop;
+
+ var mode = RUN_SYNC;
+ if (/^\s*function(\s+\w+)?\s*\(\s*\w+\s*\)/.test(script)) mode = RUN_ASYNC;
+ else if (/^(file|http|https):/.test(script)) mode = RUN_PATH;
+
+ var self = this;
+ setImmediate(function() {
+ if (mode == RUN_SYNC) {
+ if (/^\s*function(\s+\w+)?\s*\(\s*\)/.test(script)) script = '(' + script + ')()';
+ var ticket = "runticket" + self.ticket++;
+ self.loop(true);
+ self.webview.run(ticket, true, script, null, function(err, str) {
+ self.loop(false);
+ cb(err, str);
+ });
+ } else if (mode == RUN_ASYNC) {
+ var ticket = "runticket" + self.ticket++;
+ var fun = 'function(err, result) {\
+ var ticket = "' + ticket + '";\
+ window[ticket] = [err, result];\
+ setTimeout(function() {document.title = ticket;}, 0);\
+ return "nothing";\
+ }';
+ var wrapped = '(' + script + ')(' + fun + ')';
+ var retrieve = '(function(ticket) {\
+ var str = JSON.stringify(window[ticket] || null);\
+ window[ticket] = undefined;\
+ return str;\
+ })("' + ticket + '")';
+ self.loop(true);
+ self.webview.run(ticket, false, wrapped, retrieve, function(err, json) {
+ self.loop(false);
+ if (err) return cb(err);
+ var result = JSON.parse(json);
+ if (Array.isArray(result) && result.length == 2) {
+ cb.apply(null, result);
+ } else {
+ cb("bindings returned wrong data");
+ }
+ });
+ } else if (mode == RUN_PATH) {
+ console.log("TODO");
+ }
+ });
+ return this;
+};
+
+function save(rstream, filename, cb) {
+ cb = cb || noop;
+ var wstream = fs.createWriteStream(filename);
+ rstream.pipe(wstream).on('finish', cb).on('error', cb);
+ return this;
+}
+
+WebKit.prototype.png = function() {
+ var self = this;
+ function close(err) {
+ self.loop(false);
+ }
+ var passthrough = new stream.PassThrough();
+ passthrough.save = save.bind(this, passthrough);
+ if (!this.readyState || this.readyState == "loading") {
+ this.on('load', function() {
+ self.png().pipe(passthrough);
+ });
+ } else {
+ this.loop(true, true);
+ this.webview.png(function(err, buf) {
+ if (err) {
+ self.loop(false);
+ passthrough.emit('error', err);
+ }
+ else if (buf == null) {
+ self.loop(false);
+ passthrough.end();
+ } else {
+ passthrough.write(buf);
+ }
+ });
+ }
+ return passthrough;
+};
+
+WebKit.prototype.html = function(cb) {
+ if (!this.readyState || this.readyState == "loading") {
+ this.on('ready', function(view) {
+ view.html(cb);
+ });
+ } else {
+ this.run("document.documentElement.outerHTML;", cb);
+ }
+ return this;
+};
+
+WebKit.prototype.pdf = function(filepath, opts, cb) {
+ if (!cb && typeof opts == "function") {
+ cb = opts;
+ opts = {};
+ } else if (!opts) {
+ opts = {};
+ }
+ if (!cb) cb = noop;
+ var self = this;
+ if (!this.readyState || this.readyState == "loading") {
+ this.on('load', function() {
+ self.pdf(filepath, opts, cb);
+ });
+ } else {
+ this.loop(true, true);
+ this.webview.pdf("file://" + path.resolve(filepath), opts, function(err) {
+ self.loop(false);
+ cb(err);
+ });
+ }
+ return this;
+};
+
+WebKit.prototype.preload = function(uri, opts, cb) {
+ if (!opts.cookies || this.preloading !== undefined) return;
+ this.preloading = true;
+ this.once('preload', function(view) {
+ var cookies = opts.cookies;
+ if (!Array.isArray(cookies)) cookies = [cookies];
+ var script = cookies.map(function(cookie) {
+ return 'document.cookie = "' + cookie.replace(/"/g, '\\"') + '"';
+ }).join(';') + ';';
+ view.run(script, function(err) {
+ if (err) return cb(err);
+ view.preloading = false;
+ view.load(uri, opts, cb);
+ });
+ });
+};
+
+module.exports = WebKit;
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-javascript/node-webkitgtk.git
More information about the Pkg-javascript-commits
mailing list