UNPKG

strophe.js

Version:

Strophe.js is an XMPP library for JavaScript

150 lines (149 loc) 5.37 kB
import { createRequire } from 'node:module'; import { StringDecoder } from 'node:string_decoder'; import { xmlGenerator } from '../utils'; /** Cached `saxes` constructor, resolved by {@link getSaxesParser} on first use. */ let SaxesParserCtor; /** * Resolve the `saxes` parser constructor, loading the package on first use. * * `saxes` is an optional peer dependency needed only by this transport, so it is * deliberately *not* imported at module load. * * The load is synchronous because {@link Component} builds its parser in its * constructor. */ function getSaxesParser() { if (!SaxesParserCtor) { try { SaxesParserCtor = createRequire(import.meta.url)('saxes').SaxesParser; } catch (_a) { throw new Error('Strophe: the XEP-0114 component transport requires the "saxes" package, ' + 'which is an optional peer dependency. Install it with `npm install saxes`.'); } } return SaxesParserCtor; } /** * Incrementally parses an XMPP component stream. * * Feed it raw socket chunks via {@link ComponentParser#write}; it emits * stream-start, per-stanza and stream-end events through the handlers passed to * the constructor. Robust to a stanza (or a single multi-byte character) being * split across arbitrary chunk boundaries. */ export default class ComponentParser { constructor(handlers) { this._handlers = handlers; this.reset(); } /** * Discard all parser state and start afresh. Called when a (re)connection * begins so a new stream is parsed from a clean slate. */ reset() { this._decoder = new StringDecoder('utf8'); this._stack = []; this._streamOpened = false; this._errored = false; const SaxesParserClass = getSaxesParser(); this._parser = new SaxesParserClass({ xmlns: true }); this._parser.on('opentag', (tag) => this._onOpenTag(tag)); this._parser.on('closetag', () => this._onCloseTag()); this._parser.on('text', (text) => this._onText(text)); this._parser.on('cdata', (cdata) => this._onText(cdata)); this._parser.on('error', (err) => this._fail(err)); } /** * Feed a chunk of raw stream data into the parser. * * Accepts a `Buffer` (as delivered by a `node:net` socket) or an already * decoded string. Buffers are run through a {@link StringDecoder} so that a * multi-byte UTF-8 character split across two chunks is reassembled instead * of corrupted. * @param chunk - The bytes (or string) received from the socket. */ write(chunk) { if (this._errored) { return; } const str = typeof chunk === 'string' ? chunk : this._decoder.write(chunk); if (str === '') { return; } try { this._parser.write(str); } catch (e) { this._fail(e); } } _fail(error) { if (this._errored) { return; } this._errored = true; this._handlers.onError(error); } _onOpenTag(tag) { if (!this._streamOpened) { // The depth-0 element is the stream root. Capture its attributes and // treat it as the stream-start event; it won't close until the // stream ends. this._streamOpened = true; this._handlers.onStreamStart(this._flattenAttributes(tag)); return; } const el = this._createElement(tag); if (this._stack.length) { this._stack[this._stack.length - 1].appendChild(el); } this._stack.push(el); } _onText(text) { // Text (and whitespace keepalives) directly under the stream root, i.e. // in between stanzas, is ignored. if (this._stack.length) { this._stack[this._stack.length - 1].appendChild(xmlGenerator().createTextNode(text)); } } _onCloseTag() { if (!this._stack.length) { // Nothing is being built, so this closes the stream root. this._streamOpened = false; this._handlers.onStreamEnd(); return; } const el = this._stack.pop(); if (!this._stack.length) { // A complete top-level stanza has been assembled. this._handlers.onStanza(el); } } _createElement(tag) { const el = xmlGenerator().createElementNS(tag.uri || null, tag.name); for (const key of Object.keys(tag.attributes)) { const attr = tag.attributes[key]; // Namespace declarations are skipped: createElementNS already // carries the resolved URI, so re-adding xmlns/xmlns:* would only // duplicate them on serialisation. if (attr.name === 'xmlns' || attr.prefix === 'xmlns') { continue; } if (attr.uri) { el.setAttributeNS(attr.uri, attr.name, attr.value); } else { el.setAttribute(attr.name, attr.value); } } return el; } _flattenAttributes(tag) { const attrs = {}; for (const key of Object.keys(tag.attributes)) { attrs[key] = tag.attributes[key].value; } return attrs; } }