phpv8 / v8js

V8 Javascript Engine for PHP — This PHP extension embeds the Google V8 Javascript Engine
http://pecl.php.net/package/v8js
MIT License
1.83k stars 200 forks source link

Using PHP objects inside JavaScript code #259

Closed DavidGeUSA closed 7 years ago

DavidGeUSA commented 8 years ago

ClearScript (https://clearscript.codeplex.com) provides two functions, AddHostType(name, type) and AddHostObject(name, object), to allow JavaScript code to use .Net classes and objects. I hope V8Js also provides such functions to allow JavaScript code to use PHP classes and PHP objects. It can be quite useful for server side JavaScript to fully access server side functionality provided by PHP. For example, suppose we have a PHP class SendMail. We can use it in the JavaScript by first adding it to JavaScript global scope, such as $v8=new V8Js(); $v8->AddHostType('SendMail', SendMail); $v8->ExecuteString('var sendmail=new SendMail(); ...other code for setting email parameters...; sendmail.send();');

pinepain commented 8 years ago

I faced with a somehow similar problem before which resulted in different v8 extension in PHP. From my experience such integration like type/class mapping should better reside in a userland php scripts rather then in an extension. If it appropriate here, I'll give a link to my ext (though, it can be found in my github repos).

stesie commented 8 years ago

v8js extension doesn't currently allow to directly export an object. The whole flow currently is designed around passing instances.

Yet it is pretty simple to achieve anyways, using a lightweight helper function:

<?php

class SendMail
{
    private $to;

    public function setTo($to)
    {
        $this->to = $to;
    }

    public function send()
    {
        echo "sending mail to {$this->to} now ...\n";
    }
}

$v8 = new V8Js();

$typeExporter = $v8->executeString('(function(typeName, instance) { this[typeName] = instance.constructor; })');
$typeExporter('SendMail', new SendMail());

$v8->executeString('
    var sendMail = new SendMail();
    sendMail.setTo("foo@example.org");

    var otherMail = new SendMail();
    otherMail.setTo("bar@example.org");

    sendMail.send();
    otherMail.send();
');

... the trick is with JavaScript's prototypal inheritance, ... I simply provide a dummy instance of the object to the helper function, which just picks its constructor field and stores that. Subsequent new calls on that constructor then trigger object creation in PHP.

Another viable, yet more work-around, option would be to export a factory (instance) that creates the objects as needed on behalf of the JavaScript code

DavidGeUSA commented 8 years ago

Hi pinepain, I believe the link is https://github.com/pinepain/php-v8. It is very interesting to see another extension. I'll spend more times with it. Thanks!

stesie commented 8 years ago

@Limnor there's even a third extension around: http://www.php-javascript.com/

... but haven't tried it out so far.

DavidGeUSA commented 8 years ago

Hi stesie, wow, thanks a lots for providing such a clever solution. I'll digest the techniques you demonstrated and try it out!

DavidGeUSA commented 8 years ago

Hi stesie, thank you for pointing to that extension. I noticed that the first important value of this extension explained by the authors is "powerful", and why it is powerful is that it allows JavaScript code to use PHP variables and functions and other PHP features.

pinepain commented 8 years ago

@stesie I gave it a look long time ago, right before choosing v8js (and then writing php-v8).

php-js also performs a lot of transformations internally and tries to provide the simplest to use interface to end user as possible. Because it doesn't fit's my needs and because of it slightly more complex building process (they use php-cpp which adds extra abstraction layer) and it license (GPL), v8js was a clean winner for me that time.

@Limnor any of php-v8, v8js, php-js and php-spidermonkey (it may be slightly outdated these days) allows you to interact with a php entities. Basically, that's why that extension exists for.

pinepain commented 8 years ago

@stesie I briefly described my experience with v8js which lead to writing php-v8 extension, I guess some points from it may be interesting for you - https://github.com/pinepain/php-v8/issues/8#issuecomment-244601087.

DavidGeUSA commented 8 years ago

@stesie your code works like a charm. I wrapped it in a class: `<?php

class V8PhpAccess { public static function AddHostType($v8, $name, $type) { $typeExporter = $v8->executeString('(function(typeName, instance) { this[typeName] = instance.constructor; })'); $typeExporter($name, $type); } }

?>`

Now I can do $v8=new V8Js(); V8PhpAccess::AddHostType($v8, 'SendMail', new SendMail());

stesie commented 8 years ago

@Limnor you could even cache the result of your executeString call within the class in case you export a lot of types

stesie commented 8 years ago

@pinepain fair enough, I really like the low-level approach you took with your extension. Yet the tone of your writing bothers me ... maybe I got it wrong but telling that v8js is a nightmare security-wise, after months we've discussed here on Github and hours I've spent helping you out and arguing, is not what I had expected.

I actually agree with you - and you know that - that exporting those global functions by default might be problematic. So I don't see why exit should be (throwing whatever exception just stops code execution the very same way), sleep might be (and setTimeLimit should be applied to it, that's easy to fix), ... but the rest? And you should be well aware that those functions can simply be unset from JavaScript before executing user-controlled scripts. After all it's a BC break and I personally don't want to push forward just because of that

Regarding your point that JS-code could start allocating php memory, escaping from v8js' memory limits, what do you mean? Sure you can pass large strings to php functions, then a short-lived zval is allocated for PHP to handle but the script cannot mess with it.

With the other points you make, yes, v8js definitely is opinionated -- and I still think that's not a bad thing. Not for every use-case of course and I think it's fair enough to have options ... so I think we shouldn't push each other's project into a niche or otherwise devaluate each other

pinepain commented 8 years ago

@stesie I'm terribly sorry, maybe that comment was not clear enough, I didn't want to say that v8js or any other solution is a nightmare, my point was is that it's a problematic to execute untrusted code in a safe and reliable way regardless any tool. Sorry again if it looked like I blame you or v8js, I haven't that in mind. I really appreciate your help and I clearly remember and value your help. I'd better remove that "nightmare" section to not confuse anyone.

pinepain commented 8 years ago

@stesie I dropped that sentence completely. I'm sorry that it lead to misunderstanding.

As to that functions, I second you on a BC, it was one of those reasons which lead me to writing new extension and not to patch existent one. I just checked in source code and those functions are exported as a v8::ReadOnly, so I'm no sure whether they can be unset from JS runtime, afaik, I had to remove that readonly in order to remove that functions from js runtime. Also print doesn't fit me that time, I need to have a control over std in/out/err, so console.log approach with a pseudo-separate stdout for it worked for me better. But once again, it was my specific need, it has nothing about v8js by any mean.

It technically possible to remove readonly restriction, but it may potentially break BC in case some user code assign to or delete a variable with the same name as injected functions. I wouldn't do that without real need or previous warning and deprecations at least.

As to allocating php memory, I will demonstrate with meta-code. I have no v8js installed at this time, so it may be not literally working, consider it as a concept:

$v8js = new V8Js();
$v8js->evil = [];
$v8js->executeString('... se code below...', 'test.js');
var text = "abcdefghijklmnopqrstuvwyxz0123456789";
var memory = "";
PHP.evil = {}; // can be set in PHP too, it doesn't matter
for (var i = 0; i < 100; ++i) {
    memory = "";
    for (var j = 0; j < 10000; ++j) {
         memory += text;
    }
    PHP.evil["memory-" + i] = text;
}

In theory, this case in v8js should be handled with memory limits while evil also have js object representation. Basically, this problem can happen in php-v8 too, so I'm not sure how to handle it the best, I found node's guard module which track rss (resident) memory so it should cover both PHP and V8 memory usage (and all other extensions/libraries that are in use by the current process).

As to v8js being opinionated - it is not bad at all. I'm not only didn't mean that, I hope, I even didn't write anything which may be considered as claiming that as a bad thing. I used v8js on early stages and it worked like a charm. And when it didn't, you help to do figure our that and fix. Honestly, php-v8 is also opinionated 😄, but in it's own way. Less likely there will silver bullet for all cases.

It maybe worth to note that English is not my native language and while I'm working on improving it, sometimes I wrote meaning one, and it results in an absolutely different. Thank you for reading my writings and point me to what wrong. I really value your comments and suggestions and don't think that our tools are better or worse. I really hate such "drama" so sorry again if it appears that I undervalue v8js or your efforts. I will double-check my writing and will ask someone who is native to read it and check whether it has any negative connotations.

DavidGeUSA commented 8 years ago

@stesie That is a good suggestion. Cache added:

class V8PhpAccess
{
    public static function AddHostType($v8, $name, $type)
    {
        if (!property_exists($v8,'typeExporter'))
        {
            $v8->typeExporter = $v8->executeString('(function(typeName, instance) { this[typeName] = instance.constructor; })');
        }
        $typeExporter = $v8->typeExporter;
        $typeExporter($name, $type);
    }
}
stesie commented 8 years ago

@pinepain maybe I also got it wrong, who knows :) ... I'm neither an English native speaker. After all I'm happy that I shared my feelings with your writing so we could clear that up :-)

Regarding your example, attaching arbitrary data to PHP object I have mixed feelings. Actually it shouldn't be very problematic as it should hit PHP's memory limit (so yes the whole process would terminate, but after all it's all a user process -- but it shouldn't hurt the system itself)

And to the readonly thing, it's actually possible to overwrite readonly symbols on JavaScript side. The readonly simply tells that changes wouldn't reflect back to PHP side.

stesie commented 8 years ago

@Limnor that change pretty much is an (unfortunate) example to what @pinepain is saying that you can "accidentally" make properties visible to JavaScript.

... assigning $v8->typeExporter publishes the result ;-)

So if you really want to keep going with public static (which I personally almost consider an anti pattern) then you should have a private static $typeExporters = [] array in that class and store the exporters like self::$typeExporters[spl_object_hash($v8)] = ... (and also access like that)

Generally I'd very much prefer a class wrapping $v8 itself and having a single, lazily generated, typeExporter instance. Like this (untested):

class V8PhpAccess
{
  /** @var V8Js */
  private $v8;

  /** @var V8Function */
  private $typeExporter;

  public function __construct(V8Js $v8)
  {
    $this->v8 = $v8;
  }

  private function getTypeExporter() : V8Function
  {
    if ($this->typeExporter === null) {
      $this->typeExporter = $v8->executeString('(function(typeName, instance) { this[typeName] = instance.constructor; })');
    }
    return $this->typeExporter;
  }

  public function AddHostType(string $name, $type)
  {
    $typeExporter = $this->getTypeExporter();
    $typeExporter($name, $type);
  }
}
DavidGeUSA commented 8 years ago

@stesie I tried your class wrapping and it worked great! I have a little concern about holding a reference of $v8 because if a developer puts the wrapper in a scope wider than $v8 then the wrapper changed the original scope. I do not know how PHP works, so my concern may not be valid. I wrapped your techniques in a class extending V8Js:

<?php
class V8JsExt extends V8Js
{
    private $typeExporter;
    private $setters = [];
    public function AddHostType($name, $type)
    {
        if ($typeExporter === null)
        {
            $typeExporter = $this->executeString('(function(typeName, instance) { this[typeName] = instance.constructor; })');
        }
        $typeExporter($name, $type);
    }
    public function SetHostValue($variable, $value)
    {
        if(!array_key_exists ($variable, $this->setters))
        {
            $this->setters[$variable] = $this->executeString('(function(value){'.$variable.'=value;})');
        }
        $func = $this->setters[$variable];
        $func($value);
    }
}
?>

Basically I get what I originally asked for: adding AddHostType and AddHostObject to V8Js. Now, not only I can add PHP types to JavaScript, I can also pass native PHP values to JavaScript without converting the values first, for example:

$v8 = new V8JsExt();
$v8->executeString('var values={};values.test1="Hello World!";');
$v8->SetHostValue('values["v1"]','test'); //pass a string
$v8->SetHostValue('values["v2"]',899); //pass an integer

Do you see any issues in the above solution?

DavidGeUSA commented 8 years ago

@stesie there are errors in the code. I fixed:

<?php

class V8JsExt extends V8Js
{
    private $typeExporter;
    private $setters = [];
    public function AddHostType($name, $type)
    {
        if ($this->typeExporter === null)
        {
            $this->typeExporter = $this->executeString('(function(typeName, instance) { this[typeName] = instance.constructor; })');
        }
        $func = $this->typeExporter;
        $func($name, $type);
    }
    public function SetHostValue($variable, $value)
    {
        if(!array_key_exists ($variable, $this->setters))
        {
            $this->setters[$variable] = $this->executeString('(function(value){'.$variable.'=value;})');
        }
        $func = $this->setters[$variable];
        $func($value);
    }
}
?>