[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