V8Js est une extension PHP pour le moteur Javascript V8 de Google.
L'extension vous permet d'exécuter du code Javascript dans un bac à sable sécurisé à partir de PHP. Le code exécuté peut être restreint à l'aide d'une limite de temps et/ou d'une limite de mémoire. Cela offre la possibilité d’exécuter du code non fiable en toute confiance.
Maître de la bibliothèque du moteur Javascript V8 (libv8) https://github.com/v8/v8-git-mirror (trunk)
V8 est le moteur Javascript open source de Google. V8 est écrit en C++ et est utilisé dans Google Chrome, le navigateur open source de Google. La V8 implémente ECMAScript comme spécifié dans ECMA-262, 5e édition.
Cette extension nécessite V8 9.0 ou supérieur.
Les versions V8 sont publiées assez rapidement et l'équipe V8 fournit généralement un support de sécurité pour la ligne de versions livrée avec le navigateur Chrome (canal stable) et les versions plus récentes (uniquement). Pour un aperçu des versions, voir https://chromiumdash.appspot.com/branches.
PHP8.0.0+
Cette implémentation intégrée du moteur V8 utilise le verrouillage de thread afin de fonctionner avec ZTS activé.
Windows n'est actuellement pas officiellement pris en charge. Surtout parce que je n'ai pas le temps de maintenir le support moi-même et que je n'ai pas vraiment de boîtes Windows pour essayer des choses. Ce serait formidable si quelqu'un pouvait intervenir et réparer les problèmes sous Windows, fournir des binaires V8 pré-construits, etc.
Il existe une branche nommée php7
qui cible PHP 7.0.0+
Pour les toutes premières étapes, au lieu de compiler manuellement, vous souhaiterez peut-être essayer l'image Docker V8Js. Il a v8, v8js et php-cli préinstallés afin que vous puissiez l'essayer avec PHP en "mode interactif". Cependant, aucun Apache, etc., n'est en cours d'exécution.
Construire sur Microsoft Windows est un peu plus complexe, voir le fichier README.Win32.md pour un aperçu rapide. Construire sur GNU/Linux et MacOS X est simple, voir les fichiers README.Linux.md et README.MacOS.md pour une présentation détaillée des notes spécifiques à la plate-forme.
<?php
class V8Js
{
/* Constants */
const V8_VERSION = '' ;
const FLAG_NONE = 1 ;
const FLAG_FORCE_ARRAY = 2 ;
const FLAG_PROPAGATE_PHP_EXCEPTIONS = 4 ;
/* Methods */
/**
* Initializes and starts V8 engine and returns new V8Js object with it's own V8 context.
* @param string $object_name
* @param array $variables
* @param string $snapshot_blob
*/
public function __construct ( $ object_name = " PHP " , array $ variables = [], $ snapshot_blob = NULL )
{}
/**
* Provide a function or method to be used to load required modules. This can be any valid PHP callable.
* The loader function will receive the normalised module path and should return Javascript code to be executed.
* @param callable $loader
*/
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 )
{}
/**
* Provate a function or method to be used to convert/proxy PHP exceptions to JS.
* This can be any valid PHP callable.
* The converter function will receive the PHP Exception instance that has not been caught and
* is due to be forwarded to JS. Pass NULL as $filter to uninstall an existing filter.
*/
public function setExceptionFilter ( callable $ filter )
{}
/**
* 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.
* @param string $script
* @param string $identifier
* @param int $flags
* @param int $time_limit in milliseconds
* @param int $memory_limit in bytes
* @return mixed
*/
public function executeString ( $ script , $ identifier = '' , $ flags = V8Js:: FLAG_NONE , $ time_limit = 0 , $ memory_limit = 0 )
{}
/**
* Compiles a script in object's context with optional identifier string.
* @param $script
* @param string $identifier
* @return resource
*/
public function compileString ( $ script , $ identifier = '' )
{}
/**
* Executes a precompiled script in object's context.
* A time limit (milliseconds) and/or memory limit (bytes) can be provided to restrict execution. These options will throw a V8JsTimeLimitException or V8JsMemoryLimitException.
* @param resource $script
* @param int $flags
* @param int $time_limit
* @param int $memory_limit
*/
public function executeScript ( $ script , $ flags = V8Js:: FLAG_NONE , $ time_limit = 0 , $ memory_limit = 0 )
{}
/**
* Set the time limit (in milliseconds) for this V8Js object
* works similar to the set_time_limit php
* @param int $limit
*/
public function setTimeLimit ( $ limit )
{}
/**
* Set the memory limit (in bytes) for this V8Js object
* @param int $limit
*/
public function setMemoryLimit ( $ limit )
{}
/**
* Set the average object size (in bytes) for this V8Js object.
* V8's "amount of external memory" is adjusted by this value for every exported object. V8 triggers a garbage collection once this totals to 192 MB.
* @param int $average_object_size
*/
public function setAverageObjectSize ( $ average_object_size )
{}
/**
* Returns uncaught pending exception or null if there is no pending exception.
* @return V8JsScriptException|null
*/
public function getPendingException ()
{}
/**
* Clears the uncaught pending exception
*/
public function clearPendingException ()
{}
/** Static methods **/
/**
* Creates a custom V8 heap snapshot with the provided JavaScript source embedded.
* @param string $embed_source
* @return string|false
*/
public static function createSnapshot ( $ embed_source )
{}
}
final class V8JsScriptException extends Exception
{
/**
* @return string
*/
final public function getJsFileName ( ) {}
/**
* @return int
*/
final public function getJsLineNumber ( ) {}
/**
* @return int
*/
final public function getJsStartColumn ( ) {}
/**
* @return int
*/
final public function getJsEndColumn ( ) {}
/**
* @return string
*/
final public function getJsSourceLine ( ) {}
/**
* @return string
*/
final public function getJsTrace ( ) {}
}
final class V8JsTimeLimitException extends Exception
{
}
final class V8JsMemoryLimitException extends Exception
{
}
// Print a string.
print ( string ) ;
// Dump the contents of a variable.
var_dump ( value ) ;
// Terminate Javascript execution immediately.
exit ( ) ;
// CommonJS Module support to require external code.
// This makes use of the PHP module loader provided via V8Js::setModuleLoader (see PHP API above).
require ( "path/to/module" ) ;
L'opérateur JavaScript in
, lorsqu'il est appliqué à un objet PHP encapsulé, fonctionne de la même manière que la fonction PHP isset()
. De même, lorsqu'elle est appliquée à un objet PHP encapsulé, delete
JavaScript fonctionne comme PHP unset
.
<?php
class Foo {
var $ bar = null ;
}
$ v8 = new V8Js ();
$ v8 -> foo = new Foo ;
// This prints "no"
$ v8 -> executeString ( ' print( "bar" in PHP.foo ? "yes" : "no" ); ' );
?>
PHP a des espaces de noms distincts pour les propriétés et les méthodes, tandis que JavaScript n'en a qu'un. Habituellement, ce n'est pas un problème, mais si vous en avez besoin, vous pouvez utiliser un $
de début pour spécifier une propriété, ou __call
pour invoquer spécifiquement une méthode.
<?php
class Foo {
var $ bar = " bar " ;
function bar ( $ what ) { echo " I'm a " , $ what , " ! n" ; }
}
$ foo = new Foo ;
// This prints 'bar'
echo $ foo -> bar , "n" ;
// This prints "I'm a function!"
$ foo -> bar ( " function " );
$ v8 = new V8Js ();
$ v8 -> foo = new Foo ;
// This prints 'bar'
$ v8 -> executeString ( ' print(PHP.foo.$bar, "n"); ' );
// This prints "I'm a function!"
$ v8 -> executeString ( ' PHP.foo.__call("bar", ["function"]); ' );
?>
Les types de données PHP et JavaScript ne correspondent pas exactement. Bien entendu, les deux langages ont des types de données pour gérer les nombres. Pourtant, PHP fait la différence entre les entiers et les nombres à virgule flottante, contrairement à JavaScript qui n'a qu'un type Number
, qui est un nombre à virgule flottante IEEE 754. Dans de nombreux cas, cela n'a aucune importance, lorsque les deux langues peuvent bien représenter le même nombre. Il existe cependant des cas extrêmes.
Sur les systèmes 64 bits, PHP autorise les entiers à avoir 64 bits significatifs, le type de nombre JavaScript (c'est-à-dire IEEE 754) n'a cependant qu'une mantisse de 52 bits. Une certaine précision sera donc perdue. Cela commence à avoir de l'importance si vous transmettez des valeurs entières avec plus de 15 chiffres décimaux précis.
Malgré le nom commun, le concept de tableaux est très différent entre PHP et JavaScript. En JavaScript, un tableau est une collection contiguë d’éléments indexés par des nombres entiers à partir de zéro. En PHP, les tableaux peuvent être clairsemés, c'est-à-dire que les clés intégrales n'ont pas besoin d'être contiguës et peuvent même être négatives. En outre, les tableaux PHP peuvent non seulement utiliser des nombres entiers comme clés, mais également des chaînes (appelés tableaux associatifs). Les tableaux JavaScript contraires permettent d'attacher des propriétés aux tableaux, ce qui n'est pas pris en charge par PHP. Ces propriétés ne font pas partie de la collection de tableaux, par exemple, la méthode Array.prototype.forEach
ne les « voit » pas.
Généralement, les tableaux PHP sont mappés sur des tableaux JavaScript "natifs" si cela est possible, c'est-à-dire que le tableau PHP utilise des touches numériques contiguës à partir de zéro. Les tableaux associatifs et clairsemés sont mappés sur des objets JavaScript. Ces objets ont un constructeur également appelé "Array", mais ce ne sont pas des tableaux natifs et ne partagent pas le Array.prototype, ils ne prennent donc pas (directement) en charge les fonctions de tableau typiques comme join
, forEach
, etc. Les tableaux PHP sont immédiatement exporté valeur par valeur sans liaison en direct. En effet, si vous modifiez une valeur côté JavaScript ou si vous transférez d'autres valeurs sur le tableau, cette modification n'est pas reflétée côté PHP.
Si les tableaux JavaScript sont renvoyés à PHP, le tableau JavaScript est toujours converti en tableau PHP. Si le tableau JavaScript possède des (propres) propriétés attachées, celles-ci sont également converties en clés du tableau PHP.
Les objets PHP passés à JavaScript sont mappés sur des objets JavaScript natifs qui ont une fonction constructeur « virtuelle » avec le nom de la classe de l'objet PHP. Cette fonction constructeur peut être utilisée pour créer de nouvelles instances de la classe PHP tant que la classe PHP n'a pas de méthode __construct
non publique. Toutes les méthodes et propriétés publiques sont visibles par le code JavaScript et les propriétés sont liées en direct, c'est-à-dire que si la valeur d'une propriété est modifiée par le code JavaScript, l'objet PHP est également affecté.
Si un objet JavaScript natif est transmis à PHP, l'objet JavaScript est mappé à un objet PHP de la classe V8Object
. Cet objet possède toutes les propriétés de l'objet JavaScript et est entièrement modifiable. Si une fonction est affectée à l'une de ces propriétés, elle peut également être appelée par du code PHP. La fonction executeString
peut être configurée pour toujours mapper les objets JavaScript aux tableaux PHP en définissant l'indicateur V8Js::FLAG_FORCE_ARRAY
. Ensuite, le comportement standard du tableau s'applique selon lequel les valeurs ne sont pas liées en direct, c'est-à-dire que si vous modifiez les valeurs du tableau PHP résultant, l'objet JavaScript n'est pas affecté.
La règle ci-dessus selon laquelle les objets PHP sont généralement convertis en objets JavaScript s'applique également aux objets PHP de type ArrayObject
ou à d'autres classes, qui implémentent à la fois l'interface ArrayAccess
et Countable
- même s'ils se comportent comme des tableaux PHP.
Ce comportement peut être modifié en activant l'indicateur php.ini v8js.use_array_access
. S'ils sont définis, les objets des classes PHP qui implémentent les interfaces susmentionnées sont convertis en objets de type JavaScript Array. Il s'agit d'un accès par index de cet objet qui entraîne des appels immédiats aux méthodes PHP offsetGet
ou offsetSet
(en fait, il s'agit d'une liaison en direct de JavaScript avec l'objet PHP). Un tel objet Array-esque prend également en charge l'appel de chaque méthode publique attachée à l'objet PHP + les méthodes des méthodes Array.prototype natives de JavaScript (tant qu'elles ne sont pas surchargées par les méthodes PHP).
Tout d'abord, les instantanés de démarrage personnalisés sont une fonctionnalité fournie par la V8 elle-même, construite au-dessus de sa fonctionnalité générale d'instantanés de tas. L'idée est que, puisqu'il est assez courant de charger une bibliothèque JavaScript avant tout travail réel à effectuer, ce code de bibliothèque est également intégré dans l'instantané du tas.
Cette extension offre un moyen simple de créer ces instantanés personnalisés. Afin de créer un tel instantané avec une fonction fibonacci
intégrée, appelez simplement V8Js::createSnapshot
de manière statique comme ceci :
$ snapshot = V8Js:: createSnapshot ( ' var fibonacci = n => n < 3 ? 1 : fibonacci(n - 1) + fibonacci(n - 2) ' );
Conservez ensuite le contenu de $snapshot
là où vous le souhaitez, par exemple dans le système de fichiers local ou peut-être dans Redis.
Si vous devez créer une nouvelle instance V8Js, transmettez simplement l'instantané comme 5ème argument au constructeur V8Js :
$ jscript = new V8Js ( ' php ' , array (), array (), true , $ snapshot );
echo $ jscript -> executeString ( ' fibonacci(43) ' ) . "n" ;
Gardez à l’esprit que le code à inclure dans l’instantané ne peut appeler directement aucune des fonctions exportées depuis PHP, car elles sont ajoutées juste après l’exécution du code de l’instantané.
Si le code JavaScript est lancé (sans intercepter), provoque des erreurs ou ne se compile pas, des exceptions V8JsScriptException
sont levées.
Les exceptions PHP qui se produisent par défaut en raison d'appels de code JavaScript ne sont pas renvoyées dans le contexte JavaScript mais entraînent l'arrêt immédiat de l'exécution de JavaScript, puis sont signalées à l'emplacement appelant le code JS.
Ce comportement peut être modifié en définissant l'indicateur FLAG_PROPAGATE_PHP_EXCEPTIONS
. Si elle est définie, les exceptions PHP (objets) sont converties en objets JavaScript obéissant aux règles ci-dessus et renvoyées dans un contexte JavaScript. S'ils ne sont pas interceptés par le code JavaScript, l'exécution s'arrête et une exception V8JsScriptException
est levée, avec l'exception PHP d'origine accessible via la méthode getPrevious
.
Considérez que le code JS a accès à des méthodes comme getTrace
sur l'objet d'exception. Cela peut être un comportement indésirable si vous exécutez du code non fiable. En utilisant la méthode setExceptionFilter
un appelable peut être fourni, qui peut convertir l'exception PHP en une autre valeur pouvant être exposée en toute sécurité. Le filtre peut également décider de ne pas propager du tout l'exception à JS, soit en relançant l'exception transmise, soit en lançant une autre exception.