/*
  +----------------------------------------------------------------------+
  | 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                                |
  +----------------------------------------------------------------------+
  | Author: Wez Furlong <wez@php.net>                                    |
  +----------------------------------------------------------------------+
*/

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include "php.h"
#include "php_ini.h"
#include "ext/standard/info.h"
#include "SAPI.h"
#include "ext/pdo/php_pdo.h"
#include "ext/pdo/php_pdo_driver.h"
#include "php_pdo_sqlite.h"
#include "php_pdo_sqlite_int.h"
#include "zend_exceptions.h"
#include "pdo_sqlite_arginfo.h"

static zend_class_entry *pdosqlite_ce;

/* {{{ pdo_sqlite_deps */
static const zend_module_dep pdo_sqlite_deps[] = {
	ZEND_MOD_REQUIRED("pdo")
	ZEND_MOD_END
};
/* }}} */

/* {{{ pdo_sqlite_module_entry */
zend_module_entry pdo_sqlite_module_entry = {
	STANDARD_MODULE_HEADER_EX, NULL,
	pdo_sqlite_deps,
	"pdo_sqlite",
	NULL,
	PHP_MINIT(pdo_sqlite),
	PHP_MSHUTDOWN(pdo_sqlite),
	NULL,
	NULL,
	PHP_MINFO(pdo_sqlite),
	PHP_PDO_SQLITE_VERSION,
	STANDARD_MODULE_PROPERTIES
};
/* }}} */

#if defined(COMPILE_DL_PDO_SQLITE) || defined(COMPILE_DL_PDO_SQLITE_EXTERNAL)
ZEND_GET_MODULE(pdo_sqlite)
#endif

/* proto bool PdoSqlite::createFunction(string $function_name, callable $callback, int $num_args = -1, int $flags = 0)
    Creates a function that can be used in a query
*/
PHP_METHOD(Pdo_Sqlite, createFunction)
{
	pdo_sqlite_create_function_internal(INTERNAL_FUNCTION_PARAM_PASSTHRU);
}

#ifndef PDO_SQLITE_OMIT_LOAD_EXTENSION
/* Attempts to load an SQLite extension library. */
PHP_METHOD(Pdo_Sqlite, loadExtension)
{
	char *extension, *errtext = NULL;
	char fullpath[MAXPATHLEN];
	size_t extension_len;

	pdo_dbh_t *dbh;
	pdo_sqlite_db_handle *db_handle;

	if (zend_parse_parameters(ZEND_NUM_ARGS(), "p", &extension, &extension_len) == FAILURE) {
		RETURN_THROWS();
	}

	if (extension_len == 0) {
		zend_argument_must_not_be_empty_error(1);
		RETURN_THROWS();
	}

	dbh = Z_PDO_DBH_P(ZEND_THIS);
	PDO_CONSTRUCT_CHECK;

	db_handle = (pdo_sqlite_db_handle *)dbh->driver_data;

#ifdef ZTS
	if ((strncmp(sapi_module.name, "cgi", 3) != 0) &&
		(strcmp(sapi_module.name, "cli") != 0) &&
		(strncmp(sapi_module.name, "embed", 5) != 0)
	) {
		zend_throw_exception_ex(php_pdo_get_exception(), 0, "Not supported in multithreaded Web servers");
		RETURN_THROWS();
	}
#endif

	if (!VCWD_REALPATH(extension, fullpath)) {
		zend_throw_exception_ex(php_pdo_get_exception(), 0, "Unable to load extension \"%s\"", extension);
		RETURN_THROWS();
	}

	sqlite3 *sqlite_handle;
	sqlite_handle = db_handle->db;

	/* This only enables extension loading for the C api, not for SQL */
	sqlite3_db_config(sqlite_handle, SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION, 1, NULL);

	if (sqlite3_load_extension(sqlite_handle, fullpath, 0, &errtext) != SQLITE_OK) {
		zend_throw_exception_ex(php_pdo_get_exception(), 0, "Unable to load extension \"%s\"", errtext);
		sqlite3_free(errtext);
		sqlite3_db_config(sqlite_handle, SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION, 0, NULL);
		RETURN_THROWS();
	}

	/* We disable extension loading for a vague feeling of safety. This is probably not necessary
	as extensions can only be loaded through C code, not through SQL, and if someone can get
	some C code to run on the server, they can do anything.*/
	sqlite3_db_config(sqlite_handle, SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION, 0, NULL);
}
#endif

typedef struct {
	sqlite3_blob *blob;
	size_t		 position;
	size_t       size;
	int          flags;
} php_stream_pdosqlite3_data;

static ssize_t php_pdosqlite3_stream_write(php_stream *stream, const char *buf, size_t count)
{
	php_stream_pdosqlite3_data *sqlite3_stream = (php_stream_pdosqlite3_data *) stream->abstract;

	if (sqlite3_stream->flags & SQLITE_OPEN_READONLY) {
		php_error_docref(NULL, E_WARNING, "Can't write to blob stream: is open as read only");
		return -1;
	}

	if (sqlite3_stream->position + count > sqlite3_stream->size) {
		php_error_docref(NULL, E_WARNING, "It is not possible to increase the size of a BLOB");
		return -1;
	}

	if (sqlite3_blob_write(sqlite3_stream->blob, buf, count, sqlite3_stream->position) != SQLITE_OK) {
		return -1;
	}

	if (sqlite3_stream->position + count >= sqlite3_stream->size) {
		stream->eof = 1;
		sqlite3_stream->position = sqlite3_stream->size;
	}
	else {
		sqlite3_stream->position += count;
	}

	return count;
}

static ssize_t php_pdosqlite3_stream_read(php_stream *stream, char *buf, size_t count)
{
	php_stream_pdosqlite3_data *sqlite3_stream = (php_stream_pdosqlite3_data *) stream->abstract;

	if (sqlite3_stream->position + count >= sqlite3_stream->size) {
		count = sqlite3_stream->size - sqlite3_stream->position;
		stream->eof = 1;
	}
	if (count) {
		if (sqlite3_blob_read(sqlite3_stream->blob, buf, count, sqlite3_stream->position) != SQLITE_OK) {
			return -1;
		}
		sqlite3_stream->position += count;
	}
	return count;
}

static int php_pdosqlite3_stream_close(php_stream *stream, int close_handle)
{
	php_stream_pdosqlite3_data *sqlite3_stream = (php_stream_pdosqlite3_data *) stream->abstract;

	if (sqlite3_blob_close(sqlite3_stream->blob) != SQLITE_OK) {
		/* Error occurred, but it still closed */
	}

	efree(sqlite3_stream);

	return 0;
}

static int php_pdosqlite3_stream_flush(php_stream *stream)
{
	/* do nothing */
	return 0;
}

static int php_pdosqlite3_stream_seek(php_stream *stream, zend_off_t offset, int whence, zend_off_t *newoffs)
{
	php_stream_pdosqlite3_data *sqlite3_stream = (php_stream_pdosqlite3_data *) stream->abstract;

	switch(whence) {
		case SEEK_CUR:
			if (offset < 0) {
				if (sqlite3_stream->position < -(size_t)offset) {
					sqlite3_stream->position = 0;
					*newoffs = -1;
					return -1;
				} else {
					sqlite3_stream->position = sqlite3_stream->position + offset;
					*newoffs = sqlite3_stream->position;
					stream->eof = 0;
					return 0;
				}
			} else {
				if (sqlite3_stream->position + (size_t)(offset) > sqlite3_stream->size) {
					sqlite3_stream->position = sqlite3_stream->size;
					*newoffs = -1;
					return -1;
				} else {
					sqlite3_stream->position = sqlite3_stream->position + offset;
					*newoffs = sqlite3_stream->position;
					stream->eof = 0;
					return 0;
				}
			}
		case SEEK_SET:
			if (sqlite3_stream->size < (size_t)(offset)) {
				sqlite3_stream->position = sqlite3_stream->size;
				*newoffs = -1;
				return -1;
			} else {
				sqlite3_stream->position = offset;
				*newoffs = sqlite3_stream->position;
				stream->eof = 0;
				return 0;
			}
		case SEEK_END:
			if (offset > 0) {
				sqlite3_stream->position = sqlite3_stream->size;
				*newoffs = -1;
				return -1;
			} else if (sqlite3_stream->size < -(size_t)offset) {
				sqlite3_stream->position = 0;
				*newoffs = -1;
				return -1;
			} else {
				sqlite3_stream->position = sqlite3_stream->size + offset;
				*newoffs = sqlite3_stream->position;
				stream->eof = 0;
				return 0;
			}
		default:
			*newoffs = sqlite3_stream->position;
			return -1;
	}
}

static int php_pdosqlite3_stream_cast(php_stream *stream, int castas, void **ret)
{
	return FAILURE;
}

static int php_pdosqlite3_stream_stat(php_stream *stream, php_stream_statbuf *ssb)
{
	php_stream_pdosqlite3_data *sqlite3_stream = (php_stream_pdosqlite3_data *) stream->abstract;
	ssb->sb.st_size = sqlite3_stream->size;
	return 0;
}

static const php_stream_ops php_stream_pdosqlite3_ops = {
		php_pdosqlite3_stream_write,
		php_pdosqlite3_stream_read,
		php_pdosqlite3_stream_close,
		php_pdosqlite3_stream_flush,
		"PDOSQLite",
		php_pdosqlite3_stream_seek,
		php_pdosqlite3_stream_cast,
		php_pdosqlite3_stream_stat,
		NULL
};

/* Open a blob as a stream which we can read / write to. */
PHP_METHOD(Pdo_Sqlite, openBlob)
{
	char *table, *column, *dbname = "main", *mode = "rb";
	size_t table_len, column_len, dbname_len;
	zend_long rowid, flags = SQLITE_OPEN_READONLY, sqlite_flags = 0;
	sqlite3_blob *blob = NULL;
	php_stream_pdosqlite3_data *sqlite3_stream;
	php_stream *stream;

	pdo_dbh_t *dbh;
	pdo_sqlite_db_handle *db_handle;

	dbh = Z_PDO_DBH_P(ZEND_THIS);
	PDO_CONSTRUCT_CHECK;
	db_handle = (pdo_sqlite_db_handle *)dbh->driver_data;

	if (zend_parse_parameters(ZEND_NUM_ARGS(), "ppl|pl", &table, &table_len, &column, &column_len, &rowid, &dbname, &dbname_len, &flags) == FAILURE) {
		RETURN_THROWS();
	}

	sqlite3 *sqlite_handle;
	sqlite_handle = db_handle->db;
	sqlite_flags = (flags & SQLITE_OPEN_READWRITE) ? 1 : 0;

	if (sqlite3_blob_open(sqlite_handle, dbname, table, column, rowid, sqlite_flags, &blob) != SQLITE_OK) {
		zend_error(E_WARNING, "Unable to open blob: %s", sqlite3_errmsg(sqlite_handle));
		RETURN_FALSE;
	}

	sqlite3_stream = emalloc(sizeof(php_stream_pdosqlite3_data));
	sqlite3_stream->blob = blob;
	sqlite3_stream->flags = flags;
	sqlite3_stream->position = 0;
	sqlite3_stream->size = sqlite3_blob_bytes(blob);

	if (sqlite_flags != 0) {
		mode = "r+b";
	}

	stream = php_stream_alloc(&php_stream_pdosqlite3_ops, sqlite3_stream, 0, mode);

	if (stream) {
		php_stream_to_zval(stream, return_value);
	} else {
		RETURN_FALSE;
	}
}

PHP_METHOD(Pdo_Sqlite, setAuthorizer)
{
	zend_fcall_info fci;
	zend_fcall_info_cache fcc;

	ZEND_PARSE_PARAMETERS_START(1, 1)
		Z_PARAM_FUNC_NO_TRAMPOLINE_FREE_OR_NULL(fci, fcc)
	ZEND_PARSE_PARAMETERS_END();

	pdo_dbh_t *dbh = Z_PDO_DBH_P(ZEND_THIS);
	PDO_CONSTRUCT_CHECK_WITH_CLEANUP(free_fcc);
	pdo_sqlite_db_handle *db_handle = (pdo_sqlite_db_handle *) dbh->driver_data;

	/* Clear previously set callback */
	if (ZEND_FCC_INITIALIZED(db_handle->authorizer_fcc)) {
		zend_fcc_dtor(&db_handle->authorizer_fcc);
	}

	/* Only enable userland authorizer if argument is not NULL */
	if (ZEND_FCI_INITIALIZED(fci)) {
		zend_fcc_dup(&db_handle->authorizer_fcc, &fcc);
	}

	return;

free_fcc:
	zend_release_fcall_info_cache(&fcc);
	RETURN_THROWS();
}

static int php_sqlite_collation_callback(void *context, int string1_len, const void *string1,
	int string2_len, const void *string2)
{
	int ret = 0;
	zval zargs[2];
	zval retval;
	struct pdo_sqlite_collation *collation = (struct pdo_sqlite_collation*) context;

	// Prepare the arguments.
	ZVAL_STRINGL(&zargs[0], (char *) string1, string1_len);
	ZVAL_STRINGL(&zargs[1], (char *) string2, string2_len);

	zend_call_known_fcc(&collation->callback, &retval, /* argc */ 2, zargs, /* named_params */ NULL);

	zval_ptr_dtor(&zargs[0]);
	zval_ptr_dtor(&zargs[1]);

	if (!Z_ISUNDEF(retval)) {
		if (Z_TYPE(retval) != IS_LONG) {
			zend_string *func_name = get_active_function_or_method_name();
			zend_type_error("%s(): Return value of the collation callback must be of type int, %s returned",
				ZSTR_VAL(func_name), zend_zval_value_name(&retval));
			zend_string_release(func_name);
			zval_ptr_dtor(&retval);
			return FAILURE;
		}
		ret = ZEND_NORMALIZE_BOOL(Z_LVAL(retval));
	}

	return ret;
}

PHP_METHOD(Pdo_Sqlite, createAggregate)
{
	pdo_sqlite_create_aggregate_internal(INTERNAL_FUNCTION_PARAM_PASSTHRU);
}

PHP_METHOD(Pdo_Sqlite, createCollation)
{
	pdo_sqlite_create_collation_internal(INTERNAL_FUNCTION_PARAM_PASSTHRU, php_sqlite_collation_callback);
}

#define REGISTER_PDO_SQLITE_CLASS_CONST_LONG_DEPRECATED_ALIAS_85(base_name, value) \
		REGISTER_PDO_CLASS_CONST_LONG_DEPRECATED_ALIAS_85(base_name, "SQLITE_", "Pdo\\Sqlite::", value)

/* {{{ PHP_MINIT_FUNCTION */
PHP_MINIT_FUNCTION(pdo_sqlite)
{
#ifdef SQLITE_DETERMINISTIC
	REGISTER_PDO_SQLITE_CLASS_CONST_LONG_DEPRECATED_ALIAS_85("DETERMINISTIC", (zend_long)SQLITE_DETERMINISTIC);
#endif

	REGISTER_PDO_SQLITE_CLASS_CONST_LONG_DEPRECATED_ALIAS_85("ATTR_OPEN_FLAGS", (zend_long)PDO_SQLITE_ATTR_OPEN_FLAGS);
	REGISTER_PDO_SQLITE_CLASS_CONST_LONG_DEPRECATED_ALIAS_85("OPEN_READONLY", (zend_long)SQLITE_OPEN_READONLY);
	REGISTER_PDO_SQLITE_CLASS_CONST_LONG_DEPRECATED_ALIAS_85("OPEN_READWRITE", (zend_long)SQLITE_OPEN_READWRITE);
	REGISTER_PDO_SQLITE_CLASS_CONST_LONG_DEPRECATED_ALIAS_85("OPEN_CREATE", (zend_long)SQLITE_OPEN_CREATE);
	REGISTER_PDO_SQLITE_CLASS_CONST_LONG_DEPRECATED_ALIAS_85("ATTR_READONLY_STATEMENT", (zend_long)PDO_SQLITE_ATTR_READONLY_STATEMENT);
	REGISTER_PDO_SQLITE_CLASS_CONST_LONG_DEPRECATED_ALIAS_85("ATTR_EXTENDED_RESULT_CODES", (zend_long)PDO_SQLITE_ATTR_EXTENDED_RESULT_CODES);

	pdosqlite_ce = register_class_Pdo_Sqlite(pdo_dbh_ce);
	pdosqlite_ce->create_object = pdo_dbh_new;

	if (php_pdo_register_driver(&pdo_sqlite_driver) == FAILURE) {
		return FAILURE;
	}

	return php_pdo_register_driver_specific_ce(&pdo_sqlite_driver, pdosqlite_ce);
}
/* }}} */

/* {{{ PHP_MSHUTDOWN_FUNCTION */
PHP_MSHUTDOWN_FUNCTION(pdo_sqlite)
{
	php_pdo_unregister_driver(&pdo_sqlite_driver);
	return SUCCESS;
}
/* }}} */

/* {{{ PHP_MINFO_FUNCTION */
PHP_MINFO_FUNCTION(pdo_sqlite)
{
	php_info_print_table_start();
	php_info_print_table_row(2, "PDO Driver for SQLite 3.x", "enabled");
	php_info_print_table_row(2, "SQLite Library", sqlite3_libversion());
	php_info_print_table_end();
}
/* }}} */
