php_cli_finder.php
303 lines
| 1 | <?php |
| 2 | |
| 3 | // Namespace |
| 4 | namespace BMI\Plugin\PHPCLI; |
| 5 | |
| 6 | // Use |
| 7 | use BMI\Plugin\Backup_Migration_Plugin as BMP; |
| 8 | |
| 9 | // Exit on direct access |
| 10 | if (!defined('ABSPATH')) exit; |
| 11 | |
| 12 | /** |
| 13 | * Scans for PHP CLI executable and makes sure exec is working |
| 14 | */ |
| 15 | class Checker { |
| 16 | |
| 17 | /** |
| 18 | * Points if the exec is disabled or not |
| 19 | */ |
| 20 | public $ini_disabled = true; |
| 21 | |
| 22 | /** |
| 23 | * __construct - Unused construct function |
| 24 | * |
| 25 | * @return {self} |
| 26 | */ |
| 27 | function __construct() {} |
| 28 | |
| 29 | /** |
| 30 | * findPHP - Scans the system to find executable PHP |
| 31 | * |
| 32 | * @return {bool/array} false on fail, array on success |
| 33 | * $final_executable = [ |
| 34 | * 'version' => $php_cli_version, |
| 35 | * 'brand' => $php_brand, |
| 36 | * 'memory' => $php_cli_memory . '/' . $php_cli_memory_modified, |
| 37 | * 'max_exec' => $php_cli_max_exec . '/' . $php_cli_max_exec_modified, |
| 38 | * 'executable' => $executable_path_to_file |
| 39 | * ]; |
| 40 | */ |
| 41 | public function findPHP() { |
| 42 | |
| 43 | // Return false if exec is disabled |
| 44 | if ($this->isExecEnabled() === false) return false; |
| 45 | |
| 46 | // Check if user defined own PHP CLI |
| 47 | $user_defined = false; |
| 48 | if (defined('BMI_CLI_EXECUTABLE')) { |
| 49 | if (file_exists(BMI_CLI_EXECUTABLE)) { |
| 50 | $user_defined = true; |
| 51 | } |
| 52 | } |
| 53 | |
| 54 | if ($user_defined === false) { |
| 55 | |
| 56 | // Makes variable for system paths list |
| 57 | $system_paths = null; |
| 58 | |
| 59 | // Exec command which displays all paths per line |
| 60 | @exec('(sed "s/:/\n/g" <<< $PATH) 2>&1', $system_paths); |
| 61 | |
| 62 | // Concat output |
| 63 | $system_paths = implode("\n", $system_paths); |
| 64 | |
| 65 | // Check if the output is not empty, if empty abort |
| 66 | // if (empty($system_paths) || !$system_paths) return false; |
| 67 | |
| 68 | // Make variable for executables that contains php keyword |
| 69 | $executables = ['php', '/usr/bin/php', '/usr/bin/php8.0', '/usr/bin/php7.4']; |
| 70 | |
| 71 | // Make variable for "for" loop |
| 72 | $system_paths = explode("\n", $system_paths); |
| 73 | |
| 74 | if (in_array('/usr/bin', $system_paths)) $system_paths[] = '/usr/bin'; |
| 75 | if (in_array('/bin', $system_paths)) $system_paths[] = '/bin'; |
| 76 | if (in_array('/etc/alternatives', $system_paths)) $system_paths[] = '/etc/alternatives'; |
| 77 | if (in_array('/usr/local/bin', $system_paths)) $system_paths[] = '/usr/local/bin'; |
| 78 | |
| 79 | // Loop all paths and check for PHP executables |
| 80 | for ($i = 0; $i < sizeof($system_paths); ++$i) { |
| 81 | |
| 82 | // Variable for scan output |
| 83 | $executables_scan = null; |
| 84 | |
| 85 | // Trim the path just in case |
| 86 | $path = trim($system_paths[$i]); |
| 87 | |
| 88 | // If path is empty ignore and continue |
| 89 | if (empty($path)) continue; |
| 90 | |
| 91 | // Exec command which will display PHP (in name) executables |
| 92 | @exec('(for i in $(ls ' . $path . ' | grep "php"); do [ -x ' . $path . '/$i ] && echo ' . $path . '/$i || echo ""; done;) 2>&1', $executables_scan); |
| 93 | |
| 94 | // Implode the output |
| 95 | $executables_scan = implode("\n", $executables_scan); |
| 96 | |
| 97 | // Merge the array with other results |
| 98 | $executables = array_merge($executables, explode("\n", $executables_scan)); |
| 99 | |
| 100 | } |
| 101 | |
| 102 | } |
| 103 | |
| 104 | // Make variable for real PHP executables |
| 105 | $php_executables = []; |
| 106 | |
| 107 | // If used defined own PHP CLI use it |
| 108 | if ($user_defined === true) $executables = [BMI_CLI_EXECUTABLE]; |
| 109 | |
| 110 | // Filter the array to exclude empty values and remove duplicates |
| 111 | $executables = array_filter(array_unique($executables)); |
| 112 | |
| 113 | // Make variable for final executable |
| 114 | $final_executable = false; |
| 115 | |
| 116 | // Loop and test the executables |
| 117 | foreach ($executables as $exe) { |
| 118 | |
| 119 | // If path+name does not contain php ignore and continue |
| 120 | if (strpos($exe, 'php') === false) continue; |
| 121 | |
| 122 | // Make variable for CLI version in shell |
| 123 | $shell_version = null; |
| 124 | |
| 125 | // Make variable for regex check |
| 126 | $output = null; |
| 127 | |
| 128 | // Exec the command to check shell displayed version |
| 129 | @exec($exe . ' --version 2>&1', $shell_version); |
| 130 | |
| 131 | // Implode the output |
| 132 | $shell_version = implode("\n", $shell_version); |
| 133 | |
| 134 | // Test the output with regex to find PHP version and brand |
| 135 | preg_match('/PHP\ (.*)\ (.*)\ \(built: (.*)\)/i', $shell_version, $output); |
| 136 | |
| 137 | // Check if the output is not empty and contains at least 4 results |
| 138 | if ($output && !empty($output) && !empty($output[1]) && sizeof($output) >= 4) { |
| 139 | |
| 140 | // Remove additional characters from the version leave only numbers with dots |
| 141 | $php = preg_replace("/[^0-9.]/", "", $output[1]); |
| 142 | |
| 143 | // Save the brand in the $brand variable |
| 144 | $brand = $output[2]; |
| 145 | |
| 146 | // Make variable for file version test |
| 147 | $file_version = null; |
| 148 | |
| 149 | // Make variable for shell inline PHP version test |
| 150 | $inline_version = null; |
| 151 | |
| 152 | // Exec the shell inline test for version |
| 153 | @exec($exe . ' -r "echo phpversion();" 2>&1', $inline_version); |
| 154 | |
| 155 | // Implode the version |
| 156 | $inline_version = implode("\n", $inline_version); |
| 157 | |
| 158 | // Check if the version match required minimum |
| 159 | if (version_compare($inline_version, '7.0', '>=')) { |
| 160 | |
| 161 | // Path to CLI check file |
| 162 | $path_to_cli = BMI_INCLUDES . '/cli/version_check.php'; |
| 163 | |
| 164 | // Exec the file check if it can run files |
| 165 | @exec($exe . ' -f ' . $path_to_cli . ' 2>&1', $file_version); |
| 166 | |
| 167 | // Check if the output is correct |
| 168 | if ($file_version && is_array($file_version) && !empty($file_version) && sizeof($file_version) >= 5) { |
| 169 | |
| 170 | // The results from file saved to named variables |
| 171 | $php_cli_version = $file_version[0]; |
| 172 | $php_cli_memory = $file_version[1]; |
| 173 | $php_cli_max_exec = $file_version[2]; |
| 174 | $php_cli_memory_modified = $file_version[3]; |
| 175 | $php_cli_max_exec_modified = $file_version[4]; |
| 176 | |
| 177 | // Check if the version match the inline one (it's the same php.ini) |
| 178 | if (trim($php_cli_version) == trim($inline_version)) { |
| 179 | |
| 180 | // If it match use this PHP CLI module |
| 181 | $final_executable = [ |
| 182 | 'brand' => $brand, |
| 183 | 'version' => $php_cli_version, |
| 184 | 'memory' => $php_cli_memory . '/' . $php_cli_memory_modified, |
| 185 | 'max_exec' => $php_cli_max_exec . '/' . $php_cli_max_exec_modified, |
| 186 | 'executable' => $exe |
| 187 | ]; |
| 188 | |
| 189 | break; |
| 190 | |
| 191 | } |
| 192 | |
| 193 | } |
| 194 | |
| 195 | } |
| 196 | |
| 197 | } |
| 198 | |
| 199 | } |
| 200 | |
| 201 | // Return the final result |
| 202 | return $final_executable; |
| 203 | |
| 204 | } |
| 205 | |
| 206 | /** |
| 207 | * isExecAvailable - Check if exec is not blocked by php.ini |
| 208 | * |
| 209 | * @return {bool} true on success / false on fail |
| 210 | */ |
| 211 | public function isExecAvailable() { |
| 212 | |
| 213 | // Get disabled functions |
| 214 | $disabled_functions = @ini_get('disable_functions'); |
| 215 | |
| 216 | // Turn disabled functions to array |
| 217 | $disabled_functions = explode(',', $disabled_functions); |
| 218 | |
| 219 | // Check if everything is allowed |
| 220 | if (empty($disabled_functions)) { |
| 221 | |
| 222 | // Check if function is callable and not disabled (PHP 8 check) |
| 223 | if (function_exists('shell_exec') && is_callable('shell_exec')) return true; |
| 224 | else return false; |
| 225 | |
| 226 | } |
| 227 | |
| 228 | // Few checks |
| 229 | if (!is_array($disabled_functions)) return false; |
| 230 | elseif (in_array('shell_exec', $disabled_functions)) return false; |
| 231 | elseif (in_array('exec', $disabled_functions)) return false; |
| 232 | elseif (in_array('system', $disabled_functions)) return false; |
| 233 | else { |
| 234 | |
| 235 | // Check if function is callable and not disabled (PHP 8 check) |
| 236 | if (function_exists('shell_exec') && is_callable('shell_exec')) return true; |
| 237 | else return false; |
| 238 | |
| 239 | } |
| 240 | |
| 241 | } |
| 242 | |
| 243 | /** |
| 244 | * isExecEnabled - Checks if exec function is not disabled or blocked |
| 245 | * |
| 246 | * @return {bool} true on success / false on fail |
| 247 | */ |
| 248 | public function isExecEnabled() { |
| 249 | |
| 250 | // Check if the function is not disabled in php.ini |
| 251 | if ($this->isExecAvailable()) { |
| 252 | |
| 253 | // Mark as enabled in php_ini |
| 254 | $this->ini_disabled = false; |
| 255 | |
| 256 | // Try to run simple shell |
| 257 | try { |
| 258 | |
| 259 | // Output variable |
| 260 | $output = null; |
| 261 | |
| 262 | // Execute the command |
| 263 | @exec('echo "It works!" 2>&1', $output); |
| 264 | |
| 265 | // Implode the output just in case |
| 266 | if ($output) { |
| 267 | $output = implode("\n", $output); |
| 268 | } else return false; |
| 269 | |
| 270 | // Check if the output is as expected |
| 271 | if ($output === 'It works!') return true; |
| 272 | else return false; |
| 273 | |
| 274 | // Catch errors in older PHP |
| 275 | } catch (\Error $e) { |
| 276 | |
| 277 | return false; |
| 278 | |
| 279 | // Catch exceptions if any |
| 280 | } catch (\Exception $e) { |
| 281 | |
| 282 | return false; |
| 283 | |
| 284 | // Catch throwable exception if any |
| 285 | } catch (\Throwable $e) { |
| 286 | |
| 287 | return false; |
| 288 | |
| 289 | } |
| 290 | |
| 291 | // If the function is blocked do not even try and return false |
| 292 | } else { |
| 293 | |
| 294 | // Mark as disabled in php_ini |
| 295 | $this->ini_disabled = true; |
| 296 | return false; |
| 297 | |
| 298 | } |
| 299 | |
| 300 | } |
| 301 | |
| 302 | } |
| 303 |