Skip to content
Permalink
 
 
Cannot retrieve contributors at this time
<?php
namespace PhpConsole;
/**
* Execute PHP code with some security & accessibility tweaks
*
* @package PhpConsole
* @version 3.1
* @link http://consle.com
* @author Sergey Barbushin http://linkedin.com/in/barbushin
* @copyright © Sergey Barbushin, 2011-2013. All rights reserved.
* @license http://www.opensource.org/licenses/BSD-3-Clause "The BSD 3-Clause License"
*/
class EvalProvider {
protected $sharedVars = array();
protected $openBaseDirs = array();
protected $codeCallbackHandlers = array();
protected $globalsBackup;
/**
* Execute PHP code handling execution time, output & exception
* @param string $code
* @return EvalResult
*/
public function evaluate($code) {
$code = $this->applyHandlersToCode($code);
$code = $this->adaptCodeToEval($code);
$this->backupGlobals();
$this->applyOpenBaseDirSetting();
$startTime = microtime(true);
static::executeCode('', $this->sharedVars);
$selfTime = microtime(true) - $startTime;
ob_start();
$result = new EvalResult();
$startTime = microtime(true);
try {
$result->return = static::executeCode($code, $this->sharedVars);
}
catch(\Throwable $exception) {
$result->exception = $exception;
}
catch(\Exception $exception) {
$result->exception = $exception;
}
$result->time = abs(microtime(true) - $startTime - $selfTime);
$result->output = ob_get_clean();
$this->restoreGlobals();
return $result;
}
/**
* Add callback that will be called with &$code var reference before code execution
* @param $callback
* @throws \Exception
*/
public function addCodeHandler($callback) {
if(!is_callable($callback)) {
throw new \Exception('Argument is not callable');
}
$this->codeCallbackHandlers[] = $callback;
}
/**
* Call added code handlers
* @param $code
* @return mixed
*/
protected function applyHandlersToCode($code) {
foreach($this->codeCallbackHandlers as $callback) {
call_user_func_array($callback, array(&$code));
}
return $code;
}
/**
* Store global vars data in backup var
*/
protected function backupGlobals() {
$this->globalsBackup = array();
foreach($GLOBALS as $key => $value) {
if($key != 'GLOBALS') {
$this->globalsBackup[$key] = $value;
}
}
}
/**
* Restore global vars data from backup var
*/
protected function restoreGlobals() {
foreach($this->globalsBackup as $key => $value) {
$GLOBALS[$key] = $value;
}
foreach(array_diff(array_keys($GLOBALS), array_keys($this->globalsBackup)) as $newKey) {
if($newKey != 'GLOBALS') {
unset($GLOBALS[$newKey]);
}
}
}
/**
* Execute code with shared vars
* @param $_code
* @param array $_sharedVars
* @return mixed
*/
protected static function executeCode($_code, array $_sharedVars) {
foreach($_sharedVars as $var => $value) {
if(isset($GLOBALS[$var]) && $var[0] == '_') { // extract($this->sharedVars, EXTR_OVERWRITE) and $$var = $value do not overwrites global vars
$GLOBALS[$var] = $value;
}
elseif(!isset($$var)) {
$$var = $value;
}
}
return eval($_code);
}
/**
* Prepare code PHP tags be correctly passed to eval() function
* @param string $code
* @return string
*/
protected function trimPhpTags($code) {
$replace = array(
'~^(\s*)<\?=~s' => '\1echo ',
'~^(\s*)<\?(php)?~is' => '\1',
'~\?>\s*$~s' => '',
'~<\?(php)?[\s;]*$~is' => '',
);
return preg_replace(array_keys($replace), $replace, $code);
}
/**
* Add semicolon to the end of code if it's required
* @param string $code
* @return string
*/
protected function forceEndingSemicolon($code) {
$code = rtrim($code, "; \r\n");
return $code[strlen($code) - 1] != '}' ? $code . ';' : $code;
}
/**
* Apply some default code handlers
* @param string $code
* @return string
*/
protected function adaptCodeToEval($code) {
$code = $this->trimPhpTags($code);
$code = $this->forceEndingSemicolon($code);
return $code;
}
/**
* Protect response code access only to specified directories using http://www.php.net/manual/en/ini.core.php#ini.open-basedir
* IMPORTANT: classes autoload methods will work only for specified directories
* @param array $openBaseDirs
* @codeCoverageIgnore
*/
public function setOpenBaseDirs(array $openBaseDirs) {
$this->openBaseDirs = $openBaseDirs;
}
/**
* Autoload all PHP Console classes
* @codeCoverageIgnore
*/
protected function forcePhpConsoleClassesAutoLoad() {
foreach(new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator(__DIR__), \RecursiveIteratorIterator::LEAVES_ONLY) as $path) {
/** @var $path \SplFileInfo */
if($path->isFile() && $path->getExtension() == 'php' && $path->getFilename() !== 'PsrLogger.php') {
require_once($path->getPathname());
}
}
}
/**
* Set actual "open_basedir" PHP ini option
* @throws \Exception
* @codeCoverageIgnore
*/
protected function applyOpenBaseDirSetting() {
if($this->openBaseDirs) {
$value = implode(PATH_SEPARATOR, $this->openBaseDirs);
if(ini_get('open_basedir') != $value) {
$this->forcePhpConsoleClassesAutoLoad();
if(ini_set('open_basedir', $value) === false) {
throw new \Exception('Unable to set "open_basedir" php.ini setting');
}
}
}
}
/**
* Protect response code from reading/writing/including any files using http://www.php.net/manual/en/ini.core.php#ini.open-basedir
* IMPORTANT: It does not protects from system(), exec(), passthru(), popen() & etc OS commands execution functions
* IMPORTANT: Classes autoload methods will not work, so all required classes must be loaded before code evaluation
* @codeCoverageIgnore
*/
public function disableFileAccessByOpenBaseDir() {
$this->setOpenBaseDirs(array(__DIR__ . '/not_existed_dir' . mt_rand()));
}
/**
* Add var that will be implemented in PHP code executed from PHP Console debug panel (will be implemented in PHP Console > v3.0)
* @param $name
* @param $var
* @throws \Exception
*/
public function addSharedVar($name, $var) {
$this->addSharedVarReference($name, $var);
}
/**
* Add var that will be implemented in PHP code executed from PHP Console debug panel (will be implemented in PHP Console > v3.0)
* @param $name
* @param $var
* @throws \Exception
*/
public function addSharedVarReference($name, &$var) {
if(isset($this->sharedVars[$name])) {
throw new \Exception('Var with name "' . $name . '" already added');
}
$this->sharedVars[$name] =& $var;
}
}
class EvalResult {
public $return;
public $output;
public $time;
/** @var \Exception|null */
public $exception;
}