Skip to content

Commit dd536bc

Browse files
authored
Merge pull request #487 from stesie/exception-proxy-class
introduce V8Js::setExceptionProxyFactory
2 parents 0754d73 + dcb5832 commit dd536bc

13 files changed

+364
-26
lines changed

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,15 @@ class V8Js
104104
public function setModuleNormaliser(callable $normaliser)
105105
{}
106106

107+
/**
108+
* Provate a function or method to be used to convert/proxy PHP exceptions to JS.
109+
* This can be any valid PHP callable.
110+
* The converter function will receive the PHP Exception instance that has not been caught and
111+
* is due to be forwarded to JS. Pass NULL as $filter to uninstall an existing filter.
112+
*/
113+
public function setExceptionFilter(callable $filter)
114+
{}
115+
107116
/**
108117
* Compiles and executes script in object's context with optional identifier string.
109118
* A time limit (milliseconds) and/or memory limit (bytes) can be provided to restrict execution. These options will throw a V8JsTimeLimitException or V8JsMemoryLimitException.
@@ -401,3 +410,10 @@ objects obeying the above rules and re-thrown in JavaScript context. If they
401410
are not caught by JavaScript code the execution stops and a
402411
`V8JsScriptException` is thrown, which has the original PHP exception accessible
403412
via `getPrevious` method.
413+
414+
Consider that the JS code has access to methods like `getTrace` on the exception
415+
object. This might be unwanted behaviour, if you execute untrusted code.
416+
Using `setExceptionFilter` method a callable can be provided, that may convert
417+
the PHP exception to some other value that is safe to expose. The filter may
418+
also decide not to propagate the exception to JS at all by either re-throwing
419+
the passed exception or throwing another exception.

tests/exception_filter_001.phpt

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
--TEST--
2+
Test V8::setExceptionFilter() : String conversion
3+
--SKIPIF--
4+
<?php require_once(dirname(__FILE__) . '/skipif.inc'); ?>
5+
--FILE--
6+
<?php
7+
class myv8 extends V8Js
8+
{
9+
public function throwException(string $message) {
10+
throw new Exception($message);
11+
}
12+
}
13+
14+
$v8 = new myv8();
15+
$v8->setExceptionFilter(function (Throwable $ex) {
16+
echo "exception filter called.\n";
17+
return $ex->getMessage();
18+
});
19+
20+
$v8->executeString('
21+
try {
22+
PHP.throwException("Oops");
23+
}
24+
catch (e) {
25+
var_dump(typeof e); // string
26+
var_dump(e);
27+
}
28+
', null, V8Js::FLAG_PROPAGATE_PHP_EXCEPTIONS);
29+
?>
30+
===EOF===
31+
--EXPECT--
32+
exception filter called.
33+
string(6) "string"
34+
string(4) "Oops"
35+
===EOF===

tests/exception_filter_002.phpt

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
--TEST--
2+
Test V8::setExceptionFilter() : Filter handling on exception in setModuleLoader
3+
--SKIPIF--
4+
<?php require_once(dirname(__FILE__) . '/skipif.inc'); ?>
5+
--FILE--
6+
<?php
7+
8+
$v8 = new V8Js();
9+
$v8->setModuleLoader(function ($path) {
10+
throw new Error('moep');
11+
});
12+
13+
$v8->setExceptionFilter(function (Throwable $ex) {
14+
echo "exception filter called.\n";
15+
return $ex->getMessage();
16+
});
17+
18+
$v8->executeString('
19+
try {
20+
require("file");
21+
} catch(e) {
22+
var_dump(e);
23+
}
24+
', null, V8Js::FLAG_PROPAGATE_PHP_EXCEPTIONS);
25+
26+
?>
27+
===EOF===
28+
--EXPECT--
29+
exception filter called.
30+
string(4) "moep"
31+
===EOF===
32+

tests/exception_filter_003.phpt

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
--TEST--
2+
Test V8::setExceptionFilter() : Filter handling on exception in setModuleNormaliser
3+
--SKIPIF--
4+
<?php require_once(dirname(__FILE__) . '/skipif.inc'); ?>
5+
--FILE--
6+
<?php
7+
8+
$v8 = new V8Js();
9+
$v8->setModuleNormaliser(function ($path) {
10+
throw new Error('blarg');
11+
});
12+
$v8->setModuleLoader(function ($path) {
13+
throw new Error('moep');
14+
});
15+
16+
$v8->setExceptionFilter(function (Throwable $ex) {
17+
echo "exception filter called.\n";
18+
return $ex->getMessage();
19+
});
20+
21+
$v8->executeString('
22+
try {
23+
require("file");
24+
} catch(e) {
25+
var_dump(e);
26+
}
27+
', null, V8Js::FLAG_PROPAGATE_PHP_EXCEPTIONS);
28+
29+
?>
30+
===EOF===
31+
--EXPECT--
32+
exception filter called.
33+
string(5) "blarg"
34+
===EOF===

tests/exception_filter_004.phpt

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
--TEST--
2+
Test V8::setExceptionFilter() : Filter handling on exception in converter
3+
--SKIPIF--
4+
<?php require_once(dirname(__FILE__) . '/skipif.inc'); ?>
5+
--FILE--
6+
<?php
7+
class myv8 extends V8Js
8+
{
9+
public function throwException(string $message) {
10+
throw new Exception($message);
11+
}
12+
}
13+
14+
$v8 = new myv8();
15+
$v8->setExceptionFilter(function (Throwable $ex) {
16+
throw new Exception('moep');
17+
});
18+
19+
try {
20+
$v8->executeString('
21+
try {
22+
PHP.throwException("Oops");
23+
print("done\\n");
24+
}
25+
catch (e) {
26+
print("caught\\n");
27+
var_dump(e);
28+
}
29+
', null, V8Js::FLAG_PROPAGATE_PHP_EXCEPTIONS);
30+
} catch (Exception $ex) {
31+
echo "caught in php: " . $ex->getMessage() . PHP_EOL;
32+
}
33+
?>
34+
===EOF===
35+
--EXPECT--
36+
caught in php: moep
37+
===EOF===

tests/exception_filter_005.phpt

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
--TEST--
2+
Test V8::setExceptionFilter() : Uninstall filter on NULL
3+
--SKIPIF--
4+
<?php require_once(dirname(__FILE__) . '/skipif.inc'); ?>
5+
--FILE--
6+
<?php
7+
class myv8 extends V8Js
8+
{
9+
public function throwException(string $message) {
10+
throw new Exception($message);
11+
}
12+
}
13+
14+
$v8 = new myv8();
15+
$v8->setExceptionFilter(function (Throwable $ex) {
16+
echo "exception filter called.\n";
17+
return "moep";
18+
});
19+
20+
$v8->executeString('
21+
try {
22+
PHP.throwException("Oops");
23+
}
24+
catch (e) {
25+
var_dump(e);
26+
}
27+
', null, V8Js::FLAG_PROPAGATE_PHP_EXCEPTIONS);
28+
29+
$v8->setExceptionFilter(null);
30+
31+
try {
32+
$v8->executeString('
33+
try {
34+
PHP.throwException("Oops");
35+
print("done\\n");
36+
}
37+
catch (e) {
38+
print("caught\\n");
39+
var_dump(e.getMessage());
40+
}
41+
', null, V8Js::FLAG_PROPAGATE_PHP_EXCEPTIONS);
42+
} catch (Exception $ex) {
43+
echo "caught in php: " . $ex->getMessage() . PHP_EOL;
44+
}
45+
46+
?>
47+
===EOF===
48+
--EXPECT--
49+
exception filter called.
50+
string(4) "moep"
51+
caught
52+
string(4) "Oops"
53+
===EOF===

tests/exception_filter_006.phpt

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
--TEST--
2+
Test V8::setExceptionFilter() : re-throw exception in exception filter
3+
--SKIPIF--
4+
<?php require_once(dirname(__FILE__) . '/skipif.inc'); ?>
5+
--FILE--
6+
<?php
7+
class myv8 extends V8Js
8+
{
9+
public function throwException(string $message) {
10+
throw new Exception($message);
11+
}
12+
}
13+
14+
$v8 = new myv8();
15+
$v8->setExceptionFilter(function (Throwable $ex) {
16+
// re-throw exception so it is not forwarded
17+
throw $ex;
18+
});
19+
20+
try {
21+
$v8->executeString('
22+
try {
23+
PHP.throwException("Oops");
24+
print("done\\n");
25+
}
26+
catch (e) {
27+
print("caught\\n");
28+
var_dump(e);
29+
}
30+
', null, V8Js::FLAG_PROPAGATE_PHP_EXCEPTIONS);
31+
} catch (Exception $ex) {
32+
echo "caught in php: " . $ex->getMessage() . PHP_EOL;
33+
}
34+
?>
35+
===EOF===
36+
--EXPECT--
37+
caught in php: Oops
38+
===EOF===

tests/exception_filter_basic.phpt

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
--TEST--
2+
Test V8::setExceptionFilter() : Simple test
3+
--SKIPIF--
4+
<?php require_once(dirname(__FILE__) . '/skipif.inc'); ?>
5+
--FILE--
6+
<?php
7+
class myv8 extends V8Js
8+
{
9+
public function throwException(string $message) {
10+
throw new Exception($message);
11+
}
12+
}
13+
14+
class ExceptionFilter {
15+
private $ex;
16+
17+
public function __construct(Throwable $ex) {
18+
echo "ExceptionFilter::__construct called!\n";
19+
var_dump($ex->getMessage());
20+
21+
$this->ex = $ex;
22+
}
23+
24+
public function getMessage() {
25+
echo "getMessage called\n";
26+
return $this->ex->getMessage();
27+
}
28+
}
29+
30+
$v8 = new myv8();
31+
$v8->setExceptionFilter(function (Throwable $ex) {
32+
echo "exception filter called.\n";
33+
return new ExceptionFilter($ex);
34+
});
35+
36+
$v8->executeString('
37+
try {
38+
PHP.throwException("Oops");
39+
}
40+
catch (e) {
41+
var_dump(e.getMessage()); // calls ExceptionFilter::getMessage
42+
var_dump(typeof e.getTrace);
43+
}
44+
', null, V8Js::FLAG_PROPAGATE_PHP_EXCEPTIONS);
45+
?>
46+
===EOF===
47+
--EXPECT--
48+
exception filter called.
49+
ExceptionFilter::__construct called!
50+
string(4) "Oops"
51+
getMessage called
52+
string(4) "Oops"
53+
string(9) "undefined"
54+
===EOF===
55+

v8js_class.cc

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ static void v8js_free_storage(zend_object *object) /* {{{ */
9292
zval_ptr_dtor(&c->pending_exception);
9393
zval_ptr_dtor(&c->module_normaliser);
9494
zval_ptr_dtor(&c->module_loader);
95+
zval_ptr_dtor(&c->exception_filter);
9596

9697
/* Delete PHP global object from JavaScript */
9798
if (!c->context.IsEmpty()) {
@@ -400,6 +401,7 @@ static PHP_METHOD(V8Js, __construct)
400401

401402
ZVAL_NULL(&c->module_normaliser);
402403
ZVAL_NULL(&c->module_loader);
404+
ZVAL_NULL(&c->exception_filter);
403405

404406
/* Include extensions used by this context */
405407
/* Note: Extensions registered with auto_enable do not need to be added separately like this. */
@@ -880,6 +882,21 @@ static PHP_METHOD(V8Js, setModuleLoader)
880882
}
881883
/* }}} */
882884

885+
/* {{{ proto void V8Js::setExceptionFilter(callable factory)
886+
*/
887+
static PHP_METHOD(V8Js, setExceptionFilter)
888+
{
889+
zval *callable;
890+
891+
if (zend_parse_parameters(ZEND_NUM_ARGS(), "z", &callable) == FAILURE) {
892+
return;
893+
}
894+
895+
v8js_ctx *c = Z_V8JS_CTX_OBJ_P(getThis());
896+
ZVAL_COPY(&c->exception_filter, callable);
897+
}
898+
/* }}} */
899+
883900
/* {{{ proto void V8Js::setTimeLimit(int time_limit)
884901
*/
885902
static PHP_METHOD(V8Js, setTimeLimit)
@@ -1254,6 +1271,10 @@ ZEND_BEGIN_ARG_INFO_EX(arginfo_v8js_setmoduleloader, 0, 0, 1)
12541271
ZEND_ARG_INFO(0, callable)
12551272
ZEND_END_ARG_INFO()
12561273

1274+
ZEND_BEGIN_ARG_INFO_EX(arginfo_v8js_setexceptionfilter, 0, 0, 1)
1275+
ZEND_ARG_INFO(0, callable)
1276+
ZEND_END_ARG_INFO()
1277+
12571278
ZEND_BEGIN_ARG_INFO_EX(arginfo_v8js_setaverageobjectsize, 0, 0, 1)
12581279
ZEND_ARG_INFO(0, average_object_size)
12591280
ZEND_END_ARG_INFO()
@@ -1293,6 +1314,7 @@ const zend_function_entry v8js_methods[] = { /* {{{ */
12931314
PHP_ME(V8Js, clearPendingException, arginfo_v8js_clearpendingexception, ZEND_ACC_PUBLIC|ZEND_ACC_DEPRECATED)
12941315
PHP_ME(V8Js, setModuleNormaliser, arginfo_v8js_setmodulenormaliser, ZEND_ACC_PUBLIC)
12951316
PHP_ME(V8Js, setModuleLoader, arginfo_v8js_setmoduleloader, ZEND_ACC_PUBLIC)
1317+
PHP_ME(V8Js, setExceptionFilter, arginfo_v8js_setexceptionfilter, ZEND_ACC_PUBLIC)
12961318
PHP_ME(V8Js, setTimeLimit, arginfo_v8js_settimelimit, ZEND_ACC_PUBLIC)
12971319
PHP_ME(V8Js, setMemoryLimit, arginfo_v8js_setmemorylimit, ZEND_ACC_PUBLIC)
12981320
PHP_ME(V8Js, setAverageObjectSize, arginfo_v8js_setaverageobjectsize, ZEND_ACC_PUBLIC)

v8js_class.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ struct v8js_ctx {
5555

5656
zval module_normaliser;
5757
zval module_loader;
58+
zval exception_filter;
5859

5960
std::vector<char *> modules_stack;
6061
std::map<char *, v8js_persistent_value_t, cmp_str> modules_loaded;

0 commit comments

Comments
 (0)