/*
  +----------------------------------------------------------------------+
  | 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: Derick Rethans <derick@php.net>                             |
  +----------------------------------------------------------------------+
*/

#include "php_filter.h"
#include "filter_private.h"
#include "ext/standard/php_string.h"
#include "ext/standard/html.h"
#include "zend_smart_str.h"

/* {{{ STRUCTS */
typedef unsigned long filter_map[256];
/* }}} */

/* {{{ HELPER FUNCTIONS */
static void php_filter_encode_html(zval *value, const unsigned char *chars)
{
	smart_str str = {0};
	size_t len = Z_STRLEN_P(value);
	unsigned char *s = (unsigned char *)Z_STRVAL_P(value);
	unsigned char *e = s + len;
	unsigned char *last_output = s;

	if (Z_STRLEN_P(value) == 0) {
		return;
	}

	while (s < e) {
		if (chars[*s]) {
			smart_str_appendl(&str, (const char *) last_output, s - last_output);
			smart_str_appendl(&str, "&#", 2);
			smart_str_append_unsigned(&str, (zend_ulong)*s);
			smart_str_appendc(&str, ';');
			last_output = s + 1;
		}
		s++;
	}

	smart_str_appendl(&str, (const char *) last_output, s - last_output);

	zval_ptr_dtor(value);
	ZVAL_NEW_STR(value, smart_str_extract(&str));
}

static const unsigned char hexchars[] = "0123456789ABCDEF";

#define LOWALPHA    "abcdefghijklmnopqrstuvwxyz"
#define HIALPHA     "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
#define DIGIT       "0123456789"

#define DEFAULT_URL_ENCODE    LOWALPHA HIALPHA DIGIT "-._"

static void php_filter_encode_url(zval *value, const unsigned char* chars, const int char_len)
{
	unsigned char *p;
	unsigned char tmp[256];
	const unsigned char *s = chars;
	const unsigned char *e = s + char_len;
	zend_string *str;

	memset(tmp, 1, sizeof(tmp)-1);

	while (s < e) {
		tmp[*s++] = '\0';
	}

	str = zend_string_safe_alloc(Z_STRLEN_P(value), 3, 0, 0);
	p = (unsigned char*)ZSTR_VAL(str);
	s = (const unsigned char*)Z_STRVAL_P(value);
	e = s + Z_STRLEN_P(value);

	while (s < e) {
		if (tmp[*s]) {
			*p++ = '%';
			*p++ = hexchars[(unsigned char) *s >> 4];
			*p++ = hexchars[(unsigned char) *s & 15];
		} else {
			*p++ = *s;
		}
		s++;
	}
	*p = '\0';
	ZSTR_LEN(str) = p - (const unsigned char *)ZSTR_VAL(str);
	zval_ptr_dtor(value);
	ZVAL_NEW_STR(value, str);
}

static void php_filter_strip(zval *value, zend_long flags)
{
	const unsigned char *str;
	size_t c;
	zend_string *buf;

	/* Optimization for if no strip flags are set */
	if (!(flags & (FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH | FILTER_FLAG_STRIP_BACKTICK))) {
		return;
	}

	str = (const unsigned char *)Z_STRVAL_P(value);
	buf = zend_string_alloc(Z_STRLEN_P(value), 0);
	c = 0;
	for (size_t i = 0; i < Z_STRLEN_P(value); i++) {
		if ((str[i] >= 127) && (flags & FILTER_FLAG_STRIP_HIGH)) {
		} else if ((str[i] < 32) && (flags & FILTER_FLAG_STRIP_LOW)) {
		} else if ((str[i] == '`') && (flags & FILTER_FLAG_STRIP_BACKTICK)) {
		} else {
			ZSTR_VAL(buf)[c] = str[i];
			++c;
		}
	}
	/* update zval string data */
	ZSTR_VAL(buf)[c] = '\0';
	ZSTR_LEN(buf) = c;
	zval_ptr_dtor(value);
	ZVAL_NEW_STR(value, buf);
}
/* }}} */

/* {{{ FILTER MAP HELPERS */
static void filter_map_init(filter_map *map)
{
	memset(map, 0, sizeof(filter_map));
}

static void filter_map_update(filter_map *map, int flag, const unsigned char *allowed_list)
{
	size_t l, i;

	l = strlen((const char*)allowed_list);
	for (i = 0; i < l; ++i) {
		(*map)[allowed_list[i]] = flag;
	}
}

static void filter_map_apply(zval *value, const filter_map *map)
{
	const unsigned char *str;
	size_t i, c;
	zend_string *buf;

	str = (unsigned char *)Z_STRVAL_P(value);
	buf = zend_string_alloc(Z_STRLEN_P(value), 0);
	c = 0;
	for (i = 0; i < Z_STRLEN_P(value); i++) {
		if ((*map)[str[i]]) {
			ZSTR_VAL(buf)[c] = str[i];
			++c;
		}
	}
	/* update zval string data */
	ZSTR_VAL(buf)[c] = '\0';
	ZSTR_LEN(buf) = c;
	zval_ptr_dtor(value);
	ZVAL_NEW_STR(value, buf);
}
/* }}} */

/* {{{ php_filter_string */
zend_result php_filter_string(PHP_INPUT_FILTER_PARAM_DECL)
{
	size_t new_len;
	unsigned char enc[256] = {0};

	if (!Z_REFCOUNTED_P(value)) {
		ZVAL_STRINGL(value, Z_STRVAL_P(value), Z_STRLEN_P(value));
	}

	/* strip high/strip low ( see flags )*/
	php_filter_strip(value, flags);

	if (!(flags & FILTER_FLAG_NO_ENCODE_QUOTES)) {
		enc['\''] = enc['"'] = 1;
	}
	if (flags & FILTER_FLAG_ENCODE_AMP) {
		enc['&'] = 1;
	}
	if (flags & FILTER_FLAG_ENCODE_LOW) {
		memset(enc, 1, 32);
	}
	if (flags & FILTER_FLAG_ENCODE_HIGH) {
		memset(enc + 127, 1, sizeof(enc) - 127);
	}

	php_filter_encode_html(value, enc);

	/* strip tags, implicitly also removes \0 chars */
	new_len = php_strip_tags_ex(Z_STRVAL_P(value), Z_STRLEN_P(value), NULL, 0, 1);
	Z_STRLEN_P(value) = new_len;

	if (new_len == 0) {
		zval_ptr_dtor(value);
		if (flags & FILTER_FLAG_EMPTY_STRING_NULL) {
			ZVAL_NULL(value);
		} else {
			ZVAL_EMPTY_STRING(value);
		}
	}
	return SUCCESS;
}
/* }}} */

/* {{{ php_filter_encoded */
zend_result php_filter_encoded(PHP_INPUT_FILTER_PARAM_DECL)
{
	/* apply strip_high and strip_low filters */
	php_filter_strip(value, flags);
	/* urlencode */
	php_filter_encode_url(value, (unsigned char *)DEFAULT_URL_ENCODE, sizeof(DEFAULT_URL_ENCODE)-1);
	return SUCCESS;
}
/* }}} */

/* {{{ php_filter_special_chars */
zend_result php_filter_special_chars(PHP_INPUT_FILTER_PARAM_DECL)
{
	unsigned char enc[256] = {0};

	php_filter_strip(value, flags);

	/* encodes ' " < > & \0 to numerical entities */
	enc['\''] = enc['"'] = enc['<'] = enc['>'] = enc['&'] = enc[0] = 1;

	/* if strip low is not set, then we encode them as &#xx; */
	memset(enc, 1, 32);

	if (flags & FILTER_FLAG_ENCODE_HIGH) {
		memset(enc + 127, 1, sizeof(enc) - 127);
	}

	php_filter_encode_html(value, enc);
	return SUCCESS;
}
/* }}} */

/* {{{ php_filter_full_special_chars */
zend_result php_filter_full_special_chars(PHP_INPUT_FILTER_PARAM_DECL)
{
	zend_string *buf;
	int quotes;

	if (!(flags & FILTER_FLAG_NO_ENCODE_QUOTES)) {
		quotes = ENT_QUOTES;
	} else {
		quotes = ENT_NOQUOTES;
	}
	buf = php_escape_html_entities_ex(
		(unsigned char *) Z_STRVAL_P(value), Z_STRLEN_P(value), /* all */ 1, quotes,
		/* charset_hint */ NULL, /* double_encode */ 0, /* quiet */ 0);
	zval_ptr_dtor(value);
	ZVAL_STR(value, buf);
	return SUCCESS;
}
/* }}} */

/* {{{ php_filter_unsafe_raw */
zend_result php_filter_unsafe_raw(PHP_INPUT_FILTER_PARAM_DECL)
{
	/* Only if no flags are set (optimization) */
	if (flags != 0 && Z_STRLEN_P(value) > 0) {
		unsigned char enc[256] = {0};

		php_filter_strip(value, flags);

		if (flags & FILTER_FLAG_ENCODE_AMP) {
			enc['&'] = 1;
		}
		if (flags & FILTER_FLAG_ENCODE_LOW) {
			memset(enc, 1, 32);
		}
		if (flags & FILTER_FLAG_ENCODE_HIGH) {
			memset(enc + 127, 1, sizeof(enc) - 127);
		}

		php_filter_encode_html(value, enc);
	} else if (flags & FILTER_FLAG_EMPTY_STRING_NULL && Z_STRLEN_P(value) == 0) {
		zval_ptr_dtor(value);
		ZVAL_NULL(value);
	}
	return SUCCESS;
}
/* }}} */

/* {{{ php_filter_email */
#define SAFE        "$-_.+"
#define EXTRA       "!*'(),"
#define NATIONAL    "{}|\\^~[]`"
#define PUNCTUATION "<>#%\""
#define RESERVED    ";/?:@&="

zend_result php_filter_email(PHP_INPUT_FILTER_PARAM_DECL)
{
	/* Check section 6 of rfc 822 http://www.faqs.org/rfcs/rfc822.html */
	const unsigned char allowed_list[] = LOWALPHA HIALPHA DIGIT "!#$%&'*+-=?^_`{|}~@.[]";
	filter_map     map;

	filter_map_init(&map);
	filter_map_update(&map, 1, allowed_list);
	filter_map_apply(value, &map);
	return SUCCESS;
}
/* }}} */

/* {{{ php_filter_url */
zend_result php_filter_url(PHP_INPUT_FILTER_PARAM_DECL)
{
	/* Strip all chars not part of section 5 of
	 * http://www.faqs.org/rfcs/rfc1738.html */
	const unsigned char allowed_list[] = LOWALPHA HIALPHA DIGIT SAFE EXTRA NATIONAL PUNCTUATION RESERVED;
	filter_map     map;

	filter_map_init(&map);
	filter_map_update(&map, 1, allowed_list);
	filter_map_apply(value, &map);
	return SUCCESS;
}
/* }}} */

/* {{{ php_filter_number_int */
zend_result php_filter_number_int(PHP_INPUT_FILTER_PARAM_DECL)
{
	/* strip everything [^0-9+-] */
	const unsigned char allowed_list[] = "+-" DIGIT;
	filter_map     map;

	filter_map_init(&map);
	filter_map_update(&map, 1, allowed_list);
	filter_map_apply(value, &map);
	return SUCCESS;
}
/* }}} */

/* {{{ php_filter_number_float */
zend_result php_filter_number_float(PHP_INPUT_FILTER_PARAM_DECL)
{
	/* strip everything [^0-9+-] */
	const unsigned char allowed_list[] = "+-" DIGIT;
	filter_map     map;

	filter_map_init(&map);
	filter_map_update(&map, 1, allowed_list);

	/* depending on flags, strip '.', 'e', ",", "'" */
	if (flags & FILTER_FLAG_ALLOW_FRACTION) {
		filter_map_update(&map, 2, (const unsigned char *) ".");
	}
	if (flags & FILTER_FLAG_ALLOW_THOUSAND) {
		filter_map_update(&map, 3,  (const unsigned char *) ",");
	}
	if (flags & FILTER_FLAG_ALLOW_SCIENTIFIC) {
		filter_map_update(&map, 4,  (const unsigned char *) "eE");
	}
	filter_map_apply(value, &map);
	return SUCCESS;
}
/* }}} */

/* {{{ php_filter_add_slashes */
zend_result php_filter_add_slashes(PHP_INPUT_FILTER_PARAM_DECL)
{
	zend_string *buf = php_addslashes(Z_STR_P(value));

	zval_ptr_dtor(value);
	ZVAL_STR(value, buf);
	return SUCCESS;
}
/* }}} */
