<?php

// Namespace
namespace BMI\Plugin\PHPCLI;

// Use
use BMI\Plugin\Backup_Migration_Plugin as BMP;

// Exit on direct access
if (!defined('ABSPATH')) exit;

/**
 * Scans for PHP CLI executable and makes sure exec is working
 */
class Checker {

  /**
   *  Points if the exec is disabled or not
   */
  public $ini_disabled = true;

  /**
   * __construct - Unused construct function
   *
   * @return {self}
   */
  function __construct() {}

  /**
   * findPHP - Scans the system to find executable PHP
   *
   * @return {bool/array} false on fail, array on success
   * $final_executable = [
   *   'version' => $php_cli_version,
   *   'brand' => $php_brand,
   *   'memory' => $php_cli_memory . '/' . $php_cli_memory_modified,
   *   'max_exec' => $php_cli_max_exec . '/' . $php_cli_max_exec_modified,
   *   'executable' => $executable_path_to_file
   * ];
   */
  public function findPHP() {

    // Return false if exec is disabled
    if ($this->isExecEnabled() === false) return false;

    // Check if user defined own PHP CLI
    $user_defined = false;
    if (defined('BMI_CLI_EXECUTABLE')) {
      if (file_exists(BMI_CLI_EXECUTABLE)) {
        $user_defined = true;
      }
    }

    if ($user_defined === false) {

      // Makes variable for system paths list
      $system_paths = null;

      // Exec command which displays all paths per line
      @exec('(sed "s/:/\n/g" <<< $PATH) 2>&1', $system_paths);

      // Concat output
      $system_paths = implode("\n", $system_paths);

      // Check if the output is not empty, if empty abort
      // if (empty($system_paths) || !$system_paths) return false;

      // Make variable for executables that contains php keyword
      $executables = ['php', '/usr/bin/php', '/usr/bin/php8.0', '/usr/bin/php7.4'];

      // Make variable for "for" loop
      $system_paths = explode("\n", $system_paths);
      
      if (in_array('/usr/bin', $system_paths)) $system_paths[] = '/usr/bin';
      if (in_array('/bin', $system_paths)) $system_paths[] = '/bin';
      if (in_array('/etc/alternatives', $system_paths)) $system_paths[] = '/etc/alternatives';
      if (in_array('/usr/local/bin', $system_paths)) $system_paths[] = '/usr/local/bin';

      // Loop all paths and check for PHP executables
      for ($i = 0; $i < sizeof($system_paths); ++$i) {

        // Variable for scan output
        $executables_scan = null;

        // Trim the path just in case
        $path = trim($system_paths[$i]);

        // If path is empty ignore and continue
        if (empty($path)) continue;

        // Exec command which will display PHP (in name) executables
        @exec('(for i in $(ls ' . $path . ' | grep "php"); do [ -x ' . $path . '/$i ] && echo ' . $path . '/$i || echo ""; done;) 2>&1', $executables_scan);

        // Implode the output
        $executables_scan = implode("\n", $executables_scan);

        // Merge the array with other results
        $executables = array_merge($executables, explode("\n", $executables_scan));

      }

    }

    // Make variable for real PHP executables
    $php_executables = [];

    // If used defined own PHP CLI use it
    if ($user_defined === true) $executables = [BMI_CLI_EXECUTABLE];

    // Filter the array to exclude empty values and remove duplicates
    $executables = array_filter(array_unique($executables));

    // Make variable for final executable
    $final_executable = false;

    // Loop and test the executables
    foreach ($executables as $exe) {

      // If path+name does not contain php ignore and continue
      if (strpos($exe, 'php') === false) continue;

      // Make variable for CLI version in shell
      $shell_version = null;

      // Make variable for regex check
      $output = null;

      // Exec the command to check shell displayed version
      @exec($exe . ' --version 2>&1', $shell_version);

      // Implode the output
      $shell_version = implode("\n", $shell_version);

      // Test the output with regex to find PHP version and brand
      preg_match('/PHP\ (.*)\ (.*)\ \(built: (.*)\)/i', $shell_version, $output);

      // Check if the output is not empty and contains at least 4 results
      if ($output && !empty($output) && !empty($output[1]) && sizeof($output) >= 4) {

        // Remove additional characters from the version leave only numbers with dots
        $php = preg_replace("/[^0-9.]/", "", $output[1]);

        // Save the brand in the $brand variable
        $brand = $output[2];

        // Make variable for file version test
        $file_version = null;

        // Make variable for shell inline PHP version test
        $inline_version = null;

        // Exec the shell inline test for version
        @exec($exe . ' -r "echo phpversion();" 2>&1', $inline_version);

        // Implode the version
        $inline_version = implode("\n", $inline_version);

        // Check if the version match required minimum
        if (version_compare($inline_version, '7.0', '>=')) {

          // Path to CLI check file
          $path_to_cli = BMI_INCLUDES . '/cli/version_check.php';

          // Exec the file check if it can run files
          @exec($exe . ' -f ' . $path_to_cli . ' 2>&1', $file_version);

          // Check if the output is correct
          if ($file_version && is_array($file_version) && !empty($file_version) && sizeof($file_version) >= 5) {

            // The results from file saved to named variables
            $php_cli_version = $file_version[0];
            $php_cli_memory = $file_version[1];
            $php_cli_max_exec = $file_version[2];
            $php_cli_memory_modified = $file_version[3];
            $php_cli_max_exec_modified = $file_version[4];

            // Check if the version match the inline one (it's the same php.ini)
            if (trim($php_cli_version) == trim($inline_version)) {

              // If it match use this PHP CLI module
              $final_executable = [
                'brand' => $brand,
                'version' => $php_cli_version,
                'memory' => $php_cli_memory . '/' . $php_cli_memory_modified,
                'max_exec' => $php_cli_max_exec . '/' . $php_cli_max_exec_modified,
                'executable' => $exe
              ];

              break;

            }

          }

        }

      }

    }

    // Return the final result
    return $final_executable;

  }

  /**
   * isExecAvailable - Check if exec is not blocked by php.ini
   *
   * @return {bool} true on success / false on fail
   */
  public function isExecAvailable() {

    // Get disabled functions
    $disabled_functions = @ini_get('disable_functions');

    // Turn disabled functions to array
    $disabled_functions = explode(',', $disabled_functions);

    // Check if everything is allowed
    if (empty($disabled_functions)) {

      // Check if function is callable and not disabled (PHP 8 check)
      if (function_exists('shell_exec') && is_callable('shell_exec')) return true;
      else return false;

    }

    // Few checks
    if (!is_array($disabled_functions)) return false;
    elseif (in_array('shell_exec', $disabled_functions)) return false;
    elseif (in_array('exec', $disabled_functions)) return false;
    elseif (in_array('system', $disabled_functions)) return false;
    else {

      // Check if function is callable and not disabled (PHP 8 check)
      if (function_exists('shell_exec') && is_callable('shell_exec')) return true;
      else return false;

    }

  }

  /**
   * isExecEnabled - Checks if exec function is not disabled or blocked
   *
   * @return {bool} true on success / false on fail
   */
  public function isExecEnabled() {

    // Check if the function is not disabled in php.ini
    if ($this->isExecAvailable()) {

      // Mark as enabled in php_ini
      $this->ini_disabled = false;

      // Try to run simple shell
      try {

        // Output variable
        $output = null;

        // Execute the command
        @exec('echo "It works!" 2>&1', $output);

        // Implode the output just in case
        if ($output) {
          $output = implode("\n", $output);
        } else return false;

        // Check if the output is as expected
        if ($output === 'It works!') return true;
        else return false;

      // Catch errors in older PHP
      } catch (\Error $e) {

        return false;

      // Catch exceptions if any
      } catch (\Exception $e) {

        return false;

      // Catch throwable exception if any
      } catch (\Throwable $e) {

        return false;

      }

    // If the function is blocked do not even try and return false
    } else {

      // Mark as disabled in php_ini
      $this->ini_disabled = true;
      return false;

    }

  }

}
