瀏覽代碼

XML stanza parsing fixes

- Add a `Stanza` class which can be used by Strophe because it has a
  `tree()` function. This is what gets returned by the `stx` tagged
  template.

- Throw an error when no valid namespace is on the stanza.
    Strophe.Builder used to automatically add the `jabber:client` namespace,
    but that doesn't happen with `toStanza`, so we need to fail if it's not
    specified by the user.

- Use the Strophe XML Parser
    This opens the door to NodeJS support
JC Brand 2 年之前
父節點
當前提交
5029d93523

+ 3 - 3
src/headless/core.js

@@ -428,8 +428,8 @@ export const api = _converse.api = {
         }
         if (typeof stanza === 'string') {
             stanza = u.toStanza(stanza);
-        } else if (stanza?.nodeTree) {
-            stanza = stanza.nodeTree;
+        } else if (stanza?.tree) {
+            stanza = stanza.tree();
         }
 
         if (stanza.tagName === 'iq') {
@@ -454,7 +454,7 @@ export const api = _converse.api = {
      */
     sendIQ (stanza, timeout=_converse.STANZA_TIMEOUT, reject=true) {
         let promise;
-        stanza = stanza?.nodeTree ?? stanza;
+        stanza = stanza.tree?.() ?? stanza;
         if (['get', 'set'].includes(stanza.getAttribute('type'))) {
             timeout = timeout || _converse.STANZA_TIMEOUT;
             if (reject) {

+ 1 - 1
src/headless/plugins/caps/utils.js

@@ -31,7 +31,7 @@ async function createCapsNode () {
         'hash': "sha-1",
         'node': "https://conversejs.org",
         'ver': await generateVerificationString()
-    }).nodeTree;
+    }).tree();
 }
 
 

+ 2 - 0
src/headless/plugins/chat/utils.js

@@ -88,6 +88,8 @@ export function registerMessageHandlers () {
  * @param { MessageAttributes } attrs - The message attributes
  */
 export async function handleMessageStanza (stanza) {
+    stanza = stanza.tree?.() ?? stanza;
+
     if (isServerMessage(stanza)) {
         // Prosody sends headline messages with type `chat`, so we need to filter them out here.
         const from = stanza.getAttribute('from');

+ 3 - 1
src/headless/plugins/muc/muc.js

@@ -549,6 +549,8 @@ const ChatRoomMixin = {
      * @param { XMLElement } stanza
      */
     async handleMessageStanza (stanza) {
+        stanza = stanza.tree?.() ?? stanza;
+
         const type = stanza.getAttribute('type');
         if (type === 'error') {
             return this.handleErrorMessageStanza(stanza);
@@ -755,7 +757,7 @@ const ChatRoomMixin = {
         message.set({
             'retracted': new Date().toISOString(),
             'retracted_id': origin_id,
-            'retraction_id': stanza.nodeTree.getAttribute('id'),
+            'retraction_id': stanza.tree().getAttribute('id'),
             'editable': false
         });
         const result = await this.sendTimedMessage(stanza);

+ 12 - 12
src/headless/plugins/muc/tests/messages.js

@@ -124,19 +124,19 @@ describe("A MUC message", function () {
         const muc_jid = 'lounge@montague.lit';
         const model = await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
         const received_stanza = u.toStanza(`
-        <message to='${_converse.jid}' from='${muc_jid}/mallory' type='groupchat' id='${_converse.connection.getUniqueId()}' >
-            <reply xmlns='urn:xmpp:reply:0' id='${_converse.connection.getUniqueId()}' to='${_converse.jid}'/>
-            <fallback xmlns='urn:xmpp:feature-fallback:0' for='urn:xmpp:reply:0'>
-                <body start='0' end='10'/>
-            </fallback>
-            <active xmlns='http://jabber.org/protocol/chatstates'/>
-            <body>&gt; ping
-pong</body>
-            <request xmlns='urn:xmpp:receipts'/>
-        </message>
-    `);
+            <message to='${_converse.jid}' from='${muc_jid}/mallory' type='groupchat' id='${_converse.connection.getUniqueId()}' >
+                <reply xmlns='urn:xmpp:reply:0' id='${_converse.connection.getUniqueId()}' to='${_converse.jid}'/>
+                <fallback xmlns='urn:xmpp:feature-fallback:0' for='urn:xmpp:reply:0'>
+                    <body start='0' end='10'/>
+                </fallback>
+                <active xmlns='http://jabber.org/protocol/chatstates'/>
+                <body>&gt; ping
+    pong</body>
+                <request xmlns='urn:xmpp:receipts'/>
+            </message>
+        `);
         await model.handleMessageStanza(received_stanza);
         await u.waitUntil(() => model.messages.last());
-        expect(model.messages.last().get('body')).toBe('> ping\npong');
+        expect(model.messages.last().get('body')).toBe('> ping\n    pong');
     }));
 });

+ 1 - 1
src/headless/plugins/pubsub.js

@@ -74,7 +74,7 @@ converse.plugins.add('converse-pubsub', {
 
                             // The publish-options precondition couldn't be
                             // met. We re-publish but without publish-options.
-                            const el = stanza.nodeTree;
+                            const el = stanza.tree();
                             el.querySelector('publish-options').outerHTML = '';
                             log.warn(`PubSub: Republishing without publish options. ${el.outerHTML}`);
                             await api.sendIQ(el);

+ 4 - 9
src/headless/shared/connection/index.js

@@ -1,5 +1,4 @@
 import debounce from 'lodash-es/debounce';
-import isElement from 'lodash-es/isElement';
 import log from "../../log.js";
 import sizzle from 'sizzle';
 import { BOSH_WAIT } from '../../shared/constants.js';
@@ -441,9 +440,8 @@ export class MockConnection extends Connection {
     }
 
     sendIQ (iq, callback, errback) {
-        if (!isElement(iq)) {
-            iq = iq.nodeTree;
-        }
+        iq = iq.tree?.() ?? iq;
+
         this.IQ_stanzas.push(iq);
         const id = super.sendIQ(iq, callback, errback);
         this.IQ_ids.push(id);
@@ -451,11 +449,8 @@ export class MockConnection extends Connection {
     }
 
     send (stanza) {
-        if (isElement(stanza)) {
-            this.sent_stanzas.push(stanza);
-        } else {
-            this.sent_stanzas.push(stanza.nodeTree);
-        }
+        stanza = stanza.tree?.() ?? stanza;
+        this.sent_stanzas.push(stanza);
         return super.send(stanza);
     }
 

+ 2 - 2
src/headless/utils/core.js

@@ -91,8 +91,8 @@ export function prefixMentions (message) {
 const u = {};
 
 u.isTagEqual = function (stanza, name) {
-    if (stanza.nodeTree) {
-        return u.isTagEqual(stanza.nodeTree, name);
+    if (stanza.tree?.()) {
+        return u.isTagEqual(stanza.tree(), name);
     } else if (!(stanza instanceof Element)) {
         throw Error(
             "isTagEqual called with value which isn't "+

+ 50 - 14
src/headless/utils/stanza.js

@@ -1,27 +1,63 @@
-const parser = new DOMParser();
-const parserErrorNS = parser.parseFromString('invalid', 'text/xml')
-                            .getElementsByTagName("parsererror")[0].namespaceURI;
+import log from '../log.js';
+import { Strophe } from 'strophe.js/src/strophe';
 
-export function toStanza (string) {
-    const node = parser.parseFromString(string, "text/xml");
-    if (node.getElementsByTagNameNS(parserErrorNS, 'parsererror').length) {
+const PARSE_ERROR_NS = 'http://www.w3.org/1999/xhtml';
+
+export function toStanza (string, throwErrorIfInvalidNS) {
+    const doc = Strophe.xmlHtmlNode(string);
+
+    if (doc.getElementsByTagNameNS(PARSE_ERROR_NS, 'parsererror').length) {
         throw new Error(`Parser Error: ${string}`);
     }
-    return node.firstElementChild;
+
+    const node = doc.firstElementChild;
+
+    if (
+        ['message', 'iq', 'presence'].includes(node.nodeName.toLowerCase()) &&
+        node.namespaceURI !== 'jabber:client' &&
+        node.namespaceURI !== 'jabber:server'
+    ) {
+        const err_msg = `Invalid namespaceURI ${node.namespaceURI}`;
+        log.error(err_msg);
+        if (throwErrorIfInvalidNS) throw new Error(err_msg);
+    }
+    return node;
 }
 
+/**
+ * A Stanza represents a XML element used in XMPP (commonly referred to as
+ * stanzas).
+ */
+class Stanza {
+
+    constructor (strings, values) {
+        this.strings = strings;
+        this.values = values;
+    }
+
+    toString () {
+        this.string = this.string ||
+             this.strings.reduce((acc, str) => {
+                const idx = this.strings.indexOf(str);
+                const value = this.values.length > idx ? this.values[idx].toString() : '';
+                return acc + str + value;
+            }, '');
+        return this.string;
+    }
+
+    tree () {
+        this.node = this.node ?? toStanza(this.toString(), true);
+        return this.node;
+    }
+}
 
 /**
- * Tagged template literal function which can be used to generate XML stanzas.
+ * Tagged template literal function which generates {@link Stanza } objects
+ *
  * Similar to the `html` function, from Lit.
  *
  * @example stx`<presence type="${type}"><show>${show}</show></presence>`
  */
 export function stx (strings, ...values) {
-    return toStanza(
-        strings.reduce((acc, str) => {
-            const idx = strings.indexOf(str);
-            return acc + str + (values.length > idx ? values[idx] : '')
-        }, '')
-    );
+    return new Stanza(strings, values);
 }

+ 6 - 0
src/plugins/muc-views/tests/corrections.js

@@ -276,6 +276,7 @@ describe('A Groupchat Message XEP-0308 correction ', function () {
             await model.handleMessageStanza(
                 stx`
                 <message
+                    xmlns="jabber:server"
                     from="lounge@montague.lit/newguy"
                     to="_converse.connection.jid"
                     type="groupchat"
@@ -293,6 +294,7 @@ describe('A Groupchat Message XEP-0308 correction ', function () {
             await model.handleMessageStanza(
                 stx`
                 <message
+                    xmlns="jabber:server"
                     from="lounge@montague.lit/newguy"
                     to="_converse.connection.jid"
                     type="groupchat"
@@ -315,6 +317,7 @@ describe('A Groupchat Message XEP-0308 correction ', function () {
             await model.handleMessageStanza(
                 stx`
                 <message
+                    xmlns="jabber:server"
                     from="lounge@montague.lit/newguy"
                     to="_converse.connection.jid"
                     type="groupchat"
@@ -355,6 +358,7 @@ describe('A Groupchat Message XEP-0308 correction ', function () {
             await model.handleMessageStanza(
                 stx`
                 <message
+                    xmlns="jabber:server"
                     from="lounge@montague.lit/${nick}"
                     to="_converse.connection.jid"
                     type="groupchat"
@@ -372,6 +376,7 @@ describe('A Groupchat Message XEP-0308 correction ', function () {
             await model.handleMessageStanza(
                 stx`
                 <message
+                    xmlns="jabber:server"
                     from="lounge@montague.lit/${nick}"
                     to="_converse.connection.jid"
                     type="groupchat"
@@ -389,6 +394,7 @@ describe('A Groupchat Message XEP-0308 correction ', function () {
             await model.handleMessageStanza(
                 stx`
                 <message
+                    xmlns="jabber:server"
                     from="lounge@montague.lit/${nick}"
                     to="_converse.connection.jid"
                     type="groupchat"

+ 2 - 0
src/plugins/muc-views/tests/nickname.js

@@ -40,6 +40,7 @@ describe("A MUC", function () {
         _converse.connection._dataRecv(mock.createRequest(
             stx`
             <presence
+                xmlns="jabber:server"
                 from='${muc_jid}/${nick}'
                 id='DC352437-C019-40EC-B590-AF29E879AF98'
                 to='${_converse.jid}'
@@ -60,6 +61,7 @@ describe("A MUC", function () {
         _converse.connection._dataRecv(mock.createRequest(
             stx`
             <presence
+                xmlns="jabber:server"
                 from='${muc_jid}/${newnick}'
                 id='5B4F27A4-25ED-43F7-A699-382C6B4AFC67'
                 to='${_converse.jid}'>