diff --git a/README.md b/README.md index abf65ac..fb06359 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,19 @@ class V8Js public function setModuleLoader(callable $loader) {} + /** + * Provide a function or method to be used to normalise module paths. This can be any valid PHP callable. + * This can be used in combination with setModuleLoader to influence normalisation of the module path (which + * is normally done by V8Js itself but can be overriden this way). + * The normaliser function will receive the base path of the current module (if any; otherwise an empty string) + * and the literate string provided to the require method and should return an array of two strings (the new + * module base path as well as the normalised name). Both are joined by a '/' and then passed on to the + * module loader (unless the module was cached before). + * @param callable $normaliser + */ + public function setModuleNormaliser(callable $normaliser) + {} + /** * Compiles and executes script in object's context with optional identifier string. * A time limit (milliseconds) and/or memory limit (bytes) can be provided to restrict execution. These options will throw a V8JsTimeLimitException or V8JsMemoryLimitException. diff --git a/tests/commonjs_cust_normalise_001.phpt b/tests/commonjs_cust_normalise_001.phpt new file mode 100644 index 0000000..c9b9edf --- /dev/null +++ b/tests/commonjs_cust_normalise_001.phpt @@ -0,0 +1,31 @@ +--TEST-- +Test V8Js::setModuleNormaliser : Custom normalisation #001 +--SKIPIF-- + +--FILE-- +setModuleNormaliser(function($base, $module) { + var_dump($base, $module); + return [ "", "test" ]; +}); + +$v8->setModuleLoader(function($module) { + print("setModuleLoader called for ".$module."\n"); + return 'exports.bar = 23;'; +}); + +$v8->executeString($JS, 'module.js'); +?> +===EOF=== +--EXPECT-- +string(0) "" +string(6) "./test" +setModuleLoader called for test +===EOF=== diff --git a/tests/commonjs_cust_normalise_002.phpt b/tests/commonjs_cust_normalise_002.phpt new file mode 100644 index 0000000..792ebaa --- /dev/null +++ b/tests/commonjs_cust_normalise_002.phpt @@ -0,0 +1,33 @@ +--TEST-- +Test V8Js::setModuleNormaliser : Custom normalisation #002 +--SKIPIF-- + +--FILE-- +setModuleNormaliser(function($base, $module) { + var_dump($base, $module); + return [ "path/to", "test-foo" ]; +}); + +$v8->setModuleLoader(function($module) { + print("setModuleLoader called for ".$module."\n"); + return 'exports.bar = 23;'; +}); + +$v8->executeString($JS, 'module.js'); +?> +===EOF=== +--EXPECT-- +string(0) "" +string(6) "./test" +setModuleLoader called for path/to/test-foo +===EOF=== diff --git a/tests/commonjs_cust_normalise_003.phpt b/tests/commonjs_cust_normalise_003.phpt new file mode 100644 index 0000000..c417eb6 --- /dev/null +++ b/tests/commonjs_cust_normalise_003.phpt @@ -0,0 +1,41 @@ +--TEST-- +Test V8Js::setModuleNormaliser : Custom normalisation #003 +--SKIPIF-- + +--FILE-- +setModuleNormaliser(function($base, $module) { + var_dump($base, $module); + return [ "path/to", "test-foo" ]; +}); + +$v8->setModuleLoader(function($module) { + print("setModuleLoader called for ".$module."\n"); + if($module != "path/to/test-foo") { + throw new \Exception("module caching fails"); + } + return 'exports.bar = 23;'; +}); + +$v8->executeString($JS, 'module.js'); +?> +===EOF=== +--EXPECT-- +string(0) "" +string(6) "./test" +setModuleLoader called for path/to/test-foo +string(0) "" +string(4) "test" +===EOF=== diff --git a/tests/commonjs_cust_normalise_004.phpt b/tests/commonjs_cust_normalise_004.phpt new file mode 100644 index 0000000..3086396 --- /dev/null +++ b/tests/commonjs_cust_normalise_004.phpt @@ -0,0 +1,42 @@ +--TEST-- +Test V8Js::setModuleNormaliser : Custom normalisation #004 +--SKIPIF-- + +--FILE-- +setModuleNormaliser(function($base, $module) { + var_dump($base, $module); + return [ "path/to", $module ]; +}); + +$v8->setModuleLoader(function($module) { + print("setModuleLoader called for ".$module."\n"); + switch($module) { + case "path/to/foo": + return "require('bar');"; + + case "path/to/bar": + return 'exports.bar = 23;'; + } +}); + +$v8->executeString($JS, 'module.js'); +?> +===EOF=== +--EXPECT-- +string(0) "" +string(3) "foo" +setModuleLoader called for path/to/foo +string(7) "path/to" +string(3) "bar" +setModuleLoader called for path/to/bar +===EOF=== diff --git a/tests/commonjs_fatal_error.phpt b/tests/commonjs_fatal_error.phpt new file mode 100644 index 0000000..f246adf --- /dev/null +++ b/tests/commonjs_fatal_error.phpt @@ -0,0 +1,17 @@ +--TEST-- +Test V8Js::setModuleLoader : Handle fatal errors gracefully +--SKIPIF-- + +--FILE-- +setModuleLoader(function() { + trigger_error('some fatal error', E_USER_ERROR); +}); + +$v8->executeString(' require("foo"); '); +?> +===EOF=== +--EXPECTF-- +Fatal error: some fatal error in %s%ecommonjs_fatal_error.php on line 5 diff --git a/tests/commonjs_source_naming.phpt b/tests/commonjs_source_naming.phpt new file mode 100644 index 0000000..b57acfa --- /dev/null +++ b/tests/commonjs_source_naming.phpt @@ -0,0 +1,27 @@ +--TEST-- +Test V8Js::setModuleLoader : Module source naming +--SKIPIF-- + +--FILE-- +setModuleLoader(function($module) { + // return code with syntax errors to provoke script exception + return "foo(blar);"; +}); + +try { + $v8->executeString($JS, 'commonjs_source_naming.js'); +} catch (V8JsScriptException $e) { + var_dump($e->getJsFileName()); +} +?> +===EOF=== +--EXPECT-- +string(7) "foo/bar" +===EOF=== diff --git a/tests/issue_185_001.phpt b/tests/issue_185_001.phpt new file mode 100644 index 0000000..166e448 --- /dev/null +++ b/tests/issue_185_001.phpt @@ -0,0 +1,36 @@ +--TEST-- +Test V8::executeString() : Issue #185 this on direct invocation of method +--SKIPIF-- + +--FILE-- +executeString($JS); + +// now fetch `inst` from V8 and call method from PHP +$fn = $v8->executeString('(inst.tell)'); +$fn(); +?> +===EOF=== +--EXPECT-- +NULL +string(8) "function" +NULL +string(8) "function" +===EOF=== diff --git a/tests/issue_185_002.phpt b/tests/issue_185_002.phpt new file mode 100644 index 0000000..f2aa723 --- /dev/null +++ b/tests/issue_185_002.phpt @@ -0,0 +1,28 @@ +--TEST-- +Test V8::executeString() : Issue #185 this on function invocation +--SKIPIF-- + +--FILE-- +executeString($JS); + +// now fetch `inst` from V8 and call method from PHP +$fn = $v8->executeString('(fn)'); +$fn(); +?> +===EOF=== +--EXPECT-- +string(8) "function" +string(8) "function" +===EOF=== diff --git a/tests/issue_185_basic.phpt b/tests/issue_185_basic.phpt new file mode 100644 index 0000000..9bb562a --- /dev/null +++ b/tests/issue_185_basic.phpt @@ -0,0 +1,32 @@ +--TEST-- +Test V8::executeString() : Issue #185 Wrong this on V8Object method invocation +--SKIPIF-- + +--FILE-- +executeString($JS); + +// now fetch `inst` from V8 and call method from PHP +$inst = $v8->executeString('(inst)'); +$inst->tell(); +?> +===EOF=== +--EXPECT-- +int(23) +int(23) +===EOF=== diff --git a/v8js_class.cc b/v8js_class.cc index 29e2d8e..9e11677 100644 --- a/v8js_class.cc +++ b/v8js_class.cc @@ -85,7 +85,9 @@ static void v8js_free_storage(zend_object *object TSRMLS_DC) /* {{{ */ v8js_ctx *c = v8js_ctx_fetch_object(object); zend_object_std_dtor(&c->std TSRMLS_CC); + zval_dtor(&c->pending_exception); + zval_dtor(&c->module_normaliser); zval_dtor(&c->module_loader); /* Delete PHP global object from JavaScript */ @@ -358,6 +360,7 @@ static PHP_METHOD(V8Js, __construct) c->memory_limit = 0; c->memory_limit_hit = false; + ZVAL_NULL(&c->module_normaliser); ZVAL_NULL(&c->module_loader); /* Include extensions used by this context */ @@ -671,6 +674,22 @@ static PHP_METHOD(V8Js, clearPendingException) } /* }}} */ +/* {{{ proto void V8Js::setModuleNormaliser(string base, string module_id) + */ +static PHP_METHOD(V8Js, setModuleNormaliser) +{ + v8js_ctx *c; + zval *callable; + + if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z", &callable) == FAILURE) { + return; + } + + c = Z_V8JS_CTX_OBJ_P(getThis()); + ZVAL_COPY(&c->module_normaliser, callable); +} +/* }}} */ + /* {{{ proto void V8Js::setModuleLoader(string module) */ static PHP_METHOD(V8Js, setModuleLoader) @@ -964,6 +983,11 @@ ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_INFO(arginfo_v8js_clearpendingexception, 0) ZEND_END_ARG_INFO() +ZEND_BEGIN_ARG_INFO_EX(arginfo_v8js_setmodulenormaliser, 0, 0, 2) + ZEND_ARG_INFO(0, base) + ZEND_ARG_INFO(0, module_id) +ZEND_END_ARG_INFO() + ZEND_BEGIN_ARG_INFO_EX(arginfo_v8js_setmoduleloader, 0, 0, 1) ZEND_ARG_INFO(0, callable) ZEND_END_ARG_INFO() @@ -997,6 +1021,7 @@ static const zend_function_entry v8js_methods[] = { /* {{{ */ PHP_ME(V8Js, checkString, arginfo_v8js_checkstring, ZEND_ACC_PUBLIC|ZEND_ACC_DEPRECATED) PHP_ME(V8Js, getPendingException, arginfo_v8js_getpendingexception, ZEND_ACC_PUBLIC) PHP_ME(V8Js, clearPendingException, arginfo_v8js_clearpendingexception, ZEND_ACC_PUBLIC) + PHP_ME(V8Js, setModuleNormaliser, arginfo_v8js_setmodulenormaliser, ZEND_ACC_PUBLIC) PHP_ME(V8Js, setModuleLoader, arginfo_v8js_setmoduleloader, ZEND_ACC_PUBLIC) PHP_ME(V8Js, registerExtension, arginfo_v8js_registerextension, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC) PHP_ME(V8Js, getExtensions, arginfo_v8js_getextensions, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC) @@ -1039,7 +1064,7 @@ static void v8js_unset_property(zval *object, zval *member, void **cache_slot TS /* Global PHP JS object */ v8::Local object_name_js = v8::Local::New(isolate, c->object_name); v8::Local jsobj = V8JS_GLOBAL(isolate)->Get(object_name_js)->ToObject(); - + /* Delete value from PHP JS object */ jsobj->Delete(V8JS_SYML(Z_STRVAL_P(member), Z_STRLEN_P(member))); diff --git a/v8js_class.h b/v8js_class.h index 9fb8efd..f31a25f 100644 --- a/v8js_class.h +++ b/v8js_class.h @@ -49,7 +49,9 @@ struct v8js_ctx { v8js_tmpl_t global_template; v8js_tmpl_t array_tmpl; + zval module_normaliser; zval module_loader; + std::vector modules_stack; std::vector modules_base; std::map modules_loaded; diff --git a/v8js_methods.cc b/v8js_methods.cc index f96730c..753b452 100644 --- a/v8js_methods.cc +++ b/v8js_methods.cc @@ -8,6 +8,7 @@ +----------------------------------------------------------------------+ | Author: Jani Taskinen | | Author: Patrick Reilly | + | Author: Stefan Siegl | +----------------------------------------------------------------------+ */ @@ -91,10 +92,20 @@ static void v8js_dumper(v8::Isolate *isolate, v8::Local var, int leve } v8::TryCatch try_catch; /* object.toString() can throw an exception */ - v8::Local details = var->ToDetailString(); - if (try_catch.HasCaught()) { - details = V8JS_SYM(""); + v8::Local details; + + if(var->IsRegExp()) { + v8::RegExp *re = v8::RegExp::Cast(*var); + details = re->GetSource(); } + else { + details = var->ToDetailString(); + + if (try_catch.HasCaught()) { + details = V8JS_SYM(""); + } + } + v8::String::Utf8Value str(details); const char *valstr = ToCString(str); size_t valstr_len = details->ToString()->Utf8Length(); @@ -112,7 +123,7 @@ static void v8js_dumper(v8::Isolate *isolate, v8::Local var, int leve } else if (var->IsRegExp()) { - php_printf("regexp(%s)\n", valstr); + php_printf("regexp(/%s/)\n", valstr); } else if (var->IsArray()) { @@ -207,12 +218,100 @@ V8JS_METHOD(require) } v8::String::Utf8Value module_id_v8(info[0]); - const char *module_id = ToCString(module_id_v8); - char *normalised_path = (char *)emalloc(PATH_MAX); - char *module_name = (char *)emalloc(PATH_MAX); + char *normalised_path, *module_name; - v8js_commonjs_normalise_identifier(c->modules_base.back(), module_id, normalised_path, module_name); + if (Z_TYPE(c->module_normaliser) == IS_NULL) { + // No custom normalisation routine registered, use internal one + normalised_path = (char *)emalloc(PATH_MAX); + module_name = (char *)emalloc(PATH_MAX); + + v8js_commonjs_normalise_identifier(c->modules_base.back(), module_id, normalised_path, module_name); + } + else { + // Call custom normaliser + int call_result; + zval params[2]; + zval normaliser_result; + + zend_try { + { + isolate->Exit(); + v8::Unlocker unlocker(isolate); + + ZVAL_STRING(¶ms[0], c->modules_base.back()); + ZVAL_STRING(¶ms[1], module_id); + + call_result = call_user_function_ex(EG(function_table), NULL, &c->module_normaliser, + &normaliser_result, 2, params, 0, NULL TSRMLS_CC); + } + + isolate->Enter(); + + if (call_result == FAILURE) { + info.GetReturnValue().Set(isolate->ThrowException(V8JS_SYM("Module normaliser callback failed"))); + } + } + zend_catch { + v8js_terminate_execution(isolate); + V8JSG(fatal_error_abort) = 1; + call_result = FAILURE; + } + zend_end_try(); + + zval_dtor(¶ms[0]); + zval_dtor(¶ms[1]); + + if(call_result == FAILURE) { + return; + } + + // Check if an exception was thrown + if (EG(exception)) { + // Clear the PHP exception and throw it in V8 instead + zend_clear_exception(TSRMLS_C); + info.GetReturnValue().Set(isolate->ThrowException(V8JS_SYM("Module normaliser callback exception"))); + return; + } + + if (Z_TYPE(normaliser_result) != IS_ARRAY) { + zval_dtor(&normaliser_result); + info.GetReturnValue().Set(isolate->ThrowException(V8JS_SYM("Module normaliser didn't return an array"))); + return; + } + + HashTable *ht = HASH_OF(&normaliser_result); + int num_elements = zend_hash_num_elements(ht); + + if(num_elements != 2) { + zval_dtor(&normaliser_result); + info.GetReturnValue().Set(isolate->ThrowException(V8JS_SYM("Module normaliser expected to return array of 2 strings"))); + return; + } + + zval *data; + ulong index = 0; + HashPosition pos; + + ZEND_HASH_FOREACH_VAL(ht, data) { + if (Z_TYPE_P(data) != IS_STRING) { + convert_to_string(data); + } + + switch(index++) { + case 0: // normalised path + normalised_path = estrndup(Z_STRVAL_P(data), Z_STRLEN_P(data)); + break; + + case 1: // normalised module id + module_name = estrndup(Z_STRVAL_P(data), Z_STRLEN_P(data)); + break; + } + } + ZEND_HASH_FOREACH_END(); + + zval_dtor(&normaliser_result); + } char *normalised_module_id = (char *)emalloc(strlen(normalised_path)+1+strlen(module_name)+1); *normalised_module_id = 0; @@ -252,19 +351,38 @@ V8JS_METHOD(require) // Callback to PHP to load the module code zval module_code; - + int call_result; zval params[1]; - ZVAL_STRING(¶ms[0], normalised_module_id); - if (FAILURE == call_user_function_ex(EG(function_table), NULL, &c->module_loader, &module_code, 1, params, 0, NULL TSRMLS_CC)) { - zval_dtor(¶ms[0]); + zend_try { + { + isolate->Exit(); + v8::Unlocker unlocker(isolate); + + ZVAL_STRING(¶ms[0], normalised_module_id); + call_result = call_user_function_ex(EG(function_table), NULL, &c->module_loader, &module_code, 1, params, 0, NULL TSRMLS_CC); + } + + isolate->Enter(); + + if (call_result == FAILURE) { + info.GetReturnValue().Set(isolate->ThrowException(V8JS_SYM("Module loader callback failed"))); + } + } + zend_catch { + v8js_terminate_execution(isolate); + V8JSG(fatal_error_abort) = 1; + call_result = FAILURE; + } + zend_end_try(); + + zval_dtor(¶ms[0]); + + if (call_result == FAILURE) { efree(normalised_module_id); efree(normalised_path); - - info.GetReturnValue().Set(isolate->ThrowException(V8JS_SYM("Module loader callback failed"))); return; } - zval_dtor(¶ms[0]); // Check if an exception was thrown if (EG(exception)) { @@ -282,19 +400,10 @@ V8JS_METHOD(require) convert_to_string(&module_code); } - // Check that some code has been returned - if (Z_STRLEN(module_code) == 0) { - zval_dtor(&module_code); - efree(normalised_module_id); - efree(normalised_path); - - info.GetReturnValue().Set(isolate->ThrowException(V8JS_SYM("Module loader callback did not return code"))); - return; - } - // Create a template for the global object and set the built-in global functions v8::Handle global = v8::ObjectTemplate::New(); global->Set(V8JS_SYM("print"), v8::FunctionTemplate::New(isolate, V8JS_MN(print)), v8::ReadOnly); + global->Set(V8JS_SYM("var_dump"), v8::FunctionTemplate::New(isolate, V8JS_MN(var_dump)), v8::ReadOnly); global->Set(V8JS_SYM("sleep"), v8::FunctionTemplate::New(isolate, V8JS_MN(sleep)), v8::ReadOnly); global->Set(V8JS_SYM("require"), v8::FunctionTemplate::New(isolate, V8JS_MN(require), v8::External::New(isolate, c)), v8::ReadOnly); @@ -323,7 +432,7 @@ V8JS_METHOD(require) // Enter the module context v8::Context::Scope scope(context); // Set script identifier - v8::Local sname = V8JS_SYM("require"); + v8::Local sname = V8JS_STR(normalised_module_id); v8::Local source = V8JS_ZSTR(Z_STR(module_code)); zval_ptr_dtor(&module_code); diff --git a/v8js_v8object_class.cc b/v8js_v8object_class.cc index ed3bf03..a4652f8 100644 --- a/v8js_v8object_class.cc +++ b/v8js_v8object_class.cc @@ -286,6 +286,7 @@ static int v8js_v8object_call_method(zend_string *method, zend_object *object, I v8::Local method_name = V8JS_ZSYM(method); v8::Local v8obj = v8::Local::New(isolate, obj->v8obj)->ToObject(); + v8::Local thisObj; v8::Local cb; if (method_name->Equals(V8JS_SYM(V8JS_V8_INVOKE_FUNC_NAME))) { @@ -294,6 +295,15 @@ static int v8js_v8object_call_method(zend_string *method, zend_object *object, I cb = v8::Local::Cast(v8obj->Get(method_name)); } + // If a method is invoked on V8Object, then set the object itself as + // "this" on JS side. Otherwise fall back to global object. + if (obj->std.ce == php_ce_v8object) { + thisObj = v8obj; + } + else { + thisObj = V8JS_GLOBAL(isolate); + } + v8::Local *jsArgv = static_cast *>(alloca(sizeof(v8::Local) * argc)); v8::Local js_retval; @@ -302,7 +312,7 @@ static int v8js_v8object_call_method(zend_string *method, zend_object *object, I jsArgv[i] = v8::Local::New(isolate, zval_to_v8js(&argv[i], isolate TSRMLS_CC)); } - return cb->Call(V8JS_GLOBAL(isolate), argc, jsArgv); + return cb->Call(thisObj, argc, jsArgv); }; v8js_v8_call(obj->ctx, &return_value, obj->flags, obj->ctx->time_limit, obj->ctx->memory_limit, v8_call TSRMLS_CC);