/*
   +----------------------------------------------------------------------+
   | 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:                                                             |
   | Wez Furlong (wez@thebrainroom.com)                                   |
   | Sara Golemon (pollita@php.net)                                       |
   +----------------------------------------------------------------------+
*/

#include "php.h"
#include "php_globals.h"
#include "ext/standard/basic_functions.h"
#include "ext/standard/file.h"
#include "ext/standard/user_filters_arginfo.h"

#define PHP_STREAM_BRIGADE_RES_NAME	"userfilter.bucket brigade"
#define PHP_STREAM_BUCKET_RES_NAME "userfilter.bucket"

/* to provide context for calling into the next filter from user-space */
static int le_bucket_brigade;
static int le_bucket;

/* define the base filter class */

PHP_METHOD(php_user_filter, filter)
{
	zval *in, *out, *consumed;
	bool closing;
	if (zend_parse_parameters(ZEND_NUM_ARGS(), "rrzb", &in, &out, &consumed, &closing) == FAILURE) {
		RETURN_THROWS();
	}

	RETURN_LONG(PSFS_ERR_FATAL);
}

PHP_METHOD(php_user_filter, seek)
{
	zend_long offset, whence, chain;

	if (zend_parse_parameters(ZEND_NUM_ARGS(), "lll", &offset, &whence, &chain) == FAILURE) {
		RETURN_THROWS();
	}

	RETURN_TRUE;
}

PHP_METHOD(php_user_filter, onCreate)
{
	ZEND_PARSE_PARAMETERS_NONE();

	RETURN_TRUE;
}

PHP_METHOD(php_user_filter, onClose)
{
	ZEND_PARSE_PARAMETERS_NONE();
}

static zend_class_entry *user_filter_class_entry;
static zend_class_entry *stream_bucket_class_entry;

static ZEND_RSRC_DTOR_FUNC(php_bucket_dtor)
{
	php_stream_bucket *bucket = (php_stream_bucket *)res->ptr;
	if (bucket) {
		php_stream_bucket_delref(bucket);
		bucket = NULL;
	}
}

PHP_MINIT_FUNCTION(user_filters)
{
	/* init the filter class ancestor */
	user_filter_class_entry = register_class_php_user_filter();
	stream_bucket_class_entry = register_class_StreamBucket();

	/* Filters will dispose of their brigades */
	le_bucket_brigade = zend_register_list_destructors_ex(NULL, NULL, PHP_STREAM_BRIGADE_RES_NAME, module_number);
	/* Brigades will dispose of their buckets */
	le_bucket = zend_register_list_destructors_ex(php_bucket_dtor, NULL, PHP_STREAM_BUCKET_RES_NAME, module_number);

	if (le_bucket_brigade == FAILURE) {
		return FAILURE;
	}

	register_user_filters_symbols(module_number);

	return SUCCESS;
}

PHP_RSHUTDOWN_FUNCTION(user_filters)
{
	if (BG(user_filter_map)) {
		zend_hash_destroy(BG(user_filter_map));
		efree(BG(user_filter_map));
		BG(user_filter_map) = NULL;
	}

	return SUCCESS;
}

static void userfilter_dtor(php_stream_filter *thisfilter)
{
	zval *obj = &thisfilter->abstract;
	zval retval;

	if (Z_ISUNDEF_P(obj)) {
		/* If there's no object associated then there's nothing to dispose of */
		return;
	}

	zend_string *func_name = ZSTR_INIT_LITERAL("onclose", 0);
	zend_call_method_if_exists(Z_OBJ_P(obj), func_name, &retval, 0, NULL);
	zend_string_release(func_name);

	zval_ptr_dtor(&retval);

	/* kill the object */
	zval_ptr_dtor(obj);
}

static zend_result userfilter_assign_stream(php_stream *stream, zval *obj,
		zend_string **stream_name_p, uint32_t orig_no_fclose)
{
	/* Give the userfilter class a hook back to the stream */
	const zend_class_entry *old_scope = EG(fake_scope);
	EG(fake_scope) = Z_OBJCE_P(obj);

	zend_string *stream_name = ZSTR_INIT_LITERAL("stream", false);
	bool stream_property_exists = Z_OBJ_HT_P(obj)->has_property(Z_OBJ_P(obj), stream_name, ZEND_PROPERTY_EXISTS, NULL);
	if (stream_property_exists) {
		zval stream_zval;
		if (EXPECTED(stream->res && stream->res->type >= 0)) {
			php_stream_to_zval(stream, &stream_zval);
		} else {
			ZVAL_NULL(&stream_zval);
		}
		zend_update_property_ex(Z_OBJCE_P(obj), Z_OBJ_P(obj), stream_name, &stream_zval);
		/* If property update threw an exception, skip filter execution */
		if (EG(exception)) {
			EG(fake_scope) = old_scope;
			zend_string_release(stream_name);
			stream->flags &= ~PHP_STREAM_FLAG_NO_FCLOSE;
			stream->flags |= orig_no_fclose;
			return FAILURE;
		}
		*stream_name_p = stream_name;
	} else {
		zend_string_release(stream_name);
	}
	EG(fake_scope) = old_scope;

	return SUCCESS;
}

static php_stream_filter_status_t userfilter_filter(
			php_stream *stream,
			php_stream_filter *thisfilter,
			php_stream_bucket_brigade *buckets_in,
			php_stream_bucket_brigade *buckets_out,
			size_t *bytes_consumed,
			int flags
			)
{
	int ret = PSFS_ERR_FATAL;
	zval *obj = &thisfilter->abstract;
	zval func_name;
	zval retval;
	zval args[4];
	int call_result;

	/* the userfilter object probably doesn't exist anymore */
	if (CG(unclean_shutdown)) {
		return ret;
	}

	/* Make sure the stream is not closed while the filter callback executes. */
	uint32_t orig_no_fclose = stream->flags & PHP_STREAM_FLAG_NO_FCLOSE;
	stream->flags |= PHP_STREAM_FLAG_NO_FCLOSE;

	zend_string *stream_name = NULL;
	if (userfilter_assign_stream(stream, obj, &stream_name, orig_no_fclose) == FAILURE) {
		if (buckets_in->head) {
			php_error_docref(NULL, E_WARNING, "Unprocessed filter buckets remaining on input brigade");
		}
		return PSFS_ERR_FATAL;
	}

	ZVAL_STRINGL(&func_name, "filter", sizeof("filter")-1);

	/* Setup calling arguments */
	ZVAL_RES(&args[0], zend_register_resource(buckets_in, le_bucket_brigade));
	ZVAL_RES(&args[1], zend_register_resource(buckets_out, le_bucket_brigade));

	if (bytes_consumed) {
		ZVAL_LONG(&args[2], *bytes_consumed);
	} else {
		ZVAL_NULL(&args[2]);
	}
	ZVAL_MAKE_REF(&args[2]);

	ZVAL_BOOL(&args[3], flags & PSFS_FLAG_FLUSH_CLOSE);

	call_result = call_user_function(NULL,
			obj,
			&func_name,
			&retval,
			4, args);

	zval_ptr_dtor(&func_name);

	if (call_result == SUCCESS && Z_TYPE(retval) != IS_UNDEF) {
		convert_to_long(&retval);
		ret = (int)Z_LVAL(retval);
	} else if (call_result == FAILURE) {
		php_error_docref(NULL, E_WARNING, "Failed to call filter function");
	}

	if (bytes_consumed) {
		*bytes_consumed = zval_get_long(&args[2]);
	}

	if (buckets_in->head) {
		php_error_docref(NULL, E_WARNING, "Unprocessed filter buckets remaining on input brigade");
	}

	/* filter resources are cleaned up by the stream destructor,
	 * keeping a reference to the stream resource here would prevent it
	 * from being destroyed properly.
	 * Since the property accepted a resource assignment above, it must have
	 * no type hint or be typed as mixed, so we can safely assign null.
	 */
	if (stream_name != NULL) {
		zend_update_property_null(Z_OBJCE_P(obj), Z_OBJ_P(obj), ZSTR_VAL(stream_name), ZSTR_LEN(stream_name));
		zend_string_release(stream_name);
	}

	zval_ptr_dtor(&args[3]);
	zval_ptr_dtor(&args[2]);
	zval_ptr_dtor(&args[1]);
	zval_ptr_dtor(&args[0]);

	stream->flags &= ~PHP_STREAM_FLAG_NO_FCLOSE;
	stream->flags |= orig_no_fclose;

	return ret;
}

static zend_result userfilter_seek(
			php_stream *stream,
			php_stream_filter *thisfilter,
			zend_off_t offset,
			int whence
			)
{
	zval *obj = &thisfilter->abstract;
	zval retval;
	zval args[3];

	/* the userfilter object probably doesn't exist anymore */
	if (CG(unclean_shutdown)) {
		return FAILURE;
	}

	/* Check if the seek method exists */
	zend_function *seek_method = zend_hash_str_find_ptr(&Z_OBJCE_P(obj)->function_table, ZEND_STRL("seek"));
	if (seek_method == NULL) {
		/* Method doesn't exist - consider this a successful seek for BC */
		return SUCCESS;
	}

	/* Make sure the stream is not closed while the filter callback executes. */
	uint32_t orig_no_fclose = stream->flags & PHP_STREAM_FLAG_NO_FCLOSE;
	stream->flags |= PHP_STREAM_FLAG_NO_FCLOSE;

	zend_string *stream_name = NULL;
	if (userfilter_assign_stream(stream, obj, &stream_name, orig_no_fclose) == FAILURE) {
		return FAILURE;
	}

	/* Setup calling arguments */
	ZVAL_LONG(&args[0], offset);
	ZVAL_LONG(&args[1], whence);
	ZVAL_LONG(&args[2], php_stream_filter_get_chain_type(stream, thisfilter));

	zend_call_known_function(seek_method, Z_OBJ_P(obj), Z_OBJCE_P(obj), &retval, 3, args, NULL);

	zend_result ret = FAILURE;
	if (Z_TYPE(retval) != IS_UNDEF) {
		ret = zend_is_true(&retval) ? SUCCESS : FAILURE;
		zval_ptr_dtor(&retval);
	}

	/* filter resources are cleaned up by the stream destructor,
	 * keeping a reference to the stream resource here would prevent it
	 * from being destroyed properly */
	if (stream_name != NULL) {
		zend_update_property_null(Z_OBJCE_P(obj), Z_OBJ_P(obj), ZSTR_VAL(stream_name), ZSTR_LEN(stream_name));
		zend_string_release(stream_name);
	}

	stream->flags &= ~PHP_STREAM_FLAG_NO_FCLOSE;
	stream->flags |= orig_no_fclose;

	return ret;
}

static const php_stream_filter_ops userfilter_ops = {
	userfilter_filter,
	userfilter_seek,
	userfilter_dtor,
	"user-filter"
};

static php_stream_filter *user_filter_factory_create(const char *filtername,
		zval *filterparams, bool persistent)
{
	php_stream_filter *filter;
	zval obj;
	zval retval;
	size_t len;

	/* some sanity checks */
	if (persistent) {
		php_error_docref(NULL, E_WARNING,
				"Cannot use a user-space filter with a persistent stream");
		return NULL;
	}

	if (UNEXPECTED(BG(user_filter_map) == NULL)) {
		return NULL;
	}

	len = strlen(filtername);

	/* determine the class entry */
	/* const */ zend_class_entry *ce = zend_hash_str_find_ptr(BG(user_filter_map), filtername, len);
	if (UNEXPECTED(ce == NULL)) {
		const char *period;

		/* Userspace Filters using ambiguous wildcards could cause problems.
           i.e.: myfilter.foo.bar will always call into myfilter.foo.*
                 never seeing myfilter.*
           TODO: Allow failed userfilter creations to continue
                 scanning through the list */
		if ((period = strrchr(filtername, '.'))) {
			char *wildcard = safe_emalloc(len, 1, 3);

			/* Search for wildcard matches instead */
			memcpy(wildcard, filtername, len + 1); /* copy \0 */
			char *new_period = wildcard + (period - filtername);
			while (new_period) {
				ZEND_ASSERT(new_period[0] == '.');
				new_period[1] = '*';
				new_period[2] = '\0';
				ce = zend_hash_str_find_ptr(BG(user_filter_map), wildcard, strlen(wildcard));
				if (NULL != ce) {
					new_period = NULL;
				} else {
					*new_period = '\0';
					new_period = strrchr(wildcard, '.');
				}
			}
			efree(wildcard);
		}
		ZEND_ASSERT(ce);
	}

	/* create the object */
	if (object_init_ex(&obj, ce) == FAILURE) {
		return NULL;
	}

	filter = php_stream_filter_alloc(&userfilter_ops, NULL, false,
			PSFS_SEEKABLE_CHECK, PSFS_SEEKABLE_CHECK);

	/* filtername */
	add_property_string(&obj, "filtername", filtername);

	/* and the parameters, if any */
	if (filterparams) {
		add_property_zval(&obj, "params", filterparams);
	} else {
		add_property_null(&obj, "params");
	}

	/* invoke the constructor */
	zend_string *func_name = ZSTR_INIT_LITERAL("oncreate", 0);
	zend_call_method_if_exists(Z_OBJ(obj), func_name, &retval, 0, NULL);
	zend_string_release(func_name);

	if (Z_TYPE(retval) != IS_UNDEF) {
		if (Z_TYPE(retval) == IS_FALSE) {
			/* Kill the filter (safely) */
			ZVAL_UNDEF(&filter->abstract);
			php_stream_filter_free(filter);

			/* Kill the object */
			zval_ptr_dtor(&obj);

			/* Report failure to filter_alloc */
			return NULL;
		}
		zval_ptr_dtor(&retval);
	}

	ZVAL_OBJ(&filter->abstract, Z_OBJ(obj));

	return filter;
}

static const php_stream_filter_factory user_filter_factory = {
	user_filter_factory_create
};

/* {{{ Return a bucket object from the brigade for operating on */
PHP_FUNCTION(stream_bucket_make_writeable)
{
	zval *zbrigade, zbucket;
	php_stream_bucket_brigade *brigade;
	php_stream_bucket *bucket;

	ZEND_PARSE_PARAMETERS_START(1, 1)
		Z_PARAM_RESOURCE(zbrigade)
	ZEND_PARSE_PARAMETERS_END();

	if ((brigade = (php_stream_bucket_brigade*)zend_fetch_resource(
					Z_RES_P(zbrigade), PHP_STREAM_BRIGADE_RES_NAME, le_bucket_brigade)) == NULL) {
		RETURN_THROWS();
	}

	if (brigade->head && (bucket = php_stream_bucket_make_writeable(brigade->head))) {
		ZVAL_RES(&zbucket, zend_register_resource(bucket, le_bucket));
		object_init_ex(return_value, stream_bucket_class_entry);
		zend_update_property(Z_OBJCE_P(return_value), Z_OBJ_P(return_value), ZEND_STRL("bucket"), &zbucket);
		/* zend_update_property increments the refcount which is unwanted here */
		Z_DELREF(zbucket);
		zend_update_property_stringl(Z_OBJCE_P(return_value), Z_OBJ_P(return_value), ZEND_STRL("data"), bucket->buf, bucket->buflen);
		zend_update_property_long(Z_OBJCE_P(return_value), Z_OBJ_P(return_value), ZEND_STRL("datalen"), bucket->buflen);
		zend_update_property_long(Z_OBJCE_P(return_value), Z_OBJ_P(return_value), ZEND_STRL("dataLength"), bucket->buflen);
	} else {
		ZVAL_NULL(return_value);
	}
}
/* }}} */

/* {{{ php_stream_bucket_attach */
static void php_stream_bucket_attach(int append, INTERNAL_FUNCTION_PARAMETERS)
{
	zval *zbrigade, *zobject;
	zval *pzbucket, *pzdata, rv;
	php_stream_bucket_brigade *brigade;
	php_stream_bucket *bucket;

	ZEND_PARSE_PARAMETERS_START(2, 2)
		Z_PARAM_RESOURCE(zbrigade)
		Z_PARAM_OBJECT_OF_CLASS(zobject, stream_bucket_class_entry)
	ZEND_PARSE_PARAMETERS_END();

	if ((brigade = (php_stream_bucket_brigade*)zend_fetch_resource(
		Z_RES_P(zbrigade), PHP_STREAM_BRIGADE_RES_NAME, le_bucket_brigade)) == NULL) {
		RETURN_THROWS();
	}

	if (NULL == (pzbucket = zend_read_property(NULL, Z_OBJ_P(zobject), "bucket", sizeof("bucket")-1, false, &rv))) {
		zend_argument_value_error(2, "must be an object that has a \"bucket\" property");
		RETURN_THROWS();
	}
	ZVAL_DEREF(pzbucket);

	if ((bucket = (php_stream_bucket *)zend_fetch_resource_ex(pzbucket, PHP_STREAM_BUCKET_RES_NAME, le_bucket)) == NULL) {
		RETURN_THROWS();
	}

	if (NULL != (pzdata = zend_read_property(NULL, Z_OBJ_P(zobject), "data", sizeof("data")-1, false, &rv))) {
		ZVAL_DEREF(pzdata);
		if (!bucket->own_buf) {
			bucket = php_stream_bucket_make_writeable(bucket);
		}
		if (bucket->buflen != Z_STRLEN_P(pzdata)) {
			bucket->buf = perealloc(bucket->buf, MAX(Z_STRLEN_P(pzdata), 1), bucket->is_persistent);
			bucket->buflen = Z_STRLEN_P(pzdata);
		}
		memcpy(bucket->buf, Z_STRVAL_P(pzdata), bucket->buflen);
	}

	/* If the bucket is already on a brigade we have to unlink it first to keep the
	 * linked list consistent. Furthermore, we can transfer the refcount in that case. */
	if (bucket->brigade) {
		php_stream_bucket_unlink(bucket);
	} else {
		bucket->refcount++;
	}

	if (append) {
		php_stream_bucket_append(brigade, bucket);
	} else {
		php_stream_bucket_prepend(brigade, bucket);
	}
}
/* }}} */

/* {{{ Prepend bucket to brigade */
PHP_FUNCTION(stream_bucket_prepend)
{
	php_stream_bucket_attach(0, INTERNAL_FUNCTION_PARAM_PASSTHRU);
}
/* }}} */

/* {{{ Append bucket to brigade */
PHP_FUNCTION(stream_bucket_append)
{
	php_stream_bucket_attach(1, INTERNAL_FUNCTION_PARAM_PASSTHRU);
}
/* }}} */

/* {{{ Create a new bucket for use on the current stream */
PHP_FUNCTION(stream_bucket_new)
{
	zval zbucket;
	php_stream *stream;
	char *buffer;
	char *pbuffer;
	size_t buffer_len;
	php_stream_bucket *bucket;

	ZEND_PARSE_PARAMETERS_START(2, 2)
		PHP_Z_PARAM_STREAM(stream)
		Z_PARAM_STRING(buffer, buffer_len)
	ZEND_PARSE_PARAMETERS_END();

	pbuffer = pemalloc(buffer_len, php_stream_is_persistent(stream));

	memcpy(pbuffer, buffer, buffer_len);

	bucket = php_stream_bucket_new(stream, pbuffer, buffer_len, 1, php_stream_is_persistent(stream));

	ZVAL_RES(&zbucket, zend_register_resource(bucket, le_bucket));
	object_init_ex(return_value, stream_bucket_class_entry);
	zend_update_property(Z_OBJCE_P(return_value), Z_OBJ_P(return_value), ZEND_STRL("bucket"), &zbucket);
	/* zend_update_property increments the refcount which is unwanted here */
	Z_DELREF(zbucket);
	zend_update_property_stringl(Z_OBJCE_P(return_value), Z_OBJ_P(return_value), ZEND_STRL("data"), bucket->buf, bucket->buflen);
	zend_update_property_long(Z_OBJCE_P(return_value), Z_OBJ_P(return_value), ZEND_STRL("datalen"), bucket->buflen);
	zend_update_property_long(Z_OBJCE_P(return_value), Z_OBJ_P(return_value), ZEND_STRL("dataLength"), bucket->buflen);
}
/* }}} */

/* {{{ Returns a list of registered filters */
PHP_FUNCTION(stream_get_filters)
{
	zend_string *filter_name;
	HashTable *filters_hash;

	ZEND_PARSE_PARAMETERS_NONE();


	filters_hash = php_get_stream_filters_hash();

	if (filters_hash && !HT_IS_PACKED(filters_hash)) {
		array_init(return_value);
		zend_hash_real_init_packed(Z_ARRVAL_P(return_value));
		ZEND_HASH_MAP_FOREACH_STR_KEY(filters_hash, filter_name) {
			if (filter_name) {
				add_next_index_str(return_value, zend_string_copy(filter_name));
			}
		} ZEND_HASH_FOREACH_END();
	} else {
		RETURN_EMPTY_ARRAY();
	}
	/* It's okay to return an empty array if no filters are registered */
}
/* }}} */

/* {{{ Registers a custom filter handler class */
PHP_FUNCTION(stream_filter_register)
{
	zend_string *filtername;
	zend_class_entry *ce = NULL;

	ZEND_PARSE_PARAMETERS_START(2, 2)
		Z_PARAM_STR(filtername)
		Z_PARAM_CLASS(ce)
	ZEND_PARSE_PARAMETERS_END();

	if (!ZSTR_LEN(filtername)) {
		zend_argument_value_error(1, "must be a non-empty string");
		RETURN_THROWS();
	}

	/* TODO: Check class is a child of php_user_filter? */
	if (UNEXPECTED(ce->ce_flags & ZEND_ACC_UNINSTANTIABLE)) {
		zend_argument_value_error(2, "must be a concrete class");
		RETURN_THROWS();
	}

	/* Register the factory first; if that fails, don't (re)create the map,
	 * which would leak during shutdown re-registration. */
	if (php_stream_filter_register_factory_volatile(filtername, &user_filter_factory) == FAILURE) {
		RETURN_FALSE;
	}

	if (!BG(user_filter_map)) {
		BG(user_filter_map) = (HashTable*) emalloc(sizeof(HashTable));
		/* We don't need a destructor as we are only storing a CE which should be never modified */
		zend_hash_init(BG(user_filter_map), 8, NULL, NULL, 0);
	}

	/* The factory registration above already rejected a duplicate name, so the
	 * filter name cannot be present in the map either. */
	zend_hash_add_new_ptr(BG(user_filter_map), filtername, ce);

	RETURN_TRUE;
}
/* }}} */
