Reputation: 44124
Is it possible in PHP to require
an arbitrary file without leaking any variables from the current scope into the required file's variable namespace or polluting the global variable scope?
I'm wanting to do lightweight templating with PHP files and was wondering for purity sake if it was possible to load a template file without any variables in it's scope but the intended ones.
I have setup a test that I would like a solution to pass. It should beable to require RequiredFile.php
and have it return Success, no leaking variables.
.
RequiredFile.php:
<?php
print array() === get_defined_vars()
? "Success, no leaking variables."
: "Failed, leaked variables: ".implode(", ",array_keys(get_defined_vars()));
?>
The closest I've gotten was using a closure, but it still returns Failed, leaked variables: _file
.
$scope = function( $_file, array $scope_variables ) {
extract( $scope_variables ); unset( $scope_variables );
//No way to prevent $_file from leaking since it's used in the require call
require( $_file );
};
$scope( "RequiredFile.php", array() );
Any ideas?
Upvotes: 29
Views: 3231
Reputation: 6092
Look at this:
$scope = function() {
// It's very simple :)
extract(func_get_arg(1));
require func_get_arg(0);
};
$scope("RequiredFile.php", []);
Upvotes: 24
Reputation: 5271
If you need a very simple templating engine, your approach with a function is good enough.
Tell me, what are the real disadvantages of exposing that $_file
variable?
If you need to do real work, grab Twig and stop worrying.
Any proper templating engine compiles your templates into pure PHP anyway, so you don't lose
speed. You also gain significant advantages - simpler syntax, enforced htmlspecialchars
and other.
You could always hide your $_file
in a superglobal:
$_SERVER['MY_COMPLEX_NAME'] = $_file;
unset($_file);
include($_SERVER['MY_COMPLEX_NAME']);
unset($_SERVER['MY_COMPLEX_NAME']);
Upvotes: 0
Reputation: 67745
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:
$this
and not function arguments.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.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:
$this
. (Except for $GLOBALS
, see below).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.
Upvotes: 0
Reputation: 44124
I've been able to come up with a solution using eval
to inline the variable as a constant, thus preventing it from leaking.
While using eval
is definitely not a perfect solution, it does create a "perfectly clean" scope for the required file, something that PHP doesn't seem to be able to do natively.
$scope = function( $file, array $scope_array ) {
extract( $scope_array ); unset( $scope_array );
eval( "unset( \$file ); require( '".str_replace( "'", "\\'", $file )."' );" );
};
$scope( "test.php", array() );
EDIT:
This technically isn't even a perfect solution as it creates a "shadow" over the file
and scope_array
variables, preventing them from being passed into the scope naturally.
EDIT2:
I could resist trying to write a shadow free solution. The executed code should have no access to $this
, global or local variables from previous scopes, unless directly passed in.
$scope = function( $file, array $scope_array ) {
$clear_globals = function( Closure $closure ) {
$old_globals = $GLOBALS;
$GLOBALS = array();
$closure();
$GLOBALS = $old_globals;
};
$clear_globals( function() use ( $file, $scope_array ) {
//remove the only variable that will leak from the scope
$eval_code = "unset( \$eval_code );";
//we must sort the var name array so that assignments happens in order
//that forces $var = $_var before $_var = $__var;
$scope_key_array = array_keys( $scope_array );
rsort( $scope_key_array );
//build variable scope reassignment
foreach( $scope_key_array as $var_name ) {
$var_name = str_replace( "'", "\\'", $var_name );
$eval_code .= "\${'$var_name'} = \${'_{$var_name}'};";
$eval_code .= "unset( \${'_{$var_name}'} );";
}
unset( $var_name );
//extract scope into _* variable namespace
extract( $scope_array, EXTR_PREFIX_ALL, "" ); unset( $scope_array );
//add file require with inlined filename
$eval_code .= "require( '".str_replace( "'", "\\'", $file )."' );";
unset( $file );
eval( $eval_code );
} );
};
$scope( "test.php", array() );
Upvotes: 0