/*
  +----------------------------------------------------------------------+
  | 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: Brad Lafountain <rodif_bl@yahoo.com>                        |
  |          Shane Caraveo <shane@caraveo.com>                           |
  |          Dmitry Stogov <dmitry@php.net>                              |
  +----------------------------------------------------------------------+
*/

#include "php_soap.h"

static void master_to_zval_with_doc_cleanup(zval *ret, encodePtr encode, xmlNodePtr data, xmlDocPtr doc)
{
	bool bailout = false;

	ZVAL_UNDEF(ret);

	/* SoapClient can turn decode errors into a bailout before parse_packet_soap() frees the response doc. */
	zend_try {
		master_to_zval(ret, encode, data);
	} zend_catch {
		bailout = true;
	} zend_end_try();

	if (bailout) {
		if (!Z_ISUNDEF_P(ret)) {
			zval_ptr_dtor(ret);
		}
		xmlFreeDoc(doc);
		zend_bailout();
	}
}

/* SOAP client calls this function to parse response from SOAP server */
bool parse_packet_soap(zval *this_ptr, zend_string *buffer, sdlFunctionPtr fn, zval *return_value, zval *soap_headers)
{
	const char *envelope_ns = NULL;
	xmlDocPtr response;
	xmlNodePtr trav, env, head, body, resp, fault;
	xmlAttrPtr attr;
	uint32_t param_count = 0;
	int soap_version = SOAP_1_1;
	HashTable *hdrs = NULL;

	ZVAL_NULL(return_value);

	/* Response for one-way operation */
	if (ZSTR_LEN(buffer) == 0) {
		return true;
	}

	/* Parse XML packet */
	response = soap_xmlParseMemory(ZSTR_VAL(buffer), ZSTR_LEN(buffer));

	if (UNEXPECTED(!response)) {
		add_soap_fault(this_ptr, "Client", "looks like we got no XML document", NULL, NULL, soap_lang_en);
		return false;
	}
	if (UNEXPECTED(xmlGetIntSubset(response) != NULL)) {
		add_soap_fault(this_ptr, "Client", "DTD are not supported by SOAP", NULL, NULL, soap_lang_en);
		xmlFreeDoc(response);
		return false;
	}

	/* Get <Envelope> element */
	env = NULL;
	trav = response->children;
	while (trav != NULL) {
		if (trav->type == XML_ELEMENT_NODE) {
			if (env == NULL && node_is_equal_ex(trav,"Envelope",SOAP_1_1_ENV_NAMESPACE)) {
				env = trav;
				envelope_ns = SOAP_1_1_ENV_NAMESPACE;
				soap_version = SOAP_1_1;
			} else if (env == NULL && node_is_equal_ex(trav,"Envelope",SOAP_1_2_ENV_NAMESPACE)) {
				env = trav;
				envelope_ns = SOAP_1_2_ENV_NAMESPACE;
				soap_version = SOAP_1_2;
			} else {
				add_soap_fault(this_ptr, "VersionMismatch", "Wrong Version", NULL, NULL, soap_lang_en);
				xmlFreeDoc(response);
				return false;
			}
		}
		trav = trav->next;
	}
	if (UNEXPECTED(env == NULL)) {
		add_soap_fault(this_ptr, "Client", "looks like we got XML without \"Envelope\" element", NULL, NULL, soap_lang_en);
		xmlFreeDoc(response);
		return false;
	}

	attr = env->properties;
	while (attr != NULL) {
		if (attr->ns == NULL) {
			add_soap_fault(this_ptr, "Client", "A SOAP Envelope element cannot have non Namespace qualified attributes", NULL, NULL, soap_lang_en);
			xmlFreeDoc(response);
			return false;
		} else if (attr_is_equal_ex(attr,"encodingStyle",SOAP_1_2_ENV_NAMESPACE)) {
			if (soap_version == SOAP_1_2) {
				add_soap_fault(this_ptr, "Client", "encodingStyle cannot be specified on the Envelope", NULL, NULL, soap_lang_en);
				xmlFreeDoc(response);
				return false;
			} else if (strcmp((const char*)attr->children->content, SOAP_1_1_ENC_NAMESPACE) != 0) {
				add_soap_fault(this_ptr, "Client", "Unknown data encoding style", NULL, NULL, soap_lang_en);
				xmlFreeDoc(response);
				return false;
			}
		}
		attr = attr->next;
	}

	/* Get <Header> element */
	head = NULL;
	trav = env->children;
	while (trav != NULL && trav->type != XML_ELEMENT_NODE) {
		trav = trav->next;
	}
	if (trav != NULL && node_is_equal_ex(trav,"Header",envelope_ns)) {
		head = trav;
		trav = trav->next;
	}

	/* Get <Body> element */
	body = NULL;
	while (trav != NULL && trav->type != XML_ELEMENT_NODE) {
		trav = trav->next;
	}
	if (trav != NULL && node_is_equal_ex(trav,"Body",envelope_ns)) {
		body = trav;
		trav = trav->next;
	}
	while (trav != NULL && trav->type != XML_ELEMENT_NODE) {
		trav = trav->next;
	}
	if (UNEXPECTED(body == NULL)) {
		add_soap_fault(this_ptr, "Client", "Body must be present in a SOAP envelope", NULL, NULL, soap_lang_en);
		xmlFreeDoc(response);
		return false;
	}
	attr = body->properties;
	while (attr != NULL) {
		if (attr->ns == NULL) {
			if (soap_version == SOAP_1_2) {
				add_soap_fault(this_ptr, "Client", "A SOAP Body element cannot have non Namespace qualified attributes", NULL, NULL, soap_lang_en);
				xmlFreeDoc(response);
				return false;
			}
		} else if (attr_is_equal_ex(attr,"encodingStyle",SOAP_1_2_ENV_NAMESPACE)) {
			if (soap_version == SOAP_1_2) {
				add_soap_fault(this_ptr, "Client", "encodingStyle cannot be specified on the Body", NULL, NULL, soap_lang_en);
				xmlFreeDoc(response);
				return false;
			} else if (strcmp((char*)attr->children->content, SOAP_1_1_ENC_NAMESPACE) != 0) {
				add_soap_fault(this_ptr, "Client", "Unknown data encoding style", NULL, NULL, soap_lang_en);
				xmlFreeDoc(response);
				return false;
			}
		}
		attr = attr->next;
	}
	if (UNEXPECTED(trav != NULL && soap_version == SOAP_1_2)) {
		add_soap_fault(this_ptr, "Client", "A SOAP 1.2 envelope can contain only Header and Body", NULL, NULL, soap_lang_en);
		xmlFreeDoc(response);
		return false;
	}

	if (head != NULL) {
		attr = head->properties;
		while (attr != NULL) {
			if (attr->ns == NULL) {
				add_soap_fault(this_ptr, "Client", "A SOAP Header element cannot have non Namespace qualified attributes", NULL, NULL, soap_lang_en);
				xmlFreeDoc(response);
				return false;
			} else if (attr_is_equal_ex(attr,"encodingStyle",SOAP_1_2_ENV_NAMESPACE)) {
				if (soap_version == SOAP_1_2) {
					add_soap_fault(this_ptr, "Client", "encodingStyle cannot be specified on the Header", NULL, NULL, soap_lang_en);
					xmlFreeDoc(response);
					return false;
				} else if (strcmp((char*)attr->children->content, SOAP_1_1_ENC_NAMESPACE) != 0) {
					add_soap_fault(this_ptr, "Client", "Unknown data encoding style", NULL, NULL, soap_lang_en);
					xmlFreeDoc(response);
					return false;
				}
			}
			attr = attr->next;
		}
	}

	/* Check if <Body> contains <Fault> element */
	fault = get_node_ex(body->children,"Fault",envelope_ns);
	if (fault != NULL) {
		const char *faultcode = NULL;
		zend_string *lang = ZSTR_EMPTY_ALLOC();
		zend_string *faultstring = NULL, *faultactor = NULL;
		zval details;
		xmlNodePtr tmp;

		ZVAL_UNDEF(&details);
		if (soap_version == SOAP_1_1) {
			tmp = get_node(fault->children, "faultcode");
			if (tmp != NULL && tmp->children != NULL) {
				faultcode = (const char*)tmp->children->content;
			}

			tmp = get_node(fault->children, "faultstring");
			if (tmp != NULL && tmp->children != NULL) {
				zval zv;
				master_to_zval_with_doc_cleanup(&zv, get_conversion(IS_STRING), tmp, response);
				convert_to_string(&zv)
				faultstring = Z_STR(zv);
			}

			tmp = get_node(fault->children, "faultactor");
			if (tmp != NULL && tmp->children != NULL) {
				zval zv;
				master_to_zval_with_doc_cleanup(&zv, get_conversion(IS_STRING), tmp, response);
				convert_to_string(&zv)
				faultactor = Z_STR(zv);
			}

			tmp = get_node(fault->children, "detail");
			if (tmp != NULL) {
				master_to_zval_with_doc_cleanup(&details, NULL, tmp, response);
			}
		} else {
			tmp = get_node(fault->children, "Code");
			if (tmp != NULL && tmp->children != NULL) {
				tmp = get_node(tmp->children, "Value");
				if (tmp != NULL && tmp->children != NULL) {
					faultcode = (const char*)tmp->children->content;
				}
			}

			tmp = get_node(fault->children,"Reason");
			if (tmp != NULL && tmp->children != NULL) {
				tmp = get_node(tmp->children,"Text");
				if (tmp != NULL && tmp->children != NULL) {
					zval zv;
					master_to_zval_with_doc_cleanup(&zv, get_conversion(IS_STRING), tmp, response);
					convert_to_string(&zv)
					faultstring = Z_STR(zv);

					/* xml:lang is required by SOAP 1.2, but for BC reasons we allow it to be missing */
					xmlAttrPtr lang_attr = get_attribute_ex(tmp->properties, "lang", (const char *) XML_XML_NAMESPACE);
					if (lang_attr != NULL && lang_attr->children != NULL) {
						const char *lang_str = (const char *) lang_attr->children->content;
						lang = zend_string_init(lang_str, strlen(lang_str), false);
					}
				}
			}

			tmp = get_node(fault->children,"Detail");
			if (tmp != NULL) {
				master_to_zval_with_doc_cleanup(&details, NULL, tmp, response);
			}
		}
		add_soap_fault(this_ptr, faultcode, faultstring ? ZSTR_VAL(faultstring) : NULL, faultactor, &details, lang);
		if (faultstring) {
			zend_string_release_ex(faultstring, 0);
		}
		if (faultactor) {
			zend_string_release_ex(faultactor, 0);
		}
		zend_string_release_ex(lang, false);
		if (Z_REFCOUNTED(details)) {
			Z_DELREF(details);
		}
		xmlFreeDoc(response);
		return false;
	}

	/* Parse content of <Body> element */
	array_init(return_value);
	resp = body->children;
	while (resp != NULL && resp->type != XML_ELEMENT_NODE) {
		resp = resp->next;
	}
	if (resp != NULL) {
		if (fn != NULL && fn->binding && fn->binding->bindingType == BINDING_SOAP) {
		  /* Function has WSDL description */
			sdlParamPtr param = NULL;
			xmlNodePtr val = NULL;
			zval tmp;
			sdlSoapBindingFunctionPtr fnb = (sdlSoapBindingFunctionPtr)fn->bindingAttributes;

			hdrs = fnb->output.headers;

			if (fn->responseParameters) {
				uint32_t res_count = zend_hash_num_elements(fn->responseParameters);
				ZEND_HASH_FOREACH_PTR(fn->responseParameters, param) {
					const char *name, *ns = NULL;
					if (fnb->style == SOAP_DOCUMENT) {
						if (param->element) {
							name = param->element->name;
							ns = param->element->namens;
/*
							name = param->encode->details.type_str;
							ns = param->encode->details.ns;
*/
						} else {
							name = param->paramName;
						}
					} else {
						name = fn->responseName;
						/* ns = ? */
					}

					/* Get value of parameter */
					xmlNodePtr cur = get_node_ex(resp, name, ns);
					if (!cur) {
						cur = get_node(resp, name);
						/* TODO: produce warning invalid ns */
					}
					if (!cur && fnb->style == SOAP_RPC) {
					  cur = resp;
					}
					if (cur) {
						if (fnb->style == SOAP_DOCUMENT) {
							val = cur;
						} else {
							val = get_node(cur->children, param->paramName);
							if (res_count == 1) {
								if (val == NULL) {
									val = get_node(cur->children, "return");
								}
								if (val == NULL) {
									val = get_node(cur->children, "result");
								}
								if (val == NULL && cur->children && cur->children->next == NULL) {
									val = cur->children;
								}
							}
						}
					}

					if (!val) {
						/* TODO: may be "nil" is not OK? */
						ZVAL_NULL(&tmp);
/*
						add_soap_fault(this_ptr, "Client", "Can't find response data", NULL, NULL);
						xmlFreeDoc(response);
						return false;
*/
					} else {
						/* Decoding value of parameter */
						if (param != NULL) {
							master_to_zval_with_doc_cleanup(&tmp, param->encode, val, response);
						} else {
							master_to_zval_with_doc_cleanup(&tmp, NULL, val, response);
						}
					}
					add_assoc_zval(return_value, param->paramName, &tmp);

					param_count++;
				} ZEND_HASH_FOREACH_END();
			}
		} else {
		  /* Function has no WSDL description */
			xmlNodePtr val;
			val = resp->children;
			while (val != NULL) {
				while (val && val->type != XML_ELEMENT_NODE) {
					val = val->next;
				}
				if (val != NULL) {
					if (!node_is_equal_ex(val,"result",RPC_SOAP12_NAMESPACE)) {
						zval tmp;

						master_to_zval_with_doc_cleanup(&tmp, NULL, val, response);
						if (val->name) {
							const char *val_name = (const char*) val->name;
							size_t val_name_len = strlen(val_name);
							zval *arr = zend_hash_str_find(Z_ARRVAL_P(return_value), val_name, val_name_len);
							if (arr != NULL) {
								add_next_index_zval(arr, &tmp);
							} else if (val->next && get_node(val->next, val_name)) {
								zval new_arr;

								array_init(&new_arr);
								add_next_index_zval(&new_arr, &tmp);
								add_assoc_zval_ex(return_value, val_name, val_name_len, &new_arr);
							} else {
								add_assoc_zval_ex(return_value, val_name, val_name_len, &tmp);
							}
						} else {
							add_next_index_zval(return_value, &tmp);
						}
						++param_count;
					}
					val = val->next;
				}
			}
		}
	}

	if (Z_TYPE_P(return_value) == IS_ARRAY) {
		if (param_count == 0) {
			zend_array_destroy(Z_ARR_P(return_value));
			ZVAL_NULL(return_value);
		} else if (param_count == 1) {
			zval *tmp;

			zend_hash_internal_pointer_reset(Z_ARRVAL_P(return_value));
			tmp = zend_hash_get_current_data(Z_ARRVAL_P(return_value));
			if (!Z_REFCOUNTED_P(return_value)) {
				ZVAL_COPY(return_value, tmp);
			} else {
				zend_refcounted *garbage = Z_COUNTED_P(return_value);
				ZVAL_COPY(return_value, tmp);
				rc_dtor_func(garbage);
			}
		}
	}

	if (soap_headers && head) {
		trav = head->children;
		while (trav != NULL) {
			if (trav->type == XML_ELEMENT_NODE) {
				encodePtr enc = NULL;
				zval val;

				if (hdrs) {
					smart_str key = {0};
					sdlSoapBindingFunctionHeaderPtr hdr;

					if (trav->ns) {
						smart_str_appends(&key, (const char*)trav->ns->href);
						smart_str_appendc(&key,':');
					}
					smart_str_appends(&key, (const char*)trav->name);
					smart_str_0(&key);
					if ((hdr = zend_hash_find_ptr(hdrs, key.s)) != NULL) {
						enc = hdr->encode;
					}
					smart_str_free(&key);
				}
				master_to_zval_with_doc_cleanup(&val, enc, trav, response);
				add_assoc_zval(soap_headers, (const char*)trav->name, &val);
			}
			trav = trav->next;
		}
	}

	xmlFreeDoc(response);
	return true;
}
