fork(47) download
  1. <?php
  2.  
  3. class Ext_Sandbox_PHPValidator
  4. {
  5. public static function php_syntax_error($code, $tokens = null)
  6. {
  7. $braces = 0;
  8. $inString = 0;
  9.  
  10. $isCodeHtml = false;
  11.  
  12. if (!$tokens) {
  13. $tokens = token_get_all('<?php ' . $code);
  14. }
  15.  
  16. // First of all, we need to know if braces are correctly balanced.
  17. // This is not trivial due to variable interpolation which
  18. // occurs in heredoc, backticked and double quoted strings
  19. foreach ($tokens as $token) {
  20. if (is_array($token)) {
  21. switch ($token[0]) {
  22. case T_CURLY_OPEN:
  23. case T_DOLLAR_OPEN_CURLY_BRACES:
  24. case T_START_HEREDOC:
  25. ++$inString;
  26. break;
  27. case T_END_HEREDOC:
  28. --$inString;
  29. break;
  30.  
  31. case T_OPEN_TAG:
  32. $isCodeHtml = false;
  33. break;
  34. case T_CLOSE_TAG:
  35. $isCodeHtml = true;
  36. break;
  37. }
  38. } else if ($inString & 1) {
  39. switch ($token) {
  40. case '`':
  41. case '"':
  42. --$inString;
  43. break;
  44. }
  45. } else {
  46. switch ($token) {
  47. case '`':
  48. case '"':
  49. ++$inString;
  50. break;
  51.  
  52. case '{':
  53. ++$braces;
  54. break;
  55. case '}':
  56. if ($inString) --$inString;
  57. else {
  58. --$braces;
  59. if ($braces < 0) break 2;
  60. }
  61.  
  62. break;
  63. }
  64. }
  65. }
  66.  
  67. // Display parse error messages and use output buffering to catch them
  68. $inString = @ini_set('log_errors', false);
  69. $token = @ini_set('display_errors', true);
  70.  
  71. // If $braces is not zero, then we are sure that $code is broken.
  72. // We run it anyway in order to catch the error message and line number.
  73.  
  74. // Else, if $braces are correctly balanced, then we can safely put
  75. // $code in a dead code sandbox to prevent its execution.
  76. // Note that without this sandbox, a function or class declaration inside
  77. // $code could throw a "Cannot redeclare" fatal error.
  78.  
  79. $braces || $code = "if(0){?><?php {$code}\n" . ($isCodeHtml ? '<?php }' : '}');
  80.  
  81. if (false === eval($code)) {
  82. if ($braces) $braces = PHP_INT_MAX;
  83. else {
  84. // Get the maximum number of lines in $code to fix a border case
  85. false !== strpos($code, "\r") && $code = strtr(str_replace("\r\n", "\n", $code), "\r", "\n");
  86. $braces = substr_count($code, "\n");
  87. }
  88.  
  89. $code = ob_get_clean();
  90. $code = strip_tags($code);
  91.  
  92. // Get the error message and line number
  93. if (preg_match("/syntax error, (.+) in .+ on line (\\d+)$/s", $code, $code)) {
  94. $code[2] = (int)$code[2];
  95. $code = $code[2] <= $braces
  96. ? array($code[1], $code[2])
  97. : array('unexpected $end ' . substr($code[1], 14), $braces);
  98. } else $code = array('syntax error', 0);
  99. } else {
  100. $code = false;
  101. }
  102.  
  103. @ini_set('display_errors', $token);
  104. @ini_set('log_errors', $inString);
  105.  
  106. return $code;
  107. }
  108.  
  109. public static function validatePHPCode($source, $functions = array(), $enable = true)
  110. {
  111. $inner_functions = array();
  112.  
  113. $func_started = 0;
  114.  
  115. $previousToken = null;
  116.  
  117. $callStack = array();
  118.  
  119. $tokens = token_get_all('<?php ' . $source);
  120.  
  121. if ($error = self::php_syntax_error($source, $tokens)) {
  122. throw new Exception($error[0] . ': ' . $error[1]);
  123. }
  124.  
  125. $previousTokenContent = '';
  126.  
  127. foreach ($tokens as $token) {
  128. if (is_string($token)) {
  129. if ($token == '(') {
  130. if ($func_started) {
  131. $func_started = 0;
  132. } else {
  133. if ($previousToken[0] == T_STRING) {
  134. $funcSearch = implode('::', $callStack);
  135. if (strlen($funcSearch) && $funcSearch[0] != '$') {
  136. if (!count($callStack) || (!$enable ^ (false === array_search($funcSearch, $functions)))) {
  137. if (!in_array($funcSearch, $inner_functions)) {
  138. throw new Exception('Function is disabled: ' . $funcSearch);
  139. }
  140. }
  141. }
  142. }
  143.  
  144. if (in_array($previousToken[0], array(T_VARIABLE, T_STRING_VARNAME, T_ENCAPSED_AND_WHITESPACE))) {
  145. if (!in_array($previousTokenContent, array('+', '-', '*', '/', '.', '^', '&', '?', '!', '%', '@'))) {
  146. throw new Exception('Only direct function calls allowed, line ' . $previousToken[2]);
  147. }
  148. }
  149. }
  150. }
  151.  
  152. if ($token != '.') {
  153. $callStack = array();
  154. }
  155. } else {
  156. list($id, $text) = $token;
  157.  
  158. if (in_array($id, array(T_COMMENT, T_DOC_COMMENT, T_WHITESPACE))) {
  159. continue;
  160. }
  161.  
  162. switch ($id) {
  163. case T_FUNCTION:
  164. $func_started = 1;
  165. break;
  166. case T_STRING:
  167. $callStack[] = $text;
  168.  
  169. if ($func_started) {
  170. $inner_functions[] = $text;
  171. }
  172. break;
  173. case T_NS_SEPARATOR:
  174. case T_DOUBLE_COLON:
  175. case T_OBJECT_OPERATOR:
  176. break;
  177. case T_VARIABLE:
  178. $callStack[] = $token[1];
  179. break;
  180. case T_EVAL:
  181. if (!$enable ^ (false === array_search('eval', $functions))) {
  182. throw new Exception('Eval is disabled, line ' . $token[2]);
  183. }
  184. break;
  185. case T_PRINT:
  186. if (!$enable ^ (false === array_search('print', $functions))) {
  187. throw new Exception('Print is disabled, line ' . $token[2]);
  188. }
  189. break;
  190. case T_ECHO:
  191. if (!$enable ^ (false === array_search('echo', $functions))) {
  192. throw new Exception('Echo is disabled, line ' . $token[2]);
  193. }
  194. break;
  195. case T_EXIT:
  196. if (!$enable ^ ((false === array_search('die', $functions)) && (false === array_search('exit', $functions)))) {
  197. throw new Exception('Exit/Die is disabled, line ' . $token[2]);
  198. }
  199. break;
  200. case T_THROW:
  201. if (!$enable ^ (false === array_search('throw', $functions))) {
  202. throw new Exception('Throw is disabled, line ' . $token[2]);
  203. }
  204. break;
  205. default:
  206. $callStack = array();
  207. break;
  208. }
  209. }
  210.  
  211. $previousToken = $token;
  212. }
  213.  
  214. return true;
  215. }
  216. }
  217.  
  218.  
  219.  
  220. ##################################
  221.  
  222. $code = <<<PHP
  223. \$b = 1;
  224. \$c = 2;
  225. \$a = \$b + \$c;
  226. echo \$a;
  227.  
  228. class test {
  229.   public function __construct() {
  230.   echo 'construct';
  231.   }
  232.   public function foo(\$num) {
  233.   var_dump(\$num);
  234.   }
  235. }
  236.  
  237. \$test = new test();
  238. \$test->foo(\$a);
  239. PHP;
  240.  
  241. // validate the code
  242. $validator = new Ext_Sandbox_PHPValidator();
  243.  
  244. try
  245. {
  246. // we enable only one function - echo, all others will throw error
  247. $validator->validatePHPCode( $code, array('echo'), true);
  248. $status = 'passed';
  249. }
  250. catch(Exception $ex)
  251. {
  252. $status = $ex->getMessage();
  253. }
  254.  
  255. echo 'Status of validation is: ' . $status;
Success #stdin #stdout 0.01s 20520KB
stdin
Standard input is empty
stdout
Status of validation is: Function is disabled: var_dump