Требовать произвольный файл PHP без утечки переменных в область видимости - PullRequest
23 голосов
/ 09 ноября 2011

Возможно ли в 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 ]

21 голосов
/ 15 декабря 2011

Посмотрите на это:

$scope = function() {
    // It's very simple :)
    extract( func_get_arg(1) );
    require func_get_arg(0);
};
$scope( "RequiredFile.php", array() );
0 голосов
/ 09 ноября 2011

После некоторых исследований я пришел к такому выводу. Единственное (чистое) решение - использовать функции-члены и переменные экземпляра / класса.

Вам необходимо:

  • Ссылка на все, используя $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

?>

Короче говоря, это решение:

  • Скрывает все глобальные и суперглобальные значения. Сами переменные ($ _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);

... и ты облажался. И пути назад нет.

0 голосов
/ 17 ноября 2011

Если вам нужен очень простой движок шаблонов, ваш подход с функцией достаточно хорош.Скажите, каковы реальные недостатки раскрытия этой переменной $_file?

Если вам нужно заняться реальной работой, возьмите Twig и перестаньте беспокоиться.Любой правильный шаблонизатор в любом случае компилирует ваши шаблоны в чистый PHP, чтобы вы не теряли скорость.Вы также получаете значительные преимущества - более простой синтаксис, принудительный htmlspecialchars и др.

Вы всегда можете скрыть свой $_file в суперглобальном:
$_SERVER['MY_COMPLEX_NAME'] = $_file;
unset($_file);
include($_SERVER['MY_COMPLEX_NAME']);
unset($_SERVER['MY_COMPLEX_NAME']);

0 голосов
/ 09 ноября 2011

Мне удалось найти решение, использующее 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() );
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...