njs_value_t *unused, njs_value_t *retval);
static njs_int_t njs_xml_node_ext_add_child(njs_vm_t *vm, njs_value_t *args,
njs_uint_t nargs, njs_index_t unused, njs_value_t *retval);
+static njs_int_t njs_xml_node_ext_get_attribute_ns(njs_vm_t *vm,
+ njs_value_t *args, njs_uint_t nargs, njs_index_t unused,
+ njs_value_t *retval);
+static njs_int_t njs_xml_node_ext_get_child_ns(njs_vm_t *vm,
+ njs_value_t *args, njs_uint_t nargs, njs_index_t unused,
+ njs_value_t *retval);
static njs_int_t njs_xml_node_ext_attrs(njs_vm_t *vm, njs_object_prop_t *prop,
uint32_t unused, njs_value_t *value, njs_value_t *setval,
njs_value_t *retval);
static xmlNode *njs_xml_external_node(njs_vm_t *vm, njs_value_t *value);
static const u_char *njs_xml_string_to_c_string(njs_vm_t *vm, njs_str_t *str,
u_char *dst, size_t size);
+static xmlNode *njs_xml_ns_args(njs_vm_t *vm, njs_value_t *args,
+ njs_uint_t nargs, njs_str_t *uri, njs_str_t *local);
+static njs_bool_t njs_xml_ns_match(const xmlChar *name, xmlNs *ns,
+ njs_str_t *uri, njs_str_t *local);
static njs_bool_t njs_xml_node_is_live(xmlNode *node);
static xmlNode *njs_xml_list_detach(xmlNode *parent, njs_str_t *name);
static njs_int_t njs_xml_list_retire(njs_vm_t *vm, xmlNode *parent,
}
},
+ {
+ .flags = NJS_EXTERN_METHOD,
+ .name.string = njs_str("getAttributeNS"),
+ .writable = 1,
+ .configurable = 1,
+ .enumerable = 1,
+ .u.method = {
+ .native = njs_xml_node_ext_get_attribute_ns,
+ }
+ },
+
+ {
+ .flags = NJS_EXTERN_METHOD,
+ .name.string = njs_str("getChildNS"),
+ .writable = 1,
+ .configurable = 1,
+ .enumerable = 1,
+ .u.method = {
+ .native = njs_xml_node_ext_get_child_ns,
+ }
+ },
+
{
.flags = NJS_EXTERN_PROPERTY,
.name.string = njs_str("$attrs"),
}
+static njs_int_t
+njs_xml_node_ext_get_attribute_ns(njs_vm_t *vm, njs_value_t *args,
+ njs_uint_t nargs, njs_index_t unused, njs_value_t *retval)
+{
+ xmlAttr *attr;
+ xmlChar *text;
+ xmlNode *current;
+ njs_int_t ret;
+ njs_str_t uri, local;
+
+ current = njs_xml_ns_args(vm, args, nargs, &uri, &local);
+ if (njs_slow_path(current == NULL)) {
+ return NJS_ERROR;
+ }
+
+ for (attr = current->properties; attr != NULL; attr = attr->next) {
+ if (!njs_xml_ns_match(attr->name, attr->ns, &uri, &local)) {
+ continue;
+ }
+
+ text = xmlNodeGetContent((xmlNode *) attr);
+ if (njs_slow_path(text == NULL)) {
+ njs_vm_internal_error(vm, "xmlNodeGetContent() failed");
+ return NJS_ERROR;
+ }
+
+ ret = njs_vm_value_string_create(vm, retval, text, njs_strlen(text));
+ xmlFree(text);
+
+ return ret;
+ }
+
+ njs_value_undefined_set(retval);
+
+ return NJS_OK;
+}
+
+
+static njs_int_t
+njs_xml_node_ext_get_child_ns(njs_vm_t *vm, njs_value_t *args,
+ njs_uint_t nargs, njs_index_t unused, njs_value_t *retval)
+{
+ xmlNode *current, *node;
+ njs_str_t uri, local;
+
+ current = njs_xml_ns_args(vm, args, nargs, &uri, &local);
+ if (njs_slow_path(current == NULL)) {
+ return NJS_ERROR;
+ }
+
+ for (node = current->children; node != NULL; node = node->next) {
+ if (node->type == XML_ELEMENT_NODE
+ && njs_xml_ns_match(node->name, node->ns, &uri, &local))
+ {
+ return njs_vm_external_create(vm, retval, njs_xml_node_proto_id,
+ node, 0);
+ }
+ }
+
+ njs_value_undefined_set(retval);
+
+ return NJS_OK;
+}
+
+
static njs_int_t
njs_xml_node_ext_attrs(njs_vm_t *vm, njs_object_prop_t *prop, uint32_t unused,
njs_value_t *value, njs_value_t *setval, njs_value_t *retval)
}
+static xmlNode *
+njs_xml_ns_args(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
+ njs_str_t *uri, njs_str_t *local)
+{
+ xmlNode *current;
+ njs_value_t *value;
+
+ current = njs_vm_external(vm, njs_xml_node_proto_id,
+ njs_argument(args, 0));
+ if (njs_slow_path(current == NULL)) {
+ njs_vm_type_error(vm, "\"this\" is not a XMLNode object");
+ return NULL;
+ }
+
+ value = njs_arg(args, nargs, 1);
+
+ if (njs_value_is_null(value)) {
+ uri->length = 0;
+ uri->start = NULL;
+
+ } else if (njs_value_is_string(value)) {
+ njs_value_string_get(vm, value, uri);
+
+ } else {
+ njs_vm_type_error(vm, "namespace URI is not a string or null");
+ return NULL;
+ }
+
+ value = njs_arg(args, nargs, 2);
+
+ if (njs_slow_path(!njs_value_is_string(value))) {
+ njs_vm_type_error(vm, "local name is not a string");
+ return NULL;
+ }
+
+ njs_value_string_get(vm, value, local);
+
+ return current;
+}
+
+
+/*
+ * An empty or null URI matches only names without a namespace, as in
+ * DOM getAttributeNS().
+ */
+
+static njs_bool_t
+njs_xml_ns_match(const xmlChar *name, xmlNs *ns, njs_str_t *uri,
+ njs_str_t *local)
+{
+ if (local->length != njs_strlen(name)
+ || njs_strncmp(local->start, name, local->length) != 0)
+ {
+ return 0;
+ }
+
+ if (ns == NULL || ns->href == NULL) {
+ return uri->length == 0;
+ }
+
+ return uri->length == njs_strlen(ns->href)
+ && njs_strncmp(uri->start, ns->href, uri->length) == 0;
+}
+
+
static njs_bool_t
njs_xml_node_is_live(xmlNode *node)
{
JSAtom atom, JSValueConst value, int delete);
static JSValue qjs_xml_node_add_child(JSContext *cx, JSValueConst this_val,
int argc, JSValueConst *argv);
+static JSValue qjs_xml_node_get_attribute_ns(JSContext *cx,
+ JSValueConst this_val, int argc, JSValueConst *argv);
+static JSValue qjs_xml_node_get_child_ns(JSContext *cx,
+ JSValueConst this_val, int argc, JSValueConst *argv);
static JSValue qjs_xml_node_remove_children(JSContext *cx,
JSValueConst this_val, int argc, JSValueConst *argv);
static JSValue qjs_xml_node_set_attribute(JSContext *cx, JSValueConst this_val,
static qjs_xml_nset_t *qjs_xml_nset_add(qjs_xml_nset_t *nset,
qjs_xml_nset_t *add);
static void qjs_xml_nset_free(JSContext *cx, qjs_xml_nset_t *nset);
+static qjs_xml_node_t *qjs_xml_ns_args(JSContext *cx, JSValueConst this_val,
+ JSValueConst *argv, njs_str_t *uri, njs_str_t *local);
+static void qjs_xml_ns_args_free(JSContext *cx, njs_str_t *uri,
+ njs_str_t *local);
+static int qjs_xml_ns_match(const xmlChar *name, xmlNs *ns, njs_str_t *uri,
+ njs_str_t *local);
static int qjs_xml_node_is_live(xmlNode *node);
static xmlNode *qjs_xml_list_detach(xmlNode *parent, njs_str_t *name);
static int qjs_xml_list_retire(JSContext *cx, qjs_xml_doc_t *doc,
static const JSCFunctionListEntry qjs_xml_node_proto[] = {
JS_PROP_STRING_DEF("[Symbol.toStringTag]", "XMLNode", JS_PROP_CONFIGURABLE),
JS_CFUNC_DEF("addChild", 1, qjs_xml_node_add_child),
+ JS_CFUNC_DEF("getAttributeNS", 2, qjs_xml_node_get_attribute_ns),
+ JS_CFUNC_DEF("getChildNS", 2, qjs_xml_node_get_child_ns),
JS_CFUNC_DEF("removeChildren", 1, qjs_xml_node_remove_children),
JS_CFUNC_DEF("setAttribute", 2, qjs_xml_node_set_attribute),
JS_CFUNC_DEF("removeAttribute", 1, qjs_xml_node_remove_attribute),
}
+static JSValue
+qjs_xml_node_get_attribute_ns(JSContext *cx, JSValueConst this_val,
+ int argc, JSValueConst *argv)
+{
+ xmlAttr *attr;
+ xmlChar *text;
+ JSValue value;
+ njs_str_t uri, local;
+ qjs_xml_node_t *current;
+
+ current = qjs_xml_ns_args(cx, this_val, argv, &uri, &local);
+ if (current == NULL) {
+ return JS_EXCEPTION;
+ }
+
+ value = JS_UNDEFINED;
+
+ for (attr = current->node->properties; attr != NULL; attr = attr->next) {
+ if (!qjs_xml_ns_match(attr->name, attr->ns, &uri, &local)) {
+ continue;
+ }
+
+ text = xmlNodeGetContent((xmlNode *) attr);
+ if (text == NULL) {
+ value = JS_ThrowInternalError(cx, "xmlNodeGetContent() failed");
+ break;
+ }
+
+ value = JS_NewString(cx, (char *) text);
+ xmlFree(text);
+ break;
+ }
+
+ qjs_xml_ns_args_free(cx, &uri, &local);
+
+ return value;
+}
+
+
+static JSValue
+qjs_xml_node_get_child_ns(JSContext *cx, JSValueConst this_val,
+ int argc, JSValueConst *argv)
+{
+ JSValue value;
+ xmlNode *node;
+ njs_str_t uri, local;
+ qjs_xml_node_t *current;
+
+ current = qjs_xml_ns_args(cx, this_val, argv, &uri, &local);
+ if (current == NULL) {
+ return JS_EXCEPTION;
+ }
+
+ value = JS_UNDEFINED;
+
+ for (node = current->node->children; node != NULL; node = node->next) {
+ if (node->type == XML_ELEMENT_NODE
+ && qjs_xml_ns_match(node->name, node->ns, &uri, &local))
+ {
+ value = qjs_xml_node_make(cx, current->doc, node);
+ break;
+ }
+ }
+
+ qjs_xml_ns_args_free(cx, &uri, &local);
+
+ return value;
+}
+
+
static JSValue
qjs_xml_node_add_child(JSContext *cx, JSValueConst this_val,
int argc, JSValueConst *argv)
}
+static qjs_xml_node_t *
+qjs_xml_ns_args(JSContext *cx, JSValueConst this_val, JSValueConst *argv,
+ njs_str_t *uri, njs_str_t *local)
+{
+ size_t length;
+ qjs_xml_node_t *current;
+
+ current = JS_GetOpaque(this_val, QJS_CORE_CLASS_ID_XML_NODE);
+ if (current == NULL) {
+ JS_ThrowTypeError(cx, "\"this\" is not a XMLNode object");
+ return NULL;
+ }
+
+ if (!JS_IsNull(argv[0]) && !JS_IsString(argv[0])) {
+ JS_ThrowTypeError(cx, "namespace URI is not a string or null");
+ return NULL;
+ }
+
+ if (!JS_IsString(argv[1])) {
+ JS_ThrowTypeError(cx, "local name is not a string");
+ return NULL;
+ }
+
+ uri->start = NULL;
+ uri->length = 0;
+
+ if (JS_IsString(argv[0])) {
+ uri->start = (u_char *) JS_ToCStringLen(cx, &length, argv[0]);
+ if (uri->start == NULL) {
+ return NULL;
+ }
+
+ uri->length = length;
+ }
+
+ local->start = (u_char *) JS_ToCStringLen(cx, &length, argv[1]);
+ if (local->start == NULL) {
+ if (uri->start != NULL) {
+ JS_FreeCString(cx, (char *) uri->start);
+ }
+
+ return NULL;
+ }
+
+ local->length = length;
+
+ return current;
+}
+
+
+static void
+qjs_xml_ns_args_free(JSContext *cx, njs_str_t *uri, njs_str_t *local)
+{
+ if (uri->start != NULL) {
+ JS_FreeCString(cx, (char *) uri->start);
+ }
+
+ JS_FreeCString(cx, (char *) local->start);
+}
+
+
+/*
+ * An empty or null URI matches only names without a namespace, as in
+ * DOM getAttributeNS().
+ */
+
+static int
+qjs_xml_ns_match(const xmlChar *name, xmlNs *ns, njs_str_t *uri,
+ njs_str_t *local)
+{
+ if (local->length != njs_strlen(name)
+ || njs_strncmp(local->start, name, local->length) != 0)
+ {
+ return 0;
+ }
+
+ if (ns == NULL || ns->href == NULL) {
+ return uri->length == 0;
+ }
+
+ return uri->length == njs_strlen(ns->href)
+ && njs_strncmp(uri->start, ns->href, uri->length) == 0;
+}
+
+
static int
qjs_xml_node_is_live(xmlNode *node)
{
node?.$tag$xxx?.$tag$yyy?.$attr$zzz;
+ let child: XMLNode | undefined = node.getChildNS('urn:x', 'xxx');
+ child = node.getChildNS(null, 'xxx');
+ let attr: string | undefined = node.getAttributeNS('urn:x', 'yyy');
+ attr = node.getAttributeNS(null, 'yyy');
+
let buf:Buffer = xml.exclusiveC14n(node);
buf = xml.exclusiveC14n(doc, node.$tag$xxx, false);
buf = xml.exclusiveC14n(node, null, true, "aa bb");
{ doc: `<n0:pdu xmlns:n0=\"http://a\"></n0:pdu>`,
get: (doc) => doc.pdu.$ns,
expected: 'http://a' },
+ { doc: `<r xmlns:x="urn:other" x:ID="wrong" ID="correct">`
+ + `<x:Issuer>foreign</x:Issuer><Issuer>trusted</Issuer></r>`,
+ get: (doc) => [
+ doc.r.getAttributeNS('', 'ID'),
+ doc.r.getAttributeNS(null, 'ID'),
+ doc.r.getAttributeNS('urn:other', 'ID'),
+ doc.r.getAttributeNS('urn:missing', 'ID'),
+ doc.r.getChildNS('', 'Issuer').$text,
+ doc.r.getChildNS(null, 'Issuer').$text,
+ doc.r.getChildNS('urn:other', 'Issuer').$text,
+ doc.r.getChildNS('urn:missing', 'Issuer')].join(','),
+ expected: 'correct,correct,wrong,,trusted,trusted,foreign,' },
+ { doc: `<r xmlns="urn:d" ID="plain"><c/></r>`,
+ get: (doc) => [
+ doc.r.getAttributeNS(null, 'ID'),
+ doc.r.getAttributeNS('urn:d', 'ID'),
+ doc.r.getChildNS(null, 'c'),
+ doc.r.getChildNS('urn:d', 'c').$name].join(','),
+ expected: 'plain,,,c' },
+ { doc: `<r xml:lang="en"/>`,
+ get: (doc) => doc.r.getAttributeNS(
+ 'http://www.w3.org/XML/1998/namespace', 'lang'),
+ expected: 'en' },
+ { doc: `<r><a><b/></a></r>`,
+ get: (doc) => doc.r.getChildNS(null, 'b'),
+ expected: undefined },
+ { doc: `<r><!--c--><?p?>text<c/></r>`,
+ get: (doc) => doc.r.getChildNS(null, 'c').$name,
+ expected: 'c' },
+ { doc: `<!DOCTYPE r [<!ENTITY e "complete">]>`
+ + `<r xmlns:x="urn:other" x:ID="pre&e;post"/>`,
+ get: (doc) => doc.r.getAttributeNS('urn:other', 'ID'),
+ expected: 'precompletepost' },
+ { get: (doc) => doc.note.getChildNS(undefined, 'to'),
+ exception: 'TypeError: namespace URI is not a string or null' },
+ { get: (doc) => doc.note.getAttributeNS(1, 'a'),
+ exception: 'TypeError: namespace URI is not a string or null' },
+ { get: (doc) => doc.note.getChildNS(null),
+ exception: 'TypeError: local name is not a string' },
+ { get: (doc) => doc.note.to.getAttributeNS(null, null),
+ exception: 'TypeError: local name is not a string' },
+ { get: (doc) => doc.note.getChildNS.call({}, null, 'to'),
+ exception: 'TypeError: "this" is not a XMLNode object' },
{ doc: `<root><foo>FOO</foo><foo>BAR</foo></root>`,
get: (doc) => doc.root.$tags$foo[0].$text,
expected: 'FOO' },
}
export interface XMLNode {
+ /**
+ * Returns the first direct child element with this namespace URI
+ * and local name, or undefined if there is none.
+ * @param namespaceURI - namespace URI; null or an empty string
+ * matches an element without a namespace.
+ * @param localName - local name of the element.
+ * @since 1.0.2.
+ */
+ getChildNS(namespaceURI: string | null, localName: string):
+ XMLNode | undefined;
+
+ /**
+ * Returns the value of an attribute present in the document with
+ * this namespace URI and local name, or undefined if there is none.
+ * @param namespaceURI - namespace URI; null or an empty string
+ * matches an attribute without a namespace. An unprefixed
+ * attribute never belongs to the default namespace.
+ * @param localName - local name of the attribute.
+ * @since 1.0.2.
+ */
+ getAttributeNS(namespaceURI: string | null, localName: string):
+ string | undefined;
+
/**
* Adds a recursive copy of a namespace-free child node.
* @param node - XMLNode to be added.