xref: /php-src/ext/dom/xpath.c (revision 1d0fbdf4)
1 /*
2    +----------------------------------------------------------------------+
3    | Copyright (c) The PHP Group                                          |
4    +----------------------------------------------------------------------+
5    | This source file is subject to version 3.01 of the PHP license,      |
6    | that is bundled with this package in the file LICENSE, and is        |
7    | available through the world-wide-web at the following url:           |
8    | https://www.php.net/license/3_01.txt                                 |
9    | If you did not receive a copy of the PHP license and are unable to   |
10    | obtain it through the world-wide-web, please send a note to          |
11    | license@php.net so we can mail you a copy immediately.               |
12    +----------------------------------------------------------------------+
13    | Authors: Christian Stocker <chregu@php.net>                          |
14    |          Rob Richards <rrichards@php.net>                            |
15    +----------------------------------------------------------------------+
16 */
17 
18 #ifdef HAVE_CONFIG_H
19 #include <config.h>
20 #endif
21 
22 #include "php.h"
23 #if defined(HAVE_LIBXML) && defined(HAVE_DOM)
24 #include "php_dom.h"
25 #include "namespace_compat.h"
26 #include "private_data.h"
27 
28 #define PHP_DOM_XPATH_QUERY 0
29 #define PHP_DOM_XPATH_EVALUATE 1
30 
31 /*
32 * class DOMXPath
33 */
34 
35 #ifdef LIBXML_XPATH_ENABLED
36 
dom_xpath_objects_free_storage(zend_object * object)37 void dom_xpath_objects_free_storage(zend_object *object)
38 {
39 	dom_xpath_object *intern = php_xpath_obj_from_obj(object);
40 
41 	zend_object_std_dtor(&intern->dom.std);
42 
43 	if (intern->dom.ptr != NULL) {
44 		xmlXPathFreeContext((xmlXPathContextPtr) intern->dom.ptr);
45 		php_libxml_decrement_doc_ref((php_libxml_node_object *) &intern->dom);
46 	}
47 
48 	php_dom_xpath_callbacks_dtor(&intern->xpath_callbacks);
49 }
50 
dom_xpath_get_gc(zend_object * object,zval ** table,int * n)51 HashTable *dom_xpath_get_gc(zend_object *object, zval **table, int *n)
52 {
53 	dom_xpath_object *intern = php_xpath_obj_from_obj(object);
54 	return php_dom_xpath_callbacks_get_gc_for_whole_object(&intern->xpath_callbacks, object, table, n);
55 }
56 
dom_xpath_proxy_factory(xmlNodePtr node,zval * child,dom_object * intern,xmlXPathParserContextPtr ctxt)57 static void dom_xpath_proxy_factory(xmlNodePtr node, zval *child, dom_object *intern, xmlXPathParserContextPtr ctxt)
58 {
59 	ZEND_IGNORE_VALUE(ctxt);
60 
61 	ZEND_ASSERT(node->type != XML_NAMESPACE_DECL);
62 
63 	php_dom_create_object(node, child, intern);
64 }
65 
dom_xpath_ext_fetch_intern(xmlXPathParserContextPtr ctxt)66 static dom_xpath_object *dom_xpath_ext_fetch_intern(xmlXPathParserContextPtr ctxt)
67 {
68 	if (UNEXPECTED(!zend_is_executing())) {
69 		xmlGenericError(xmlGenericErrorContext,
70 		"xmlExtFunctionTest: Function called from outside of PHP\n");
71 		return NULL;
72 	}
73 
74 	dom_xpath_object *intern = (dom_xpath_object *) ctxt->context->userData;
75 	if (UNEXPECTED(intern == NULL)) {
76 		xmlGenericError(xmlGenericErrorContext,
77 		"xmlExtFunctionTest: failed to get the internal object\n");
78 		return NULL;
79 	}
80 
81 	return intern;
82 }
83 
dom_xpath_ext_function_php(xmlXPathParserContextPtr ctxt,int nargs,php_dom_xpath_nodeset_evaluation_mode evaluation_mode)84 static void dom_xpath_ext_function_php(xmlXPathParserContextPtr ctxt, int nargs, php_dom_xpath_nodeset_evaluation_mode evaluation_mode) /* {{{ */
85 {
86 	dom_xpath_object *intern = dom_xpath_ext_fetch_intern(ctxt);
87 	if (!intern) {
88 		php_dom_xpath_callbacks_clean_argument_stack(ctxt, nargs);
89 	} else {
90 		php_dom_xpath_callbacks_call_php_ns(&intern->xpath_callbacks, ctxt, nargs, evaluation_mode, &intern->dom, dom_xpath_proxy_factory);
91 	}
92 }
93 /* }}} */
94 
dom_xpath_ext_function_string_php(xmlXPathParserContextPtr ctxt,int nargs)95 static void dom_xpath_ext_function_string_php(xmlXPathParserContextPtr ctxt, int nargs) /* {{{ */
96 {
97 	dom_xpath_ext_function_php(ctxt, nargs, PHP_DOM_XPATH_EVALUATE_NODESET_TO_STRING);
98 }
99 /* }}} */
100 
dom_xpath_ext_function_object_php(xmlXPathParserContextPtr ctxt,int nargs)101 static void dom_xpath_ext_function_object_php(xmlXPathParserContextPtr ctxt, int nargs) /* {{{ */
102 {
103 	dom_xpath_ext_function_php(ctxt, nargs, PHP_DOM_XPATH_EVALUATE_NODESET_TO_NODESET);
104 }
105 /* }}} */
106 
dom_xpath_ext_function_trampoline(xmlXPathParserContextPtr ctxt,int nargs)107 static void dom_xpath_ext_function_trampoline(xmlXPathParserContextPtr ctxt, int nargs)
108 {
109 	dom_xpath_object *intern = dom_xpath_ext_fetch_intern(ctxt);
110 	if (!intern) {
111 		php_dom_xpath_callbacks_clean_argument_stack(ctxt, nargs);
112 	} else {
113 		php_dom_xpath_callbacks_call_custom_ns(&intern->xpath_callbacks, ctxt, nargs, PHP_DOM_XPATH_EVALUATE_NODESET_TO_NODESET, &intern->dom, dom_xpath_proxy_factory);
114 	}
115 }
116 
117 /* {{{ */
dom_xpath_construct(INTERNAL_FUNCTION_PARAMETERS,zend_class_entry * document_ce)118 static void dom_xpath_construct(INTERNAL_FUNCTION_PARAMETERS, zend_class_entry *document_ce)
119 {
120 	zval *doc;
121 	bool register_node_ns = true;
122 	xmlDocPtr docp = NULL;
123 	dom_object *docobj;
124 
125 	if (zend_parse_parameters(ZEND_NUM_ARGS(), "O|b", &doc, document_ce, &register_node_ns) != SUCCESS) {
126 		RETURN_THROWS();
127 	}
128 
129 	DOM_GET_OBJ(docp, doc, xmlDocPtr, docobj);
130 
131 	xmlXPathContextPtr ctx = xmlXPathNewContext(docp);
132 	if (ctx == NULL) {
133 		php_dom_throw_error(INVALID_STATE_ERR, true);
134 		RETURN_THROWS();
135 	}
136 
137 	dom_xpath_object *intern = Z_XPATHOBJ_P(ZEND_THIS);
138 	xmlXPathContextPtr oldctx = intern->dom.ptr;
139 	if (oldctx != NULL) {
140 		php_libxml_decrement_doc_ref((php_libxml_node_object *) &intern->dom);
141 		xmlXPathFreeContext(oldctx);
142 		php_dom_xpath_callbacks_dtor(&intern->xpath_callbacks);
143 		php_dom_xpath_callbacks_ctor(&intern->xpath_callbacks);
144 	}
145 
146 	xmlXPathRegisterFuncNS (ctx, (const xmlChar *) "functionString",
147 					(const xmlChar *) "http://php.net/xpath",
148 					dom_xpath_ext_function_string_php);
149 	xmlXPathRegisterFuncNS (ctx, (const xmlChar *) "function",
150 					(const xmlChar *) "http://php.net/xpath",
151 					dom_xpath_ext_function_object_php);
152 
153 	intern->dom.ptr = ctx;
154 	ctx->userData = (void *)intern;
155 	intern->dom.document = docobj->document;
156 	intern->register_node_ns = register_node_ns;
157 	php_libxml_increment_doc_ref((php_libxml_node_object *) &intern->dom, docp);
158 }
159 
PHP_METHOD(DOMXPath,__construct)160 PHP_METHOD(DOMXPath, __construct)
161 {
162 	dom_xpath_construct(INTERNAL_FUNCTION_PARAM_PASSTHRU, dom_document_class_entry);
163 }
164 
PHP_METHOD(Dom_XPath,__construct)165 PHP_METHOD(Dom_XPath, __construct)
166 {
167 	dom_xpath_construct(INTERNAL_FUNCTION_PARAM_PASSTHRU, dom_abstract_base_document_class_entry);
168 }
169 /* }}} end DOMXPath::__construct */
170 
171 /* {{{ document DOMDocument*/
dom_xpath_document_read(dom_object * obj,zval * retval)172 zend_result dom_xpath_document_read(dom_object *obj, zval *retval)
173 {
174 	xmlDoc *docp = NULL;
175 	xmlXPathContextPtr ctx = (xmlXPathContextPtr) obj->ptr;
176 
177 	if (ctx) {
178 		docp = (xmlDocPtr) ctx->doc;
179 	}
180 
181 	if (UNEXPECTED(!docp)) {
182 		php_dom_throw_error(INVALID_STATE_ERR, /* strict */ true);
183 		return FAILURE;
184 	}
185 
186 	php_dom_create_object((xmlNodePtr) docp, retval, obj);
187 	return SUCCESS;
188 }
189 /* }}} */
190 
191 /* {{{ registerNodeNamespaces bool*/
php_xpath_obj_from_dom_obj(dom_object * obj)192 static inline dom_xpath_object *php_xpath_obj_from_dom_obj(dom_object *obj) {
193 	return (dom_xpath_object*)((char*)(obj) - XtOffsetOf(dom_xpath_object, dom));
194 }
195 
dom_xpath_register_node_ns_read(dom_object * obj,zval * retval)196 zend_result dom_xpath_register_node_ns_read(dom_object *obj, zval *retval)
197 {
198 	ZVAL_BOOL(retval, php_xpath_obj_from_dom_obj(obj)->register_node_ns);
199 
200 	return SUCCESS;
201 }
202 
dom_xpath_register_node_ns_write(dom_object * obj,zval * newval)203 zend_result dom_xpath_register_node_ns_write(dom_object *obj, zval *newval)
204 {
205 	php_xpath_obj_from_dom_obj(obj)->register_node_ns = zend_is_true(newval);
206 
207 	return SUCCESS;
208 }
209 /* }}} */
210 
211 /* {{{ */
PHP_METHOD(DOMXPath,registerNamespace)212 PHP_METHOD(DOMXPath, registerNamespace)
213 {
214 	size_t prefix_len, ns_uri_len;
215 	unsigned char *prefix, *ns_uri;
216 
217 	if (zend_parse_parameters(ZEND_NUM_ARGS(), "ss", &prefix, &prefix_len, &ns_uri, &ns_uri_len) == FAILURE) {
218 		RETURN_THROWS();
219 	}
220 
221 	dom_xpath_object *intern = Z_XPATHOBJ_P(ZEND_THIS);
222 
223 	xmlXPathContextPtr ctxp = intern->dom.ptr;
224 	if (ctxp == NULL) {
225 		zend_throw_error(NULL, "Invalid XPath Context");
226 		RETURN_THROWS();
227 	}
228 
229 	if (xmlXPathRegisterNs(ctxp, prefix, ns_uri) != 0) {
230 		RETURN_FALSE;
231 	}
232 	RETURN_TRUE;
233 }
234 /* }}} */
235 
dom_xpath_iter(zval * baseobj,dom_object * intern)236 static void dom_xpath_iter(zval *baseobj, dom_object *intern) /* {{{ */
237 {
238 	dom_nnodemap_object *mapptr = (dom_nnodemap_object *) intern->ptr;
239 
240 	ZVAL_COPY_VALUE(&mapptr->baseobj_zv, baseobj);
241 	mapptr->nodetype = DOM_NODESET;
242 }
243 /* }}} */
244 
php_xpath_eval(INTERNAL_FUNCTION_PARAMETERS,int type,bool modern)245 static void php_xpath_eval(INTERNAL_FUNCTION_PARAMETERS, int type, bool modern) /* {{{ */
246 {
247 	zval *context = NULL;
248 	xmlNodePtr nodep = NULL;
249 	size_t expr_len, xpath_type;
250 	dom_object *nodeobj;
251 	char *expr;
252 
253 	dom_xpath_object *intern = Z_XPATHOBJ_P(ZEND_THIS);
254 	bool register_node_ns = intern->register_node_ns;
255 
256 	if (zend_parse_parameters(ZEND_NUM_ARGS(), "s|O!b", &expr, &expr_len, &context, modern ? dom_modern_node_class_entry : dom_node_class_entry, &register_node_ns) == FAILURE) {
257 		RETURN_THROWS();
258 	}
259 
260 	xmlXPathContextPtr ctxp = intern->dom.ptr;
261 	if (ctxp == NULL) {
262 		zend_throw_error(NULL, "Invalid XPath Context");
263 		RETURN_THROWS();
264 	}
265 
266 	xmlDocPtr docp = ctxp->doc;
267 	if (docp == NULL) {
268 		if (modern) {
269 			zend_throw_error(NULL, "Invalid XPath Document Pointer");
270 			RETURN_THROWS();
271 		} else {
272 			php_error_docref(NULL, E_WARNING, "Invalid XPath Document Pointer");
273 			RETURN_FALSE;
274 		}
275 	}
276 
277 	if (context != NULL) {
278 		DOM_GET_OBJ(nodep, context, xmlNodePtr, nodeobj);
279 	}
280 
281 	if (!nodep) {
282 		nodep = xmlDocGetRootElement(docp);
283 	}
284 
285 	if (nodep && docp != nodep->doc) {
286 		zend_throw_error(NULL, "Node from wrong document");
287 		RETURN_THROWS();
288 	}
289 
290 	ctxp->node = nodep;
291 
292 	php_dom_in_scope_ns in_scope_ns;
293 	if (register_node_ns && nodep != NULL) {
294 		if (modern) {
295 			php_dom_libxml_ns_mapper *ns_mapper = php_dom_get_ns_mapper(&intern->dom);
296 			in_scope_ns = php_dom_get_in_scope_ns(ns_mapper, nodep, false);
297 		} else {
298 			in_scope_ns = php_dom_get_in_scope_ns_legacy(nodep);
299 		}
300 		ctxp->namespaces = in_scope_ns.list;
301 		ctxp->nsNr = in_scope_ns.count;
302 	}
303 
304 	xmlXPathObjectPtr xpathobjp = xmlXPathEvalExpression(BAD_CAST expr, ctxp);
305 	ctxp->node = NULL;
306 
307 	if (register_node_ns && nodep != NULL) {
308 		php_dom_in_scope_ns_destroy(&in_scope_ns);
309 		ctxp->namespaces = NULL;
310 		ctxp->nsNr = 0;
311 	}
312 
313 	if (! xpathobjp) {
314 		if (modern) {
315 			if (!EG(exception)) {
316 				zend_throw_error(NULL, "Could not evaluate XPath expression");
317 			}
318 			RETURN_THROWS();
319 		} else {
320 			/* Should have already emit a warning by libxml */
321 			RETURN_FALSE;
322 		}
323 	}
324 
325 	if (type == PHP_DOM_XPATH_QUERY) {
326 		xpath_type = XPATH_NODESET;
327 	} else {
328 		xpath_type = xpathobjp->type;
329 	}
330 
331 	switch (xpath_type) {
332 
333 		case  XPATH_NODESET:
334 		{
335 			xmlNodeSetPtr nodesetp;
336 			zval retval;
337 
338 			if (xpathobjp->type == XPATH_NODESET && NULL != (nodesetp = xpathobjp->nodesetval) && nodesetp->nodeNr) {
339 				array_init_size(&retval, nodesetp->nodeNr);
340 				zend_hash_real_init_packed(Z_ARRVAL_P(&retval));
341 				for (int i = 0; i < nodesetp->nodeNr; i++) {
342 					xmlNodePtr node = nodesetp->nodeTab[i];
343 					zval child;
344 
345 					if (node->type == XML_NAMESPACE_DECL) {
346 						if (modern) {
347 							if (!EG(exception)) {
348 								php_dom_throw_error_with_message(NOT_SUPPORTED_ERR,
349 									"The namespace axis is not well-defined in the living DOM specification. "
350 									"Use Dom\\Element::getInScopeNamespaces() or Dom\\Element::getDescendantNamespaces() instead.",
351 									/* strict */ true
352 								);
353 							}
354 							break;
355 						}
356 
357 						xmlNodePtr nsparent = node->_private;
358 						xmlNsPtr original = (xmlNsPtr) node;
359 
360 						/* Make sure parent dom object exists, so we can take an extra reference. */
361 						zval parent_zval; /* don't destroy me, my lifetime is transferred to the fake namespace decl */
362 						php_dom_create_object(nsparent, &parent_zval, &intern->dom);
363 						dom_object *parent_intern = Z_DOMOBJ_P(&parent_zval);
364 
365 						node = php_dom_create_fake_namespace_decl(nsparent, original, &child, parent_intern);
366 					} else {
367 						php_dom_create_object(node, &child, &intern->dom);
368 					}
369 					add_next_index_zval(&retval, &child);
370 				}
371 			} else {
372 				ZVAL_EMPTY_ARRAY(&retval);
373 			}
374 			php_dom_create_iterator(return_value, DOM_NODELIST, modern);
375 			nodeobj = Z_DOMOBJ_P(return_value);
376 			dom_xpath_iter(&retval, nodeobj);
377 			break;
378 		}
379 
380 		case XPATH_BOOLEAN:
381 			RETVAL_BOOL(xpathobjp->boolval);
382 			break;
383 
384 		case XPATH_NUMBER:
385 			RETVAL_DOUBLE(xpathobjp->floatval);
386 			break;
387 
388 		case XPATH_STRING:
389 			RETVAL_STRING((char *) xpathobjp->stringval);
390 			break;
391 
392 		default:
393 			RETVAL_NULL();
394 			break;
395 	}
396 
397 	xmlXPathFreeObject(xpathobjp);
398 }
399 /* }}} */
400 
401 /* {{{ */
PHP_METHOD(DOMXPath,query)402 PHP_METHOD(DOMXPath, query)
403 {
404 	php_xpath_eval(INTERNAL_FUNCTION_PARAM_PASSTHRU, PHP_DOM_XPATH_QUERY, false);
405 }
406 
PHP_METHOD(Dom_XPath,query)407 PHP_METHOD(Dom_XPath, query)
408 {
409 	php_xpath_eval(INTERNAL_FUNCTION_PARAM_PASSTHRU, PHP_DOM_XPATH_QUERY, true);
410 }
411 /* }}} end dom_xpath_query */
412 
413 /* {{{ */
PHP_METHOD(DOMXPath,evaluate)414 PHP_METHOD(DOMXPath, evaluate)
415 {
416 	php_xpath_eval(INTERNAL_FUNCTION_PARAM_PASSTHRU, PHP_DOM_XPATH_EVALUATE, false);
417 }
418 
PHP_METHOD(Dom_XPath,evaluate)419 PHP_METHOD(Dom_XPath, evaluate)
420 {
421 	php_xpath_eval(INTERNAL_FUNCTION_PARAM_PASSTHRU, PHP_DOM_XPATH_EVALUATE, true);
422 }
423 /* }}} end dom_xpath_evaluate */
424 
425 /* {{{ */
PHP_METHOD(DOMXPath,registerPhpFunctions)426 PHP_METHOD(DOMXPath, registerPhpFunctions)
427 {
428 	dom_xpath_object *intern = Z_XPATHOBJ_P(ZEND_THIS);
429 
430 	zend_string *callable_name = NULL;
431 	HashTable *callable_ht = NULL;
432 
433 	ZEND_PARSE_PARAMETERS_START(0, 1)
434 		Z_PARAM_OPTIONAL
435 		Z_PARAM_ARRAY_HT_OR_STR_OR_NULL(callable_ht, callable_name)
436 	ZEND_PARSE_PARAMETERS_END();
437 
438 	php_dom_xpath_callbacks_update_method_handler(
439 		&intern->xpath_callbacks,
440 		intern->dom.ptr,
441 		NULL,
442 		callable_name,
443 		callable_ht,
444 		PHP_DOM_XPATH_CALLBACK_NAME_VALIDATE_NULLS,
445 		NULL
446 	);
447 }
448 /* }}} end dom_xpath_register_php_functions */
449 
dom_xpath_register_func_in_ctx(void * ctxt,const zend_string * ns,const zend_string * name)450 static void dom_xpath_register_func_in_ctx(void *ctxt, const zend_string *ns, const zend_string *name)
451 {
452 	xmlXPathRegisterFuncNS((xmlXPathContextPtr) ctxt, (const xmlChar *) ZSTR_VAL(name), (const xmlChar *) ZSTR_VAL(ns), dom_xpath_ext_function_trampoline);
453 }
454 
PHP_METHOD(DOMXPath,registerPhpFunctionNS)455 PHP_METHOD(DOMXPath, registerPhpFunctionNS)
456 {
457 	dom_xpath_object *intern = Z_XPATHOBJ_P(ZEND_THIS);
458 
459 	zend_string *namespace, *name;
460 	zend_fcall_info fci;
461 	zend_fcall_info_cache fcc;
462 
463 	ZEND_PARSE_PARAMETERS_START(3, 3)
464 		Z_PARAM_PATH_STR(namespace)
465 		Z_PARAM_PATH_STR(name)
466 		Z_PARAM_FUNC_NO_TRAMPOLINE_FREE(fci, fcc)
467 	ZEND_PARSE_PARAMETERS_END();
468 
469 	if (zend_string_equals_literal(namespace, "http://php.net/xpath")) {
470 		zend_release_fcall_info_cache(&fcc);
471 		zend_argument_value_error(1, "must not be \"http://php.net/xpath\" because it is reserved by PHP");
472 		RETURN_THROWS();
473 	}
474 
475 	if (php_dom_xpath_callbacks_update_single_method_handler(
476 		&intern->xpath_callbacks,
477 		intern->dom.ptr,
478 		namespace,
479 		name,
480 		&fcc,
481 		PHP_DOM_XPATH_CALLBACK_NAME_VALIDATE_NCNAME,
482 		dom_xpath_register_func_in_ctx
483 	) != SUCCESS) {
484 		zend_release_fcall_info_cache(&fcc);
485 	}
486 }
487 
488 /* {{{ */
PHP_METHOD(DOMXPath,quote)489 PHP_METHOD(DOMXPath, quote) {
490 	const char *input;
491 	size_t input_len;
492 	if (zend_parse_parameters(ZEND_NUM_ARGS(), "p", &input, &input_len) == FAILURE) {
493 		RETURN_THROWS();
494 	}
495 	if (memchr(input, '\'', input_len) == NULL) {
496 		zend_string *const output = zend_string_safe_alloc(1, input_len, 2, false);
497 		ZSTR_VAL(output)[0] = '\'';
498 		memcpy(ZSTR_VAL(output) + 1, input, input_len);
499 		ZSTR_VAL(output)[input_len + 1] = '\'';
500 		ZSTR_VAL(output)[input_len + 2] = '\0';
501 		RETURN_STR(output);
502 	} else if (memchr(input, '"', input_len) == NULL) {
503 		zend_string *const output = zend_string_safe_alloc(1, input_len, 2, false);
504 		ZSTR_VAL(output)[0] = '"';
505 		memcpy(ZSTR_VAL(output) + 1, input, input_len);
506 		ZSTR_VAL(output)[input_len + 1] = '"';
507 		ZSTR_VAL(output)[input_len + 2] = '\0';
508 		RETURN_STR(output);
509 	} else {
510 		smart_str output = {0};
511 		// need to use the concat() trick published by Robert Rossney at https://stackoverflow.com/a/1352556/1067003
512 		smart_str_appendl(&output, ZEND_STRL("concat("));
513 		const char *ptr = input;
514 		const char *const end = input + input_len;
515 		while (ptr < end) {
516 			const char *const single_quote_ptr = memchr(ptr, '\'', end - ptr);
517 			const char *const double_quote_ptr = memchr(ptr, '"', end - ptr);
518 			const size_t distance_to_single_quote = single_quote_ptr ? single_quote_ptr - ptr : end - ptr;
519 			const size_t distance_to_double_quote = double_quote_ptr ? double_quote_ptr - ptr : end - ptr;
520 			const size_t bytes_until_quote = MAX(distance_to_single_quote, distance_to_double_quote);
521 			const char quote_method = (distance_to_single_quote > distance_to_double_quote) ? '\'' : '"';
522 			smart_str_appendc(&output, quote_method);
523 			smart_str_appendl(&output, ptr, bytes_until_quote);
524 			smart_str_appendc(&output, quote_method);
525 			ptr += bytes_until_quote;
526 			smart_str_appendc(&output, ',');
527 		}
528 		ZEND_ASSERT(ptr == end);
529 		ZSTR_VAL(output.s)[output.s->len - 1] = ')';
530 		RETURN_STR(smart_str_extract(&output));
531 	}
532 }
533 /* }}} */
534 
535 #endif /* LIBXML_XPATH_ENABLED */
536 
537 #endif
538