#include "php_snuffleupagus.h"
#include "ext/standard/php_string.h"

static void (*orig_execute_ex)(zend_execute_data *execute_data) = NULL;
static void (*orig_zend_execute_internal)(zend_execute_data *execute_data,
                                          zval *return_value) = NULL;
#if PHP_VERSION_ID < 80100
static int (*orig_zend_stream_open)(const char *filename, zend_file_handle *handle) = NULL;
#else
static zend_result (*orig_zend_stream_open)(zend_file_handle *handle) = NULL;
#endif

// FIXME handle symlink
ZEND_COLD static inline void terminate_if_writable(char const* const filename) {
  sp_config_readonly_exec const* const config_ro_exec = &(SPCFG(readonly_exec));
  char const *errmsg = "unknown access problem";

  if (0 == access(filename, W_OK)) {
    errmsg = "Attempted execution of a writable file";
    goto violation;
  }
  if (errno != EACCES && errno != EROFS && errno != EPERM) {
    goto err;
  }

  // other checks are 'extended checks' that can be enabled/disabled via config
  if (!config_ro_exec->extended_checks) {
    return;
  }

  struct stat buf;
  if (0 != stat(filename, &buf)) {
    goto err;
  }
  if (buf.st_uid == geteuid()) {
    errmsg = "Attempted execution of a file owned by the PHP process";
    goto violation;
  }

  char *const dirname = estrndup(filename, strlen(filename));
  php_dirname(dirname, strlen(dirname));
  if (0 == access(dirname, W_OK)) {
    errmsg = "Attempted execution of a file in a writable directory";

    efree(dirname);
    goto violation;
  }
  if (errno != EACCES && errno != EROFS && errno != EPERM) {
    efree(dirname);
    goto err;
  }

  if (0 != stat(dirname, &buf)) {
    efree(dirname);
    goto err;
  }
  efree(dirname);
  if (buf.st_uid == geteuid()) {
    errmsg = "Attempted execution of a file in directory owned by the PHP process";
    goto violation;
  }

  return;

violation:
  if (config_ro_exec->dump) {
    sp_log_request(config_ro_exec->dump, config_ro_exec->textual_representation);
  }
  if (config_ro_exec->simulation) {
    sp_log_simulation("readonly_exec", "%s (%s)", errmsg, filename);
  } else {
    sp_log_drop("readonly_exec", "%s (%s)", errmsg, filename);
  }
  return;

err:
  sp_log_err("readonly_exec", "Error while accessing %s: %s", filename, strerror(errno));
}

inline static void is_builtin_matching(
    const zend_string *restrict const param_value,
    const char *restrict const function_name,
    const char *restrict const param_name, const sp_list_node *config,
    const HashTable *ht) {
  if (!config || !config->data) {
    return;
  }

  should_disable_ht(EG(current_execute_data), function_name, param_value, param_name, SPCFG(disabled_functions_reg).disabled_functions, ht);
}

static void is_in_eval_and_whitelisted(zend_execute_data const* const execute_data) {
  sp_config_eval const* const config_eval = &(SPCFG(eval));

  if (EXPECTED(0 == SPG(in_eval))) {
    return;
  }

  if (EXPECTED(NULL == config_eval->whitelist)) {
    return;
  }

  if (zend_is_executing() && !EX(func)) {
    return;  // LCOV_EXCL_LINE
  }

  char *function_name = get_complete_function_path(execute_data);
  if (!function_name) {
    return;
  }

  if (UNEXPECTED(false == check_is_in_eval_whitelist(function_name))) {
    if (config_eval->dump) {
      sp_log_request(config_eval->dump, config_eval->textual_representation);
    }
    if (config_eval->simulation) {
      sp_log_simulation("Eval_whitelist", "The function '%s' isn't in the eval whitelist, logging its call.", function_name);
      goto out;
    } else {
      sp_log_drop("Eval_whitelist", "The function '%s' isn't in the eval whitelist, dropping its call.", function_name);
    }
  }

out:
  efree(function_name);
}

/* This function gets the filename in which `eval()` is called from,
 * since it looks like "foo.php(1) : eval()'d code", so we're starting
 * from the end of the string until the second closing parenthesis. */
zend_string *get_eval_filename(const char *const filename) {
  int count = 0;
  zend_string *clean_filename = zend_string_init(filename, strlen(filename), 0);

  for (int i = ZSTR_LEN(clean_filename); i >= 0; i--) {
    if (ZSTR_VAL(clean_filename)[i] == '(') {
      if (count == 1) {
        ZSTR_VAL(clean_filename)[i] = '\0';
        clean_filename = zend_string_truncate(clean_filename, i, 0);
        break;
      }
      count++;
    }
  }
  return clean_filename;
}

static inline void sp_orig_execute(zend_execute_data *execute_data) {
  SPG(execution_depth)++;
  if (SPCFG(max_execution_depth) > 0 && SPG(execution_depth) > SPCFG(max_execution_depth)) {
    sp_log_drop("execute", "Maximum recursion limit reached. Script terminated.");
  }
  orig_execute_ex(execute_data);
  SPG(execution_depth)--;
}

static inline void sp_check_writable(zend_execute_data *execute_data) {
  if (execute_data && EX(func) && EX(func)->op_array.filename && SPCFG(readonly_exec).enable) {
    terminate_if_writable(ZSTR_VAL(EX(func)->op_array.filename));
  }
}

static inline void sp_call_orig_execute(INTERNAL_FUNCTION_PARAMETERS, bool internal) {
  if (internal) {
    if (UNEXPECTED(NULL != orig_zend_execute_internal)) {
      orig_zend_execute_internal(INTERNAL_FUNCTION_PARAM_PASSTHRU);
    } else {
      EX(func)->internal_function.handler(INTERNAL_FUNCTION_PARAM_PASSTHRU);
    }
  } else {
    sp_orig_execute(execute_data);
  }
}

static inline void sp_execute_handler(INTERNAL_FUNCTION_PARAMETERS, bool internal) {
  if (!execute_data) {
    return;  // LCOV_EXCL_LINE
  }

  is_in_eval_and_whitelisted(execute_data);

  if (!internal) {
    if (UNEXPECTED(EX(func)->op_array.type == ZEND_EVAL_CODE)) {
      sp_list_node const* const config = zend_hash_str_find_ptr(SPCFG(disabled_functions), ZEND_STRL("eval"));

#if PHP_VERSION_ID >= 80000
      is_builtin_matching(SPG(eval_source_string), "eval", "code", config, SPCFG(disabled_functions));
#else
      is_builtin_matching(Z_STR_P(SPG(eval_source_string)), "eval", "code", config, SPCFG(disabled_functions));
#endif

      SPG(in_eval)++;
      sp_orig_execute(execute_data);
      SPG(in_eval)--;
      return;
    }

    sp_check_writable(execute_data);
  }

  if (!SPG(hook_execute)) {
    sp_call_orig_execute(INTERNAL_FUNCTION_PARAM_PASSTHRU, internal);
    return;
  }

  char *function_name = get_complete_function_path(execute_data);

  if (!function_name) {
    sp_call_orig_execute(INTERNAL_FUNCTION_PARAM_PASSTHRU, internal);
    return;
  }

  bool is_hooked = (zend_hash_str_find(SPG(disabled_functions_hook), VAR_AND_LEN(function_name)) || zend_hash_str_find(SPG(disabled_functions_hook), VAR_AND_LEN(function_name)));
  if (is_hooked) {
    sp_call_orig_execute(INTERNAL_FUNCTION_PARAM_PASSTHRU, internal);
    efree(function_name);
    return;
  }

  // If we're at an internal function
  if (!execute_data->prev_execute_data ||
      !execute_data->prev_execute_data->func ||
      !ZEND_USER_CODE(execute_data->prev_execute_data->func->type) ||
      !execute_data->prev_execute_data->opline) {
    should_disable_ht(execute_data, function_name, NULL, NULL, SPCFG(disabled_functions_reg).disabled_functions, SPCFG(disabled_functions));
  } else {  // If we're at a userland function call
    switch (execute_data->prev_execute_data->opline->opcode) {
      case ZEND_DO_FCALL:
      case ZEND_DO_FCALL_BY_NAME:
      case ZEND_DO_ICALL:
      case ZEND_DO_UCALL:
      case ZEND_TICKS:
        should_disable_ht(execute_data, function_name, NULL, NULL, SPCFG(disabled_functions_reg).disabled_functions, SPCFG(disabled_functions));
      default:
        break;
    }
  }

  // When a function's return value isn't used, php doesn't store it in the
  // execute_data, so we need to use a local variable to be able to match on
  // it later.
  zval ret_val;
  if (EX(return_value) == NULL && return_value == NULL) {
    memset(&ret_val, 0, sizeof(ret_val));
    return_value = EX(return_value) = &ret_val;
  }

  sp_call_orig_execute(INTERNAL_FUNCTION_PARAM_PASSTHRU, internal);

  should_drop_on_ret_ht(return_value, function_name, SPCFG(disabled_functions_reg_ret).disabled_functions, SPCFG(disabled_functions_ret), execute_data);

  efree(function_name);

  if (EX(return_value) == &ret_val) {
    return_value = EX(return_value) = NULL;
  }
}

static void sp_execute_ex(zend_execute_data *execute_data) {
  sp_execute_handler(execute_data, execute_data ? EX(return_value) : NULL, false);
}

static void sp_zend_execute_internal(INTERNAL_FUNCTION_PARAMETERS) {
  sp_execute_handler(INTERNAL_FUNCTION_PARAM_PASSTHRU, true);
}

static inline void sp_stream_open_checks(zend_string *zend_filename, zend_file_handle *handle) {
  zend_execute_data const *const data = EG(current_execute_data);

  if ((NULL == data) || (NULL == data->opline) ||
      (data->func->type != ZEND_USER_FUNCTION)) {
    return;
  }

  HashTable const* const disabled_functions_hooked = SPCFG(disabled_functions_hooked);

  switch (data->opline->opcode) {
    case ZEND_INCLUDE_OR_EVAL:
      if (SPCFG(readonly_exec).enable) {
        char *fn = ZSTR_VAL(zend_filename);
        if (ZSTR_LEN(zend_filename) >= strlen("file://") && memcmp(fn, "file://", strlen("file://")) == 0) {
          fn += strlen("file://");
        } else if (!php_memnstr(ZSTR_VAL(zend_filename), "://", strlen("://"), ZSTR_VAL(zend_filename) + ZSTR_LEN(zend_filename))) {
          // ignore stream wrappers other than file:// for now
          terminate_if_writable(fn);
        }
      }
      switch (data->opline->extended_value) {
        case ZEND_INCLUDE:
          is_builtin_matching(
              zend_filename, "include", "inclusion path",
              zend_hash_str_find_ptr(disabled_functions_hooked, ZEND_STRL("include")),
              disabled_functions_hooked);
          break;
        case ZEND_REQUIRE:
          is_builtin_matching(
              zend_filename, "require", "inclusion path",
              zend_hash_str_find_ptr(disabled_functions_hooked, ZEND_STRL("require")),
              disabled_functions_hooked);
          break;
        case ZEND_REQUIRE_ONCE:
          is_builtin_matching(
              zend_filename, "require_once", "inclusion path",
              zend_hash_str_find_ptr(disabled_functions_hooked, ZEND_STRL("require_once")),
              disabled_functions_hooked);
          break;
        case ZEND_INCLUDE_ONCE:
          is_builtin_matching(
              zend_filename, "include_once", "inclusion path",
              zend_hash_str_find_ptr(disabled_functions_hooked, ZEND_STRL("include_once")),
              disabled_functions_hooked);
          break;
          EMPTY_SWITCH_DEFAULT_CASE();  // LCOV_EXCL_LINE
      }
  }
}

#if PHP_VERSION_ID < 80100

static int sp_stream_open(const char *filename, zend_file_handle *handle) {
  zend_string *zend_filename = zend_string_init(filename, strlen(filename), 0);

  sp_stream_open_checks(zend_filename, handle);

  zend_string_release_ex(zend_filename, 0);
  return orig_zend_stream_open(filename, handle);
}

#else // PHP >= 8.1

static zend_result sp_stream_open(zend_file_handle *handle) {
  sp_stream_open_checks(handle->filename, handle);
  return orig_zend_stream_open(handle);
}

#endif

ZEND_API zend_op_array* (*orig_zend_compile_file)(zend_file_handle* file_handle,
                                                  int type) = NULL;
#if PHP_VERSION_ID >= 80200
ZEND_API zend_op_array* (*orig_zend_compile_string)(
    zend_string* source_string, const char* filename,
    enum _zend_compile_position position) = NULL;
#elif PHP_VERSION_ID >= 80000
ZEND_API zend_op_array* (*orig_zend_compile_string)(
    zend_string* source_string, const char* filename) = NULL;
#else
ZEND_API zend_op_array* (*orig_zend_compile_string)(zval* source_string,
                                                    char* filename) = NULL;
#endif

#if PHP_VERSION_ID >= 80200
ZEND_API zend_op_array* sp_compile_string(zend_string* source_string,
                                          const char* filename,
					  enum _zend_compile_position position) {
#elif PHP_VERSION_ID >= 80000
ZEND_API zend_op_array* sp_compile_string(zend_string* source_string,
                                          const char* filename) {
#else
ZEND_API zend_op_array* sp_compile_string(zval* source_string, char* filename) {
#endif
  // TODO(jvoisin) handle recursive calls to `eval`
  SPG(eval_source_string) = source_string;
  zend_op_array* opline = orig_zend_compile_string(source_string, filename
#if PHP_VERSION_ID >= 80200
    , position
#endif
    );
  if (SPCFG(sloppy).enable) {
    sp_sloppy_modify_opcode(opline);
  }
  return opline;
}

ZEND_API zend_op_array* sp_compile_file(zend_file_handle* file_handle,
                                        int type) {
  zend_op_array* opline = orig_zend_compile_file(file_handle, type);
  if (SPCFG(sloppy).enable) {
    sp_sloppy_modify_opcode(opline);
  }
  return opline;
}

int hook_execute(void) {
  TSRMLS_FETCH();

  if (NULL == orig_execute_ex && NULL == orig_zend_stream_open) {
    if (zend_execute_ex != sp_execute_ex) {
      /* zend_execute_ex is used for "user" function calls */
      orig_execute_ex = zend_execute_ex;
      zend_execute_ex = sp_execute_ex;
    }

    if (zend_execute_internal != sp_zend_execute_internal) {
      /* zend_execute_internal is used for "builtin" functions calls */
      orig_zend_execute_internal = zend_execute_internal;
      zend_execute_internal = sp_zend_execute_internal;
    }

    if (zend_stream_open_function != sp_stream_open) {
      /* zend_stream_open_function is used for include-related stuff */
      orig_zend_stream_open = zend_stream_open_function;
      zend_stream_open_function = sp_stream_open;
    }
  }

  if (NULL == orig_zend_compile_file && zend_compile_file != sp_compile_file) {
    orig_zend_compile_file = zend_compile_file;
    zend_compile_file = sp_compile_file;
  }

  if (NULL == orig_zend_compile_string &&
      zend_compile_string != sp_compile_string) {
    orig_zend_compile_string = zend_compile_string;
    zend_compile_string = sp_compile_string;
  }

  return SUCCESS;
}
