Passed
Push — master ( b0f5d7...c171ec )
by Sergey
02:30
created

EvalProvider::forceEndingSemicolon()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
ccs 3
cts 3
cp 1
crap 2
1
<?php
2
3
namespace PhpConsole;
4
5
/**
6
 * Execute PHP code with some security & accessibility tweaks
7
 *
8
 * @package PhpConsole
9
 * @version 3.1
10
 * @link http://consle.com
11
 * @author Sergey Barbushin http://linkedin.com/in/barbushin
12
 * @copyright © Sergey Barbushin, 2011-2013. All rights reserved.
13
 * @license http://www.opensource.org/licenses/BSD-3-Clause "The BSD 3-Clause License"
14
 */
15
class EvalProvider {
16
17
	protected $sharedVars = array();
18
	protected $openBaseDirs = array();
19
	protected $codeCallbackHandlers = array();
20
	protected $globalsBackup;
21
22
	/**
23
	 * Execute PHP code handling execution time, output & exception
24
	 * @param string $code
25
	 * @return EvalResult
26
	 */
27 22
	public function evaluate($code) {
28 22
		$code = $this->applyHandlersToCode($code);
29 22
		$code = $this->adaptCodeToEval($code);
30
31 22
		$this->backupGlobals();
32 22
		$this->applyOpenBaseDirSetting();
33
34 22
		$startTime = microtime(true);
35 22
		static::executeCode('', $this->sharedVars);
36 22
		$selfTime = microtime(true) - $startTime;
37
38 22
		ob_start();
39 22
		$result = new EvalResult();
40 22
		$startTime = microtime(true);
41
		try {
42 22
			$result->return = static::executeCode($code, $this->sharedVars);
43
		}
44 3
		catch(\Throwable $exception) {
0 ignored issues
show
Bug introduced by
The class Throwable does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
45 3
			$result->exception = $exception;
0 ignored issues
show
Documentation Bug introduced by
It seems like $exception of type object<Throwable> is incompatible with the declared type object<Exception>|null of property $exception.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
46
		}
47
		catch(\Exception $exception) {
48
			$result->exception = $exception;
49
		}
50 22
		$result->time = abs(microtime(true) - $startTime - $selfTime);
51 22
		$result->output = ob_get_clean();
52
53 22
		$this->restoreGlobals();
54
55 22
		return $result;
56
	}
57
58
	/**
59
	 * Add callback that will be called with &$code var reference before code execution
60
	 * @param $callback
61
	 * @throws \Exception
62
	 */
63 2
	public function addCodeHandler($callback) {
64 2
		if(!is_callable($callback)) {
65 1
			throw new \Exception('Argument is not callable');
66
		}
67 1
		$this->codeCallbackHandlers[] = $callback;
68 1
	}
69
70
	/**
71
	 * Call added code handlers
72
	 * @param $code
73
	 * @return mixed
74
	 */
75 22
	protected function applyHandlersToCode($code) {
76 22
		foreach($this->codeCallbackHandlers as $callback) {
77 1
			call_user_func_array($callback, array(&$code));
78
		}
79 22
		return $code;
80
	}
81
82
	/**
83
	 * Store global vars data in backup var
84
	 */
85 22
	protected function backupGlobals() {
0 ignored issues
show
Coding Style introduced by
backupGlobals uses the super-global variable $GLOBALS which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
86 22
		$this->globalsBackup = array();
87 22
		foreach($GLOBALS as $key => $value) {
88 22
			if($key != 'GLOBALS') {
89 22
				$this->globalsBackup[$key] = $value;
90
			}
91
		}
92 22
	}
93
94
	/**
95
	 * Restore global vars data from backup var
96
	 */
97 22
	protected function restoreGlobals() {
0 ignored issues
show
Coding Style introduced by
restoreGlobals uses the super-global variable $GLOBALS which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
98 22
		foreach($this->globalsBackup as $key => $value) {
99 22
			$GLOBALS[$key] = $value;
100
		}
101 22
		foreach(array_diff(array_keys($GLOBALS), array_keys($this->globalsBackup)) as $newKey) {
102 22
			if($newKey != 'GLOBALS') {
103 22
				unset($GLOBALS[$newKey]);
104
			}
105
		}
106 22
	}
107
108
	/**
109
	 * Execute code with shared vars
110
	 * @param $_code
111
	 * @param array $_sharedVars
112
	 * @return mixed
113
	 */
114 22
	protected static function executeCode($_code, array $_sharedVars) {
0 ignored issues
show
Coding Style introduced by
executeCode uses the super-global variable $GLOBALS which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
115 22
		foreach($_sharedVars as $var => $value) {
116 3
			if(isset($GLOBALS[$var]) && $var[0] == '_') { // extract($this->sharedVars, EXTR_OVERWRITE) and $$var = $value do not overwrites global vars
0 ignored issues
show
Unused Code Comprehensibility introduced by
38% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
117 1
				$GLOBALS[$var] = $value;
118
			}
119 2
			elseif(!isset($$var)) {
120 3
				$$var = $value;
121
			}
122
		}
123
124 22
		return eval($_code);
0 ignored issues
show
Coding Style introduced by
It is generally not recommended to use eval unless absolutely required.

On one hand, eval might be exploited by malicious users if they somehow manage to inject dynamic content. On the other hand, with the emergence of faster PHP runtimes like the HHVM, eval prevents some optimization that they perform.

Loading history...
125
	}
126
127
	/**
128
	 * Prepare code PHP tags be correctly passed to eval() function
129
	 * @param string $code
130
	 * @return string
131
	 */
132 22
	protected function trimPhpTags($code) {
133
		$replace = array(
134 22
			'~^(\s*)<\?=~s' => '\1echo ',
135
			'~^(\s*)<\?(php)?~is' => '\1',
136
			'~\?>\s*$~s' => '',
137
			'~<\?(php)?[\s;]*$~is' => '',
138
		);
139 22
		return preg_replace(array_keys($replace), $replace, $code);
140
	}
141
142
	/**
143
	 * Add semicolon to the end of code if it's required
144
	 * @param string $code
145
	 * @return string
146
	 */
147 22
	protected function forceEndingSemicolon($code) {
148 22
		$code = rtrim($code, "; \r\n");
149 22
		return $code[strlen($code) - 1] != '}' ? $code . ';' : $code;
150
	}
151
152
	/**
153
	 * Apply some default code handlers
154
	 * @param string $code
155
	 * @return string
156
	 */
157 22
	protected function adaptCodeToEval($code) {
158 22
		$code = $this->trimPhpTags($code);
159 22
		$code = $this->forceEndingSemicolon($code);
160 22
		return $code;
161
	}
162
163
	/**
164
	 * Protect response code access only to specified directories using http://www.php.net/manual/en/ini.core.php#ini.open-basedir
165
	 * IMPORTANT: classes autoload methods will work only for specified directories
166
	 * @param array $openBaseDirs
167
	 * @codeCoverageIgnore
168
	 */
169
	public function setOpenBaseDirs(array $openBaseDirs) {
170
		$this->openBaseDirs = $openBaseDirs;
171
	}
172
173
	/**
174
	 * Autoload all PHP Console classes
175
	 * @codeCoverageIgnore
176
	 */
177
	protected function forcePhpConsoleClassesAutoLoad() {
178
		foreach(new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator(__DIR__), \RecursiveIteratorIterator::LEAVES_ONLY) as $path) {
179
			/** @var $path \SplFileInfo */
180
			if($path->isFile() && $path->getExtension() == 'php' && $path->getFilename() !== 'PsrLogger.php') {
181
				require_once($path->getPathname());
182
			}
183
		}
184
	}
185
186
	/**
187
	 * Set actual "open_basedir" PHP ini option
188
	 * @throws \Exception
189
	 * @codeCoverageIgnore
190
	 */
191
	protected function applyOpenBaseDirSetting() {
192
		if($this->openBaseDirs) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->openBaseDirs of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
193
			$value = implode(PATH_SEPARATOR, $this->openBaseDirs);
194
			if(ini_get('open_basedir') != $value) {
195
				$this->forcePhpConsoleClassesAutoLoad();
196
				if(ini_set('open_basedir', $value) === false) {
197
					throw new \Exception('Unable to set "open_basedir" php.ini setting');
198
				}
199
			}
200
		}
201
	}
202
203
	/**
204
	 * Protect response code from reading/writing/including any files using http://www.php.net/manual/en/ini.core.php#ini.open-basedir
205
	 * IMPORTANT: It does not protects from system(), exec(), passthru(), popen() & etc OS commands execution functions
206
	 * IMPORTANT: Classes autoload methods will not work, so all required classes must be loaded before code evaluation
207
	 * @codeCoverageIgnore
208
	 */
209
	public function disableFileAccessByOpenBaseDir() {
210
		$this->setOpenBaseDirs(array(__DIR__ . '/not_existed_dir' . mt_rand()));
211
	}
212
213
	/**
214
	 * Add var that will be implemented in PHP code executed from PHP Console debug panel (will be implemented in PHP Console > v3.0)
215
	 * @param $name
216
	 * @param $var
217
	 * @throws \Exception
218
	 */
219 4
	public function addSharedVar($name, $var) {
220 4
		$this->addSharedVarReference($name, $var);
221 4
	}
222
223
	/**
224
	 * Add var that will be implemented in PHP code executed from PHP Console debug panel (will be implemented in PHP Console > v3.0)
225
	 * @param $name
226
	 * @param $var
227
	 * @throws \Exception
228
	 */
229 5
	public function addSharedVarReference($name, &$var) {
230 5
		if(isset($this->sharedVars[$name])) {
231 2
			throw new \Exception('Var with name "' . $name . '" already added');
232
		}
233 5
		$this->sharedVars[$name] =& $var;
234 5
	}
235
}
236
237
class EvalResult {
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class should be in its own file to aid autoloaders.

Having each class in a dedicated file usually plays nice with PSR autoloaders and is therefore a well established practice. If you use other autoloaders, you might not want to follow this rule.

Loading history...
238
239
	public $return;
240
	public $output;
241
	public $time;
242
	/** @var  \Exception|null */
243
	public $exception;
244
}
245