EvalProvider   A
last analyzed

Complexity

Total Complexity 37

Size/Duplication

Total Lines 221
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 1

Test Coverage

Coverage 97.01%

Importance

Changes 0
Metric Value
dl 0
loc 221
rs 9.44
c 0
b 0
f 0
ccs 65
cts 67
cp 0.9701
wmc 37
lcom 1
cbo 1

15 Methods

Rating   Name   Duplication   Size   Complexity  
A evaluate() 0 30 3
A addCodeHandler() 0 6 2
A applyHandlersToCode() 0 6 2
A backupGlobals() 0 8 3
A restoreGlobals() 0 10 4
A executeCode() 0 12 5
A trimPhpTags() 0 9 1
A forceEndingSemicolon() 0 4 2
A adaptCodeToEval() 0 5 1
A setOpenBaseDirs() 0 3 1
A forcePhpConsoleClassesAutoLoad() 0 8 5
A applyOpenBaseDirSetting() 0 11 4
A disableFileAccessByOpenBaseDir() 0 3 1
A addSharedVar() 0 3 1
A addSharedVarReference() 0 6 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() {
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() {
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) {
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
117 1
				$GLOBALS[$var] = $value;
118
			}
119 2
			elseif(!isset($$var)) {
120 3
				$$var = $value;
121
			}
122
		}
123
124 22
		return eval($_code);
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 {
238
239
	public $return;
240
	public $output;
241
	public $time;
242
	/** @var  \Exception|null */
243
	public $exception;
244
}
245