diff --git a/NEWS b/NEWS index 5a865955281d..841a7f49349f 100644 --- a/NEWS +++ b/NEWS @@ -16,6 +16,11 @@ PHP NEWS passing an array of CURLFile or CURLStringFile objects to CURLOPT_POSTFIELDS. (Sjoerd Langkemper) +- DOM: + . Fixed use-after-free when a php:function() callback of DOMXPath or + Dom\XPath modifies nodes that are still being evaluated; such + modifications now throw a DOMException or Error. (Ilia Alshanetsky) + - FPM: . Use recommended TYPE, HELP order for OpenMetrics metadata. (Marcel Hernandez) @@ -204,6 +209,13 @@ PHP NEWS . Fixed xml_get_current_byte_index() returning negative values for byte indexes beyond 2 GiB on Windows x64. (Marc Bennewitz) +- XSL: + . Fixed a use-after-free when a transformation was started on a processor + that is already transforming. (Ilia Alshanetsky) + . Fixed use-after-free when a php:function() callback or an entity loader + modifies the document being transformed; such modifications now throw a + DOMException or Error. (Ilia Alshanetsky) + - Zlib: . Fixed inflate_init() dropping the preset dictionary for raw streams with a non-default window. (Ilia Alshanetsky) diff --git a/UPGRADING b/UPGRADING index a4df23352639..42e4f845e463 100644 --- a/UPGRADING +++ b/UPGRADING @@ -29,6 +29,17 @@ PHP 8.7 UPGRADE NOTES . Nested arrays in CURLOPT_POSTFIELDS now throw a TypeError, instead of being silently converted to the string "Array". +- DOM: + . While DOMXPath or Dom\XPath evaluates an expression with PHP functions + registered, the nodes it can reach are read-only: the tree of the + evaluated document, the tree of every node a php:function() callback + returned, and the detached subtree of the context node or of a node + passed to a callback. Modifying such a node throws a DOMException with + code DOM_NO_MODIFICATION_ALLOWED_ERR (a warning when strictErrorChecking + is disabled), and modifying it through SimpleXML throws an Error. + Creating and modifying new nodes and setting document properties such as + formatOutput or documentURI are still allowed. + - PDO: . PDOStatement::bindColumn() now throws a ValueError when the column name is not present in the result set. It previously reported the condition @@ -64,6 +75,21 @@ PHP 8.7 UPGRADE NOTES to 16 by default. Set the stream context option max_filter_count to change this. +- XSL: + . XSLTProcessor::transformToDoc(), XSLTProcessor::transformToUri() and + XSLTProcessor::transformToXml() now throw an Error when called while the + same processor is already transforming, for example from a php:function() + callback or from a suspended Fiber. Use a separate XSLTProcessor instance + instead. + . While XSLTProcessor transforms a document, the tree of that document, the + tree of every node a php:function() callback returned, and the detached + subtree of the current node or of a node passed to a callback are + read-only. Modifying such a node, for example from a callback or an entity + loader, throws a DOMException with code DOM_NO_MODIFICATION_ALLOWED_ERR (a + warning when strictErrorChecking is disabled), and modifying it through + SimpleXML throws an Error. Set XSLTProcessor::$cloneDocument to true to + transform a copy and keep the original document modifiable. + ======================================== 2. New Features ======================================== diff --git a/ext/dom/attr.c b/ext/dom/attr.c index aa613a9f6d37..a0b41d10d5a5 100644 --- a/ext/dom/attr.c +++ b/ext/dom/attr.c @@ -45,6 +45,10 @@ PHP_METHOD(DOMAttr, __construct) RETURN_THROWS(); } + if (dom_throw_if_node_read_only(Z_DOMOBJ_P(ZEND_THIS)->document, dom_object_get_node(Z_DOMOBJ_P(ZEND_THIS)))) { + return; + } + intern = Z_DOMOBJ_P(ZEND_THIS); name_valid = xmlValidateName(BAD_CAST name, 0); diff --git a/ext/dom/cdatasection.c b/ext/dom/cdatasection.c index b735a93f3bee..9822c725d195 100644 --- a/ext/dom/cdatasection.c +++ b/ext/dom/cdatasection.c @@ -40,6 +40,10 @@ PHP_METHOD(DOMCdataSection, __construct) RETURN_THROWS(); } + if (dom_throw_if_node_read_only(Z_DOMOBJ_P(ZEND_THIS)->document, dom_object_get_node(Z_DOMOBJ_P(ZEND_THIS)))) { + return; + } + nodep = xmlNewCDataBlock(NULL, BAD_CAST value, value_len); if (!nodep) { diff --git a/ext/dom/characterdata.c b/ext/dom/characterdata.c index 80dce80cf2f0..39d0b6632fde 100644 --- a/ext/dom/characterdata.c +++ b/ext/dom/characterdata.c @@ -164,6 +164,9 @@ static void dom_character_data_append_data(INTERNAL_FUNCTION_PARAMETERS) } DOM_GET_OBJ(nodep, ZEND_THIS, xmlNodePtr, intern); + if (dom_throw_if_node_read_only(intern->document, nodep)) { + return; + } xmlTextConcat(nodep, BAD_CAST arg, arg_len); } @@ -202,6 +205,10 @@ static void dom_character_data_insert_data(INTERNAL_FUNCTION_PARAMETERS, bool re DOM_GET_OBJ(node, id, xmlNodePtr, intern); + if (dom_throw_if_node_read_only(intern->document, node)) { + RETURN_FALSE; + } + const xmlChar *cur = php_dom_get_content_or_empty(node); length = xmlUTF8Strlen(cur); @@ -267,6 +274,10 @@ static void dom_character_data_delete_data(INTERNAL_FUNCTION_PARAMETERS, bool re DOM_GET_OBJ(node, id, xmlNodePtr, intern); + if (dom_throw_if_node_read_only(intern->document, node)) { + RETURN_FALSE; + } + const xmlChar *cur = php_dom_get_content_or_empty(node); length = xmlUTF8Strlen(cur); @@ -337,6 +348,10 @@ static void dom_character_data_replace_data(INTERNAL_FUNCTION_PARAMETERS, bool r DOM_GET_OBJ(node, id, xmlNodePtr, intern); + if (dom_throw_if_node_read_only(intern->document, node)) { + RETURN_FALSE; + } + const xmlChar *cur = php_dom_get_content_or_empty(node); length = xmlUTF8Strlen(cur); diff --git a/ext/dom/comment.c b/ext/dom/comment.c index b8ee8780d926..66cbf02cb768 100644 --- a/ext/dom/comment.c +++ b/ext/dom/comment.c @@ -40,6 +40,10 @@ PHP_METHOD(DOMComment, __construct) RETURN_THROWS(); } + if (dom_throw_if_node_read_only(Z_DOMOBJ_P(ZEND_THIS)->document, dom_object_get_node(Z_DOMOBJ_P(ZEND_THIS)))) { + return; + } + nodep = xmlNewComment(BAD_CAST value); if (!nodep) { diff --git a/ext/dom/document.c b/ext/dom/document.c index ccb2c08bc815..1b5e2470a5e8 100644 --- a/ext/dom/document.c +++ b/ext/dom/document.c @@ -1213,6 +1213,10 @@ static void dom_document_adopt_node(INTERNAL_FUNCTION_PARAMETERS, bool modern) RETURN_FALSE; } + if (dom_throw_if_node_read_only(dom_object_nodep->document, nodep)) { + RETURN_FALSE; + } + xmlDocPtr new_document; dom_object *dom_object_new_document; zval *new_document_zval = ZEND_THIS; @@ -1254,6 +1258,10 @@ PHP_METHOD(DOMDocument, normalizeDocument) DOM_GET_OBJ(docp, id, xmlDocPtr, intern); + if (dom_throw_if_node_read_only(intern->document, (xmlNodePtr) docp)) { + return; + } + php_dom_normalize_legacy((xmlNodePtr) docp); } /* }}} end dom_document_normalize_document */ @@ -1801,6 +1809,10 @@ PHP_METHOD(DOMDocument, xinclude) DOM_GET_OBJ(docp, ZEND_THIS, xmlDocPtr, intern); + if (dom_throw_if_node_read_only(intern->document, (xmlNodePtr) docp)) { + RETURN_FALSE; + } + int err = dom_perform_xinclude(docp, intern, flags); if (err) { @@ -1827,6 +1839,10 @@ PHP_METHOD(Dom_XMLDocument, xinclude) DOM_GET_OBJ(docp, ZEND_THIS, xmlDocPtr, intern); + if (dom_throw_if_node_read_only(intern->document, (xmlNodePtr) docp)) { + RETURN_THROWS(); + } + int err = dom_perform_xinclude(docp, intern, flags); if (err < 0) { diff --git a/ext/dom/documentfragment.c b/ext/dom/documentfragment.c index 01af436c69ed..ce43f701fab0 100644 --- a/ext/dom/documentfragment.c +++ b/ext/dom/documentfragment.c @@ -36,6 +36,10 @@ PHP_METHOD(DOMDocumentFragment, __construct) ZEND_PARSE_PARAMETERS_NONE(); + if (dom_throw_if_node_read_only(Z_DOMOBJ_P(ZEND_THIS)->document, dom_object_get_node(Z_DOMOBJ_P(ZEND_THIS)))) { + return; + } + nodep = xmlNewDocFragment(NULL); if (!nodep) { @@ -74,6 +78,10 @@ PHP_METHOD(DOMDocumentFragment, appendXML) { RETURN_FALSE; } + if (dom_throw_if_node_read_only(intern->document, nodep)) { + RETURN_FALSE; + } + if (data) { PHP_LIBXML_SANITIZE_GLOBALS(parse); err = xmlParseBalancedChunkMemory(nodep->doc, NULL, NULL, 0, BAD_CAST data, &lst); diff --git a/ext/dom/domimplementation.c b/ext/dom/domimplementation.c index e7fc2a414834..22a2cc9765b7 100644 --- a/ext/dom/domimplementation.c +++ b/ext/dom/domimplementation.c @@ -174,6 +174,9 @@ PHP_METHOD(DOMImplementation, createDocument) php_dom_throw_error(WRONG_DOCUMENT_ERR, true); RETURN_THROWS(); } + if (dom_throw_if_node_read_only(doctobj->document, (xmlNodePtr) doctype)) { + RETURN_THROWS(); + } } else { doctobj = NULL; } @@ -261,6 +264,9 @@ PHP_METHOD(Dom_Implementation, createDocument) if (dtd != NULL) { DOM_GET_OBJ(doctype, dtd, xmlDtdPtr, doctobj); + if (dom_throw_if_node_read_only(doctobj->document, (xmlNodePtr) doctype)) { + RETURN_THROWS(); + } } xmlDocPtr document = NULL; diff --git a/ext/dom/element.c b/ext/dom/element.c index bab828815a63..aacfcb13df8b 100644 --- a/ext/dom/element.c +++ b/ext/dom/element.c @@ -50,6 +50,10 @@ PHP_METHOD(DOMElement, __construct) RETURN_THROWS(); } + if (dom_throw_if_node_read_only(Z_DOMOBJ_P(ZEND_THIS)->document, dom_object_get_node(Z_DOMOBJ_P(ZEND_THIS)))) { + return; + } + name_valid = xmlValidateName(BAD_CAST name, 0); if (name_valid != 0) { php_dom_throw_error(INVALID_CHARACTER_ERR, true); @@ -429,6 +433,10 @@ PHP_METHOD(DOMElement, setAttribute) DOM_GET_OBJ(nodep, ZEND_THIS, xmlNodePtr, intern); + if (dom_throw_if_node_read_only(intern->document, nodep)) { + RETURN_FALSE; + } + if (php_dom_follow_spec_intern(intern)) { xmlChar *name_processed = BAD_CAST name; if (php_dom_ns_is_html_and_document_is_html(nodep)) { @@ -619,6 +627,10 @@ PHP_METHOD(DOMElement, removeAttribute) DOM_GET_OBJ(nodep, ZEND_THIS, xmlNodePtr, intern); + if (dom_throw_if_node_read_only(intern->document, nodep)) { + RETURN_FALSE; + } + attrp = dom_get_attribute_or_nsdecl(intern, nodep, BAD_CAST name, name_len); if (attrp == NULL) { RETURN_FALSE; @@ -640,6 +652,10 @@ PHP_METHOD(Dom_Element, removeAttribute) DOM_GET_OBJ(nodep, ZEND_THIS, xmlNodePtr, intern); + if (dom_throw_if_node_read_only(intern->document, nodep)) { + RETURN_THROWS(); + } + attrp = dom_get_attribute_or_nsdecl(intern, nodep, BAD_CAST name, name_len); if (attrp != NULL) { dom_remove_attribute(nodep, attrp, intern->document); @@ -699,6 +715,10 @@ static void dom_element_set_attribute_node_common(INTERNAL_FUNCTION_PARAMETERS, DOM_GET_OBJ(nodep, id, xmlNodePtr, intern); DOM_GET_OBJ(attrp, node, xmlAttrPtr, attrobj); + if (dom_throw_if_node_read_only(intern->document, nodep) || dom_throw_if_node_read_only(attrobj->document, (xmlNodePtr) attrp)) { + RETURN_FALSE; + } + /* ZPP Guarantees that a DOMAttr class is given, as it is converted to a xmlAttr * to pass to libxml (see http://www.xmlsoft.org/html/libxml-tree.html#xmlAttr) * if it is not of type XML_ATTRIBUTE_NODE it indicates a bug somewhere */ @@ -795,6 +815,10 @@ static void dom_element_remove_attribute_node(INTERNAL_FUNCTION_PARAMETERS, zend ZEND_ASSERT(attrp->type == XML_ATTRIBUTE_NODE); + if (dom_throw_if_node_read_only(intern->document, nodep)) { + RETURN_FALSE; + } + if (attrp->parent != nodep) { php_dom_throw_error(NOT_FOUND_ERR, dom_get_strict_error(intern->document)); RETURN_FALSE; @@ -1095,6 +1119,10 @@ PHP_METHOD(DOMElement, setAttributeNS) DOM_GET_OBJ(elemp, id, xmlNodePtr, intern); + if (dom_throw_if_node_read_only(intern->document, elemp)) { + return; + } + if (php_dom_follow_spec_intern(intern)) { dom_set_attribute_ns_modern(intern, elemp, uri, name, value); } else { @@ -1185,6 +1213,10 @@ PHP_METHOD(DOMElement, removeAttributeNS) DOM_GET_OBJ(nodep, id, xmlNodePtr, intern); + if (dom_throw_if_node_read_only(intern->document, nodep)) { + return; + } + bool follow_spec = php_dom_follow_spec_intern(intern); if (follow_spec && uri_len == 0) { uri = NULL; @@ -1408,6 +1440,10 @@ PHP_METHOD(DOMElement, setIdAttribute) DOM_GET_OBJ(nodep, id, xmlNodePtr, intern); + if (dom_throw_if_node_read_only(intern->document, nodep)) { + return; + } + attrp = xmlHasNsProp(nodep, BAD_CAST name, NULL); if (attrp == NULL || attrp->type == XML_ATTRIBUTE_DECL) { php_dom_throw_error(NOT_FOUND_ERR, dom_get_strict_error(intern->document)); @@ -1437,6 +1473,10 @@ PHP_METHOD(DOMElement, setIdAttributeNS) DOM_GET_OBJ(elemp, id, xmlNodePtr, intern); + if (dom_throw_if_node_read_only(intern->document, elemp)) { + return; + } + attrp = xmlHasNsProp(elemp, BAD_CAST name, BAD_CAST uri); if (attrp == NULL || attrp->type == XML_ATTRIBUTE_DECL) { php_dom_throw_error(NOT_FOUND_ERR, dom_get_strict_error(intern->document)); @@ -1465,6 +1505,10 @@ static void dom_element_set_id_attribute_node(INTERNAL_FUNCTION_PARAMETERS, zend DOM_GET_OBJ(nodep, id, xmlNodePtr, intern); DOM_GET_OBJ(attrp, node, xmlAttrPtr, attrobj); + if (dom_throw_if_node_read_only(intern->document, nodep)) { + return; + } + if (attrp->parent != nodep) { php_dom_throw_error(NOT_FOUND_ERR, dom_get_strict_error(intern->document)); } else { @@ -1665,6 +1709,10 @@ static void dom_element_insert_adjacent_element(INTERNAL_FUNCTION_PARAMETERS, ze DOM_GET_THIS_OBJ(thisp, id, xmlNodePtr, this_intern); DOM_GET_OBJ(otherp, element_zval, xmlNodePtr, other_intern); + if (dom_throw_if_node_read_only(this_intern->document, thisp) || dom_throw_if_node_read_only(other_intern->document, otherp)) { + RETURN_NULL(); + } + xmlNodePtr result = dom_insert_adjacent(where, thisp, this_intern, otherp); if (result == NULL) { RETURN_NULL(); @@ -1741,6 +1789,10 @@ static void dom_element_insert_adjacent_text(INTERNAL_FUNCTION_PARAMETERS, zend_ DOM_GET_THIS_OBJ(thisp, id, xmlNodePtr, this_intern); + if (dom_throw_if_node_read_only(this_intern->document, thisp)) { + return; + } + if (UNEXPECTED(ZEND_SIZE_T_INT_OVFL(ZSTR_LEN(data)))) { zend_argument_value_error(2, "is too long"); RETURN_THROWS(); @@ -1809,6 +1861,10 @@ PHP_METHOD(Dom_Element, insertAdjacentHTML) DOM_GET_THIS_OBJ(thisp, id, xmlNodePtr, this_intern); + if (dom_throw_if_node_read_only(this_intern->document, thisp)) { + RETURN_THROWS(); + } + /* 1. We don't do injection sinks. */ /* 2. Let context be NULL */ @@ -1899,6 +1955,10 @@ PHP_METHOD(DOMElement, toggleAttribute) DOM_GET_THIS_OBJ(thisp, id, xmlNodePtr, intern); + if (dom_throw_if_node_read_only(intern->document, thisp)) { + RETURN_FALSE; + } + /* Step 1 */ if (xmlValidateName(BAD_CAST qname, 0) != 0) { php_dom_throw_error(INVALID_CHARACTER_ERR, true); @@ -2177,6 +2237,10 @@ PHP_METHOD(Dom_Element, rename) xmlNodePtr nodep; DOM_GET_THIS_OBJ(nodep, id, xmlNodePtr, intern); + if (dom_throw_if_node_read_only(intern->document, nodep)) { + RETURN_THROWS(); + } + xmlChar *localname = NULL, *prefix = NULL; int errorcode = dom_validate_and_extract(namespace_uri, qualified_name, &localname, &prefix); if (UNEXPECTED(errorcode != 0)) { diff --git a/ext/dom/entityreference.c b/ext/dom/entityreference.c index a669fd2aa791..be14144bc3f3 100644 --- a/ext/dom/entityreference.c +++ b/ext/dom/entityreference.c @@ -42,6 +42,10 @@ PHP_METHOD(DOMEntityReference, __construct) RETURN_THROWS(); } + if (dom_throw_if_node_read_only(Z_DOMOBJ_P(ZEND_THIS)->document, dom_object_get_node(Z_DOMOBJ_P(ZEND_THIS)))) { + return; + } + name_valid = xmlValidateName(BAD_CAST name, 0); if (name_valid != 0) { php_dom_throw_error(INVALID_CHARACTER_ERR, true); diff --git a/ext/dom/html_document.c b/ext/dom/html_document.c index 5f190ec7c97b..2f866f94add8 100644 --- a/ext/dom/html_document.c +++ b/ext/dom/html_document.c @@ -1547,6 +1547,9 @@ zend_result dom_html_document_body_write(dom_object *obj, zval *newval) dom_object *newval_intern = Z_DOMOBJ_P(newval); if (newval_intern->ptr != NULL) { xmlNodePtr newval_node = ((php_libxml_node_ptr *) newval_intern->ptr)->node; + if (dom_throw_if_node_read_only(obj->document, (xmlNodePtr) docp) || dom_throw_if_node_read_only(newval_intern->document, newval_node)) { + return FAILURE; + } /* Note: because this property has type HTMLElement, we know the namespace is correct. */ if (dom_accept_body_name(newval_node->name)) { /* 2. If the new value is the same as the body element, return. */ @@ -1684,6 +1687,9 @@ static void dom_string_replace_all(xmlDocPtr docp, xmlNodePtr element, zval *zv) zend_result dom_html_document_title_write(dom_object *obj, zval *newval) { DOM_PROP_NODE(xmlDocPtr, docp, obj); + if (dom_throw_if_node_read_only(obj->document, (xmlNodePtr) docp)) { + return FAILURE; + } xmlNodePtr root = xmlDocGetRootElement(docp); if (root == NULL) { diff --git a/ext/dom/node.c b/ext/dom/node.c index 88c058a6ac51..9e5cb43b86b8 100644 --- a/ext/dom/node.c +++ b/ext/dom/node.c @@ -1104,6 +1104,10 @@ static void dom_node_insert_before(INTERNAL_FUNCTION_PARAMETERS, bool modern) DOM_GET_OBJ(child, node, xmlNodePtr, childobj); + if (dom_throw_if_node_read_only(intern->document, parentp) || dom_throw_if_node_read_only(childobj->document, child)) { + RETURN_FALSE; + } + if (modern) { dom_node_insert_before_modern(return_value, ref, intern, parentp, child); } else { @@ -1226,6 +1230,10 @@ static void dom_node_replace_child(INTERNAL_FUNCTION_PARAMETERS, bool modern) DOM_GET_OBJ(newchild, newnode, xmlNodePtr, newchildobj); DOM_GET_OBJ(oldchild, oldnode, xmlNodePtr, oldchildobj); + if (dom_throw_if_node_read_only(intern->document, nodep) || dom_throw_if_node_read_only(newchildobj->document, newchild)) { + RETURN_FALSE; + } + bool stricterror = dom_get_strict_error(intern->document); if (newchild->doc != nodep->doc && newchild->doc != NULL) { @@ -1325,6 +1333,10 @@ static void dom_node_remove_child(INTERNAL_FUNCTION_PARAMETERS, zend_class_entry DOM_GET_OBJ(child, node, xmlNodePtr, childobj); + if (dom_throw_if_node_read_only(intern->document, nodep)) { + RETURN_FALSE; + } + bool stricterror = dom_get_strict_error(intern->document); if (!nodep->children || child->parent != nodep) { @@ -1456,6 +1468,10 @@ PHP_METHOD(DOMNode, appendChild) DOM_GET_OBJ(nodep, ZEND_THIS, xmlNodePtr, intern); DOM_GET_OBJ(child, node, xmlNodePtr, childobj); + if (dom_throw_if_node_read_only(intern->document, nodep) || dom_throw_if_node_read_only(childobj->document, child)) { + RETURN_FALSE; + } + dom_node_append_child_legacy(return_value, intern, childobj, nodep, child); } @@ -1472,6 +1488,10 @@ PHP_METHOD(Dom_Node, appendChild) DOM_GET_OBJ(nodep, ZEND_THIS, xmlNodePtr, intern); DOM_GET_OBJ(child, node, xmlNodePtr, childobj); + if (dom_throw_if_node_read_only(intern->document, nodep) || dom_throw_if_node_read_only(childobj->document, child)) { + RETURN_THROWS(); + } + /* Parent check from pre-insertion validation done here: * If parent is not a Document, DocumentFragment, or Element node, then throw a "HierarchyRequestError" DOMException. */ if (php_dom_pre_insert_is_parent_invalid(nodep)) { @@ -1583,6 +1603,10 @@ PHP_METHOD(DOMNode, normalize) DOM_GET_OBJ(nodep, id, xmlNodePtr, intern); + if (dom_throw_if_node_read_only(intern->document, nodep)) { + return; + } + if (php_dom_follow_spec_intern(intern)) { php_dom_normalize_modern(nodep); } else { diff --git a/ext/dom/parentnode/tree.c b/ext/dom/parentnode/tree.c index aacc991cc735..75f385d0fb55 100644 --- a/ext/dom/parentnode/tree.c +++ b/ext/dom/parentnode/tree.c @@ -482,6 +482,24 @@ static zend_result dom_sanity_check_node_list_types(zval *nodes, uint32_t nodesc return SUCCESS; } +static bool dom_throw_if_nodes_read_only(dom_object *context, const zval *nodes, uint32_t nodesc) +{ + if (dom_throw_if_node_read_only(context->document, dom_object_get_node(context))) { + return true; + } + + for (uint32_t i = 0; i < nodesc; i++) { + if (Z_TYPE(nodes[i]) == IS_OBJECT) { + dom_object *intern = Z_DOMOBJ_P(&nodes[i]); + if (dom_throw_if_node_read_only(intern->document, dom_object_get_node(intern))) { + return true; + } + } + } + + return false; +} + static void php_dom_pre_insert_helper(xmlNodePtr insertion_point, xmlNodePtr parentNode, xmlNodePtr newchild, xmlNodePtr last) { if (!insertion_point) { @@ -609,6 +627,10 @@ void dom_parent_node_append(dom_object *context, zval *nodes, uint32_t nodesc) return; } + if (dom_throw_if_nodes_read_only(context, nodes, nodesc)) { + return; + } + xmlNode *parentNode = dom_object_get_node(context); php_libxml_invalidate_node_list_cache(context->document); @@ -637,6 +659,10 @@ void dom_parent_node_prepend(dom_object *context, zval *nodes, uint32_t nodesc) return; } + if (dom_throw_if_nodes_read_only(context, nodes, nodesc)) { + return; + } + php_libxml_invalidate_node_list_cache(context->document); /* 1. Let node be the result of converting nodes into a node given nodes and this’s node document. */ @@ -656,6 +682,10 @@ void dom_parent_node_after(dom_object *context, zval *nodes, uint32_t nodesc) return; } + if (dom_throw_if_nodes_read_only(context, nodes, nodesc)) { + return; + } + xmlNode *thisp = dom_object_get_node(context); /* 1. Let parent be this’s parent. */ @@ -688,6 +718,10 @@ void dom_parent_node_before(dom_object *context, zval *nodes, uint32_t nodesc) return; } + if (dom_throw_if_nodes_read_only(context, nodes, nodesc)) { + return; + } + xmlNode *thisp = dom_object_get_node(context); /* 1. Let parent be this’s parent. */ @@ -722,6 +756,10 @@ void dom_parent_node_before(dom_object *context, zval *nodes, uint32_t nodesc) static zend_result dom_child_removal_preconditions(const xmlNode *child, const dom_object *context) { + if (dom_throw_if_node_read_only(context->document, child)) { + return FAILURE; + } + if (dom_node_is_read_only(child) || (child->parent != NULL && dom_node_is_read_only(child->parent))) { php_dom_throw_error(NO_MODIFICATION_ALLOWED_ERR, dom_get_strict_error(context->document)); @@ -756,6 +794,10 @@ void dom_child_replace_with(dom_object *context, zval *nodes, uint32_t nodesc) return; } + if (dom_throw_if_nodes_read_only(context, nodes, nodesc)) { + return; + } + xmlNodePtr child = dom_object_get_node(context); /* 1. Let parent be this’s parent. */ @@ -805,6 +847,10 @@ void dom_parent_node_replace_children(dom_object *context, zval *nodes, uint32_t return; } + if (dom_throw_if_nodes_read_only(context, nodes, nodesc)) { + return; + } + xmlNodePtr thisp = dom_object_get_node(context); php_libxml_invalidate_node_list_cache(context->document); diff --git a/ext/dom/php_dom.c b/ext/dom/php_dom.c index 0833d9a3c9be..c70664673ba3 100644 --- a/ext/dom/php_dom.c +++ b/ext/dom/php_dom.c @@ -170,6 +170,11 @@ bool dom_node_is_read_only(const xmlNode *node) { } } +ZEND_COLD void php_dom_throw_read_only_node_error(php_libxml_ref_obj *document) +{ + php_dom_throw_error_with_message(NO_MODIFICATION_ALLOWED_ERR, "Cannot modify a node while an XPath evaluation or XSLT transformation that uses it is in progress", dom_get_strict_error(document)); +} + bool dom_node_children_valid(const xmlNode *node) { switch (node->type) { case XML_DOCUMENT_TYPE_NODE: @@ -432,6 +437,15 @@ zval *dom_write_property(zend_object *object, zend_string *name, zval *value, vo return &EG(error_zval); } + if (UNEXPECTED(obj->document == NULL || obj->document->read_only_depth > 0) + && (instanceof_function(object->ce, dom_node_class_entry) || instanceof_function(object->ce, dom_modern_node_class_entry))) { + const xmlNode *nodep = dom_object_get_node(obj); + if (nodep != NULL && nodep->type != XML_DOCUMENT_NODE && nodep->type != XML_HTML_DOCUMENT_NODE + && dom_throw_if_node_read_only(obj->document, nodep)) { + return value; + } + } + ZEND_ASSERT(prop && ZEND_TYPE_IS_SET(prop->type)); zval tmp; ZVAL_COPY(&tmp, value); diff --git a/ext/dom/php_dom.h b/ext/dom/php_dom.h index 24475022e614..32f77257af79 100644 --- a/ext/dom/php_dom.h +++ b/ext/dom/php_dom.h @@ -126,6 +126,7 @@ void php_dom_create_implementation(zval *retval, bool modern); int dom_hierarchy(xmlNodePtr parent, xmlNodePtr child); bool dom_has_feature(zend_string *feature, zend_string *version); bool dom_node_is_read_only(const xmlNode *node); +ZEND_COLD void php_dom_throw_read_only_node_error(php_libxml_ref_obj *document); bool dom_node_children_valid(const xmlNode *node); xmlNodePtr create_notation(xmlDtdPtr parent_dtd, const xmlChar *name, const xmlChar *ExternalID, const xmlChar *SystemID); void dom_free_notation(xmlEntityPtr entity); @@ -259,6 +260,15 @@ static zend_always_inline void php_dom_mark_cache_tag_up_to_date_from_doc_ref(ph cache_tag->modification_nr = document->cache_tag.modification_nr; } +static zend_always_inline bool dom_throw_if_node_read_only(php_libxml_ref_obj *document, const xmlNode *node) +{ + if (php_libxml_is_node_read_only(document, node)) { + php_dom_throw_read_only_node_error(document); + return true; + } + return false; +} + static zend_always_inline bool php_dom_follow_spec_node(const xmlNode *node) { ZEND_ASSERT(node != NULL); diff --git a/ext/dom/processinginstruction.c b/ext/dom/processinginstruction.c index 94e5d5b35067..75bff8594839 100644 --- a/ext/dom/processinginstruction.c +++ b/ext/dom/processinginstruction.c @@ -42,6 +42,10 @@ PHP_METHOD(DOMProcessingInstruction, __construct) RETURN_THROWS(); } + if (dom_throw_if_node_read_only(Z_DOMOBJ_P(ZEND_THIS)->document, dom_object_get_node(Z_DOMOBJ_P(ZEND_THIS)))) { + return; + } + name_valid = xmlValidateName(BAD_CAST name, 0); if (name_valid != 0) { php_dom_throw_error(INVALID_CHARACTER_ERR, true); diff --git a/ext/dom/tests/xpath_php_function_detached_tree.phpt b/ext/dom/tests/xpath_php_function_detached_tree.phpt new file mode 100644 index 000000000000..88f7fe691b42 --- /dev/null +++ b/ext/dom/tests/xpath_php_function_detached_tree.phpt @@ -0,0 +1,123 @@ +--TEST-- +DOMXPath: detached subtrees and returned nodes stay alive while a php:function() evaluation uses them +--EXTENSIONS-- +dom +--FILE-- +loadXML(''); +$xp = new DOMXPath($doc); +$xp->registerNamespace('php', 'http://php.net/xpath'); +$xp->registerPhpFunctions(); + +function subtree() { + $tree = $GLOBALS['doc']->createElement('tree'); + foreach (['a', 'b', 'c'] as $name) { + $tree->appendChild($GLOBALS['doc']->createElement($name)); + } + return $tree; +} + +function drop() { + unset($GLOBALS['tree']); + return true; +} + +function first_of_tree() { + return $GLOBALS['tree']->firstChild; +} + +function reconstruct() { + try { + $GLOBALS['tree']->__construct('other'); + } catch (DOMException $e) { + echo $e::class, ": ", $e->getMessage(), "\n"; + } + return true; +} + +$tree = subtree(); +$context = $tree->firstChild; +foreach ($xp->query('../*[php:function("drop")]', $context) as $node) { + echo $node->nodeName, "\n"; +} + +$tree = subtree(); +foreach ($xp->query('php:function("first_of_tree")/../*[php:function("drop")]') as $node) { + echo $node->nodeName, "\n"; +} + +$tree = subtree(); +foreach ($xp->query('*[php:function("reconstruct")]', $tree) as $node) { + echo $node->nodeName, "\n"; +} + +class Unconstructed extends DOMElement { + public function __construct() {} +} + +function unconstructed() { + return new Unconstructed(); +} + +var_dump($xp->evaluate('count(php:function("unconstructed"))')); + +$docless = new DOMElement('docless', 'abc'); + +function docless() { + return $GLOBALS['docless']; +} + +function modify_docless() { + try { + $GLOBALS['docless']->textContent = 'xyz'; + } catch (DOMException $e) { + echo $e::class, ": ", $e->getMessage(), "\n"; + } + return true; +} + +foreach ($xp->query('php:function("docless")/text()[php:function("modify_docless")]') as $node) { + var_dump($node->nodeValue); +} +$docless->textContent = 'xyz'; +var_dump($docless->textContent); + +$doctype = (new DOMImplementation())->createDocumentType('q'); + +function doctype() { + return $GLOBALS['doctype']; +} + +function attach_doctype() { + try { + (new DOMImplementation())->createDocument('', 'r', $GLOBALS['doctype']); + } catch (DOMException $e) { + echo $e::class, ": ", $e->getMessage(), "\n"; + } + return 1; +} + +var_dump($xp->evaluate('count(php:function("doctype")) + php:function("attach_doctype")')); +var_dump($doctype->ownerDocument); +?> +--EXPECT-- +a +b +c +a +b +c +DOMException: Cannot modify a node while an XPath evaluation or XSLT transformation that uses it is in progress +DOMException: Cannot modify a node while an XPath evaluation or XSLT transformation that uses it is in progress +DOMException: Cannot modify a node while an XPath evaluation or XSLT transformation that uses it is in progress +a +b +c +float(0) +DOMException: Cannot modify a node while an XPath evaluation or XSLT transformation that uses it is in progress +string(3) "abc" +string(3) "xyz" +DOMException: Cannot modify a node while an XPath evaluation or XSLT transformation that uses it is in progress +float(2) +NULL diff --git a/ext/dom/tests/xpath_php_function_modifies_document.phpt b/ext/dom/tests/xpath_php_function_modifies_document.phpt new file mode 100644 index 000000000000..064a0dd2bb48 --- /dev/null +++ b/ext/dom/tests/xpath_php_function_modifies_document.phpt @@ -0,0 +1,120 @@ +--TEST-- +DOMXPath: a php:function() callback cannot modify the document that is being evaluated +--EXTENSIONS-- +dom +--FILE-- +getMessage() . " (" . $e->getCode() . ")"] = true; + } + return true; +} + +function evaluate(object $xp, string $label, Closure $mutation, string $expr = '/root/node()[php:function("cb")]') { + $GLOBALS['mutation'] = $mutation; + $GLOBALS['errors'] = []; + $res = $xp->evaluate($expr); + echo $label, ": ", count($res), " nodes\n"; + echo implode("\n", array_keys($GLOBALS['errors'])), "\n"; +} + +$doc = new DOMDocument(); +$doc->loadXML('123'); +$root = $doc->documentElement; +$xp = new DOMXPath($doc); +$xp->registerNamespace('php', 'http://php.net/xpath'); +$xp->registerPhpFunctions(); + +evaluate($xp, 'removeChild', function () use ($root) { + foreach (iterator_to_array($root->childNodes) as $child) { + $root->removeChild($child); + } +}); +evaluate($xp, 'replaceChild', function () use ($doc, $root) { + $root->replaceChild($doc->createElement('y'), $root->lastChild); +}); +evaluate($xp, 'textContent', function () use ($root) { + $root->textContent = 'x'; +}); +evaluate($xp, 'removeAttribute', function () use ($root) { + $root->removeAttribute('a'); +}, '/root/@*[php:function("cb")]'); +echo $doc->saveXML($root), "\n"; + +$other = new DOMDocument(); +$other->loadXML(''); +evaluate($xp, 'other document', function () use ($other) { + $other->documentElement->firstChild?->remove(); +}); +echo $other->saveXML($other->documentElement), "\n"; + +function build($nodes) { + $doc = $nodes[0]->ownerDocument; + $span = $doc->createElement('span'); + $span->setAttribute('class', 'w'); + $span->appendChild($doc->createTextNode(strtoupper($nodes[0]->textContent))); + $doc->formatOutput = true; + $doc->documentURI = 'urn:built'; + try { + $doc->documentElement->appendChild($doc->createElement('extra')); + } catch (DOMException $e) { + echo $e::class, ": ", $e->getMessage(), "\n"; + } + return $span; +} +foreach ($xp->query('php:function("build", /root/x[1])') as $node) { + echo $doc->saveXML($node), " ", $doc->documentURI, "\n"; +} +$doc->formatOutput = false; + +$doc->strictErrorChecking = false; +$GLOBALS['mutation'] = function () use ($root) { + var_dump($root->removeChild($root->firstChild)); +}; +var_dump($xp->evaluate('boolean(/root[php:function("cb")])')); +$doc->strictErrorChecking = true; + +$root->removeChild($root->firstChild); +$root->removeAttribute('a'); +echo $doc->saveXML($root), "\n"; + +$modern = Dom\XMLDocument::createFromString('12'); +$modern_xp = new Dom\XPath($modern); +$modern_xp->registerNamespace('php', 'http://php.net/xpath'); +$modern_xp->registerPhpFunctions(); +evaluate($modern_xp, 'Dom\Element::remove', function () use ($modern) { + $modern->documentElement->firstChild->remove(); +}); +evaluate($modern_xp, 'Dom\Element::$innerHTML', function () use ($modern) { + $modern->documentElement->innerHTML = ''; +}); +echo $modern->saveXml($modern->documentElement), "\n"; +?> +--EXPECTF-- +removeChild: 3 nodes +DOMException: Cannot modify a node while an XPath evaluation or XSLT transformation that uses it is in progress (7) +replaceChild: 3 nodes +DOMException: Cannot modify a node while an XPath evaluation or XSLT transformation that uses it is in progress (7) +textContent: 3 nodes +DOMException: Cannot modify a node while an XPath evaluation or XSLT transformation that uses it is in progress (7) +removeAttribute: 2 nodes +DOMException: Cannot modify a node while an XPath evaluation or XSLT transformation that uses it is in progress (7) +123 +other document: 3 nodes + + +DOMException: Cannot modify a node while an XPath evaluation or XSLT transformation that uses it is in progress +1 urn:built + +Warning: DOMNode::removeChild(): Cannot modify a node while an XPath evaluation or XSLT transformation that uses it is in progress in %s on line %d +bool(false) +bool(true) +23 +Dom\Element::remove: 2 nodes +DOMException: Cannot modify a node while an XPath evaluation or XSLT transformation that uses it is in progress (7) +Dom\Element::$innerHTML: 2 nodes +DOMException: Cannot modify a node while an XPath evaluation or XSLT transformation that uses it is in progress (7) +12 diff --git a/ext/dom/tests/xpath_php_function_modifies_document_bailout.phpt b/ext/dom/tests/xpath_php_function_modifies_document_bailout.phpt new file mode 100644 index 000000000000..68aaa065a4d4 --- /dev/null +++ b/ext/dom/tests/xpath_php_function_modifies_document_bailout.phpt @@ -0,0 +1,38 @@ +--TEST-- +DOMXPath: a document becomes writable again after a fatal error in a php:function() callback +--EXTENSIONS-- +dom +zend_test +--SKIPIF-- + +--FILE-- +loadXML(''); +$xp = new DOMXPath($doc); +$xp->registerNamespace('php', 'http://php.net/xpath'); +$xp->registerPhpFunctions(); + +function cb() { + try { + $GLOBALS['doc']->documentElement->removeChild($GLOBALS['doc']->documentElement->firstChild); + } catch (DOMException $e) { + echo $e::class, ": ", $e->getMessage(), "\n"; + } + zend_trigger_bailout(); +} + +register_shutdown_function(function () use ($doc) { + $doc->documentElement->removeChild($doc->documentElement->firstChild); + echo $doc->saveXML($doc->documentElement), "\n"; +}); + +$xp->evaluate('boolean(php:function("cb"))'); +?> +--EXPECTF-- +DOMException: Cannot modify a node while an XPath evaluation or XSLT transformation that uses it is in progress + +Fatal error: Bailout in %s on line %d + diff --git a/ext/dom/tests/xpath_php_function_modifies_document_fiber.phpt b/ext/dom/tests/xpath_php_function_modifies_document_fiber.phpt new file mode 100644 index 000000000000..d4cd58e72d6d --- /dev/null +++ b/ext/dom/tests/xpath_php_function_modifies_document_fiber.phpt @@ -0,0 +1,44 @@ +--TEST-- +DOMXPath: a document stays read-only while a Fiber is suspended inside a php:function() callback +--EXTENSIONS-- +dom +--FILE-- +loadXML(''); +$xp = new DOMXPath($doc); +$xp->registerNamespace('php', 'http://php.net/xpath'); +$xp->registerPhpFunctions(); + +function suspend() { + Fiber::suspend(); + return true; +} + +$fiber = new Fiber(function () use ($xp) { + $names = []; + foreach ($xp->query('/root/*[php:function("suspend")]') as $node) { + $names[] = $node->nodeName; + } + return $names; +}); +$fiber->start(); + +try { + $doc->documentElement->removeChild($doc->documentElement->firstChild); +} catch (DOMException $e) { + echo $e::class, ": ", $e->getMessage(), "\n"; +} + +while (!$fiber->isTerminated()) { + $fiber->resume(); +} +echo implode(",", $fiber->getReturn()), "\n"; + +$doc->documentElement->removeChild($doc->documentElement->firstChild); +echo $doc->saveXML($doc->documentElement), "\n"; +?> +--EXPECT-- +DOMException: Cannot modify a node while an XPath evaluation or XSLT transformation that uses it is in progress +a,b,c + diff --git a/ext/dom/tests/xpath_php_function_modifies_foreign_document.phpt b/ext/dom/tests/xpath_php_function_modifies_foreign_document.phpt new file mode 100644 index 000000000000..46762a3b11f9 --- /dev/null +++ b/ext/dom/tests/xpath_php_function_modifies_foreign_document.phpt @@ -0,0 +1,44 @@ +--TEST-- +DOMXPath: a php:function() callback cannot modify a document whose node another callback returned +--EXTENSIONS-- +dom +--FILE-- +loadXML(''); +$other = new DOMDocument(); +$other->loadXML('123'); +$xp = new DOMXPath($doc); +$xp->registerNamespace('php', 'http://php.net/xpath'); +$xp->registerPhpFunctions(); + +function other_root() { + return $GLOBALS['other']->documentElement; +} + +function cb() { + $root = $GLOBALS['other']->documentElement; + try { + foreach (iterator_to_array($root->childNodes) as $child) { + $root->removeChild($child); + } + } catch (DOMException $e) { + $GLOBALS['errors'][$e::class . ": " . $e->getMessage()] = true; + } + return true; +} + +$errors = []; +$res = $xp->evaluate('php:function("other_root")/a[php:function("cb")]'); +echo "count: ", count($res), "\n"; +echo implode("\n", array_keys($errors)), "\n"; +echo $other->saveXML($other->documentElement), "\n"; + +$other->documentElement->removeChild($other->documentElement->firstChild); +echo $other->saveXML($other->documentElement), "\n"; +?> +--EXPECT-- +count: 3 +DOMException: Cannot modify a node while an XPath evaluation or XSLT transformation that uses it is in progress +123 +23 diff --git a/ext/dom/tests/xpath_php_function_removes_argument_node.phpt b/ext/dom/tests/xpath_php_function_removes_argument_node.phpt index e6696617be25..9cc50c14f94c 100644 --- a/ext/dom/tests/xpath_php_function_removes_argument_node.phpt +++ b/ext/dom/tests/xpath_php_function_removes_argument_node.phpt @@ -1,5 +1,5 @@ --TEST-- -DOMXPath: a php:function callback that removes its argument node must not free it mid-evaluation +DOMXPath: a php:function callback cannot remove its argument node mid-evaluation --EXTENSIONS-- dom --FILE-- @@ -12,8 +12,10 @@ $xp->registerPhpFunctions(); function cb($nodes) { foreach ($nodes as $n) { - if ($n->parentNode) { + try { $n->parentNode->removeChild($n); + } catch (DOMException $e) { + echo $e::class, ": ", $e->getMessage(), "\n"; } } return true; @@ -26,6 +28,10 @@ foreach ($res as $r) { echo "done\n"; ?> --EXPECT-- +DOMException: Cannot modify a node while an XPath evaluation or XSLT transformation that uses it is in progress +DOMException: Cannot modify a node while an XPath evaluation or XSLT transformation that uses it is in progress +DOMException: Cannot modify a node while an XPath evaluation or XSLT transformation that uses it is in progress +DOMException: Cannot modify a node while an XPath evaluation or XSLT transformation that uses it is in progress string(1) "a" string(1) "a" string(1) "a" diff --git a/ext/dom/tests/xpath_php_function_simplexml_modifies_document.phpt b/ext/dom/tests/xpath_php_function_simplexml_modifies_document.phpt new file mode 100644 index 000000000000..b444b4d56a3b --- /dev/null +++ b/ext/dom/tests/xpath_php_function_simplexml_modifies_document.phpt @@ -0,0 +1,50 @@ +--TEST-- +DOMXPath: a php:function() callback cannot modify the document that is being evaluated through SimpleXML +--EXTENSIONS-- +dom +simplexml +--FILE-- +loadXML('1234'); +$xp = new DOMXPath($doc); +$xp->registerNamespace('php', 'http://php.net/xpath'); +$xp->registerPhpFunctions(); + +function cb() { + $sxe = simplexml_import_dom($GLOBALS['doc']); + $mutations = [ + 'assign' => function () use ($sxe) { $sxe->a[1] = 'z'; }, + 'unset' => function () use ($sxe) { unset($sxe->a[0]); }, + 'attribute' => function () use ($sxe) { $sxe['a'] = '2'; }, + 'addChild' => function () use ($sxe) { $sxe->addChild('c'); }, + 'addAttribute' => function () use ($sxe) { $sxe->addAttribute('d', '3'); }, + ]; + foreach ($mutations as $label => $mutation) { + try { + $mutation(); + } catch (Error $e) { + $GLOBALS['errors'][$label . ": " . $e::class . ": " . $e->getMessage()] = true; + } + } + return true; +} + +$errors = []; +var_dump(count($xp->query('(//a/node())[php:function("cb")]'))); +echo implode("\n", array_keys($errors)), "\n"; +echo $doc->saveXML($doc->documentElement), "\n"; + +$sxe = simplexml_import_dom($doc); +$sxe->a[1] = 'z'; +echo $doc->saveXML($doc->documentElement), "\n"; +?> +--EXPECT-- +int(6) +assign: Error: Cannot modify a node while an XPath evaluation or XSLT transformation that uses it is in progress +unset: Error: Cannot modify a node while an XPath evaluation or XSLT transformation that uses it is in progress +attribute: Error: Cannot modify a node while an XPath evaluation or XSLT transformation that uses it is in progress +addChild: Error: Cannot modify a node while an XPath evaluation or XSLT transformation that uses it is in progress +addAttribute: Error: Cannot modify a node while an XPath evaluation or XSLT transformation that uses it is in progress +1234 +12z diff --git a/ext/dom/text.c b/ext/dom/text.c index 357ff1969490..a807e81734eb 100644 --- a/ext/dom/text.c +++ b/ext/dom/text.c @@ -41,6 +41,10 @@ PHP_METHOD(DOMText, __construct) RETURN_THROWS(); } + if (dom_throw_if_node_read_only(Z_DOMOBJ_P(ZEND_THIS)->document, dom_object_get_node(Z_DOMOBJ_P(ZEND_THIS)))) { + return; + } + nodep = xmlNewText(BAD_CAST value); if (!nodep) { @@ -107,6 +111,10 @@ PHP_METHOD(DOMText, splitText) } DOM_GET_OBJ(node, ZEND_THIS, xmlNodePtr, intern); + if (dom_throw_if_node_read_only(intern->document, node)) { + RETURN_FALSE; + } + if (offset < 0) { zend_argument_value_error(1, "must be greater than or equal to 0"); RETURN_THROWS(); diff --git a/ext/dom/token_list.c b/ext/dom/token_list.c index 4adaf94c203d..c73770d991e4 100644 --- a/ext/dom/token_list.c +++ b/ext/dom/token_list.c @@ -431,6 +431,9 @@ zend_result dom_token_list_value_write(dom_object *obj, zval *newval) zend_value_error("Value must not contain any null bytes"); return FAILURE; } + if (dom_throw_if_node_read_only(intern->dom.document, dom_token_list_get_element(intern))) { + return FAILURE; + } php_libxml_invalidate_node_list_cache(intern->dom.document); dom_set_attribute_value(dom_token_list_get_element(intern), NULL, BAD_CAST "class", BAD_CAST Z_STRVAL_P(newval)); /* Note: we don't update the set here, the set is always lazily updated for performance reasons. */ @@ -519,6 +522,9 @@ PHP_METHOD(Dom_TokenList, add) /* 2. For each token in tokens, append token to this’s token set. */ dom_token_list_object *intern = TOKEN_LIST_GET_INTERNAL(); + if (dom_throw_if_node_read_only(intern->dom.document, dom_token_list_get_element(intern))) { + RETURN_THROWS(); + } dom_token_list_ensure_set_up_to_date(intern); HashTable *token_set = TOKEN_LIST_GET_SET(intern); for (uint32_t i = 0; i < argc; i++) { @@ -545,6 +551,9 @@ PHP_METHOD(Dom_TokenList, remove) /* 2. For each token in tokens, remove token from this’s token set. */ dom_token_list_object *intern = TOKEN_LIST_GET_INTERNAL(); + if (dom_throw_if_node_read_only(intern->dom.document, dom_token_list_get_element(intern))) { + RETURN_THROWS(); + } dom_token_list_ensure_set_up_to_date(intern); HashTable *token_set = TOKEN_LIST_GET_SET(intern); for (uint32_t i = 0; i < argc; i++) { @@ -573,6 +582,9 @@ PHP_METHOD(Dom_TokenList, toggle) /* 3. If this’s token set[token] exists, then: */ dom_token_list_object *intern = TOKEN_LIST_GET_INTERNAL(); + if (dom_throw_if_node_read_only(intern->dom.document, dom_token_list_get_element(intern))) { + RETURN_THROWS(); + } dom_token_list_ensure_set_up_to_date(intern); HashTable *token_set = TOKEN_LIST_GET_SET(intern); zval *found_token = zend_hash_find(token_set, token); @@ -619,6 +631,9 @@ PHP_METHOD(Dom_TokenList, replace) /* 3. If this’s token set does not contain token, then return false. */ dom_token_list_object *intern = TOKEN_LIST_GET_INTERNAL(); + if (dom_throw_if_node_read_only(intern->dom.document, dom_token_list_get_element(intern))) { + RETURN_THROWS(); + } dom_token_list_ensure_set_up_to_date(intern); HashTable *token_set = TOKEN_LIST_GET_SET(intern); zval *found_token = zend_hash_find(token_set, token); diff --git a/ext/dom/xpath.c b/ext/dom/xpath.c index e3c5e48f6cd5..235a988a001e 100644 --- a/ext/dom/xpath.c +++ b/ext/dom/xpath.c @@ -273,12 +273,51 @@ PHP_METHOD(DOMXPath, registerNamespace) } /* }}} */ +static xmlXPathObjectPtr dom_xpath_eval_expression(dom_xpath_object *intern, xmlXPathContextPtr ctxp, const char *expr, dom_object *context_obj) +{ + intern->evaluation_depth++; + + if (!php_dom_xpath_callbacks_registered(&intern->xpath_callbacks)) { + xmlXPathObjectPtr xpathobjp = xmlXPathEvalExpression(BAD_CAST expr, ctxp); + intern->evaluation_depth--; + return xpathobjp; + } + + php_libxml_ref_obj *document = intern->dom.document; + xmlXPathObjectPtr xpathobjp = NULL; + bool bailout = false; + + document->read_only_depth++; + intern->xpath_callbacks.read_only_depth++; + if (context_obj != NULL) { + xmlNodePtr nodep = ctxp->node; + php_dom_xpath_callbacks_pin_tree(&intern->xpath_callbacks, nodep->type == XML_NAMESPACE_DECL ? nodep->parent : nodep, context_obj); + } + zend_try { + xpathobjp = xmlXPathEvalExpression(BAD_CAST expr, ctxp); + } zend_catch { + bailout = true; + } zend_end_try(); + intern->xpath_callbacks.read_only_depth--; + document->read_only_depth--; + intern->evaluation_depth--; + + if (bailout) { + if (intern->evaluation_depth == 0) { + php_dom_xpath_callbacks_release_read_only(&intern->xpath_callbacks); + } + zend_bailout(); + } + + return xpathobjp; +} + static void php_xpath_eval(INTERNAL_FUNCTION_PARAMETERS, int type, bool modern) /* {{{ */ { zval *context = NULL; xmlNodePtr nodep = NULL; size_t expr_len, xpath_type; - dom_object *nodeobj; + dom_object *nodeobj = NULL; char *expr; dom_xpath_object *intern = Z_XPATHOBJ_P(ZEND_THIS); @@ -336,9 +375,7 @@ static void php_xpath_eval(INTERNAL_FUNCTION_PARAMETERS, int type, bool modern) ctxp->nsNr = in_scope_ns.count; } - intern->evaluation_depth++; - xmlXPathObjectPtr xpathobjp = xmlXPathEvalExpression(BAD_CAST expr, ctxp); - intern->evaluation_depth--; + xmlXPathObjectPtr xpathobjp = dom_xpath_eval_expression(intern, ctxp, expr, context != NULL ? nodeobj : NULL); ctxp->node = old_node; ctxp->namespaces = old_namespaces; ctxp->nsNr = old_nsNr; @@ -455,27 +492,37 @@ static void php_xpath_eval(INTERNAL_FUNCTION_PARAMETERS, int type, bool modern) } /* }}} */ +static void dom_xpath_evaluate(INTERNAL_FUNCTION_PARAMETERS, int type, bool modern) +{ + php_xpath_eval(INTERNAL_FUNCTION_PARAM_PASSTHRU, type, modern); + + dom_xpath_object *intern = Z_XPATHOBJ_P(ZEND_THIS); + if (intern->evaluation_depth == 0) { + php_dom_xpath_callbacks_release_read_only(&intern->xpath_callbacks); + } +} + /* {{{ */ PHP_METHOD(DOMXPath, query) { - php_xpath_eval(INTERNAL_FUNCTION_PARAM_PASSTHRU, PHP_DOM_XPATH_QUERY, false); + dom_xpath_evaluate(INTERNAL_FUNCTION_PARAM_PASSTHRU, PHP_DOM_XPATH_QUERY, false); } PHP_METHOD(Dom_XPath, query) { - php_xpath_eval(INTERNAL_FUNCTION_PARAM_PASSTHRU, PHP_DOM_XPATH_QUERY, true); + dom_xpath_evaluate(INTERNAL_FUNCTION_PARAM_PASSTHRU, PHP_DOM_XPATH_QUERY, true); } /* }}} end dom_xpath_query */ /* {{{ */ PHP_METHOD(DOMXPath, evaluate) { - php_xpath_eval(INTERNAL_FUNCTION_PARAM_PASSTHRU, PHP_DOM_XPATH_EVALUATE, false); + dom_xpath_evaluate(INTERNAL_FUNCTION_PARAM_PASSTHRU, PHP_DOM_XPATH_EVALUATE, false); } PHP_METHOD(Dom_XPath, evaluate) { - php_xpath_eval(INTERNAL_FUNCTION_PARAM_PASSTHRU, PHP_DOM_XPATH_EVALUATE, true); + dom_xpath_evaluate(INTERNAL_FUNCTION_PARAM_PASSTHRU, PHP_DOM_XPATH_EVALUATE, true); } /* }}} end dom_xpath_evaluate */ diff --git a/ext/dom/xpath_callbacks.c b/ext/dom/xpath_callbacks.c index e0f75fbf6a7c..9dcd4b37005f 100644 --- a/ext/dom/xpath_callbacks.c +++ b/ext/dom/xpath_callbacks.c @@ -48,6 +48,9 @@ PHP_DOM_EXPORT void php_dom_xpath_callbacks_ctor(php_dom_xpath_callbacks *regist registry->php_ns = NULL; registry->namespaces = NULL; registry->node_list = NULL; + registry->read_only_documents = NULL; + registry->read_only_trees = NULL; + registry->read_only_depth = 0; } PHP_DOM_EXPORT void php_dom_xpath_callbacks_clean_node_list(php_dom_xpath_callbacks *registry) @@ -60,6 +63,81 @@ PHP_DOM_EXPORT void php_dom_xpath_callbacks_clean_node_list(php_dom_xpath_callba } } +static void php_dom_xpath_callbacks_make_read_only(php_dom_xpath_callbacks *registry, php_libxml_ref_obj *document) +{ + if (registry->read_only_depth == 0 || document == NULL) { + return; + } + + if (registry->read_only_documents == NULL) { + registry->read_only_documents = zend_new_array(0); + } + + if (zend_hash_index_add_ptr(registry->read_only_documents, (zend_ulong) (uintptr_t) document, document) != NULL) { + document->read_only_depth++; + document->refcount++; + } +} + +PHP_DOM_EXPORT void php_dom_xpath_callbacks_pin_tree(php_dom_xpath_callbacks *registry, xmlNodePtr node, dom_object *intern) +{ + if (registry->read_only_depth == 0 || node == NULL || node->type == XML_NAMESPACE_DECL) { + return; + } + + xmlNodePtr root = node; + while (root->parent != NULL) { + root = root->parent; + } + + if (root->type == XML_DOCUMENT_NODE || root->type == XML_HTML_DOCUMENT_NODE) { + return; + } + + zend_ulong key = (zend_ulong) (uintptr_t) root; + if (registry->read_only_trees != NULL && zend_hash_index_exists(registry->read_only_trees, key)) { + return; + } + + if (root->doc == NULL) { + intern = NULL; + } else if (php_dom_object_get_data(root) == NULL && (intern == NULL || intern->document == NULL || intern->document->ptr != root->doc)) { + return; + } + + zval root_zval; + php_dom_create_object(root, &root_zval, intern); + ((php_libxml_node_ptr *) root->_private)->read_only_depth++; + if (registry->read_only_trees == NULL) { + registry->read_only_trees = zend_new_array(0); + } + zend_hash_index_add_new(registry->read_only_trees, key, &root_zval); +} + +PHP_DOM_EXPORT void php_dom_xpath_callbacks_release_read_only(php_dom_xpath_callbacks *registry) +{ + if (registry->read_only_trees) { + HashTable *read_only_trees = registry->read_only_trees; + registry->read_only_trees = NULL; + zend_ulong key; + ZEND_HASH_FOREACH_NUM_KEY(read_only_trees, key) { + xmlNodePtr root = (xmlNodePtr) (uintptr_t) key; + ((php_libxml_node_ptr *) root->_private)->read_only_depth--; + } ZEND_HASH_FOREACH_END(); + zend_array_destroy(read_only_trees); + } + if (registry->read_only_documents) { + HashTable *read_only_documents = registry->read_only_documents; + registry->read_only_documents = NULL; + php_libxml_ref_obj *document; + ZEND_HASH_FOREACH_PTR(read_only_documents, document) { + document->read_only_depth--; + php_libxml_decrement_doc_ref_directly(document); + } ZEND_HASH_FOREACH_END(); + zend_array_destroy(read_only_documents); + } +} + PHP_DOM_EXPORT void php_dom_xpath_callbacks_clean_argument_stack(xmlXPathParserContextPtr ctxt, uint32_t num_args) { for (uint32_t i = 0; i < num_args; i++) { @@ -93,6 +171,7 @@ PHP_DOM_EXPORT void php_dom_xpath_callbacks_dtor(php_dom_xpath_callbacks *regist FREE_HASHTABLE(namespaces); } php_dom_xpath_callbacks_clean_node_list(registry); + php_dom_xpath_callbacks_release_read_only(registry); } static void php_dom_xpath_callback_ns_get_gc(php_dom_xpath_callback_ns *ns, zend_get_gc_buffer *gc_buffer) @@ -323,8 +402,10 @@ PHP_DOM_EXPORT zend_result php_dom_xpath_callbacks_update_single_method_handler( return SUCCESS; } -static zval *php_dom_xpath_callback_fetch_args(xmlXPathParserContextPtr ctxt, uint32_t param_count, php_dom_xpath_nodeset_evaluation_mode evaluation_mode, dom_object *intern, php_dom_xpath_callbacks_proxy_factory proxy_factory) +static zval *php_dom_xpath_callback_fetch_args(php_dom_xpath_callbacks *xpath_callbacks, xmlXPathParserContextPtr ctxt, uint32_t param_count, php_dom_xpath_nodeset_evaluation_mode evaluation_mode, dom_object *intern, php_dom_xpath_callbacks_proxy_factory proxy_factory) { + php_dom_xpath_callbacks_pin_tree(xpath_callbacks, ctxt->context->node, intern); + if (param_count == 0) { return NULL; } @@ -354,6 +435,7 @@ static zval *php_dom_xpath_callback_fetch_args(xmlXPathParserContextPtr ctxt, ui if (obj->nodesetval && obj->nodesetval->nodeNr > 0) { array_init_size(param, obj->nodesetval->nodeNr); zend_hash_real_init_packed(Z_ARRVAL_P(param)); + xmlNodePtr previous = NULL; for (int j = 0; j < obj->nodesetval->nodeNr; j++) { xmlNodePtr node = obj->nodesetval->nodeTab[j]; zval child; @@ -369,6 +451,11 @@ static zval *php_dom_xpath_callback_fetch_args(xmlXPathParserContextPtr ctxt, ui php_dom_create_fake_namespace_decl(parent, original, &child, parent_intern); } else { proxy_factory(node, &child, intern, ctxt); + if (dom_object_get_node(Z_DOMOBJ_P(&child)) == node + && (previous == NULL || node->parent == NULL || (node->parent != previous && node->parent != previous->parent))) { + php_dom_xpath_callbacks_pin_tree(xpath_callbacks, node, Z_DOMOBJ_P(&child)); + } + previous = node; } zend_hash_next_index_insert_new(Z_ARRVAL_P(param), &child); } @@ -454,6 +541,10 @@ static zend_result php_dom_xpath_callback_dispatch(php_dom_xpath_callbacks *xpat zend_hash_next_index_insert_new(xpath_callbacks->node_list, &callback_retval); dom_object *obj = Z_DOMOBJ_P(&callback_retval); xmlNodePtr nodep = dom_object_get_node(obj); + if (nodep != NULL) { + php_dom_xpath_callbacks_make_read_only(xpath_callbacks, obj->document); + php_dom_xpath_callbacks_pin_tree(xpath_callbacks, nodep, obj); + } valuePush(ctxt, xmlXPathNewNodeSet(nodep)); } else if (Z_TYPE(callback_retval) == IS_FALSE || Z_TYPE(callback_retval) == IS_TRUE) { valuePush(ctxt, xmlXPathNewBoolean(Z_TYPE(callback_retval) == IS_TRUE)); @@ -481,7 +572,7 @@ PHP_DOM_EXPORT zend_result php_dom_xpath_callbacks_call_php_ns(php_dom_xpath_cal } uint32_t param_count = num_args - 1; - zval *params = php_dom_xpath_callback_fetch_args(ctxt, param_count, evaluation_mode, intern, proxy_factory); + zval *params = php_dom_xpath_callback_fetch_args(xpath_callbacks, ctxt, param_count, evaluation_mode, intern, proxy_factory); /* Last element of the stack is the function name */ xmlXPathObjectPtr obj = valuePop(ctxt); @@ -510,7 +601,7 @@ PHP_DOM_EXPORT zend_result php_dom_xpath_callbacks_call_php_ns(php_dom_xpath_cal PHP_DOM_EXPORT zend_result php_dom_xpath_callbacks_call_custom_ns(php_dom_xpath_callbacks *xpath_callbacks, xmlXPathParserContextPtr ctxt, int num_args, php_dom_xpath_nodeset_evaluation_mode evaluation_mode, dom_object *intern, php_dom_xpath_callbacks_proxy_factory proxy_factory) { uint32_t param_count = num_args; - zval *params = php_dom_xpath_callback_fetch_args(ctxt, param_count, evaluation_mode, intern, proxy_factory); + zval *params = php_dom_xpath_callback_fetch_args(xpath_callbacks, ctxt, param_count, evaluation_mode, intern, proxy_factory); const char *namespace = (const char *) ctxt->context->functionURI; /* Impossible because it wouldn't have been registered inside the context. */ diff --git a/ext/dom/xpath_callbacks.h b/ext/dom/xpath_callbacks.h index 4bf79b5df84f..a937b866cccc 100644 --- a/ext/dom/xpath_callbacks.h +++ b/ext/dom/xpath_callbacks.h @@ -41,6 +41,9 @@ typedef struct { php_dom_xpath_callback_ns *php_ns; HashTable *namespaces; HashTable *node_list; + HashTable *read_only_documents; + HashTable *read_only_trees; + uint32_t read_only_depth; } php_dom_xpath_callbacks; typedef enum { @@ -48,9 +51,16 @@ typedef enum { PHP_DOM_XPATH_CALLBACK_NAME_VALIDATE_NCNAME, } php_dom_xpath_callback_name_validation; +static zend_always_inline bool php_dom_xpath_callbacks_registered(const php_dom_xpath_callbacks *registry) +{ + return registry->php_ns != NULL || registry->namespaces != NULL; +} + PHP_DOM_EXPORT void php_dom_xpath_callbacks_ctor(php_dom_xpath_callbacks *registry); PHP_DOM_EXPORT void php_dom_xpath_callbacks_dtor(php_dom_xpath_callbacks *registry); PHP_DOM_EXPORT void php_dom_xpath_callbacks_clean_node_list(php_dom_xpath_callbacks *registry); +PHP_DOM_EXPORT void php_dom_xpath_callbacks_pin_tree(php_dom_xpath_callbacks *registry, xmlNodePtr node, dom_object *intern); +PHP_DOM_EXPORT void php_dom_xpath_callbacks_release_read_only(php_dom_xpath_callbacks *registry); PHP_DOM_EXPORT void php_dom_xpath_callbacks_clean_argument_stack(xmlXPathParserContextPtr ctxt, uint32_t num_args); PHP_DOM_EXPORT void php_dom_xpath_callbacks_get_gc(php_dom_xpath_callbacks *registry, zend_get_gc_buffer *gc_buffer); PHP_DOM_EXPORT HashTable *php_dom_xpath_callbacks_get_gc_for_whole_object(php_dom_xpath_callbacks *registry, zend_object *object, zval **table, int *n); diff --git a/ext/libxml/libxml.c b/ext/libxml/libxml.c index d09ec299ff3c..10617d8c350c 100644 --- a/ext/libxml/libxml.c +++ b/ext/libxml/libxml.c @@ -1335,6 +1335,7 @@ PHP_LIBXML_API unsigned int php_libxml_increment_node_ptr(php_libxml_node_object ret_refcount = 1; object->node->node = node; object->node->refcount = 1; + object->node->read_only_depth = 0; object->node->_private = private_data; node->_private = object->node; } @@ -1387,6 +1388,7 @@ PHP_LIBXML_API unsigned int php_libxml_increment_doc_ref(php_libxml_node_object object->document->class_type = PHP_LIBXML_CLASS_UNSET; object->document->handlers = &php_libxml_default_document_handlers; object->document->quirks_mode = PHP_LIBXML_NO_QUIRKS; + object->document->read_only_depth = 0; } return ret_refcount; @@ -1427,6 +1429,20 @@ PHP_LIBXML_API unsigned int php_libxml_decrement_doc_ref(php_libxml_node_object return ret_refcount; } +PHP_LIBXML_API bool php_libxml_is_tree_read_only(const php_libxml_ref_obj *document, const xmlNode *node) +{ + while (node->parent != NULL) { + node = node->parent; + } + + if (node->type == XML_DOCUMENT_NODE || node->type == XML_HTML_DOCUMENT_NODE) { + return document != NULL && document->read_only_depth > 0; + } + + const php_libxml_node_ptr *ptr = node->_private; + return ptr != NULL && ptr->read_only_depth > 0; +} + PHP_LIBXML_API void php_libxml_node_free_resource(xmlNodePtr node) { if (!node) { diff --git a/ext/libxml/php_libxml.h b/ext/libxml/php_libxml.h index 5896f70803f8..89e35b710f9a 100644 --- a/ext/libxml/php_libxml.h +++ b/ext/libxml/php_libxml.h @@ -114,11 +114,13 @@ typedef struct _php_libxml_ref_obj { unsigned int refcount; php_libxml_class_type class_type : 8; php_libxml_quirks_mode quirks_mode : 8; + unsigned int read_only_depth; } php_libxml_ref_obj; typedef struct _php_libxml_node_ptr { xmlNodePtr node; unsigned int refcount; + unsigned int read_only_depth; void *_private; } php_libxml_node_ptr; @@ -166,6 +168,16 @@ static zend_always_inline void php_libxml_invalidate_node_list_cache(php_libxml_ } } +PHP_LIBXML_API bool php_libxml_is_tree_read_only(const php_libxml_ref_obj *document, const xmlNode *node); + +static zend_always_inline bool php_libxml_is_node_read_only(const php_libxml_ref_obj *document, const xmlNode *node) +{ + if (node == NULL || (document != NULL && EXPECTED(document->read_only_depth == 0))) { + return false; + } + return php_libxml_is_tree_read_only(document, node); +} + static zend_always_inline void php_libxml_invalidate_node_list_cache_from_doc(xmlDocPtr docp) { if (docp && docp->_private) { /* docp is NULL for detached nodes */ diff --git a/ext/simplexml/simplexml.c b/ext/simplexml/simplexml.c index 0e02a843e95a..268a0e02a465 100644 --- a/ext/simplexml/simplexml.c +++ b/ext/simplexml/simplexml.c @@ -373,6 +373,15 @@ static void change_node_zval(xmlNodePtr node, zend_string *value) } /* }}} */ +static bool sxe_throw_if_document_read_only(const php_sxe_object *sxe) +{ + if (php_libxml_is_node_read_only(sxe->document, sxe->node ? sxe->node->node : NULL)) { + zend_throw_error(NULL, "Cannot modify a node while an XPath evaluation or XSLT transformation that uses it is in progress"); + return true; + } + return false; +} + /* {{{ sxe_property_write() */ static zval *sxe_prop_dim_write(zend_object *object, zval *member, zval *value, sxe_access_mode access_mode, xmlNodePtr *pnewnode) { @@ -393,6 +402,10 @@ static zval *sxe_prop_dim_write(zend_object *object, zval *member, zval *value, sxe = php_sxe_fetch_object(object); + if (sxe_throw_if_document_read_only(sxe)) { + return &EG(error_zval); + } + if (!member) { if (sxe->iter.type == SXE_ITER_ATTRLIST) { /* This happens when the user did: $sxe[] = $value @@ -796,6 +809,12 @@ static void sxe_prop_dim_delete(zend_object *object, zval *member, sxe_access_mo zval tmp_zv; int test = 0; + sxe = php_sxe_fetch_object(object); + + if (sxe_throw_if_document_read_only(sxe)) { + return; + } + if (Z_TYPE_P(member) != IS_STRING && Z_TYPE_P(member) != IS_LONG) { zend_string *str = zval_try_get_string_func(member); if (UNEXPECTED(!str)) { @@ -805,8 +824,6 @@ static void sxe_prop_dim_delete(zend_object *object, zval *member, sxe_access_mo member = &tmp_zv; } - sxe = php_sxe_fetch_object(object); - GET_NODE(sxe, node); if (Z_TYPE_P(member) == IS_LONG) { @@ -1732,6 +1749,10 @@ PHP_METHOD(SimpleXMLElement, addChild) sxe = Z_SXEOBJ_P(ZEND_THIS); GET_NODE(sxe, node); + if (sxe_throw_if_document_read_only(sxe)) { + RETURN_THROWS(); + } + if (sxe->iter.type == SXE_ITER_ATTRLIST) { php_error_docref(NULL, E_WARNING, "Cannot add element to attributes"); return; @@ -1807,6 +1828,10 @@ PHP_METHOD(SimpleXMLElement, addAttribute) sxe = Z_SXEOBJ_P(ZEND_THIS); GET_NODE(sxe, node); + if (sxe_throw_if_document_read_only(sxe)) { + RETURN_THROWS(); + } + node = php_sxe_get_first_node_non_destructive(sxe, node); if (node && node->type != XML_ELEMENT_NODE) { diff --git a/ext/xsl/tests/bug69168.phpt b/ext/xsl/tests/bug69168.phpt index 41fc3aafd820..4f1f44c11561 100644 --- a/ext/xsl/tests/bug69168.phpt +++ b/ext/xsl/tests/bug69168.phpt @@ -19,7 +19,11 @@ $xsl = <<nodeValue .= 'a'; + try { + $input[0]->nodeValue .= 'a'; + } catch (DOMException $e) { + echo $e->getMessage(), "\n"; + } return $input[0]->getNodePath() . ' = ' . $input[0]->nodeValue; } @@ -32,12 +36,14 @@ $xmlDoc = new DOMDocument(); $xmlDoc->loadXML($xml); echo @$proc->transformToXML($xmlDoc); -// Tests modification of the nodes +// Tests that the callback did not modify the nodes var_dump($xmlDoc->firstChild->firstChild->firstChild->getNodePath()); var_dump($xmlDoc->firstChild->firstChild->firstChild->nodeValue); ?> --EXPECT-- +Cannot modify a node while an XPath evaluation or XSLT transformation that uses it is in progress +Cannot modify a node while an XPath evaluation or XSLT transformation that uses it is in progress -/allusers/user[1]/uid = boba
/allusers/user[2]/uid = joea
+/allusers/user[1]/uid = bob
/allusers/user[2]/uid = joe
string(21) "/allusers/user[1]/uid" -string(4) "boba" +string(3) "bob" diff --git a/ext/xsl/tests/cloneDocument.phpt b/ext/xsl/tests/cloneDocument.phpt index 0af1da4c6e35..153346ee6f01 100644 --- a/ext/xsl/tests/cloneDocument.phpt +++ b/ext/xsl/tests/cloneDocument.phpt @@ -11,7 +11,11 @@ $xml->loadXML('hello'); function test() { global $xml; - $xml->documentElement->firstChild->textContent = "bye"; + try { + $xml->documentElement->firstChild->textContent = "bye"; + } catch (DOMException $e) { + echo $e->getMessage(), "\n"; + } } $xsl = new DOMDocument; @@ -41,5 +45,6 @@ echo $xslt->transformToXml($xml); --EXPECT-- hello +Cannot modify a node while an XPath evaluation or XSLT transformation that uses it is in progress bye diff --git a/ext/xsl/tests/xsltprocessor_entity_loader_modifies_source.phpt b/ext/xsl/tests/xsltprocessor_entity_loader_modifies_source.phpt new file mode 100644 index 000000000000..483cc9d92592 --- /dev/null +++ b/ext/xsl/tests/xsltprocessor_entity_loader_modifies_source.phpt @@ -0,0 +1,42 @@ +--TEST-- +XSLTProcessor: an entity loader cannot modify the document that is being transformed +--EXTENSIONS-- +xsl +--FILE-- +loadXML('123'); + +libxml_set_external_entity_loader(function ($public, $system, $context) use ($doc) { + try { + $doc->documentElement->textContent = ''; + } catch (DOMException $e) { + echo $e::class, ": ", $e->getMessage(), "\n"; + } + $stream = fopen('php://memory', 'w+'); + fwrite($stream, 'loaded'); + rewind($stream); + return $stream; +}); + +$xsl = new DOMDocument(); +$xsl->loadXML(<< + + +[|] + + +XSL); +$proc = new XSLTProcessor(); +$proc->importStylesheet($xsl); +var_dump($proc->transformToXml($doc)); +$doc->documentElement->textContent = ''; +echo $doc->saveXML($doc->documentElement), "\n"; +?> +--EXPECT-- +DOMException: Cannot modify a node while an XPath evaluation or XSLT transformation that uses it is in progress +DOMException: Cannot modify a node while an XPath evaluation or XSLT transformation that uses it is in progress +DOMException: Cannot modify a node while an XPath evaluation or XSLT transformation that uses it is in progress +string(30) "[loaded|1][loaded|2][loaded|3]" + diff --git a/ext/xsl/tests/xsltprocessor_error_handler_bailout.phpt b/ext/xsl/tests/xsltprocessor_error_handler_bailout.phpt new file mode 100644 index 000000000000..93b482ba98a4 --- /dev/null +++ b/ext/xsl/tests/xsltprocessor_error_handler_bailout.phpt @@ -0,0 +1,39 @@ +--TEST-- +XSLTProcessor: the source becomes writable again after a fatal error in user code during a transformation +--EXTENSIONS-- +xsl +zend_test +--SKIPIF-- + +--FILE-- +loadXML(''); + +register_shutdown_function(function () use ($doc) { + $doc->documentElement->firstChild->remove(); + echo $doc->saveXML($doc->documentElement), "\n"; +}); + +set_error_handler(function () use ($doc) { + try { + $doc->documentElement->firstChild->remove(); + } catch (DOMException $e) { + echo $e::class, ": ", $e->getMessage(), "\n"; + } + zend_trigger_bailout(); +}); + +$xsl = new DOMDocument(); +$xsl->loadXML('m'); +$proc = new XSLTProcessor(); +$proc->importStylesheet($xsl); +$proc->transformToXml($doc); +?> +--EXPECTF-- +DOMException: Cannot modify a node while an XPath evaluation or XSLT transformation that uses it is in progress + +Fatal error: Bailout in %s on line %d + diff --git a/ext/xsl/tests/xsltprocessor_nested_transform.phpt b/ext/xsl/tests/xsltprocessor_nested_transform.phpt new file mode 100644 index 000000000000..cef56cc67925 --- /dev/null +++ b/ext/xsl/tests/xsltprocessor_nested_transform.phpt @@ -0,0 +1,100 @@ +--TEST-- +XSLTProcessor refuses a nested transformation on the same processor +--EXTENSIONS-- +dom +xsl +--FILE-- +transformToXml($doc)); + } catch (Error $e) { + echo $e::class, ": ", $e->getMessage(), PHP_EOL; + } + try { + var_dump($proc->transformToDoc($doc)); + } catch (Error $e) { + echo $e::class, ": ", $e->getMessage(), PHP_EOL; + } + try { + var_dump($proc->transformToUri($doc, $GLOBALS['uri'])); + } catch (Error $e) { + echo $e::class, ": ", $e->getMessage(), PHP_EOL; + } + return 'v'; +} + +function suspend($nodes) { + Fiber::suspend(); + return 'f'; +} + +function text($nodes) { + return $nodes[0]->textContent; +} + +$doc = new DOMDocument(); +$doc->loadXML('123'); + +$xsl = new DOMDocument(); +$xsl->loadXML(<< + + + [] + +XML); + +$uri = __DIR__ . '/xsltprocessor_nested_transform.xml'; + +$proc = new XSLTProcessor(); +$proc->registerPHPFunctions(); +$proc->importStylesheet($xsl); + +var_dump($proc->transformToXml($doc)); + +$proc->setParameter('', 'cb', 'suspend'); +$fiber = new Fiber(fn () => $proc->transformToXml($doc)); +$fiber->start(); +try { + var_dump($proc->transformToXml($doc)); +} catch (Error $e) { + echo $e::class, ": ", $e->getMessage(), PHP_EOL; +} +while (!$fiber->isTerminated()) { + $fiber->resume(); +} +var_dump($fiber->getReturn()); + +$proc->setParameter('', 'cb', 'text'); +var_dump($proc->transformToXml($doc)); +var_dump($proc->transformToDoc($doc)->textContent); + +?> +--CLEAN-- + +--EXPECT-- +Error: Cannot call XSLTProcessor::transformToXml() while a transformation is in progress +Error: Cannot call XSLTProcessor::transformToDoc() while a transformation is in progress +Error: Cannot call XSLTProcessor::transformToUri() while a transformation is in progress +string(35) " +[va][vb][vc] +" +Error: Cannot call XSLTProcessor::transformToXml() while a transformation is in progress +string(35) " +[fa][fb][fc] +" +string(35) " +[1a][2b][3c] +" +string(12) "[1a][2b][3c]" diff --git a/ext/xsl/tests/xsltprocessor_php_function_bailout.phpt b/ext/xsl/tests/xsltprocessor_php_function_bailout.phpt new file mode 100644 index 000000000000..a8160df404d1 --- /dev/null +++ b/ext/xsl/tests/xsltprocessor_php_function_bailout.phpt @@ -0,0 +1,55 @@ +--TEST-- +XSLTProcessor: documents become writable again after a fatal error in a php:function() callback +--EXTENSIONS-- +xsl +zend_test +--SKIPIF-- + +--FILE-- +loadXML('1'); +$other = new DOMDocument(); +$other->loadXML(''); + +function other_root() { + return $GLOBALS['other']->documentElement; +} + +function bail() { + try { + $GLOBALS['other']->documentElement->firstChild->remove(); + } catch (DOMException $e) { + echo $e::class, ": ", $e->getMessage(), "\n"; + } + zend_trigger_bailout(); +} + +function stylesheet(string $select): DOMDocument { + $xsl = new DOMDocument(); + $xsl->loadXML(''); + return $xsl; +} + +$proc = new XSLTProcessor(); +$proc->registerPHPFunctions(); +$proc->importStylesheet(stylesheet("concat(count(php:function('other_root')), php:function('bail'))")); + +register_shutdown_function(function () use ($proc, $doc, $other) { + $doc->documentElement->firstChild->remove(); + $other->documentElement->firstChild->remove(); + echo $doc->saveXML($doc->documentElement), $other->saveXML($other->documentElement), "\n"; + $proc->importStylesheet(stylesheet("count(php:function('other_root'))")); + echo $proc->transformToXml($doc), "\n"; +}); + +$proc->transformToXml($doc); +?> +--EXPECTF-- +DOMException: Cannot modify a node while an XPath evaluation or XSLT transformation that uses it is in progress + +Fatal error: Bailout in %s on line %d + +1 diff --git a/ext/xsl/tests/xsltprocessor_php_function_modifies_source.phpt b/ext/xsl/tests/xsltprocessor_php_function_modifies_source.phpt new file mode 100644 index 000000000000..1ffa183a5f31 --- /dev/null +++ b/ext/xsl/tests/xsltprocessor_php_function_modifies_source.phpt @@ -0,0 +1,144 @@ +--TEST-- +XSLTProcessor: a php:function() callback cannot modify the document that is being transformed +--EXTENSIONS-- +xsl +--FILE-- +getMessage()] = true; + } + return ''; +} + +function show($nodes) { + return $nodes[0]->nodeName . ': ' . $nodes[0]->textContent . "\n"; +} + +function transform(string $label, DOMDocument $doc, string $select, Closure $mutation) { + $GLOBALS['mutation'] = $mutation; + $GLOBALS['errors'] = []; + $xsl = new DOMDocument(); + $xsl->loadXML(<< + + + + + + XSL); + $proc = new XSLTProcessor(); + $proc->importStylesheet($xsl); + $proc->registerPHPFunctions(); + echo "$label:\n", $proc->transformToXml($doc); + echo implode("\n", array_keys($GLOBALS['errors'])), "\n"; +} + +$doc = new DOMDocument(); +$doc->loadXML('123'); +$root = $doc->documentElement; +transform('removeChild', $doc, '/root/a', function () use ($root) { + foreach (iterator_to_array($root->childNodes) as $child) { + $root->removeChild($child); + } +}); +transform('textContent', $doc, '/root/a', function () use ($root) { + $root->textContent = 'x'; +}); +transform('removeAttribute', $doc, '/root/@*', function () use ($root) { + $root->removeAttribute('a'); +}); +echo $doc->saveXML($root), "\n"; + +$text = new DOMDocument(); +$text->loadXML(''); +foreach (['1', '2', '3'] as $data) { + $text->documentElement->appendChild($text->createTextNode($data)); +} +transform('normalize', $text, '/root/text()', function () use ($text) { + $text->documentElement->normalize(); +}); +echo $text->saveXML($text->documentElement), "\n"; + +$other = new DOMDocument(); +$other->loadXML('123'); +function other_root() { + return $GLOBALS['other']->documentElement; +} +transform('foreign document', $doc, "php:function('other_root')/a", function () use ($other) { + $other->documentElement->firstChild->remove(); +}); +echo $other->saveXML($other->documentElement), "\n"; + +$root->firstChild->remove(); +$other->documentElement->firstChild->remove(); +echo $doc->saveXML($root), "\n", $other->saveXML($other->documentElement), "\n"; + +function wrap($nodes) { + $doc = $nodes[0]->ownerDocument; + $span = $doc->createElement('span'); + $span->setAttribute('class', 'w'); + $span->appendChild($doc->createTextNode(strtoupper($nodes[0]->textContent))); + return $span; +} +$docless = new DOMElement('e', 'abc'); +function docless() { + return $GLOBALS['docless']; +} +function modify_docless() { + try { + $GLOBALS['docless']->textContent = 'xyz'; + } catch (DOMException $e) { + $GLOBALS['errors'][$e::class . ": " . $e->getMessage()] = true; + } + return ''; +} +$GLOBALS['errors'] = []; +$xsl = new DOMDocument(); +$xsl->loadXML(<< + + + + + +XSL); +$proc = new XSLTProcessor(); +$proc->importStylesheet($xsl); +$proc->registerPHPFunctions(); +echo $proc->transformToXml($doc), implode("\n", array_keys($GLOBALS['errors'])), "\n"; +?> +--EXPECT-- +removeChild: +a: 1 +a: 2 +a: 3 +DOMException: Cannot modify a node while an XPath evaluation or XSLT transformation that uses it is in progress +textContent: +a: 1 +a: 2 +a: 3 +DOMException: Cannot modify a node while an XPath evaluation or XSLT transformation that uses it is in progress +removeAttribute: +a: 1 +b: 2 +DOMException: Cannot modify a node while an XPath evaluation or XSLT transformation that uses it is in progress +123 +normalize: +#text: 1 +#text: 2 +#text: 3 +DOMException: Cannot modify a node while an XPath evaluation or XSLT transformation that uses it is in progress +123 +foreign document: +a: 1 +a: 2 +a: 3 +DOMException: Cannot modify a node while an XPath evaluation or XSLT transformation that uses it is in progress +123 +23 +23 +23abc +DOMException: Cannot modify a node while an XPath evaluation or XSLT transformation that uses it is in progress diff --git a/ext/xsl/xsltprocessor.c b/ext/xsl/xsltprocessor.c index 3f6a7b6aa923..1e76f383e554 100644 --- a/ext/xsl/xsltprocessor.c +++ b/ext/xsl/xsltprocessor.c @@ -303,6 +303,7 @@ static xmlDocPtr php_xsl_apply_stylesheet(zval *id, xsl_object *intern, xsltStyl FILE *f; int secPrefsError = 0; xsltSecurityPrefsPtr secPrefs = NULL; + bool bailout = false; node = php_libxml_import_node(docp); @@ -323,6 +324,13 @@ static xmlDocPtr php_xsl_apply_stylesheet(zval *id, xsl_object *intern, xsltStyl return NULL; } + if (UNEXPECTED(intern->transform_depth > 0)) { + zend_string *name = get_active_function_or_method_name(); + zend_throw_error(NULL, "Cannot call %s() while a transformation is in progress", ZSTR_VAL(name)); + zend_string_release(name); + return NULL; + } + intern->transform_depth++; if (intern->profiling) { @@ -409,7 +417,17 @@ static xmlDocPtr php_xsl_apply_stylesheet(zval *id, xsl_object *intern, xsltStyl if (secPrefsError == 1) { php_error_docref(NULL, E_WARNING, "Can't set libxslt security properties, not doing transformation for security reasons"); } else { - newdocp = xsltApplyStylesheetUser(style, doc, /* params (handled manually) */ NULL, /* output */ NULL, f, ctxt); + php_libxml_ref_obj *document = intern->doc->document; + document->read_only_depth++; + intern->xpath_callbacks.read_only_depth++; + zend_try { + newdocp = xsltApplyStylesheetUser(style, doc, NULL, NULL, f, ctxt); + } zend_catch { + bailout = true; + } zend_end_try(); + intern->xpath_callbacks.read_only_depth--; + document->read_only_depth--; + php_dom_xpath_callbacks_release_read_only(&intern->xpath_callbacks); } out: @@ -430,6 +448,10 @@ static xmlDocPtr php_xsl_apply_stylesheet(zval *id, xsl_object *intern, xsltStyl intern->transform_depth--; + if (bailout) { + zend_bailout(); + } + return newdocp; }