Thursday, May 10, 2012

Require an arbitrary PHP file without leaking variables into scope


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?


Source: Tips4all

2 comments:

  1. Look at this:

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

    ReplyDelete
  2. 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() );

    ReplyDelete