After some research, here is what I came up with. The only (clean) solution is to use member functions and instance/class variables.
You need to:
- Reference everything using
$this
and not function arguments.
- Unset all globals, superglobals and restore them afterwards.
- Use a possible race condition of some sorts. i.e.: In my example below,
render()
will set instance variables that _render()
will use afterwards. In a multi-threaded system, this creates a race condition: thread A may call render() at the same time as thread B and the data will be inexact for one of them. Fortunately, for now, PHP isn't multi-threaded.
- Use a temporary file to include, containing a closure, to avoid the use of
eval
.
The template class I came up with:
class template {
// Store the template data
protected $_data = array();
// Store the template filename
protected $_file, $_tmpfile;
// Store the backed up $GLOBALS and superglobals
protected $_backup;
// Render a template $file with some $data
public function render($file, $data) {
$this->_file = $file;
$this->_data = $data;
$this->_render();
}
// Restore the unset superglobals
protected function _restore() {
// Unset all variables to make sure the template don't inject anything
foreach ($GLOBALS as $var => $value) {
// Unset $GLOBALS and you're screwed
if ($var === 'GLOBALS') continue;
unset($GLOBALS[$var]);
}
// Restore all variables
foreach ($this->_backup as $var => $value) {
// Set back all global variables
$GLOBALS[$var] = $value;
}
}
// Backup the global variables and superglobals
protected function _backup() {
foreach ($GLOBALS as $var => $value) {
// Unset $GLOBALS and you're screwed
if ($var === 'GLOBALS') continue;
$this->_backup[$var] = $value;
unset($GLOBALS[$var]);
}
}
// Render the template
protected function _render() {
$this->_backup();
$this->_tmpfile = tempnam(sys_get_temp_dir(), __CLASS__);
$code = '<?php $render = function() {'.
'extract('.var_export($this->_data, true).');'.
'require "'.$this->_file.'";'.
'}; $render();'
file_put_contents($this->_tmpfile, $code);
include $this->_tmpfile;
$this->_restore();
}
}
And here's the test case:
// Setting some global/superglobals
$_GET['get'] = 'get is still set';
$hello = 'hello is still set';
$t = new template;
$t->render('template.php', array('foo'=>'bar', 'this'=>'hello world'));
// Checking if those globals/superglobals are still set
var_dump($_GET['get'], $hello);
// Those shouldn't be set anymore
var_dump($_SERVER['bar'], $GLOBALS['stack']); // undefined indices
And the template file:
<?php
var_dump($GLOBALS); // prints an empty list
$_SERVER['bar'] = 'baz'; // will be unset later
$GLOBALS['stack'] = 'overflow'; // will be unset later
var_dump(get_defined_vars()); // foo, this
?>
In short, this solution:
- Hides all globals and superglobals. The variables themselves ($_GET, $_POST, etc.) can still be modified, but they will revert back to what they were previously.
- Does not shadow variables. (Almost) everything can be used, including
$this
. (Except for $GLOBALS
, see below).
- Does not bring anything into scope that wasn't passed.
- Does not lose any data nor trigger destructors, because the refcount never reaches zero for any variable.
- Does not use
eval
or anything like that.
Here's the result I have for the above:
array(1) {
["GLOBALS"]=>
*RECURSION*
}
array(2) {
["this"]=>
string(11) "hello world"
["foo"]=>
string(3) "bar"
}
string(10) "get is still set"
string(12) "hello is still set"
Notice: Undefined index: bar in /var/www/temp/test.php on line 75
Call Stack:
0.0003 658056 1. {main}() /var/www/temp/test.php:0
Notice: Undefined index: stack in /var/www/temp/test.php on line 75
Call Stack:
0.0003 658056 1. {main}() /var/www/temp/test.php:0
NULL
NULL
If you dump $GLOBALS
after the fact it should be just like it was before the call.
The only possible issue is that someone still can execute something like:
unset($GLOBALS);
... and you're screwed. And there is no way around that.