需要一个任意PHP文件而不将变量泄漏到范围内

时间:2011-11-09 01:05:56

标签: php global-variables scope

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

有什么想法吗?

4 个答案:

答案 0 :(得分:21)

看看这个:

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

答案 1 :(得分:0)

我已经能够使用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() );

答案 2 :(得分:0)

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

你需要:

  • 使用$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除外,见下文)。
  • 不会将任何内容带入未通过的范围。
  • 不丢失任何数据也不会触发析构函数,因为引用计数永远不会为任何变量达到零。
  • 不使用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);

......你被搞砸了。并且没有办法解决这个问题。

答案 3 :(得分:0)

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

如果你需要做真正的工作,请抓住Twig并停止担心。 任何适当的模板引擎都会将你的模板编译成纯PHP,所以你不要输 速度。您还获得了显着的优势 - 更简单的语法,强制htmlspecialchars和其他。

您可以随时在超级全局中隐藏$_file
$_SERVER['MY_COMPLEX_NAME'] = $_file;
unset($_file);
include($_SERVER['MY_COMPLEX_NAME']);
unset($_SERVER['MY_COMPLEX_NAME']);