#include "php_snuffleupagus.h"

static void should_disable(zend_execute_data* execute_data,
                           const char* complete_function_path,
                           const zend_string* builtin_param,
                           const char* builtin_param_name,
                           const sp_list_node* config,
                           const zend_string* current_filename);

static void should_drop_on_ret(const zval* return_value,
                               const sp_list_node* config,
                               const char* complete_function_path,
                               zend_execute_data* execute_data);

char* get_complete_function_path(zend_execute_data const* const execute_data) {
  if (!execute_data) {
    return NULL;  // LCOV_EXCL_LINE
  }
  const zend_function *const func = execute_data->func;
  if (!func || !(func->common.function_name)) {
    return NULL;
  }

  char const* const function_name = ZSTR_VAL(func->common.function_name);
  char* complete_path_function = NULL;

  if ((func->type == ZEND_USER_FUNCTION || func->type == ZEND_INTERNAL_FUNCTION) && func->common.scope) {
    const char *const class_name = ZSTR_VAL(func->common.scope->name);
    const size_t len = strlen(class_name) + 2 + strlen(function_name) + 1;
    complete_path_function = emalloc(len);
    snprintf(complete_path_function, len, "%s::%s", class_name, function_name);
  } else {
    complete_path_function = estrdup(function_name);
  }

  return complete_path_function;
}

static bool is_functions_list_matching(zend_execute_data* execute_data,
                                       sp_list_node* functions_list) {
  zend_execute_data *orig_execute_data, *current;
  orig_execute_data = current = execute_data;
  sp_list_node const* it = functions_list;

  while (current) {
    if (it == NULL) {  // every function in the list matched, we've got a match!
      EG(current_execute_data) = orig_execute_data;
      return true;
    }

    EG(current_execute_data) = current;

    char* const complete_path_function = get_complete_function_path(current);
    if (!complete_path_function) {
      break;
    }
    int match = strcmp(((char*)it->data), complete_path_function);
    efree(complete_path_function);

    if (0 == match) {
      it = it->next;
    }
    current = current->prev_execute_data;
  }

  EG(current_execute_data) = orig_execute_data;
  return false;
}

static bool is_local_var_matching(zend_execute_data* execute_data, const sp_disabled_function* const config_node) {
  zval* var_value = {0};

  var_value = sp_get_var_value(execute_data, config_node->var, false);
  if (var_value) {
    if (Z_TYPE_P(var_value) == IS_ARRAY) {
      if (config_node->key || config_node->r_key) {
        if (sp_match_array_key(var_value, config_node->key, config_node->r_key)) {
          return true;
        }
      } else if (sp_match_array_value(var_value, config_node->value, config_node->r_value)) {
        return true;
      }
    } else {
      zend_string const* const var_value_str = sp_zval_to_zend_string(var_value);
      bool match = sp_match_value(var_value_str, config_node->value, config_node->r_value);

      if (true == match) {
        return true;
      }
    }
  }
  return false;
}

static inline const char* get_fn_arg_name(zend_function *fn, uint32_t i) {
  if (fn->type == ZEND_USER_FUNCTION || (fn->common.fn_flags & ZEND_ACC_USER_ARG_INFO)) {
    return ZSTR_VAL(fn->op_array.arg_info[i].name);
  } else {
    return fn->internal_function.arg_info[i].name;
  }
}

static bool is_param_matching(zend_execute_data* execute_data,
                              sp_disabled_function const* const config_node,
                              const zend_string* builtin_param,
                              const char* builtin_param_name,
                              const char** arg_name,
                              const zend_string** arg_value_str) {
  // builtin functions
  if (builtin_param) {
    /* We're matching on a language construct (here named "builtin"),
    * and they can only take a single argument, but PHP considers them
    * differently than functions arguments. */
    *arg_name = builtin_param_name;
    *arg_value_str = builtin_param;
    return sp_match_value(builtin_param, config_node->value,
                          config_node->r_value);
  }

  // safeguards
  if (!execute_data || !execute_data->func) {
    sp_log_debug("no execute data -> silently ignore parameter matching");
    return false;
  }

  *arg_name = NULL;
  int call_num_args = EX_NUM_ARGS();
  zend_function *fn = execute_data->func;
  int fn_num_args = fn->common.num_args;

  if (!call_num_args) {
    sp_log_debug("no call arguments -> return");
    return false; // no arguments to check
  }

  if (config_node->pos > call_num_args - 1 || config_node->pos > fn_num_args) {
    // trying to match argument beyond last given argument OR beyond last declared argument.
    // this is perfectly normal for functions with
    //   (a) optional arguments
    //   (b) excess arguments
    //   (c) variadic arguments which are not supported
    return false;
  }

  zval* arg_value = NULL;

  if (config_node->pos > -1) {
    if (config_node->pos < fn_num_args) {
      *arg_name = get_fn_arg_name(fn, config_node->pos);
    }
    arg_value = ZEND_CALL_ARG(execute_data, config_node->pos + 1);
  } else if (config_node->param) {
    *arg_name = config_node->param->value;
    arg_value = sp_get_var_value(execute_data, config_node->param, true);
  } else if (config_node->r_param) {
    for (int i = 0; i < call_num_args; i++) {
      *arg_name = get_fn_arg_name(fn, i);
      if (true == sp_is_regexp_matching(config_node->r_param, *arg_name)) {
        arg_value = ZEND_CALL_ARG(execute_data, i + 1);
      }
    }
  }
  
  if (!arg_value) {
    sp_log_debug("no argument match -> return");
    return false;
  }

  if (config_node->param_type) {
    if (config_node->param_type == Z_TYPE_P(arg_value)) {
      if (!(config_node->key || config_node->r_key || config_node->value || config_node->r_value)) { // Are we matching on the `type` only?
        sp_log_debug("arg type match only.");
        return true;
      }
    } else {
      sp_log_debug("arg type mismatch -> return");
      return false;
    }
  }

  *arg_value_str = sp_zval_to_zend_string(arg_value);
  if (Z_TYPE_P(arg_value) == IS_ARRAY) {
    if (config_node->key || config_node->r_key) {
      if (sp_match_array_key(arg_value, config_node->key, config_node->r_key)) {
        return true;
      }
    } else if (sp_match_array_value(arg_value, config_node->value, config_node->r_value)) {
        return true;
    }
  } else if (sp_match_value(*arg_value_str, config_node->value, config_node->r_value)) {
    return true;
  }

  return false;
}

static zend_execute_data* is_file_matching(
    zend_execute_data* const execute_data,
    sp_disabled_function const* const config_node,
    zend_string const* const current_filename) {
#define ITERATE(ex)                                            \
  ex = ex->prev_execute_data;                                  \
  while (ex && (!ex->func || !ZEND_USER_CODE(ex->func->type))) \
    ex = ex->prev_execute_data;                                \
  if (!ex) return NULL;

  zend_execute_data* ex = execute_data;
  if (config_node->filename) {
    if (sp_zend_string_equals(current_filename, config_node->filename)) {
      return ex;  // LCOV_EXCL_LINE
    }
    ITERATE(ex);
    if (zend_string_equals(ex->func->op_array.filename, config_node->filename)) {
      return ex;  // LCOV_EXCL_LINE
    }
  } else if (config_node->r_filename) {
    if (sp_is_regexp_matching_zstr(config_node->r_filename, current_filename)) {
      return ex;
    }
    ITERATE(ex);
    if (sp_is_regexp_matching_zstr(config_node->r_filename, ex->func->op_array.filename)) {
      return ex;
    }
  }
  return NULL;
#undef ITERATE
}

inline static bool check_is_builtin_name(
    sp_disabled_function const* const config_node) {
  if (EXPECTED(config_node->function)) {
    return (zend_string_equals_literal(config_node->function, "include") ||
            zend_string_equals_literal(config_node->function, "include_once") ||
            zend_string_equals_literal(config_node->function, "require") ||
            zend_string_equals_literal(config_node->function, "require_once") ||
            zend_string_equals_literal(config_node->function, "echo"));
  }
  return false;  // LCOV_EXCL_LINE
}

void should_disable_ht(zend_execute_data* execute_data,
                       const char* function_name,
                       const zend_string* builtin_param,
                       const char* builtin_param_name,
                       const sp_list_node* config, const HashTable* ht) {
  const sp_list_node* ht_entry = NULL;
  zend_string* current_filename;

  if (!execute_data) {
    return;  // LCOV_EXCL_LINE
  }

  if (UNEXPECTED(builtin_param && !strcmp(function_name, "eval"))) {
    current_filename = get_eval_filename(zend_get_executed_filename());
  } else {
    const char* tmp = zend_get_executed_filename();
    current_filename = zend_string_init(tmp, strlen(tmp), 0);
  }

  ht_entry = zend_hash_str_find_ptr(ht, VAR_AND_LEN(function_name));

  if (ht_entry) {
    should_disable(execute_data, function_name, builtin_param,
                   builtin_param_name, ht_entry, current_filename);
  } else if (config && config->data) {
    should_disable(execute_data, function_name, builtin_param,
                   builtin_param_name, config, current_filename);
  }

  efree(current_filename);
}

static void should_disable(zend_execute_data* execute_data,
                           const char* complete_function_path,
                           const zend_string* builtin_param,
                           const char* builtin_param_name,
                           const sp_list_node* config,
                           const zend_string* current_filename) {
  char current_file_hash[SHA256_SIZE * 2 + 1] = {0};

  while (config) {
    sp_disabled_function const* const config_node = (sp_disabled_function*)(config->data);
    const char* arg_name = NULL;
    const zend_string* arg_value_str = NULL;

    /* The order matters, since when we have `config_node->functions_list`,
    we also do have `config_node->function` */
    if (config_node->functions_list) {
      if (false == is_functions_list_matching(execute_data, config_node->functions_list)) {
        goto next;
      }
    } else if (config_node->function) {
      if (0 != strcmp(ZSTR_VAL(config_node->function), complete_function_path)) {
        goto next;  // LCOV_EXCL_LINE
      }
    } else if (config_node->r_function) {
      if (false == sp_is_regexp_matching(config_node->r_function, complete_function_path)) {
        goto next;
      }
    }

    if (config_node->line) {
      if (config_node->line != zend_get_executed_lineno()) {
        goto next;
      }
    }

    if (config_node->filename || config_node->r_filename) {
      zend_execute_data* ex = is_file_matching(execute_data, config_node, current_filename);
      if (!ex) {
        goto next;
      }
    }

    if (config_node->cidr) {
      const char* client_ip = get_ipaddr();
      if (client_ip && false == cidr_match(client_ip, config_node->cidr)) {
        goto next;
      }
    }

    if (config_node->var) {
      if (false == is_local_var_matching(execute_data, config_node)) {
        goto next;
      }
    }

    if (config_node->hash) {
      if ('\0' == current_file_hash[0]) {
        compute_hash(ZSTR_VAL(current_filename), current_file_hash);
      }
      if (0 != strncmp(current_file_hash, ZSTR_VAL(config_node->hash), SHA256_SIZE)) {
        goto next;
      }
    }

    /* Check if we filter on parameter value*/
    if (config_node->param || config_node->r_param ||
        (config_node->pos != -1)  ||
        ((config_node->r_value || config_node->value) && !config_node->var)) {
      if (!builtin_param &&
#if PHP_VERSION_ID >= 80000
          ZEND_ARG_IS_VARIADIC(execute_data->func->op_array.arg_info)
#else
          execute_data->func->op_array.arg_info->is_variadic
#endif
      ) {
        sp_log_warn(
            "disable_function",
            "Snuffleupagus doesn't support variadic functions yet, sorry. "
            "Check https://github.com/jvoisin/snuffleupagus/issues/164 for "
            "details.");
      } else if (false == is_param_matching(execute_data, config_node, builtin_param, builtin_param_name, &arg_name, &arg_value_str)) {
        goto next;
      }
    }

    /* Everything matched.*/
    if (true == config_node->allow) {
      return;
    }

    if (config_node->functions_list) {
      sp_log_disable(ZSTR_VAL(config_node->function), arg_name, arg_value_str, config_node);
    } else {
      sp_log_disable(complete_function_path, arg_name, arg_value_str, config_node);
    }

  next:
    config = config->next;
  }
}

void should_drop_on_ret_ht(const zval* return_value, const char* function_name,
                           const sp_list_node* config, const HashTable* ht,
                           zend_execute_data* execute_data) {
  const sp_list_node* ht_entry = NULL;

  if (!function_name) {
    return;  // LCOV_EXCL_LINE
  }

  ht_entry = zend_hash_str_find_ptr(ht, function_name, strlen(function_name));

  if (ht_entry) {
    should_drop_on_ret(return_value, ht_entry, function_name, execute_data);
  } else if (config && config->data) {
    should_drop_on_ret(return_value, config, function_name, execute_data);
  }
}

static void should_drop_on_ret(const zval* return_value,
                               const sp_list_node* config,
                               const char* complete_function_path,
                               zend_execute_data* execute_data) {
  const char* current_filename = zend_get_executed_filename(TSRMLS_C);
  char current_file_hash[SHA256_SIZE * 2 + 1] = {0};
  bool match_type = false, match_value = false;

  while (config) {
    sp_disabled_function const* const config_node =
        (sp_disabled_function*)(config->data);

    assert(config_node->function || config_node->r_function);

    if (config_node->functions_list) {
      if (false == is_functions_list_matching(execute_data,
                                              config_node->functions_list)) {
        goto next;
      }
    } else if (config_node->function) {
      if (0 !=
          strcmp(ZSTR_VAL(config_node->function), complete_function_path)) {
        goto next;  // LCOV_EXCL_LINE
      }
    } else if (config_node->r_function) {
      if (false == sp_is_regexp_matching(config_node->r_function,
                                         complete_function_path)) {
        goto next;
      }
    }

    if (config_node->filename) { /* Check the current file name. */
      if (0 != strcmp(current_filename, ZSTR_VAL(config_node->filename))) {
        goto next;
      }
    } else if (config_node->r_filename) {
      if (false ==
          sp_is_regexp_matching(config_node->r_filename, current_filename)) {
        goto next;
      }
    }

    if (config_node->hash) {
      if ('\0' == current_file_hash[0]) {
        compute_hash(current_filename, current_file_hash);
      }
      if (0 != strncmp(current_file_hash, ZSTR_VAL(config_node->hash),
                       SHA256_SIZE)) {
        goto next;
      }
    }

    const zend_string* ret_value_str = NULL;
    sp_php_type ret_type = SP_PHP_TYPE_NULL;

    if (return_value) {
      ret_value_str = sp_zval_to_zend_string(return_value);
      ret_type = Z_TYPE_P(return_value);
    }

    match_type = (config_node->ret_type) &&
                 (config_node->ret_type == ret_type);
    match_value = return_value && (config_node->ret || config_node->r_ret) &&
                  (true == sp_match_value(ret_value_str, config_node->ret, config_node->r_ret));

    if (true == match_type || true == match_value) {
      if (true == config_node->allow) {
        return;
      }
      sp_log_disable_ret(complete_function_path, ret_value_str, config_node);
    }
  next:
    config = config->next;
  }
}

ZEND_FUNCTION(check_disabled_function) {
  zif_handler orig_handler;
  const char* current_function_name = get_active_function_name();

  should_disable_ht(execute_data, current_function_name, NULL, NULL, SPCFG(disabled_functions_reg).disabled_functions, SPCFG(disabled_functions_hooked));

  orig_handler = zend_hash_str_find_ptr(SPG(disabled_functions_hook), VAR_AND_LEN(current_function_name));
  orig_handler(INTERNAL_FUNCTION_PARAM_PASSTHRU);

  should_drop_on_ret_ht(return_value, current_function_name, SPCFG(disabled_functions_reg_ret).disabled_functions, SPCFG(disabled_functions_ret_hooked), execute_data);
}

static int hook_functions_regexp(const sp_list_node* config) {
  while (config && config->data) {
    const zend_string* function_name = ((sp_disabled_function*)config->data)->function;
    sp_regexp *function_name_sp_regexp = ((sp_disabled_function*)config->data)->r_function;
    const sp_pcre* function_name_regexp = function_name_sp_regexp ? function_name_sp_regexp->re : NULL;

    assert(function_name || function_name_regexp);

    if (function_name) {
      HOOK_FUNCTION(ZSTR_VAL(function_name), disabled_functions_hook, PHP_FN(check_disabled_function));
    } else {
      HOOK_FUNCTION_BY_REGEXP(function_name_regexp, disabled_functions_hook, PHP_FN(check_disabled_function));
    }

    config = config->next;
  }
  return SUCCESS;
}

static void hook_functions(HashTable* to_hook_ht, HashTable* hooked_ht) {
  zend_string* key;
  zval* value;

  ZEND_HASH_FOREACH_STR_KEY_VAL(to_hook_ht, key, value) {
    bool hooked = HOOK_FUNCTION(ZSTR_VAL(key), disabled_functions_hook, PHP_FN(check_disabled_function));
    bool is_builtin = check_is_builtin_name(((sp_list_node*)Z_PTR_P(value))->data);
    if (hooked || is_builtin) {
      zend_symtable_add_new(hooked_ht, key, value);
      zend_hash_del(to_hook_ht, key);
    }
  }
  ZEND_HASH_FOREACH_END();
}

ZEND_FUNCTION(eval_blacklist_callback) {
  zif_handler orig_handler;
  char* current_function_name = get_complete_function_path(EG(current_execute_data));

  if (!current_function_name) {
    return;
  }

  if( true == check_is_in_eval_whitelist(current_function_name)) {
    goto whitelisted;
  }

  if (SPG(in_eval) > 0) {
    const sp_config_eval* config_eval = &(SPCFG(eval));

    if (config_eval->dump) {
      sp_log_request(config_eval->dump, config_eval->textual_representation);
    }
    if (config_eval->simulation) {
      sp_log_simulation("eval", "A call to '%s' was tried in eval. logging it.", current_function_name);
    } else {
      sp_log_drop("eval", "A call to '%s' was tried in eval. dropping it.", current_function_name);
    }
  }

whitelisted:

  orig_handler = zend_hash_str_find_ptr(SPG(sp_eval_blacklist_functions_hook), current_function_name, strlen(current_function_name));
  efree(current_function_name);
  orig_handler(INTERNAL_FUNCTION_PARAM_PASSTHRU);
}

int hook_disabled_functions(void) {
  TSRMLS_FETCH();

  int ret = SUCCESS;

  hook_functions(SPCFG(disabled_functions), SPCFG(disabled_functions_hooked));
  hook_functions(SPCFG(disabled_functions_ret), SPCFG(disabled_functions_ret_hooked));

  ret |= hook_functions_regexp(SPCFG(disabled_functions_reg).disabled_functions);

  ret |= hook_functions_regexp(SPCFG(disabled_functions_reg_ret).disabled_functions);

  if (NULL != SPCFG(eval).blacklist) {
    sp_list_node* it = SPCFG(eval).blacklist;

    while (it) {
      hook_function(ZSTR_VAL((zend_string*)it->data),
                    SPG(sp_eval_blacklist_functions_hook),
                    PHP_FN(eval_blacklist_callback));
      it = it->next;
    }
  }
  return ret;
}

zend_write_func_t zend_write_default = NULL;

#if PHP_VERSION_ID >= 80000
size_t hook_echo(const char* str, size_t str_length) {
#else
int hook_echo(const char* str, size_t str_length) {
#endif
  zend_string* zs = zend_string_init(str, str_length, 0);

  should_disable_ht(EG(current_execute_data), "echo", zs, NULL, SPCFG(disabled_functions_reg).disabled_functions, SPCFG(disabled_functions_hooked));

  zend_string_release(zs);

  return zend_write_default(str, str_length);
}
