Ответ 1
Посмотрите на это:
$scope = function() {
// It very simple :)
extract( func_get_arg(1) );
require func_get_arg(0);
};
$scope( "RequiredFile.php", array() );
Возможно ли в PHP require
произвольный файл без утечки каких-либо переменных из текущей области в требуемое пространство имен переменных файлов или загрязнение области глобальных переменных?
Я хочу делать легкие шаблоны с PHP файлами и задавался вопросом, насколько это возможно, если бы можно было загрузить файл шаблона без каких-либо переменных в его области, кроме предполагаемых.
У меня есть тест, который я бы хотел решить. Должно потребоваться RequiredFile.php
и вернуть его 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()));
?>
Самое близкое, что я получил, - это использовать закрытие, но оно все равно возвращает 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 used in the require call
require( $_file );
};
$scope( "RequiredFile.php", array() );
Любые идеи?
Посмотрите на это:
$scope = function() {
// It very simple :)
extract( func_get_arg(1) );
require func_get_arg(0);
};
$scope( "RequiredFile.php", array() );
Мне удалось найти решение с помощью eval
, чтобы вставить переменную в качестве константы, тем самым предотвращая ее утечку.
При использовании eval
определенно не идеальное решение, оно создает "совершенно чистую" область для требуемого файла, что-то, что PHP, похоже, не может сделать изначально.
$scope = function( $file, array $scope_array ) {
extract( $scope_array ); unset( $scope_array );
eval( "unset( \$file ); require( '".str_replace( "'", "\\'", $file )."' );" );
};
$scope( "test.php", array() );
EDIT:
Это технически даже не идеальное решение, поскольку оно создает "тень" над переменными file
и scope_array
, не позволяя им естественным образом переноситься в область.
EDIT2:
Я мог бы сопротивляться попытке написать теневое решение. Выполненный код не должен иметь доступ к $this
, глобальным или локальным переменным из предыдущих областей, если только не передан непосредственно.
$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() );
После некоторых исследований, вот что я придумал. Единственным (чистым) решением является использование функций-членов и переменных экземпляра/класса.
Вам необходимо:
$this
, а не аргументы функции.render()
будет устанавливать переменные экземпляра, которые будет использоваться после _render()
. В многопоточной системе это создает условие гонки: поток A может вызывать render() одновременно с потоком B, и данные будут неточными для одного из них. К счастью, на данный момент PHP не многопоточен.eval
.В классе шаблонов я придумал:
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();
}
}
И вот тестовый пример:
// 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
И файл шаблона:
<?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
?>
Короче говоря, это решение:
$this
. (За исключением $GLOBALS
, см. Ниже).eval
или что-то в этом роде.Вот результат, который я привел выше:
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
Если вы сбросите $GLOBALS
после того, как он будет таким же, как и до вызова.
Единственная возможная проблема заключается в том, что кто-то еще может выполнить что-то вроде:
unset($GLOBALS);
... и вы ввернуты. И нет никакого способа обойти это.
Если вам нужен очень простой механизм шаблонов, ваш подход с функцией достаточно хорош.
Скажите, каковы реальные недостатки раскрытия переменной $_file
?
Если вам нужно сделать настоящую работу, возьмите Twig и перестаньте беспокоиться.
Любой правильный механизм шаблонов компилирует ваши шаблоны в чистый PHP в любом случае, поэтому вы не теряете
скорость. Вы также получаете значительные преимущества - более простой синтаксис, принудительный htmlspecialchars
и другие.
Вы всегда можете скрыть свой $_file
в суперглобале: $_SERVER['MY_COMPLEX_NAME'] = $_file;
unset($_file);
include($_SERVER['MY_COMPLEX_NAME']);
unset($_SERVER['MY_COMPLEX_NAME']);
суб >