/*
   +----------------------------------------------------------------------+
   | 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: Nora Dossche  <ndossche@php.net>                            |
   +----------------------------------------------------------------------+
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "php.h"
#include "image_svg.h"
#include "php_libxml.h"

#include "ext/standard/php_image.h"

#include <libxml/xmlreader.h>

#ifdef HAVE_LIBXML

static int svg_image_type_id;

static int php_libxml_svg_stream_read(void *context, char *buffer, int len)
{
	return php_stream_read(context, buffer, len);
}

/* Sanity check that the input only contains characters valid for a dimension (numbers with units, e.g. 5cm).
 * This also protects the user against injecting XSS.
 * Only accept [0-9]+[a-zA-Z]* */
static bool php_libxml_parse_dimension(const xmlChar *input, const xmlChar **unit_position)
{
	if (!(*input >= '0' && *input <= '9')) {
		return false;
	}

	input++;

	while (*input) {
		if (!(*input >= '0' && *input <= '9')) {
			if ((*input >= 'a' && *input <= 'z') || (*input >= 'A' && *input <= 'Z')) {
				break;
			}
			return false;
		}
		input++;
	}

	*unit_position = input;

	while (*input) {
		if (!((*input >= 'a' && *input <= 'z') || (*input >= 'A' && *input <= 'Z'))) {
			return false;
		}
		input++;
	}

	return true;
}

zend_result php_libxml_svg_image_handle(php_stream *stream, struct php_gfxinfo **result)
{
	if (php_stream_rewind(stream)) {
		return FAILURE;
	}

	/* Early check before doing more expensive work */
	if (php_stream_getc(stream) != '<') {
		return FAILURE;
	}

	if (php_stream_rewind(stream)) {
		return FAILURE;
	}

	PHP_LIBXML_SANITIZE_GLOBALS(reader_for_stream);
	xmlTextReaderPtr reader = xmlReaderForIO(
		php_libxml_svg_stream_read,
		NULL,
		stream,
		NULL,
		NULL,
		XML_PARSE_NOWARNING | XML_PARSE_NOERROR | XML_PARSE_NONET
	);
	PHP_LIBXML_RESTORE_GLOBALS(reader_for_stream);

	if (!reader) {
		return FAILURE;
	}

	bool is_svg = false;
	while (xmlTextReaderRead(reader) == 1) {
		if (xmlTextReaderNodeType(reader) == XML_READER_TYPE_ELEMENT) {
			/* Root must be an svg element */
			const xmlChar *name = xmlTextReaderConstLocalName(reader);
			if (!name || strcasecmp((const char *) name, "svg") != 0) {
				break;
			}

			xmlChar *width = xmlTextReaderGetAttribute(reader, BAD_CAST "width");
			xmlChar *height = xmlTextReaderGetAttribute(reader, BAD_CAST "height");
			const xmlChar *width_unit_position, *height_unit_position;
			if (!width || !height
				|| !php_libxml_parse_dimension(width, &width_unit_position)
				|| !php_libxml_parse_dimension(height, &height_unit_position)) {
				xmlFree(width);
				xmlFree(height);
				break;
			}

			is_svg = true;
			if (result) {
				*result = ecalloc(1, sizeof(**result));
				(*result)->width = ZEND_STRTOL((const char *) width, NULL, 10);
				(*result)->height = ZEND_STRTOL((const char *) height, NULL, 10);
				if (*width_unit_position) {
					(*result)->width_unit = zend_string_init((const char*) width_unit_position,
															 xmlStrlen(width_unit_position), false);
				}
				if (*height_unit_position) {
					(*result)->height_unit = zend_string_init((const char*) height_unit_position,
															  xmlStrlen(height_unit_position), false);
				}
			}

			xmlFree(width);
			xmlFree(height);
			break;
		}
	}

	xmlFreeTextReader(reader);

	return is_svg ? SUCCESS : FAILURE;
}

zend_result php_libxml_svg_image_identify(php_stream *stream)
{
	return php_libxml_svg_image_handle(stream, NULL);
}

struct php_gfxinfo *php_libxml_svg_image_get_info(php_stream *stream)
{
	struct php_gfxinfo *result = NULL;
	zend_result status = php_libxml_svg_image_handle(stream, &result);
	ZEND_ASSERT((status == SUCCESS) == (result != NULL));
	return result;
}

static const struct php_image_handler svg_image_handler = {
	"image/svg+xml",
	".svg",
	PHP_IMAGE_CONST_NAME("SVG"),
	php_libxml_svg_image_identify,
	php_libxml_svg_image_get_info,
};

void php_libxml_register_image_svg_handler(void)
{
	svg_image_type_id = php_image_register_handler(&svg_image_handler);
}

zend_result php_libxml_unregister_image_svg_handler(void)
{
	return php_image_unregister_handler(svg_image_type_id);
}

#endif
