/*
   +----------------------------------------------------------------------+
   | Copyright © The PHP Group and Contributors.                          |
   +----------------------------------------------------------------------+
   | This source file is subject to the Modified BSD License that is      |
   | bundled with this package in the file LICENSE, and is available      |
   | through the World Wide Web at <https://www.php.net/license/>.        |
   |                                                                      |
   | SPDX-License-Identifier: BSD-3-Clause                                |
   +----------------------------------------------------------------------+
   | Authors: Sara Golemon <pollita@php.net>                              |
   +----------------------------------------------------------------------+
*/

#include "php_http.h"
#include "php_ini.h"
#include "url.h"
#include "SAPI.h"
#include "zend_exceptions.h"
#include "basic_functions.h"
#include "zend_enum.h"

static void php_url_encode_scalar(zval *scalar, smart_str *form_str,
	int encoding_type, zend_ulong index_int,
	const char *index_string, size_t index_string_len,
	const char *num_prefix, size_t num_prefix_len,
	const zend_string *key_prefix,
	const zend_string *arg_sep)
{
	if (form_str->s) {
		smart_str_append(form_str, arg_sep);
	}
	/* Simple key=value */
	if (key_prefix) {
		smart_str_append(form_str, key_prefix);
	}
	if (index_string) {
		php_url_encode_to_smart_str(form_str, index_string, index_string_len, encoding_type == PHP_QUERY_RFC3986);
	} else {
		/* Numeric key */
		if (num_prefix) {
			smart_str_appendl(form_str, num_prefix, num_prefix_len);
		}
		smart_str_append_long(form_str, index_int);
	}
	if (key_prefix) {
		smart_str_appendl(form_str, "%5D", strlen("%5D"));
	}
	smart_str_appendc(form_str, '=');

try_again:
	switch (Z_TYPE_P(scalar)) {
		case IS_STRING:
			php_url_encode_to_smart_str(form_str, Z_STRVAL_P(scalar), Z_STRLEN_P(scalar), encoding_type == PHP_QUERY_RFC3986);
			break;
		case IS_LONG:
			smart_str_append_long(form_str, Z_LVAL_P(scalar));
			break;
		case IS_DOUBLE: {
			zend_string *tmp = zend_double_to_str(Z_DVAL_P(scalar));
			php_url_encode_to_smart_str(form_str, ZSTR_VAL(tmp), ZSTR_LEN(tmp), encoding_type == PHP_QUERY_RFC3986);
			zend_string_free(tmp);
			break;
		}
		case IS_FALSE:
			smart_str_appendc(form_str, '0');
			break;
		case IS_TRUE:
			smart_str_appendc(form_str, '1');
			break;
		case IS_OBJECT:
			ZEND_ASSERT(Z_OBJCE_P(scalar)->ce_flags & ZEND_ACC_ENUM);
			if (Z_OBJCE_P(scalar)->enum_backing_type == IS_UNDEF) {
				zend_value_error("Unbacked enum %s cannot be converted to a string", ZSTR_VAL(Z_OBJCE_P(scalar)->name));
				return;
			}
			scalar = zend_enum_fetch_case_value(Z_OBJ_P(scalar));
			goto try_again;
		/* All possible types are either handled here or previously */
		default: ZEND_UNREACHABLE();
	}
}

static zend_always_inline bool php_url_check_stack_limit(void)
{
#ifdef ZEND_CHECK_STACK_LIMIT
	return zend_call_stack_overflowed(EG(stack_limit));
#else
	return false;
#endif
}

/* {{{ php_url_encode_hash */
PHPAPI void php_url_encode_hash_ex(HashTable *ht, smart_str *formstr,
				const char *num_prefix, size_t num_prefix_len,
				const zend_string *key_prefix,
				zval *type, const zend_string *arg_sep, int enc_type)
{
	zend_string *key = NULL;
	const char *prop_name;
	size_t prop_len;
	zend_ulong idx;
	zval *zdata = NULL;
	ZEND_ASSERT(ht);

	if (GC_IS_RECURSIVE(ht)) {
		/* Prevent recursion */
		return;
	}

	/* Very deeply structured data could trigger a stack overflow, even without recursion. */
	if (UNEXPECTED(php_url_check_stack_limit())) {
		zend_throw_error(NULL, "Maximum call stack size reached.");
		return;
	}

	if (!arg_sep) {
		arg_sep = PG(arg_separator).output;
		if (ZSTR_LEN(arg_sep) == 0) {
			arg_sep = ZSTR_CHAR('&');
		}
	}

	ZEND_HASH_FOREACH_KEY_VAL(ht, idx, key, zdata) {
		bool is_dynamic = true;
		if (Z_TYPE_P(zdata) == IS_INDIRECT) {
			zdata = Z_INDIRECT_P(zdata);
			if (Z_ISUNDEF_P(zdata)) {
				continue;
			}

			is_dynamic = false;
		}

		/* handling for private & protected object properties */
		if (key) {
			prop_name = ZSTR_VAL(key);
			prop_len = ZSTR_LEN(key);

			if (type != NULL && zend_check_property_access(Z_OBJ_P(type), key, is_dynamic) != SUCCESS) {
				/* property not visible in this scope */
				continue;
			}

			if (ZSTR_VAL(key)[0] == '\0' && type != NULL) {
				const char *tmp;
				zend_unmangle_property_name_ex(key, &tmp, &prop_name, &prop_len);
			} else {
				prop_name = ZSTR_VAL(key);
				prop_len = ZSTR_LEN(key);
			}
		} else {
			prop_name = NULL;
			prop_len = 0;
		}

		ZVAL_DEREF(zdata);
		if (Z_TYPE_P(zdata) == IS_ARRAY
		 || (Z_TYPE_P(zdata) == IS_OBJECT
		  && !(Z_OBJCE_P(zdata)->ce_flags & ZEND_ACC_ENUM))) {
			zend_string *new_prefix;
			if (key) {
				zend_string *encoded_key;
				if (enc_type == PHP_QUERY_RFC3986) {
					encoded_key = php_raw_url_encode(prop_name, prop_len);
				} else {
					encoded_key = php_url_encode(prop_name, prop_len);
				}

				if (key_prefix) {
					new_prefix = zend_string_concat3(ZSTR_VAL(key_prefix), ZSTR_LEN(key_prefix), ZSTR_VAL(encoded_key), ZSTR_LEN(encoded_key), "%5D%5B", strlen("%5D%5B"));
				} else {
					new_prefix = zend_string_concat2(ZSTR_VAL(encoded_key), ZSTR_LEN(encoded_key), "%5B", strlen("%5B"));
				}
				zend_string_efree(encoded_key);
			} else { /* is integer index */
				char *index_int_as_str;
				size_t index_int_as_str_len;

				index_int_as_str_len = spprintf(&index_int_as_str, 0, ZEND_LONG_FMT, idx);

				if (key_prefix && num_prefix) {
					/* zend_string_concat4() */
					size_t len = ZSTR_LEN(key_prefix) + num_prefix_len + index_int_as_str_len + strlen("%5D%5B");
					new_prefix = zend_string_alloc(len, 0);

					memcpy(ZSTR_VAL(new_prefix), ZSTR_VAL(key_prefix), ZSTR_LEN(key_prefix));
					memcpy(ZSTR_VAL(new_prefix) + ZSTR_LEN(key_prefix), num_prefix, num_prefix_len);
					memcpy(ZSTR_VAL(new_prefix) + ZSTR_LEN(key_prefix) + num_prefix_len, index_int_as_str, index_int_as_str_len);
					memcpy(ZSTR_VAL(new_prefix) + ZSTR_LEN(key_prefix) + num_prefix_len +index_int_as_str_len, "%5D%5B", strlen("%5D%5B"));
					ZSTR_VAL(new_prefix)[len] = '\0';
				} else if (key_prefix) {
					new_prefix = zend_string_concat3(ZSTR_VAL(key_prefix), ZSTR_LEN(key_prefix), index_int_as_str, index_int_as_str_len, "%5D%5B", strlen("%5D%5B"));
				} else if (num_prefix) {
					new_prefix = zend_string_concat3(num_prefix, num_prefix_len, index_int_as_str, index_int_as_str_len, "%5B", strlen("%5B"));
				} else {
					new_prefix = zend_string_concat2(index_int_as_str, index_int_as_str_len, "%5B", strlen("%5B"));
				}
				efree(index_int_as_str);
			}
			GC_TRY_PROTECT_RECURSION(ht);
			php_url_encode_hash_ex(HASH_OF(zdata), formstr, NULL, 0, new_prefix, (Z_TYPE_P(zdata) == IS_OBJECT ? zdata : NULL), arg_sep, enc_type);
			GC_TRY_UNPROTECT_RECURSION(ht);
			zend_string_efree(new_prefix);
		} else if (Z_TYPE_P(zdata) == IS_NULL || Z_TYPE_P(zdata) == IS_RESOURCE) {
			/* Skip these types */
			continue;
		} else {
			php_url_encode_scalar(zdata, formstr,
				enc_type, idx,
				prop_name, prop_len,
				num_prefix, num_prefix_len,
				key_prefix,
				arg_sep);
		}
	} ZEND_HASH_FOREACH_END();
}
/* }}} */

	/* If there is a prefix we need to close the key with an encoded ] ("%5D") */
/* {{{ Generates a form-encoded query string from an associative array or object. */
PHP_FUNCTION(http_build_query)
{
	zval *formdata;
	char *prefix = NULL;
	size_t prefix_len = 0;
	zend_string *arg_sep = NULL;
	smart_str formstr = {0};
	zend_long enc_type = PHP_QUERY_RFC1738;

	ZEND_PARSE_PARAMETERS_START(1, 4)
		Z_PARAM_ARRAY_OR_OBJECT(formdata)
		Z_PARAM_OPTIONAL
		Z_PARAM_STRING(prefix, prefix_len)
		Z_PARAM_STR_OR_NULL(arg_sep)
		Z_PARAM_LONG(enc_type)
	ZEND_PARSE_PARAMETERS_END();

	if (UNEXPECTED(Z_TYPE_P(formdata) == IS_OBJECT && (Z_OBJCE_P(formdata)->ce_flags & ZEND_ACC_ENUM))) {
		zend_argument_type_error(1, "must not be an enum, %s given", zend_zval_value_name(formdata));
		RETURN_THROWS();
	}

	php_url_encode_hash_ex(HASH_OF(formdata), &formstr, prefix, prefix_len, /* key_prefix */ NULL, (Z_TYPE_P(formdata) == IS_OBJECT ? formdata : NULL), arg_sep, (int)enc_type);

	RETURN_STR(smart_str_extract(&formstr));
}
/* }}} */

static zend_result cache_request_parse_body_option(HashTable *options, zval *option, int cache_offset)
{
	if (option) {
		zend_long result;
		ZVAL_DEREF(option);
		if (Z_TYPE_P(option) == IS_STRING) {
			zend_string *errstr;
			result = zend_ini_parse_quantity(Z_STR_P(option), &errstr);
			if (errstr) {
				zend_error(E_WARNING, "%s", ZSTR_VAL(errstr));
				zend_string_release(errstr);
			}
		} else if (Z_TYPE_P(option) == IS_LONG) {
			result = Z_LVAL_P(option);
		} else {
			zend_value_error("Invalid %s value in $options argument", zend_zval_value_name(option));
			return FAILURE;
		}
		SG(request_parse_body_context).options_cache[cache_offset].set = true;
		SG(request_parse_body_context).options_cache[cache_offset].value = result;
	} else {
		SG(request_parse_body_context).options_cache[cache_offset].set = false;
	}

	return SUCCESS;
}

static zend_result cache_request_parse_body_options(HashTable *options)
{
	zend_string *key;
	zval *value;
	ZEND_HASH_FOREACH_STR_KEY_VAL(options, key, value) {
		if (!key) {
			zend_value_error("Invalid integer key in $options argument");
			return FAILURE;
		}
		if (ZSTR_LEN(key) == 0) {
			zend_value_error("Invalid empty string key in $options argument");
			return FAILURE;
		}

#define CHECK_OPTION(name) \
	if (zend_string_equals_literal_ci(key, #name)) { \
		if (cache_request_parse_body_option(options, value, REQUEST_PARSE_BODY_OPTION_ ## name) == FAILURE) { \
			return FAILURE; \
		} \
		continue; \
	}

		switch (ZSTR_VAL(key)[0]) {
			case 'm':
			case 'M':
				CHECK_OPTION(max_file_uploads);
				CHECK_OPTION(max_input_vars);
				CHECK_OPTION(max_multipart_body_parts);
				break;
			case 'p':
			case 'P':
				CHECK_OPTION(post_max_size);
				break;
			case 'u':
			case 'U':
				CHECK_OPTION(upload_max_filesize);
				break;
		}

		zend_value_error("Invalid key \"%s\" in $options argument", ZSTR_VAL(key));
		return FAILURE;
	} ZEND_HASH_FOREACH_END();

#undef CHECK_OPTION

	return SUCCESS;
}

PHP_FUNCTION(request_parse_body)
{
	HashTable *options = NULL;

	ZEND_PARSE_PARAMETERS_START(0, 1)
		Z_PARAM_OPTIONAL
		Z_PARAM_ARRAY_HT_OR_NULL(options)
	ZEND_PARSE_PARAMETERS_END();

	SG(request_parse_body_context).throw_exceptions = true;
	if (options) {
		if (cache_request_parse_body_options(options) == FAILURE) {
			goto exit;
		}
	}

	if (!SG(request_info).content_type) {
		zend_throw_error(zend_ce_request_parse_body_exception, "Request does not provide a content type");
		goto exit;
	}

	sapi_read_post_data();
	if (!SG(request_info).post_entry) {
		zend_throw_error(zend_ce_request_parse_body_exception, "Content-Type \"%s\" is not supported", SG(request_info).content_type);
		goto exit;
	}

	zval post, files, old_post, old_files;
	zval *global_post = &PG(http_globals)[TRACK_VARS_POST];
	zval *global_files = &PG(http_globals)[TRACK_VARS_FILES];

	ZVAL_COPY_VALUE(&old_post, global_post);
	ZVAL_COPY_VALUE(&old_files, global_files);
	array_init(global_post);
	array_init(global_files);
	sapi_handle_post(global_post);
	ZVAL_COPY_VALUE(&post, global_post);
	ZVAL_COPY_VALUE(&files, global_files);
	ZVAL_COPY_VALUE(global_post, &old_post);
	ZVAL_COPY_VALUE(global_files, &old_files);

	RETVAL_ARR(zend_new_pair(&post, &files));

exit:
	SG(request_parse_body_context).throw_exceptions = false;
	memset(&SG(request_parse_body_context).options_cache, 0, sizeof(SG(request_parse_body_context).options_cache));
}

PHP_FUNCTION(http_get_last_response_headers)
{
	ZEND_PARSE_PARAMETERS_NONE();

	if (!Z_ISUNDEF(BG(last_http_headers))) {
		RETURN_COPY(&BG(last_http_headers));
	} else {
		RETURN_NULL();
	}
}

PHP_FUNCTION(http_clear_last_response_headers)
{
	ZEND_PARSE_PARAMETERS_NONE();

	zval_ptr_dtor(&BG(last_http_headers));
	ZVAL_UNDEF(&BG(last_http_headers));
}
