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
This commit is contained in:
JC Brand 2023-02-02 21:59:38 +01:00
parent bab11b682b
commit 5029d93523
11 changed files with 86 additions and 43 deletions

View File

@ -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) {

View File

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

View File

@ -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');

View File

@ -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);

View File

@ -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');
}));
});

View File

@ -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);

View File

@ -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);
}

View File

@ -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 "+

View File

@ -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);
}

View File

@ -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"

View File

@ -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}'>