#include "php_snuffleupagus.h"

static inline const sp_cookie *sp_lookup_cookie_config(const zend_string *key) {
  const sp_list_node *it = SPCFG(cookie).cookies;

  while (it) {
    const sp_cookie *config = it->data;
    if (config && sp_match_value(key, config->name, config->name_r)) {
      return config;
    }
    it = it->next;
  }
  return NULL;
}

/* called at RINIT time with each cookie, eventually decrypt said cookie */
int decrypt_cookie(zval *pDest, int num_args, va_list args,
                   zend_hash_key *hash_key) {
  const zend_string *key = hash_key->key;
  const sp_cookie *cookie;

  /* If there is no key, it shouldn't be encrypted. */
  if (!key) {
    return ZEND_HASH_APPLY_KEEP;
  }

  cookie = sp_lookup_cookie_config(key);

  /* If the cookie isn't in the conf, it shouldn't be encrypted. */
  if (!cookie || !cookie->encrypt) {
    return ZEND_HASH_APPLY_KEEP;
  }

  /* If the cookie has no value, it shouldn't be encrypted. */
  if (0 == Z_STRLEN_P(pDest)) {
    return ZEND_HASH_APPLY_KEEP;
  }

  return decrypt_zval(pDest, cookie->simulation, hash_key);
}

static zend_string *encrypt_data(zend_string *data) {
  zend_string *z = encrypt_zval(data);
  sp_log_debug("Cookie value:%s:", ZSTR_VAL(z));
  return z;
}

#if PHP_VERSION_ID >= 70300
static void php_head_parse_cookie_options_array(
    zval *options, zend_long *expires, zend_string **path, zend_string **domain,
    zend_bool *secure, zend_bool *httponly, zend_string **samesite
#if PHP_VERSION_ID >= 80500
    ,bool *partitioned
#endif
    ) {
  int found = 0;
  zend_string *key;
  zval *value;

  ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(options), key, value) {
    if (key) {
      if (zend_string_equals_literal_ci(key, "expires")) {
        *expires = zval_get_long(value);
        found++;
      } else if (zend_string_equals_literal_ci(key, "path")) {
        *path = zval_get_string(value);
        found++;
      } else if (zend_string_equals_literal_ci(key, "domain")) {
        *domain = zval_get_string(value);
        found++;
      } else if (zend_string_equals_literal_ci(key, "secure")) {
        *secure = zval_is_true(value);
        found++;
      } else if (zend_string_equals_literal_ci(key, "httponly")) {
        *httponly = zval_is_true(value);
        found++;
      } else if (zend_string_equals_literal_ci(key, "samesite")) {
        *samesite = zval_get_string(value);
        found++;
#if PHP_VERSION_ID >= 80500
      } else if (zend_string_equals_literal_ci(key, "partitioned")) {
        *partitioned = zval_is_true(value);
        found++;
#endif
      } else {
        php_error_docref(NULL, E_WARNING,
                         "Unrecognized key '%s' found in the options array",
                         ZSTR_VAL(key));
      }
    } else {
      php_error_docref(NULL, E_WARNING,
                       "Numeric key found in the options array");
    }
  }
  ZEND_HASH_FOREACH_END();

  /* Array is not empty but no valid keys were found */
  if (found == 0 && zend_hash_num_elements(Z_ARRVAL_P(options)) > 0) {
    php_error_docref(NULL, E_WARNING,
                     "No valid options were found in the given array");
  }
}
#endif

PHP_FUNCTION(sp_setcookie) {
#if PHP_VERSION_ID >= 80500
  zend_bool partitioned = false;
#endif
  zend_string *name = NULL, *value = NULL, *path = NULL, *domain = NULL,
              *value_enc = NULL,
#if PHP_VERSION_ID < 70300
              *path_samesite = NULL;
#else
              *samesite = NULL;
#endif

  zend_long expires = 0;
  zval *expires_or_options = NULL;
  zend_bool secure = 0, httponly = 0;
  const sp_cookie *cookie_node = NULL;
  const char *cookie_samesite;

  // LCOV_EXCL_BR_START
  ZEND_PARSE_PARAMETERS_START(1, 7)
  Z_PARAM_STR(name)
  Z_PARAM_OPTIONAL
  Z_PARAM_STR(value)
  Z_PARAM_ZVAL(expires_or_options)
  Z_PARAM_STR(path)
  Z_PARAM_STR(domain)
  Z_PARAM_BOOL(secure)
  Z_PARAM_BOOL(httponly)
  ZEND_PARSE_PARAMETERS_END();
  // LCOV_EXCL_BR_END

  if (expires_or_options) {
#if PHP_VERSION_ID < 70300
    expires = zval_get_long(expires_or_options);
#else
    if (Z_TYPE_P(expires_or_options) == IS_ARRAY) {
      if (UNEXPECTED(ZEND_NUM_ARGS() > 3)) {
        php_error_docref(NULL, E_WARNING,
                         "Cannot pass arguments after the options array");
        RETURN_FALSE;
      }
      php_head_parse_cookie_options_array(expires_or_options, &expires, &path,
                                          &domain, &secure, &httponly, &samesite
#if PHP_VERSION_ID >= 80500
                                          , &partitioned
#endif
      );
    } else {
      expires = zval_get_long(expires_or_options);
    }
#endif
  }

  /* If the request was issued over HTTPS, the cookie should be "secure" */
  if (SPCFG(auto_cookie_secure).enable) {
    const zval server_vars = PG(http_globals)[TRACK_VARS_SERVER];
    if (Z_TYPE(server_vars) == IS_ARRAY) {
      const zval *is_https =
          zend_hash_str_find(Z_ARRVAL(server_vars), ZEND_STRL("HTTPS"));
      if (NULL != is_https) {
        secure = 1;
      }
    }
  }

  /* lookup existing configuration for said cookie */
  cookie_node = sp_lookup_cookie_config(name);

  /* If the cookie's value is encrypted, it won't be usable by
   * javascript anyway.
   */
  if (cookie_node && cookie_node->encrypt) {
    httponly = 1;
  }

  /* Shall we encrypt the cookie's value? */
  if (cookie_node && cookie_node->encrypt && value) {
    value_enc = encrypt_data(value);
  }

  if (cookie_node && cookie_node->samesite) {
    if (!path) {
      path = zend_string_init("", 0, 0);
    }
#if PHP_VERSION_ID < 70300
    cookie_samesite = (cookie_node->samesite == lax)
                          ? SAMESITE_COOKIE_FORMAT SP_TOKEN_SAMESITE_LAX
                          : SAMESITE_COOKIE_FORMAT SP_TOKEN_SAMESITE_STRICT;

    /* Concatenating everything, as is in PHP internals */
    path_samesite = zend_string_init(ZSTR_VAL(path), ZSTR_LEN(path), 0);
    path_samesite = zend_string_extend(
        path_samesite, ZSTR_LEN(path) + strlen(cookie_samesite) + 1, 0);
    memcpy(ZSTR_VAL(path_samesite) + ZSTR_LEN(path), cookie_samesite,
           strlen(cookie_samesite) + 1);
#else
    cookie_samesite = (cookie_node->samesite == lax) ? SP_TOKEN_SAMESITE_LAX
                                                     : SP_TOKEN_SAMESITE_STRICT;

    samesite = zend_string_init(cookie_samesite, strlen(cookie_samesite), 0);
#endif
  }

#if PHP_VERSION_ID < 70300
  if (php_setcookie(name, (value_enc ? value_enc : value), expires,
                    (path_samesite ? path_samesite : path), domain, secure, 1,
                    httponly) == SUCCESS) {
#elif PHP_VERSION_ID < 80500
  if (php_setcookie(name, (value_enc ? value_enc : value), expires, path,
                    domain, secure, httponly, samesite, 1) == SUCCESS) {
#else
  if (!secure) {
    // Can't have partitioned cookies without the secure flag.
    partitioned = false;
  }
  if (php_setcookie(name, (value_enc ? value_enc : value), expires, path,
                    domain, secure, httponly, samesite, partitioned, 1) == SUCCESS) {
#endif
    RETVAL_TRUE;
  } else {
    RETVAL_FALSE;
  }

  if (value_enc) {
    zend_string_release(value_enc);
  }
#if PHP_VERSION_ID < 70300
  if (path_samesite) {
    zend_string_release(path_samesite);
  }
#endif
}

int hook_cookies() {
  HOOK_FUNCTION("setcookie", sp_internal_functions_hook, PHP_FN(sp_setcookie));

  return SUCCESS;
}
