UNPKG

strophe.js

Version:

Strophe.js is an XMPP library for JavaScript

1,477 lines (1,459 loc) 253 kB
'use strict'; var ws = require('ws'); var xmldom = require('@xmldom/xmldom'); var node_net = require('node:net'); var node_crypto = require('node:crypto'); var node_module = require('node:module'); var node_string_decoder = require('node:string_decoder'); const _NS = { AUTH: 'jabber:iq:auth', BIND: 'urn:ietf:params:xml:ns:xmpp-bind', BOSH: 'urn:xmpp:xbosh', CLIENT: 'jabber:client', COMPONENT: 'jabber:component:accept' /** XEP-0114 */, DISCO_INFO: 'http://jabber.org/protocol/disco#info', DISCO_ITEMS: 'http://jabber.org/protocol/disco#items', DELAY: 'urn:xmpp:delay' /** XEP-0203 */, FRAMING: 'urn:ietf:params:xml:ns:xmpp-framing', HTTPBIND: 'http://jabber.org/protocol/httpbind', MUC: 'http://jabber.org/protocol/muc', PROFILE: 'jabber:iq:profile', ROSTER: 'jabber:iq:roster', SASL: 'urn:ietf:params:xml:ns:xmpp-sasl', SERVER: 'jabber:server', SESSION: 'urn:ietf:params:xml:ns:xmpp-session', SM: 'urn:xmpp:sm:3', STANZAS: 'urn:ietf:params:xml:ns:xmpp-stanzas', STREAM: 'http://etherx.jabber.org/streams', VERSION: 'jabber:iq:version', XHTML: 'http://www.w3.org/1999/xhtml', XHTML_IM: 'http://jabber.org/protocol/xhtml-im', }; /** * Common namespace constants from the XMPP RFCs and XEPs. * Extensible at runtime via {@link Strophe.addNamespace}, hence the string * index signature. */ const NS = _NS; const PARSE_ERROR_NS = 'http://www.w3.org/1999/xhtml'; /** * The version of the page↔worker message protocol spoken between * WorkerWebsocket and dist/shared-connection-worker.js. A SharedWorker can * outlive the pages that spawned it, so after a deploy a freshly loaded page * may attach to a worker from an older build (or vice versa). The version is * exchanged on _connect/_attach so that a mismatch fails loudly instead of * silently misbehaving. */ const SHARED_WORKER_PROTOCOL_VERSION = 3; /** * Contains allowed tags, tag attributes, and css properties. * Used in the {@link Strophe.createHtml} function to filter incoming html into the allowed XHTML-IM subset. * See [XEP-0071](http://xmpp.org/extensions/xep-0071.html#profile-summary) for the list of recommended * allowed tags and their attributes. */ const XHTML = { tags: ['a', 'blockquote', 'br', 'cite', 'em', 'img', 'li', 'ol', 'p', 'span', 'strong', 'ul', 'body'], attributes: { 'a': ['href'], 'blockquote': ['style'], 'br': [], 'cite': ['style'], 'em': [], 'img': ['src', 'alt', 'style', 'height', 'width'], 'li': ['style'], 'ol': ['style'], 'p': ['style'], 'span': ['style'], 'strong': [], 'ul': ['style'], 'body': [], }, css: [ 'background-color', 'color', 'font-family', 'font-size', 'font-style', 'font-weight', 'margin-left', 'margin-right', 'text-align', 'text-decoration', ], }; /** * Connection status constants for use by the connection handler * callback. */ const Status = { ERROR: 0, CONNECTING: 1, CONNFAIL: 2, AUTHENTICATING: 3, AUTHFAIL: 4, CONNECTED: 5, DISCONNECTED: 6, DISCONNECTING: 7, ATTACHED: 8, REDIRECT: 9, CONNTIMEOUT: 10, BINDREQUIRED: 11, ATTACHFAIL: 12, RECONNECTING: 13, }; const ErrorCondition = { BAD_FORMAT: 'bad-format', CONFLICT: 'conflict', MISSING_JID_NODE: 'x-strophe-bad-non-anon-jid', NO_AUTH_MECH: 'no-auth-mech', UNKNOWN_REASON: 'unknown', }; const LOG_LEVELS = { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3, FATAL: 4, }; /** * DOM element types. * * - ElementType.NORMAL - Normal element. * - ElementType.TEXT - Text data element. * - ElementType.FRAGMENT - XHTML fragment element. */ const ElementType = { NORMAL: 1, TEXT: 3, CDATA: 4, FRAGMENT: 11, }; /** * Node.js DOM shim. * * Browsers provide `DOMParser`, `XMLSerializer` and a `document` natively; * Node.js does not. The Node build therefore wires up `@xmldom/xmldom`, a * small, pure-JS, XML-only W3C DOM, together with the `ws` WebSocket. * * It is imported for its side effects by the Node entry point (see * `index-node.ts`) before any Connection is created, and is never part of the * browser build. * * Two thin compatibility shims bridge the gaps between `@xmldom/xmldom` and the * browser DOM that Strophe's shared code assumes: * * - `firstElementChild`: `@xmldom/xmldom` does not implement this `ParentNode` * getter, so it is polyfilled from the child nodes. * - parse errors: browsers return a document whose root is a `<parsererror>` * element for malformed input, whereas `@xmldom/xmldom` throws. `DOMParser` * is wrapped to restore the browser behaviour, which is what * {@link getParserError} and the BOSH/WebSocket parse paths expect. */ const domImplementation = new xmldom.DOMImplementation(); globalThis.WebSocket = ws; globalThis.XMLSerializer = xmldom.XMLSerializer; // `xmlGenerator()` only ever calls `document.implementation.createDocument(...)`, // so an empty document is the entire global `document` surface Strophe needs. globalThis.document = domImplementation.createDocument(null, null, null); // --- DOM API polyfills ----------------------------------------------------- // `@xmldom/xmldom` omits a couple of browser DOM APIs that Strophe's shared code // assumes. Add them to xmldom's Element/Document prototypes, grabbed off a // sample tree, so the rest of the library stays browser-idiomatic. const sample = domImplementation.createDocument(null, 'sample', null); const elementProto = Object.getPrototypeOf(sample.documentElement); const documentProto = Object.getPrototypeOf(sample); // `ParentNode.firstElementChild`: the first child whose nodeType is ELEMENT_NODE. for (const proto of [elementProto, documentProto]) { if (proto && !('firstElementChild' in proto)) { Object.defineProperty(proto, 'firstElementChild', { configurable: true, get() { var _a; let node = this.firstChild; while (node && node.nodeType !== 1) { node = node.nextSibling; } return (_a = node) !== null && _a !== void 0 ? _a : null; }, }); } } // `Element.innerHTML`: used by Builder.h() for XHTML-IM. jsdom parsed this as // HTML; a lightweight XML DOM cannot, but XHTML-IM payloads are well-formed XML // by definition, so parse the assigned markup as XML. Malformed input leaves the // element empty rather than throwing (Builder.h() then produces no XHTML body). // The getter serialises the children back, escaping text as XML. if (elementProto && !('innerHTML' in elementProto)) { Object.defineProperty(elementProto, 'innerHTML', { configurable: true, get() { const serializer = new xmldom.XMLSerializer(); let out = ''; for (let i = 0; i < this.childNodes.length; i++) { out += serializer.serializeToString(this.childNodes[i]); } return out; }, set(html) { while (this.firstChild) { this.removeChild(this.firstChild); } let root; try { const doc = new xmldom.DOMParser({ onError: (level, message) => { if (level === 'fatalError') { throw new Error(typeof message === 'string' ? message : String(message)); } }, }).parseFromString(`<xhtml>${html}</xhtml>`, 'text/xml'); root = doc.documentElement; } catch (_a) { return; } if (root) { while (root.firstChild) { this.appendChild(root.firstChild); } } }, }); } // --- DOMParser parse-error compatibility ----------------------------------- // Browsers return a document rooted at a `<parsererror>` element on malformed // XML; `@xmldom/xmldom` throws instead. Wrap it so downstream code keeps seeing // the browser shape. let DOMParser$1 = class DOMParser { parseFromString(source, mimeType) { var _a; try { const parser = new xmldom.DOMParser({ onError: (level, message) => { if (level === 'fatalError') { throw new Error(typeof message === 'string' ? message : String(message)); } }, }); return parser.parseFromString(source, mimeType); } catch (e) { const doc = domImplementation.createDocument(PARSE_ERROR_NS, 'parsererror', null); (_a = doc.documentElement) === null || _a === void 0 ? void 0 : _a.appendChild(doc.createTextNode(e.message)); return doc; } } }; globalThis.DOMParser = DOMParser$1; /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */ function __awaiter(thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); } function __classPrivateFieldGet(receiver, state, kind, f) { if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); } function __classPrivateFieldSet(receiver, state, value, kind, f) { if (kind === "m") throw new TypeError("Private method is not writable"); if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value; } typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; let logLevel = LOG_LEVELS.DEBUG; const log = { /** * Library consumers can use this function to set the log level of Strophe. * The default log level is Strophe.LogLevel.INFO. * @param level * @example Strophe.setLogLevel(Strophe.LogLevel.DEBUG); */ setLogLevel(level) { if (level < LOG_LEVELS.DEBUG || level > LOG_LEVELS.FATAL) { throw new Error("Invalid log level supplied to setLogLevel"); } logLevel = level; }, /** * * Please note that data sent and received over the wire is logged * via {@link Strophe.Connection#rawInput|Strophe.Connection.rawInput()} * and {@link Strophe.Connection#rawOutput|Strophe.Connection.rawOutput()}. * * The different levels and their meanings are * * DEBUG - Messages useful for debugging purposes. * INFO - Informational messages. This is mostly information like * 'disconnect was called' or 'SASL auth succeeded'. * WARN - Warnings about potential problems. This is mostly used * to report transient connection errors like request timeouts. * ERROR - Some error occurred. * FATAL - A non-recoverable fatal error occurred. * * @param level - The log level of the log message. * This will be one of the values in Strophe.LOG_LEVELS. * @param msg - The log message. */ log(level, msg) { if (level < logLevel) { return; } if (level >= LOG_LEVELS.ERROR) { console === null || console === void 0 ? void 0 : console.error(msg); } else if (level === LOG_LEVELS.INFO) { console === null || console === void 0 ? void 0 : console.info(msg); } else if (level === LOG_LEVELS.WARN) { console === null || console === void 0 ? void 0 : console.warn(msg); } else if (level === LOG_LEVELS.DEBUG) { console === null || console === void 0 ? void 0 : console.debug(msg); } }, /** * Log a message at the Strophe.LOG_LEVELS.DEBUG level. * @param msg - The log message. */ debug(msg) { this.log(LOG_LEVELS.DEBUG, msg); }, /** * Log a message at the Strophe.LOG_LEVELS.INFO level. * @param msg - The log message. */ info(msg) { this.log(LOG_LEVELS.INFO, msg); }, /** * Log a message at the Strophe.LOG_LEVELS.WARN level. * @param msg - The log message. */ warn(msg) { this.log(LOG_LEVELS.WARN, msg); }, /** * Log a message at the Strophe.LOG_LEVELS.ERROR level. * @param msg - The log message. */ error(msg) { this.log(LOG_LEVELS.ERROR, msg); }, /** * Log a message at the Strophe.LOG_LEVELS.FATAL level. * @param msg - The log message. */ fatal(msg) { this.log(LOG_LEVELS.FATAL, msg); }, }; /** * Takes a string and turns it into an XML Element. * @param string * @param throwErrorIfInvalidNS * @returns */ function toElement(string, throwErrorIfInvalidNS) { const doc = xmlHtmlNode(string); const parserError = getParserError(doc); if (parserError) { throw new Error(`Parser Error: ${parserError}`); } const node = getFirstElementChild(doc); if (['message', 'iq', 'presence'].includes(node.nodeName.toLowerCase()) && node.namespaceURI !== 'jabber:client' && node.namespaceURI !== 'jabber:server') { const err_msg = `Invalid namespaceURI ${node.namespaceURI}`; if (throwErrorIfInvalidNS) { throw new Error(err_msg); } else { log.error(err_msg); } } return node; } /** * Properly logs an error to the console * @param e */ function handleError(e) { if (typeof e.stack !== 'undefined') { log.fatal(e.stack); } log.fatal('error: ' + e.message); } /** * @param str * @returns */ function utf16to8(str) { let out = ''; const len = str.length; for (let i = 0; i < len; i++) { const c = str.charCodeAt(i); if (c >= 0x0000 && c <= 0x007f) { out += str.charAt(i); } else if (c > 0x07ff) { out += String.fromCharCode(0xe0 | ((c >> 12) & 0x0f)); out += String.fromCharCode(0x80 | ((c >> 6) & 0x3f)); out += String.fromCharCode(0x80 | ((c >> 0) & 0x3f)); } else { out += String.fromCharCode(0xc0 | ((c >> 6) & 0x1f)); out += String.fromCharCode(0x80 | ((c >> 0) & 0x3f)); } } return out; } /** * @param x * @param y * @returns */ function xorArrayBuffers(x, y) { const xIntArray = new Uint8Array(x); const yIntArray = new Uint8Array(y); const zIntArray = new Uint8Array(x.byteLength); for (let i = 0; i < x.byteLength; i++) { zIntArray[i] = xIntArray[i] ^ yIntArray[i]; } return zIntArray.buffer; } /** * @param buffer * @returns */ function arrayBufToBase64(buffer) { let binary = ''; const bytes = new Uint8Array(buffer); const len = bytes.byteLength; for (let i = 0; i < len; i++) { binary += String.fromCharCode(bytes[i]); } return btoa(binary); } /** * @param str * @returns */ function base64ToArrayBuf(str) { var _a; return (_a = Uint8Array.from(atob(str), (c) => c.charCodeAt(0))) === null || _a === void 0 ? void 0 : _a.buffer; } /** * @param str * @returns */ function stringToArrayBuf(str) { const bytes = new TextEncoder().encode(str); return bytes.buffer; } /** * @param cookies */ function addCookies(cookies) { if (typeof document === 'undefined') { log.error(`addCookies: not adding any cookies, since there's no document object`); return; } const cookieMap = cookies || {}; for (const cookieName in cookieMap) { if (Object.prototype.hasOwnProperty.call(cookieMap, cookieName)) { let expires = ''; let domain = ''; let path = ''; const cookieObj = cookieMap[cookieName]; const isObj = typeof cookieObj === 'object'; const cookieValue = escape(unescape(isObj ? cookieObj.value : cookieObj)); if (isObj) { const cv = cookieObj; expires = cv.expires ? ';expires=' + cv.expires : ''; domain = cv.domain ? ';domain=' + cv.domain : ''; path = cv.path ? ';path=' + cv.path : ''; } document.cookie = cookieName + '=' + cookieValue + expires + domain + path; } } } let _xmlGenerator = null; /** * Get the DOM document to generate elements. * @returns The currently used DOM document. */ function xmlGenerator() { if (!_xmlGenerator) { _xmlGenerator = document.implementation.createDocument('jabber:client', 'strophe', null); } return _xmlGenerator; } /** * Creates an XML DOM text node. * Provides a cross implementation version of document.createTextNode. * @param text - The content of the text node. * @returns A new XML DOM text node. */ function xmlTextNode(text) { return xmlGenerator().createTextNode(text); } /** * @param stanza * @returns */ function stripWhitespace(stanza) { const childNodes = Array.from(stanza.childNodes); if (childNodes.length === 1 && childNodes[0].nodeType === ElementType.TEXT) { return stanza; } childNodes.forEach((node) => { if (node.nodeName.toLowerCase() === 'body') { return; } if (node.nodeType === ElementType.TEXT && !/\S/.test(node.nodeValue)) { stanza.removeChild(node); } else if (node.nodeType === ElementType.NORMAL) { stripWhitespace(node); } }); return stanza; } /** * Creates an XML DOM node. * @param text - The contents of the XML element. * @returns */ function xmlHtmlNode(text) { const parser = new DOMParser(); return parser.parseFromString(text, 'text/xml'); } /** * @param doc * @returns */ function getParserError(doc) { var _a; const el = ((_a = doc.firstElementChild) === null || _a === void 0 ? void 0 : _a.nodeName) === 'parsererror' ? doc.firstElementChild : doc.getElementsByTagNameNS(PARSE_ERROR_NS, 'parsererror')[0]; return (el === null || el === void 0 ? void 0 : el.nodeName) === 'parsererror' ? el === null || el === void 0 ? void 0 : el.textContent : null; } /** * @param el * @returns */ function getFirstElementChild(el) { if (el.firstElementChild) return el.firstElementChild; let node; let i = 0; const nodes = el.childNodes; while ((node = nodes[i++])) { if (node.nodeType === 1) return node; } return null; } /** * Create an XML DOM element. * * This function creates an XML DOM element correctly across all * implementations. Note that these are not HTML DOM elements, which * aren't appropriate for XMPP stanzas. * * @param name - The name for the element. * @param attrs * An optional array or object containing * key/value pairs to use as element attributes. * The object should be in the format `{'key': 'value'}`. * The array should have the format `[['key1', 'value1'], ['key2', 'value2']]`. * @param text - The text child data for the element. * * @returns A new XML DOM element. */ function xmlElement(name, attrs, text) { if (!name) return null; const node = xmlGenerator().createElement(name); if (text && (typeof text === 'string' || typeof text === 'number')) { node.appendChild(xmlTextNode(text.toString())); } else if (typeof attrs === 'string' || typeof attrs === 'number') { node.appendChild(xmlTextNode(attrs.toString())); return node; } if (!attrs) { return node; } else if (Array.isArray(attrs)) { for (const attr of attrs) { if (Array.isArray(attr)) { if (attr[0] != null && attr[1] != null) { node.setAttribute(attr[0], attr[1]); } } } } else if (typeof attrs === 'object') { for (const k of Object.keys(attrs)) { if (k && attrs[k] != null) { node.setAttribute(k, attrs[k].toString()); } } } return node; } /** * Utility method to determine whether a tag is allowed * in the XHTML_IM namespace. * * XHTML tag names are case sensitive and must be lower case. * @method Strophe.XHTML.validTag * @param tag */ function validTag(tag) { for (let i = 0; i < XHTML.tags.length; i++) { if (tag === XHTML.tags[i]) { return true; } } return false; } /** * Utility method to determine whether an attribute is allowed * as recommended per XEP-0071 * * XHTML attribute names are case sensitive and must be lower case. * @method Strophe.XHTML.validAttribute * @param tag * @param attribute */ function validAttribute(tag, attribute) { const attrs = XHTML.attributes[tag]; if ((attrs === null || attrs === void 0 ? void 0 : attrs.length) > 0) { for (let i = 0; i < attrs.length; i++) { if (attribute === attrs[i]) { return true; } } } return false; } /** * @method Strophe.XHTML.validCSS * @param style */ function validCSS(style) { for (let i = 0; i < XHTML.css.length; i++) { if (style === XHTML.css[i]) { return true; } } return false; } /** * Copy an HTML DOM Element into an XML DOM. * This function copies a DOM element and all its descendants and returns * the new copy. * @param elem - A DOM element. * @returns A new, copied DOM element tree. */ function createFromHtmlElement(elem) { var _a; let el; const tag = elem.nodeName.toLowerCase(); if (validTag(tag)) { try { el = xmlElement(tag); if (tag in XHTML.attributes) { const attrs = XHTML.attributes[tag]; for (let i = 0; i < attrs.length; i++) { const attribute = attrs[i]; let value = elem.getAttribute(attribute); if (typeof value === 'undefined' || value === null || value === '') { continue; } if (attribute === 'style' && typeof value === 'object') { value = (_a = value.cssText) !== null && _a !== void 0 ? _a : value; } if (attribute === 'style') { const css = []; const cssAttrs = value.split(';'); for (let j = 0; j < cssAttrs.length; j++) { const attr = cssAttrs[j].split(':'); const cssName = attr[0].replace(/^\s*/, '').replace(/\s*$/, '').toLowerCase(); if (validCSS(cssName)) { const cssValue = attr[1].replace(/^\s*/, '').replace(/\s*$/, ''); css.push(cssName + ': ' + cssValue); } } if (css.length > 0) { value = css.join('; '); el.setAttribute(attribute, value); } } else { el.setAttribute(attribute, value); } } for (let i = 0; i < elem.childNodes.length; i++) { el.appendChild(createHtml(elem.childNodes[i])); } } } catch (_e) { el = xmlTextNode(''); } } else { el = xmlGenerator().createDocumentFragment(); for (let i = 0; i < elem.childNodes.length; i++) { el.appendChild(createHtml(elem.childNodes[i])); } } return el; } /** * Copy an HTML DOM Node into an XML DOM. * This function copies a DOM element and all its descendants and returns * the new copy. * @method Strophe.createHtml * @param node - A DOM element. * @returns A new, copied DOM element tree. */ function createHtml(node) { if (node.nodeType === ElementType.NORMAL) { return createFromHtmlElement(node); } else if (node.nodeType === ElementType.FRAGMENT) { const el = xmlGenerator().createDocumentFragment(); for (let i = 0; i < node.childNodes.length; i++) { el.appendChild(createHtml(node.childNodes[i])); } return el; } else if (node.nodeType === ElementType.TEXT) { return xmlTextNode(node.nodeValue); } } /** * Copy an XML DOM element. * * This function copies a DOM element and all its descendants and returns * the new copy. * @method Strophe.copyElement * @param node - A DOM element. * @returns A new, copied DOM element tree. */ function copyElement(node) { let out; if (node.nodeType === ElementType.NORMAL) { const el = node; out = xmlElement(el.tagName); for (let i = 0; i < el.attributes.length; i++) { out.setAttribute(el.attributes[i].nodeName, el.attributes[i].value); } for (let i = 0; i < el.childNodes.length; i++) { out.appendChild(copyElement(el.childNodes[i])); } } else if (node.nodeType === ElementType.TEXT) { out = xmlGenerator().createTextNode(node.nodeValue); } return out; } /** * Excapes invalid xml characters. * @method Strophe.xmlescape * @param text - text to escape. * @returns Escaped text. */ function xmlescape(text) { return text .replace(/\&/g, '&amp;') .replace(/</g, '&lt;') .replace(/>/g, '&gt;') .replace(/'/g, '&apos;') .replace(/"/g, '&quot;'); } /** * Unexcapes invalid xml characters. * @method Strophe.xmlunescape * @param text - text to unescape. * @returns Unescaped text. */ function xmlunescape(text) { text = text.replace(/\&amp;/g, '&'); text = text.replace(/&lt;/g, '<'); text = text.replace(/&gt;/g, '>'); text = text.replace(/&apos;/g, "'"); text = text.replace(/&quot;/g, '"'); return text; } /** * Map a function over some or all child elements of a given element. * * This is a small convenience function for mapping a function over * some or all of the children of an element. If elemName is null, all * children will be passed to the function, otherwise only children * whose tag names match elemName will be passed. * * @method Strophe.forEachChild * @param elem - The element to operate on. * @param elemName - The child element tag name filter. * @param func - The function to apply to each child. This * function should take a single argument, a DOM element. */ function forEachChild(elem, elemName, func) { for (let i = 0; i < elem.childNodes.length; i++) { const childNode = elem.childNodes[i]; if (childNode.nodeType === ElementType.NORMAL && (!elemName || isTagEqual(childNode, elemName))) { func(childNode); } } } /** * Compare an element's tag name with a string. * This function is case sensitive. * @method Strophe.isTagEqual * @param el - A DOM element. * @param name - The element name. * @returns * true if the element's tag name matches _el_, and false * otherwise. */ function isTagEqual(el, name) { return el.tagName === name; } /** * Return the XML namespace of an element. * * Prefers the serialized `xmlns` attribute and falls back to the DOM * `namespaceURI`, because the two diverge depending on how the element was * built and neither is reliable on its own: * * - Locally-built stanzas (`$iq`, `stx`, {@link Builder}) are created with * `createElement` and carry their namespace only in the `xmlns` attribute; * their `namespaceURI` is null. * - Stanzas received over the XEP-0114 component transport are built with * `createElementNS` and carry their namespace only on `namespaceURI`; the * redundant `xmlns` attribute is omitted. * - WebSocket / BOSH stanzas parsed by `DOMParser` carry both, except on * child elements that inherit the default namespace without redeclaring it * (those have only `namespaceURI`). * * Checking both is the transport-agnostic way to read an element's namespace. * * @method Strophe.getNamespace * @param elem - The element whose namespace is wanted. * @returns The namespace URI, or null if the element has none. */ function getNamespace(elem) { return elem.getAttribute('xmlns') || elem.namespaceURI; } /** * Get the concatenation of all text children of an element. * @method Strophe.getText * @param elem - A DOM element. * @returns A String with the concatenated text of all text element children. */ function getText(elem) { if (!elem) return null; let str = ''; if (!elem.childNodes.length && elem.nodeType === ElementType.TEXT) { str += elem.nodeValue; } for (const child of elem.childNodes) { if (child.nodeType === ElementType.TEXT) { str += child.nodeValue; } } return xmlescape(str); } /** * Escape the node part (also called local part) of a JID. * @method Strophe.escapeNode * @param node - A node (or local part). * @returns An escaped node (or local part). */ function escapeNode(node) { if (typeof node !== 'string') { return node; } return node .replace(/^\s+|\s+$/g, '') .replace(/\\/g, '\\5c') .replace(/ /g, '\\20') .replace(/\"/g, '\\22') .replace(/\&/g, '\\26') .replace(/\'/g, '\\27') .replace(/\//g, '\\2f') .replace(/:/g, '\\3a') .replace(/</g, '\\3c') .replace(/>/g, '\\3e') .replace(/@/g, '\\40'); } /** * Unescape a node part (also called local part) of a JID. * @method Strophe.unescapeNode * @param node - A node (or local part). * @returns An unescaped node (or local part). */ function unescapeNode(node) { if (typeof node !== 'string') { return node; } return node .replace(/\\20/g, ' ') .replace(/\\22/g, '"') .replace(/\\26/g, '&') .replace(/\\27/g, "'") .replace(/\\2f/g, '/') .replace(/\\3a/g, ':') .replace(/\\3c/g, '<') .replace(/\\3e/g, '>') .replace(/\\40/g, '@') .replace(/\\5c/g, '\\'); } /** * Get the node portion of a JID String. * @method Strophe.getNodeFromJid * @param jid - A JID. * @returns A String containing the node. */ function getNodeFromJid(jid) { if (jid.indexOf('@') < 0) { return null; } return jid.split('@')[0]; } /** * Get the domain portion of a JID String. * @method Strophe.getDomainFromJid * @param jid - A JID. * @returns A String containing the domain. */ function getDomainFromJid(jid) { const bare = getBareJidFromJid(jid); if (bare.indexOf('@') < 0) { return bare; } else { const parts = bare.split('@'); parts.splice(0, 1); return parts.join('@'); } } /** * Get the resource portion of a JID String. * @method Strophe.getResourceFromJid * @param jid - A JID. * @returns A String containing the resource. */ function getResourceFromJid(jid) { if (!jid) { return null; } const s = jid.split('/'); if (s.length < 2) { return null; } s.splice(0, 1); return s.join('/'); } /** * Get the bare JID from a JID String. * @method Strophe.getBareJidFromJid * @param jid - A JID. * @returns A String containing the bare JID. */ function getBareJidFromJid(jid) { return jid ? jid.split('/')[0] : null; } const utils = { utf16to8, xorArrayBuffers, arrayBufToBase64, base64ToArrayBuf, stringToArrayBuf, addCookies, }; var utils$1 = /*#__PURE__*/Object.freeze({ __proto__: null, addCookies: addCookies, arrayBufToBase64: arrayBufToBase64, base64ToArrayBuf: base64ToArrayBuf, copyElement: copyElement, createHtml: createHtml, default: utils, escapeNode: escapeNode, forEachChild: forEachChild, getBareJidFromJid: getBareJidFromJid, getDomainFromJid: getDomainFromJid, getFirstElementChild: getFirstElementChild, getNamespace: getNamespace, getNodeFromJid: getNodeFromJid, getParserError: getParserError, getResourceFromJid: getResourceFromJid, getText: getText, handleError: handleError, isTagEqual: isTagEqual, stringToArrayBuf: stringToArrayBuf, stripWhitespace: stripWhitespace, toElement: toElement, unescapeNode: unescapeNode, utf16to8: utf16to8, validAttribute: validAttribute, validCSS: validCSS, validTag: validTag, xmlElement: xmlElement, xmlGenerator: xmlGenerator, xmlHtmlNode: xmlHtmlNode, xmlTextNode: xmlTextNode, xmlescape: xmlescape, xmlunescape: xmlunescape, xorArrayBuffers: xorArrayBuffers }); /** * _Private_ helper class for managing stanza handlers. * * A Handler encapsulates a user provided callback function to be * executed when matching stanzas are received by the connection. * Handlers can be either one-off or persistant depending on their * return value. Returning true will cause a Handler to remain active, and * returning false will remove the Handler. * * Users will not use Handler objects directly, but instead they * will use {@link Connection.addHandler} and * {@link Connection.deleteHandler}. */ class Handler { /** * Create and initialize a new Handler. * * @param handler - A function to be executed when the handler is run. * @param ns - The namespace to match. * @param name - The element name to match. * @param type - The stanza type (or types if an array) to match. * @param id - The element id attribute to match. * @param from - The element from attribute to match. * @param options - Handler options */ constructor(handler, ns, name, type, id, from, options) { this.handler = handler; this.ns = ns; this.name = name; this.type = type; this.id = id; this.options = options || { matchBareFromJid: false, ignoreNamespaceFragment: false }; if (this.options.matchBareFromJid) { this.from = from ? getBareJidFromJid(from) : null; } else { this.from = from; } this.user = true; } /** * Returns the XML namespace of an element. * Resolved via {@link Strophe.getNamespace}, which reads the `xmlns` * attribute and falls back to `namespaceURI` so matching works regardless * of how the stanza's DOM was built (locally, via DOMParser, or via the * component transport's `createElementNS`). * If `ignoreNamespaceFragment` was passed in for this handler, then the * URL fragment will be stripped. * @param elem - The XML element with the namespace. * @returns The namespace, with optionally the fragment stripped. */ getNamespace(elem) { let elNamespace = getNamespace(elem); if (elNamespace && this.options.ignoreNamespaceFragment) { elNamespace = elNamespace.split('#')[0]; } return elNamespace; } /** * Tests if a stanza element (or any of its children) matches the * namespace set for this Handler. * @param elem - The XML element to test. * @returns true if the stanza matches and false otherwise. */ namespaceMatch(elem) { var _a; if (!this.ns || this.getNamespace(elem) === this.ns) { return true; } for (const child of (_a = elem.children) !== null && _a !== void 0 ? _a : []) { if (this.getNamespace(child) === this.ns) { return true; } else if (this.namespaceMatch(child)) { return true; } } return false; } /** * Tests if a stanza matches the Handler. * @param elem - The XML element to test. * @returns true if the stanza matches and false otherwise. */ isMatch(elem) { let from = elem.getAttribute('from'); if (this.options.matchBareFromJid) { from = getBareJidFromJid(from); } const elem_type = elem.getAttribute('type'); if (this.namespaceMatch(elem) && (!this.name || isTagEqual(elem, this.name)) && (!this.type || (Array.isArray(this.type) ? this.type.indexOf(elem_type !== null && elem_type !== void 0 ? elem_type : '') !== -1 : elem_type === this.type)) && (!this.id || elem.getAttribute('id') === this.id) && (!this.from || from === this.from)) { return true; } return false; } /** * Run the callback on a matching stanza. * @param elem - The DOM element that triggered the Handler. * @returns A boolean indicating if the handler should remain active. */ run(elem) { let result = null; try { result = this.handler(elem); } catch (e) { handleError(e); throw e; } return result; } /** * Get a String representation of the Handler object. */ toString() { return '{Handler: ' + this.handler + '(' + this.name + ',' + this.id + ',' + this.ns + ')}'; } } /** * _Private_ helper class for managing timed handlers. * * A Strophe.TimedHandler encapsulates a user provided callback that * should be called after a certain period of time or at regular * intervals. The return value of the callback determines whether the * Strophe.TimedHandler will continue to fire. * * Users will not use Strophe.TimedHandler objects directly, but instead * they will use {@link Strophe.Connection#addTimedHandler|addTimedHandler()} and * {@link Strophe.Connection#deleteTimedHandler|deleteTimedHandler()}. * * @memberof Strophe */ class TimedHandler { /** * Create and initialize a new Strophe.TimedHandler object. * @param period - The number of milliseconds to wait before the * handler is called. * @param handler - The callback to run when the handler fires. This * function should take no arguments. */ constructor(period, handler) { this.period = period; this.handler = handler; this.lastCalled = new Date().getTime(); this.user = true; } /** * Run the callback for the Strophe.TimedHandler. * * @returns Returns the result of running the handler, * which is `true` if the Strophe.TimedHandler should be called again, * and `false` otherwise. */ run() { this.lastCalled = new Date().getTime(); return this.handler(); } /** * Reset the last called time for the Strophe.TimedHandler. */ reset() { this.lastCalled = new Date().getTime(); } /** * Get a string representation of the Strophe.TimedHandler object. */ toString() { return '{TimedHandler: ' + this.handler + '(' + this.period + ')}'; } } var _Builder_nodeTree, _Builder_node, _Builder_name, _Builder_attrs; /** * Create a {@link Strophe.Builder} * This is an alias for `new Strophe.Builder(name, attrs)`. * @param name - The root element name. * @param attrs - The attributes for the root element in object notation. * @returns A new Strophe.Builder object. */ function $build(name, attrs) { return new Builder(name, attrs); } /** * Create a {@link Strophe.Builder} with a `<message/>` element as the root. * @param attrs - The <message/> element attributes in object notation. * @returns A new Strophe.Builder object. */ function $msg(attrs) { return new Builder('message', attrs); } /** * Create a {@link Strophe.Builder} with an `<iq/>` element as the root. * @param attrs - The <iq/> element attributes in object notation. * @returns A new Strophe.Builder object. */ function $iq(attrs) { return new Builder('iq', attrs); } /** * Create a {@link Strophe.Builder} with a `<presence/>` element as the root. * @param attrs - The <presence/> element attributes in object notation. * @returns A new Strophe.Builder object. */ function $pres(attrs) { return new Builder('presence', attrs); } /** * This class provides an interface similar to JQuery but for building * DOM elements easily and rapidly. All the functions except for `toString()` * and tree() return the object, so calls can be chained. * * The corresponding DOM manipulations to get a similar fragment would be * a lot more tedious and probably involve several helper variables. * * Since adding children makes new operations operate on the child, up() * is provided to traverse up the tree. To add two children, do * > builder.c('child1', ...).up().c('child2', ...) * * The next operation on the Builder will be relative to the second child. * * @example * // Here's an example using the $iq() builder helper. * $iq({to: 'you', from: 'me', type: 'get', id: '1'}) * .c('query', {xmlns: 'strophe:example'}) * .c('example') * .toString() * * // The above generates this XML fragment * // <iq to='you' from='me' type='get' id='1'> * // <query xmlns='strophe:example'> * // <example/> * // </query> * // </iq> */ class Builder { /** * The attributes should be passed in object notation. * @param name - The name of the root element. * @param attrs - The attributes for the root element in object notation. * @example const b = new Builder('message', {to: 'you', from: 'me'}); * @example const b = new Builder('messsage', {'xml:lang': 'en'}); */ constructor(name, attrs) { _Builder_nodeTree.set(this, void 0); _Builder_node.set(this, void 0); _Builder_name.set(this, void 0); _Builder_attrs.set(this, void 0); // Set correct namespace for jabber:client elements if (name === 'presence' || name === 'message' || name === 'iq') { if (attrs && !attrs.xmlns) { attrs.xmlns = NS.CLIENT; } else if (!attrs) { attrs = { xmlns: NS.CLIENT }; } } __classPrivateFieldSet(this, _Builder_name, name, "f"); __classPrivateFieldSet(this, _Builder_attrs, attrs, "f"); } /** * Creates a new Builder object from an XML string. * @param str * @returns * @example const stanza = Builder.fromString('<presence from="juliet@example.com/chamber"></presence>'); */ static fromString(str) { const el = toElement(str, true); const b = new Builder(''); __classPrivateFieldSet(b, _Builder_nodeTree, el, "f"); return b; } buildTree() { return xmlElement(__classPrivateFieldGet(this, _Builder_name, "f"), __classPrivateFieldGet(this, _Builder_attrs, "f")); } get nodeTree() { if (!__classPrivateFieldGet(this, _Builder_nodeTree, "f")) { // Holds the tree being built. __classPrivateFieldSet(this, _Builder_nodeTree, this.buildTree(), "f"); } return __classPrivateFieldGet(this, _Builder_nodeTree, "f"); } get node() { if (!__classPrivateFieldGet(this, _Builder_node, "f")) { __classPrivateFieldSet(this, _Builder_node, this.tree(), "f"); } return __classPrivateFieldGet(this, _Builder_node, "f"); } set node(el) { __classPrivateFieldSet(this, _Builder_node, el, "f"); } /** * Render a DOM element and all descendants to a String. * @param elem - A DOM element. * @returns The serialized element tree as a String. */ static serialize(elem) { if (!elem) return null; const el = elem instanceof Builder ? elem.tree() : elem; const names = [...Array(el.attributes.length).keys()].map((i) => el.attributes[i].nodeName); names.sort(); let result = names.reduce((a, n) => `${a} ${n}="${xmlescape(el.attributes.getNamedItem(n).value)}"`, `<${el.nodeName}`); if (el.childNodes.length > 0) { result += '>'; for (let i = 0; i < el.childNodes.length; i++) { const child = el.childNodes[i]; switch (child.nodeType) { case ElementType.NORMAL: result += Builder.serialize(child); break; case ElementType.TEXT: result += xmlescape(child.nodeValue); break; case ElementType.CDATA: result += '<![CDATA[' + child.nodeValue + ']]>'; } } result += '</' + el.nodeName + '>'; } else { result += '/>'; } return result; } /** * Return the DOM tree. * * This function returns the current DOM tree as an element object. This * is suitable for passing to functions like Strophe.Connection.send(). * * @returns The DOM tree as a element object. */ tree() { return this.nodeTree; } /** * Serialize the DOM tree to a String. * * This function returns a string serialization of the current DOM * tree. It is often used internally to pass data to a * Strophe.Request object. * * @returns The serialized DOM tree in a String. */ toString() { return Builder.serialize(this.tree()); } /** * Make the current parent element the new current element. * This function is often used after c() to traverse back up the tree. * * @example * // For example, to add two children to the same element * builder.c('child1', {}).up().c('child2', {}); * * @returns The Strophe.Builder object. */ up() { this.node = this.node.parentElement ? this.node.parentElement : this.node.parentNode; return this; } /** * Make the root element the new current element. * * When at a deeply nested element in the tree, this function can be used * to jump back to the root of the tree, instead of having to repeatedly * call up(). * * @returns The Strophe.Builder object. */ root() { this.node = this.tree(); return this; } /** * Add or modify attributes of the current element. * * The attributes should be passed in object notation. * This function does not move the current element pointer. * @param moreattrs - The attributes to add/modify in object notation. * If an attribute is set to `null` or `undefined`, it will be removed. * @returns The Strophe.Builder object. */ attrs(moreattrs) { for (const k in moreattrs) { if (Object.prototype.hasOwnProperty.call(moreattrs, k)) { if (moreattrs[k] != null) { this.node.setAttribute(k, moreattrs[k].toString()); } else { this.node.removeAttribute(k); } } } return this; } /** * Add a child to the current element and make it the new current * element. * * This function moves the current element pointer to the child, * unless text is provided. If you need to add another child, it * is necessary to use up() to go back to the parent in the tree. * * @param name - The name of the child. * @param attrs - The attributes of the child in object notation. * @param text - The text to add to the child. * * @returns The Strophe.Builder object. */ c(name, attrs, text) { const child = xmlElement(name, attrs, text); this.node.appendChild(child); if (typeof text !== 'string' && typeof text !== 'number') { this.node = child;