需要一个任意 PHP 文件,而不会将变量泄漏到作用域中

Posted

技术标签:

【中文标题】需要一个任意 PHP 文件,而不会将变量泄漏到作用域中【英文标题】:Require an arbitrary PHP file without leaking variables into scope 【发布时间】:2011-12-24 22:40:09 【问题描述】:

是否有可能在 phprequire 一个 任意 文件而不会将任何变量从当前范围泄漏到所需文件的变量命名空间或污染全局变量范围?

我想用 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's used in the require call
    require( $_file );
;
$scope( "RequiredFile.php", array() );

有什么想法吗?

【问题讨论】:

大概您希望require 仅将类和函数拉入全局范围?你打算如何处理碰撞? 我对 PHP 的了解还不够,无法写出正确的答案,但namespaces 可能是您的问题的解决方案。 @OliCharlesworth 我不太明白你在问什么。你能再解释一下吗? PHP 脚本包含变量、类和函数。您说过要避免污染变量,这意味着您实际上只是在关注类和函数。也许您需要编辑您的问题以解释这里的总体目标。 @OliCharlesworth Smarty(与 PHP 之上的任何其他模板引擎一样)是非常不必要的开销。最重要的是,使用模板引擎严重限制了您可以做的事情。它们不仅毫无价值,而且实际上弊大于利。 【参考方案1】:

看看这个:

$scope = function() 
    // It's very simple :)
    extract(func_get_arg(1));
    require func_get_arg(0);
;
$scope("RequiredFile.php", []);

【讨论】:

对于任何想要使用它但有某种智能感知/PHPDoc 支持的人,我已经重写了它以在函数中使用实际变量:gist.github.com/Eraknelo/6795b983825fc6a720ef 这是一个了不起的答案!如果有人想在不声明函数的情况下执行此操作(例如,上述代码中的 $scope),您可以使用匿名函数,如下所示:call_user_func(function() extract( func_get_arg(1) ); return require func_get_arg(0); , "RequiredFile.php", array( 'your_var_1' =&gt; 'hello', 'your_var_2' =&gt; 'world', ));【参考方案2】:

我已经想出了一个解决方案,使用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() );

编辑:

从技术上讲,这甚至不是一个完美的解决方案,因为它会在 filescope_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() );

【讨论】:

作为我所知道的biggest opponent to eval 之一,我不认为你会想出,甚至不会接受这样的解决方案,比如模板......似乎你可能毕竟敞开心扉。在此解决方案中,还可以使用 global 关键字和/或超全局变量来访问变量。 ;-) @netcoder 我不会接受这个答案,因为我不希望它被使用,但我更感兴趣的是这个问题是否有“正确”的答案,即使这意味着诉诸“邪恶”eval:P @KendallHopkins 同意我的推理,但你为什么要传递一个空数组作为范围数组?还是空数组是 get_defined_vars 的化名?提取空数组的动机是什么?【参考方案3】:

经过一番研究,这是我想出的。唯一(干净)的解决方案是使用成员函数和实例/类变量。

你需要:

使用 $this 而不是函数参数引用所有内容。 取消设置所有全局变量、超全局变量并在之后恢复它们。 使用某种可能的竞争条件。即:在下面的示例中,render() 将设置 _render() 之后将使用的实例变量。在多线程系统中,这会产生竞争条件:线程 A 可能与线程 B 同时调用 render(),并且其中一个的数据将不准确。幸运的是,目前 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

?>

简而言之,这个解决方案:

隐藏所有全局变量和超全局变量。变量本身($_GET、$_POST 等)仍然可以修改,但它们会恢复到以前的状态。 不隐藏变量。(几乎)所有东西都可以使用,包括$this。 ($GLOBALS 除外,见下文)。 不会将任何未通过的内容带入范围。 不会丢失任何数据,也不会触发析构函数,因为任何变量的 refcount 永远不会为零。 不使用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);

...你完蛋了。没有办法解决这个问题。

【讨论】:

这是否保护$this-&gt;_backup = array() 破坏还原或$this = NULL 导致错误? 另外你为什么不能通过存储它来备份$GLOBALS var? @Kendall Hopkins:分配$this = NULL(直接分配)总是会导致致命错误。 $this-&gt;_backup = array() 似乎打破了它......可能有办法解决这个问题......:P 您可能会使用与我正在做的类似的方法来使用范围和闭包自然地从模板中隐藏变量。 我认为这可以在没有eval 或类的情况下完成。您可以执行类似require( specialFunction() ) 的操作,其中function specialFunction( $input = NULL ) static $cache = NULL; if( is_null( $input ) ) return $cache; $cache = $input; 【参考方案4】:

如果您需要一个非常简单的模板引擎,那么您使用函数的方法就足够了。 告诉我,暴露 $_file 变量的真正缺点是什么?

如果您需要做真正的工作,请抓住 Twig 并停止担心。 无论如何,任何适当的模板引擎都会将您的模板编译成纯 PHP,因此您不会丢失 速度。您还将获得显着优势 - 更简单的语法、强制执行 htmlspecialchars 等。

您总是可以将您的$_file 隐藏在一个超全局中:$_SERVER['MY_COMPLEX_NAME'] = $_file;unset($_file);include($_SERVER['MY_COMPLEX_NAME']);unset($_SERVER['MY_COMPLEX_NAME']);

【讨论】:

我确实使用Twig,老实说,这更像是思想实验。同样将$_file 存储在超全局中并没有准备好隐藏它,因为在include 期间仍然可以访问它。 我知道,这就是我说“隐藏”的原因。 :)

以上是关于需要一个任意 PHP 文件,而不会将变量泄漏到作用域中的主要内容,如果未能解决你的问题,请参考以下文章

前端知识体系:JavaScript基础-作用域和闭包-闭包的实现原理和作用以及堆栈溢出和内存泄漏原理和相应解决办法

什么是闭包

Rails中内存泄漏的主要因素

将 JS 变量输出到我的 PHP 文件中

PHP session 与cookie

垃圾回收机制