UNPKG

strophe.js

Version:

Strophe.js is an XMPP library for JavaScript

1,574 lines (1,486 loc) 185 kB
'use strict'; const { JSDOM } = require('jsdom'); const WebSocket = require('ws'); const { window } = new JSDOM(); globalThis.WebSocket = WebSocket; globalThis.XMLSerializer = window.XMLSerializer; globalThis.DOMParser = window.DOMParser; globalThis.document = window.document; Object.defineProperty(exports, '__esModule', { value: true }); /** * Common namespace constants from the XMPP RFCs and XEPs. * * @typedef { Object } NS * @property {string} NS.HTTPBIND - HTTP BIND namespace from XEP 124. * @property {string} NS.BOSH - BOSH namespace from XEP 206. * @property {string} NS.CLIENT - Main XMPP client namespace. * @property {string} NS.AUTH - Legacy authentication namespace. * @property {string} NS.ROSTER - Roster operations namespace. * @property {string} NS.PROFILE - Profile namespace. * @property {string} NS.DISCO_INFO - Service discovery info namespace from XEP 30. * @property {string} NS.DISCO_ITEMS - Service discovery items namespace from XEP 30. * @property {string} NS.MUC - Multi-User Chat namespace from XEP 45. * @property {string} NS.SASL - XMPP SASL namespace from RFC 3920. * @property {string} NS.STREAM - XMPP Streams namespace from RFC 3920. * @property {string} NS.BIND - XMPP Binding namespace from RFC 3920 and RFC 6120. * @property {string} NS.SESSION - XMPP Session namespace from RFC 3920. * @property {string} NS.XHTML_IM - XHTML-IM namespace from XEP 71. * @property {string} NS.XHTML - XHTML body namespace from XEP 71. * @property {string} NS.STANZAS * @property {string} NS.FRAMING */ const NS = { HTTPBIND: 'http://jabber.org/protocol/httpbind', BOSH: 'urn:xmpp:xbosh', CLIENT: 'jabber:client', SERVER: 'jabber:server', AUTH: 'jabber:iq:auth', ROSTER: 'jabber:iq:roster', PROFILE: 'jabber:iq:profile', DISCO_INFO: 'http://jabber.org/protocol/disco#info', DISCO_ITEMS: 'http://jabber.org/protocol/disco#items', MUC: 'http://jabber.org/protocol/muc', SASL: 'urn:ietf:params:xml:ns:xmpp-sasl', STREAM: 'http://etherx.jabber.org/streams', FRAMING: 'urn:ietf:params:xml:ns:xmpp-framing', BIND: 'urn:ietf:params:xml:ns:xmpp-bind', SESSION: 'urn:ietf:params:xml:ns:xmpp-session', VERSION: 'jabber:iq:version', STANZAS: 'urn:ietf:params:xml:ns:xmpp-stanzas', XHTML_IM: 'http://jabber.org/protocol/xhtml-im', XHTML: 'http://www.w3.org/1999/xhtml' }; const PARSE_ERROR_NS = 'http://www.w3.org/1999/xhtml'; /** * 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'], /** @type {never[]} */ 'br': [], 'cite': ['style'], /** @type {never[]} */ 'em': [], 'img': ['src', 'alt', 'style', 'height', 'width'], 'li': ['style'], 'ol': ['style'], 'p': ['style'], 'span': ['style'], /** @type {never[]} */ 'strong': [], 'ul': ['style'], /** @type {never[]} */ 'body': [] }, css: ['background-color', 'color', 'font-family', 'font-size', 'font-style', 'font-weight', 'margin-left', 'margin-right', 'text-align', 'text-decoration'] }; /** @typedef {number} connstatus */ /** * Connection status constants for use by the connection handler * callback. * * @typedef {Object} Status * @property {connstatus} Status.ERROR - An error has occurred * @property {connstatus} Status.CONNECTING - The connection is currently being made * @property {connstatus} Status.CONNFAIL - The connection attempt failed * @property {connstatus} Status.AUTHENTICATING - The connection is authenticating * @property {connstatus} Status.AUTHFAIL - The authentication attempt failed * @property {connstatus} Status.CONNECTED - The connection has succeeded * @property {connstatus} Status.DISCONNECTED - The connection has been terminated * @property {connstatus} Status.DISCONNECTING - The connection is currently being terminated * @property {connstatus} Status.ATTACHED - The connection has been attached * @property {connstatus} Status.REDIRECT - The connection has been redirected * @property {connstatus} Status.CONNTIMEOUT - The connection has timed out * @property {connstatus} Status.BINDREQUIRED - The JID resource needs to be bound for this session * @property {connstatus} Status.ATTACHFAIL - Failed to attach to a pre-existing session * @property {connstatus} Status.RECONNECTING - Not used by Strophe, but added for integrators */ 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' }; /** * Logging level indicators. * @typedef {0|1|2|3|4} LogLevel * @typedef {'DEBUG'|'INFO'|'WARN'|'ERROR'|'FATAL'} LogLevelName * @typedef {Record<LogLevelName, LogLevel>} LogLevels */ 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 }; /** * @typedef {import('./constants').LogLevel} LogLevel */ 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 {LogLevel} 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 {number} level - The log level of the log message. * This will be one of the values in Strophe.LOG_LEVELS. * @param {string} msg - The log message. */ log(level, msg) { if (level < logLevel) { return; } if (level >= LOG_LEVELS.ERROR) { var _console; (_console = console) === null || _console === void 0 ? void 0 : _console.error(msg); } else if (level === LOG_LEVELS.INFO) { var _console2; (_console2 = console) === null || _console2 === void 0 ? void 0 : _console2.info(msg); } else if (level === LOG_LEVELS.WARN) { var _console3; (_console3 = console) === null || _console3 === void 0 ? void 0 : _console3.warn(msg); } else if (level === LOG_LEVELS.DEBUG) { var _console4; (_console4 = console) === null || _console4 === void 0 ? void 0 : _console4.debug(msg); } }, /** * Log a message at the Strophe.LOG_LEVELS.DEBUG level. * @param {string} msg - The log message. */ debug(msg) { this.log(LOG_LEVELS.DEBUG, msg); }, /** * Log a message at the Strophe.LOG_LEVELS.INFO level. * @param {string} msg - The log message. */ info(msg) { this.log(LOG_LEVELS.INFO, msg); }, /** * Log a message at the Strophe.LOG_LEVELS.WARN level. * @param {string} msg - The log message. */ warn(msg) { this.log(LOG_LEVELS.WARN, msg); }, /** * Log a message at the Strophe.LOG_LEVELS.ERROR level. * @param {string} msg - The log message. */ error(msg) { this.log(LOG_LEVELS.ERROR, msg); }, /** * Log a message at the Strophe.LOG_LEVELS.FATAL level. * @param {string} msg - The log message. */ fatal(msg) { this.log(LOG_LEVELS.FATAL, msg); } }; /* global btoa */ /** * Takes a string and turns it into an XML Element. * @param {string} string * @param {boolean} [throwErrorIfInvalidNS] * @returns {Element} */ 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 {Error} e */ function handleError(e) { if (typeof e.stack !== 'undefined') { log.fatal(e.stack); } log.fatal('error: ' + e.message); } /** * @param {string} str * @return {string} */ 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 {ArrayBufferLike} x * @param {ArrayBufferLike} y */ 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 {ArrayBufferLike} buffer * @return {string} */ function arrayBufToBase64(buffer) { // This function is due to mobz (https://stackoverflow.com/users/1234628/mobz) // and Emmanuel (https://stackoverflow.com/users/288564/emmanuel) 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 {string} str * @return {ArrayBufferLike} */ function base64ToArrayBuf(str) { var _Uint8Array$from; return (_Uint8Array$from = Uint8Array.from(atob(str), c => c.charCodeAt(0))) === null || _Uint8Array$from === void 0 ? void 0 : _Uint8Array$from.buffer; } /** * @param {string} str * @return {ArrayBufferLike} */ function stringToArrayBuf(str) { const bytes = new TextEncoder().encode(str); return bytes.buffer; } /** * @param {Cookies} cookies */ function addCookies(cookies) { if (typeof document === 'undefined') { log.error(`addCookies: not adding any cookies, since there's no document object`); } /** * @typedef {Object.<string, string>} Cookie * * A map of cookie names to string values or to maps of cookie values. * @typedef {Cookie|Object.<string, Cookie>} Cookies * * For example: * { "myCookie": "1234" } * * or: * { "myCookie": { * "value": "1234", * "domain": ".example.org", * "path": "/", * "expires": expirationDate * } * } * * These values get passed to {@link Strophe.Connection} via options.cookies */ cookies = cookies || {}; for (const cookieName in cookies) { if (Object.prototype.hasOwnProperty.call(cookies, cookieName)) { let expires = ''; let domain = ''; let path = ''; const cookieObj = cookies[cookieName]; const isObj = typeof cookieObj === 'object'; const cookieValue = escape(unescape(isObj ? cookieObj.value : cookieObj)); if (isObj) { expires = cookieObj.expires ? ';expires=' + cookieObj.expires : ''; domain = cookieObj.domain ? ';domain=' + cookieObj.domain : ''; path = cookieObj.path ? ';path=' + cookieObj.path : ''; } document.cookie = cookieName + '=' + cookieValue + expires + domain + path; } } } /** @type {Document} */ let _xmlGenerator = null; /** * Get the DOM document to generate elements. * @return {Document} - 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 {string} text - The content of the text node. * @return {Text} - A new XML DOM text node. */ function xmlTextNode(text) { return xmlGenerator().createTextNode(text); } /** * @param {Element} stanza * @return {Element} */ function stripWhitespace(stanza) { const childNodes = Array.from(stanza.childNodes); if (childNodes.length === 1 && childNodes[0].nodeType === ElementType.TEXT) { // If the element has only one child and it's a text node, we assume // it's significant and don't remove it, even if it's only whitespace. return stanza; } childNodes.forEach(node => { if (node.nodeName.toLowerCase() === 'body') { // We don't remove anything inside <body> elements return; } if (node.nodeType === ElementType.TEXT && !/\S/.test(node.nodeValue)) { stanza.removeChild(node); } else if (node.nodeType === ElementType.NORMAL) { stripWhitespace(/** @type {Element} */node); } }); return stanza; } /** * Creates an XML DOM node. * @param {string} text - The contents of the XML element. * @return {XMLDocument} */ function xmlHtmlNode(text) { const parser = new DOMParser(); return parser.parseFromString(text, 'text/xml'); } /** * @param {XMLDocument} doc * @returns {string|null} */ function getParserError(doc) { var _doc$firstElementChil; const el = ((_doc$firstElementChil = doc.firstElementChild) === null || _doc$firstElementChil === void 0 ? void 0 : _doc$firstElementChil.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 {XMLDocument} el * @returns {Element} */ function getFirstElementChild(el) { if (el.firstElementChild) return el.firstElementChild; let node, i = 0; const nodes = el.childNodes; while (node = nodes[i++]) { if (node.nodeType === 1) return /** @type {Element} */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 {string} name - The name for the element. * @param {Array<Array<string>>|Object.<string,string|number>|string|number} [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 {string|number} [text] - The text child data for the element. * * @return {Element} 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(/** @type {number|string} */attrs.toString())); return node; } if (!attrs) { return node; } else if (Array.isArray(attrs)) { for (const attr of attrs) { if (Array.isArray(attr)) { // eslint-disable-next-line no-eq-null 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)) { // eslint-disable-next-line no-eq-null 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 {string} tag */ function validTag(tag) { for (let i = 0; i < XHTML.tags.length; i++) { if (tag === XHTML.tags[i]) { return true; } } return false; } /** * @typedef {'a'|'blockquote'|'br'|'cite'|'em'|'img'|'li'|'ol'|'p'|'span'|'strong'|'ul'|'body'} XHTMLAttrs */ /** * 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 {string} tag * @param {string} attribute */ function validAttribute(tag, attribute) { const attrs = XHTML.attributes[(/** @type {XHTMLAttrs} */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 {string} 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 {HTMLElement} elem - A DOM element. * @return {Node} - A new, copied DOM element tree. */ function createFromHtmlElement(elem) { let el; const tag = elem.nodeName.toLowerCase(); // XHTML tags must be lower case. if (validTag(tag)) { try { el = xmlElement(tag); if (tag in XHTML.attributes) { const attrs = XHTML.attributes[(/** @type {XHTMLAttrs} */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') { var _value$cssText; value = /** @type {Object.<'csstext',string>} */(_value$cssText = value.cssText) !== null && _value$cssText !== void 0 ? _value$cssText : value; // we're dealing with IE, need to get CSS out } // filter out invalid css styles 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) { // invalid elements 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} node - A DOM element. * @return {Node} - A new, copied DOM element tree. */ function createHtml(node) { if (node.nodeType === ElementType.NORMAL) { return createFromHtmlElement(/** @type {HTMLElement} */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} node - A DOM element. * @return {Element|Text} - A new, copied DOM element tree. */ function copyElement(node) { let out; if (node.nodeType === ElementType.NORMAL) { const el = /** @type {Element} */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 {string} text - text to escape. * @return {string} - Escaped text. */ function xmlescape(text) { text = text.replace(/\&/g, '&amp;'); text = text.replace(/</g, '&lt;'); text = text.replace(/>/g, '&gt;'); text = text.replace(/'/g, '&apos;'); text = text.replace(/"/g, '&quot;'); return text; } /** * Unexcapes invalid xml characters. * @method Strophe.xmlunescape * @param {string} text - text to unescape. * @return {string} - 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 {Element} elem - The element to operate on. * @param {string} elemName - The child element tag name filter. * @param {Function} 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 || this.isTagEqual(childNode, elemName))) { func(childNode); } } } /** * Compare an element's tag name with a string. * This function is case sensitive. * @method Strophe.isTagEqual * @param {Element} el - A DOM element. * @param {string} name - The element name. * @return {boolean} * true if the element's tag name matches _el_, and false * otherwise. */ function isTagEqual(el, name) { return el.tagName === name; } /** * Get the concatenation of all text children of an element. * @method Strophe.getText * @param {Element} elem - A DOM element. * @return {string} - 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 {string} node - A node (or local part). * @return {string} 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 {string} node - A node (or local part). * @return {string} 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 {string} jid - A JID. * @return {string} - 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 {string} jid - A JID. * @return {string} - 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 {string} jid - A JID. * @return {string} - 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 {string} jid - A JID. * @return {string} - 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, toElement: toElement, handleError: handleError, utf16to8: utf16to8, xorArrayBuffers: xorArrayBuffers, arrayBufToBase64: arrayBufToBase64, base64ToArrayBuf: base64ToArrayBuf, stringToArrayBuf: stringToArrayBuf, addCookies: addCookies, xmlGenerator: xmlGenerator, xmlTextNode: xmlTextNode, stripWhitespace: stripWhitespace, xmlHtmlNode: xmlHtmlNode, getParserError: getParserError, getFirstElementChild: getFirstElementChild, xmlElement: xmlElement, validTag: validTag, validAttribute: validAttribute, validCSS: validCSS, createHtml: createHtml, copyElement: copyElement, xmlescape: xmlescape, xmlunescape: xmlunescape, forEachChild: forEachChild, isTagEqual: isTagEqual, getText: getText, escapeNode: escapeNode, unescapeNode: unescapeNode, getNodeFromJid: getNodeFromJid, getDomainFromJid: getDomainFromJid, getResourceFromJid: getResourceFromJid, getBareJidFromJid: getBareJidFromJid, 'default': utils }); /** * Create a {@link Strophe.Builder} * This is an alias for `new Strophe.Builder(name, attrs)`. * @param {string} name - The root element name. * @param {Object.<string,string|number>} [attrs] - The attributes for the root element in object notation. * @return {Builder} 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 {Object.<string,string>} [attrs] - The <message/> element attributes in object notation. * @return {Builder} 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 {Object.<string,string>} [attrs] - The <iq/> element attributes in object notation. * @return {Builder} 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 {Object.<string,string>} [attrs] - The <presence/> element attributes in object notation. * @return {Builder} 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 { /** * @typedef {Object.<string, string|number>} StanzaAttrs * @property {string} [StanzaAttrs.xmlns] */ /** @type {Element} */ #nodeTree; /** @type {Element} */ #node; /** @type {string} */ #name; /** @type {StanzaAttrs} */ #attrs; /** * The attributes should be passed in object notation. * @param {string} name - The name of the root element. * @param {StanzaAttrs} [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) { // 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 }; } } this.#name = name; this.#attrs = attrs; } /** * Creates a new Builder object from an XML string. * @param {string} str * @returns {Builder} * @example const stanza = Builder.fromString('<presence from="juliet@example.com/chamber"></presence>'); */ static fromString(str) { const el = toElement(str, true); const b = new Builder(''); b.#nodeTree = el; return b; } buildTree() { return xmlElement(this.#name, this.#attrs); } /** @return {Element} */ get nodeTree() { if (!this.#nodeTree) { // Holds the tree being built. this.#nodeTree = this.buildTree(); } return this.#nodeTree; } /** @return {Element} */ get node() { if (!this.#node) { this.#node = this.tree(); } return this.#node; } /** @param {Element} el */ set node(el) { this.#node = el; } /** * Render a DOM element and all descendants to a String. * @param {Element|Builder} elem - A DOM element. * @return {string} - 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: // normal element, so recurse result += Builder.serialize(/** @type {Element} */child); break; case ElementType.TEXT: // text element to escape values result += xmlescape(child.nodeValue); break; case ElementType.CDATA: // cdata section so don't escape values 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(). * * @return {Element} 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. * * @return {string} 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', {}); * * @return {Builder} The Strophe.Builder object. */ up() { // Depending on context, parentElement is not always available this.node = this.node.parentElement ? this.node.parentElement : (/** @type {Element} */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(). * * @return {Builder} 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 {Object.<string, string|number|null>} moreattrs - The attributes to add/modify in object notation. * If an attribute is set to `null` or `undefined`, it will be removed. * @return {Builder} The Strophe.Builder object. */ attrs(moreattrs) { for (const k in moreattrs) { if (Object.prototype.hasOwnProperty.call(moreattrs, k)) { // eslint-disable-next-line no-eq-null 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 {string} name - The name of the child. * @param {Object.<string, string>|string} [attrs] - The attributes of the child in object notation. * @param {string} [text] - The text to add to the child. * * @return {Builder} 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; } return this; } /** * Add a child to the current element and make it the new current * element. * * This function is the same as c() except that instead of using a * name and an attributes object to create the child it uses an * existing DOM element object. * * @param {Element|Builder} elem - A DOM element. * @return {Builder} The Strophe.Builder object. */ cnode(elem) { if (elem instanceof Builder) { elem = elem.tree(); } let impNode; const xmlGen = xmlGenerator(); try { impNode = xmlGen.importNode !== undefined; } catch (_e) { impNode = false; } const newElem = impNode ? xmlGen.importNode(elem, true) : copyElement(elem); this.node.appendChild(newElem); this.node = /** @type {Element} */newElem; return this; } /** * Add a child text element. * * This *does not* make the child the new current element since there * are no children of text elements. * * @param {string} text - The text data to append to the current element. * @return {Builder} The Strophe.Builder object. */ t(text) { const child = xmlTextNode(text); this.node.appendChild(child); return this; } /** * Replace current element contents with the HTML passed in. * * This *does not* make the child the new current element * * @param {string} html - The html to insert as contents of current element. * @return {Builder} The Strophe.Builder object. */ h(html) { const fragment = xmlGenerator().createElement('body'); // force the browser to try and fix any invalid HTML tags fragment.innerHTML = html; // copy cleaned html into an xml dom const xhtml = createHtml(fragment); while (xhtml.childNodes.length > 0) { this.node.appendChild(xhtml.childNodes[0]); } return this; } } /** * _Private_ variable that keeps track of the request ids for connections. */ let _requestId = 0; /** * Helper class that provides a cross implementation abstraction * for a BOSH related XMLHttpRequest. * * The Request class is used internally to encapsulate BOSH request * information. It is not meant to be used from user's code. * * @property {number} id * @property {number} sends * @property {XMLHttpRequest} xhr */ class Request { /** * Create and initialize a new Request object. * * @param {Element} elem - The XML data to be sent in the request. * @param {Function} func - The function that will be called when the * XMLHttpRequest readyState changes. * @param {number} rid - The BOSH rid attribute associated with this request. * @param {number} [sends=0] - The number of times this same request has been sent. */ constructor(elem, func, rid, sends = 0) { this.id = ++_requestId; this.xmlData = elem; this.data = Builder.serialize(elem); // save original function in case we need to make a new request // from this one. this.origFunc = func; this.func = func; this.rid = rid; this.date = NaN; this.sends = sends; this.abort = false; this.dead = null; this.age = () => this.date ? (new Date().valueOf() - this.date.valueOf()) / 1000 : 0; this.timeDead = () => this.dead ? (new Date().valueOf() - this.dead.valueOf()) / 1000 : 0; this.xhr = this._newXHR(); } /** * Get a response from the underlying XMLHttpRequest. * This function attempts to get a response from the request and checks * for errors. * @throws "parsererror" - A parser error occured. * @throws "bad-format" - The entity has sent XML that cannot be processed. * @return {Element} - The DOM element tree of the response. */ getResponse() { var _this$xhr$responseXML; const node = (_this$xhr$responseXML = this.xhr.responseXML) === null || _this$xhr$responseXML === void 0 ? void 0 : _this$xhr$responseXML.documentElement; if (node) { if (node.tagName === 'parsererror') { log.error('invalid response received'); log.error('responseText: ' + this.xhr.responseText); log.error('responseXML: ' + Builder.serialize(node)); throw new Error('parsererror'); } } else if (this.xhr.responseText) { // In Node (with xhr2) or React Native, we may get responseText but no responseXML. // We can try to parse it manually. log.debug('Got responseText but no responseXML; attempting to parse it with DOMParser...'); const doc = xmlHtmlNode(this.xhr.responseText); const parserError = getParserError(doc); if (!doc || parserError) { if (parserError) { log.error('invalid response received: ' + parserError); log.error('responseText: ' + this.xhr.responseText); } const error = new Error(); error.name = ErrorCondition.BAD_FORMAT; throw error; } } return node; } /** * _Private_ helper function to create XMLHttpRequests. * This function creates XMLHttpRequests across all implementations. * @private * @return {XMLHttpRequest} */ _newXHR() { const xhr = new XMLHttpRequest(); if (xhr.overrideMimeType) { xhr.overrideMimeType('text/xml; charset=utf-8'); } // use Function.bind() to prepend ourselves as an argument xhr.onreadystatechange = this.func.bind(null, this); return xhr; } } /** * A JavaScript library to enable BOSH in Strophejs. * * this library uses Bidirectional-streams Over Synchronous HTTP (BOSH) * to emulate a persistent, stateful, two-way connection to an XMPP server. * More information on BOSH can be found in XEP 124. */ let timeoutMultiplier = 1.1; let secondaryTimeoutMultiplier = 0.1; /** * _Private_ helper class that handles BOSH Connections * The Bosh class is used internally by Connection * to encapsulate BOSH sessions. It is not meant to be used from user's code. */ class Bosh { /** * @param {Connection} connection - The Connection that will use BOSH. */ constructor(connection) { var _Bosh$prototype$strip; this._conn = connection; /* request id for body tags */ this.rid = Math.floor(Math.random() * 4294967295); /* The current session ID. */ this.sid = null; // default BOSH values this.hold = 1; this.wait = 60; this.window = 5; this.errors = 0; this.inactivity = null; /** * BOSH-Connections will have all stanzas wrapped in a <body> tag when * passed to {@link Connection#xmlInput|xmlInput()} or {@link Connection#xmlOutput|xmlOutput()}. * To strip this tag, User code can set {@link Bosh#strip|strip} to `true`: * * > // You can set `strip` on the prototype * > Bosh.prototype.strip = true; * * > // Or you can set it on the Bosh instance (which is `._proto` on the connection instance. * > const conn = new Connection(); * > conn._proto.strip = true; * * This will enable stripping of the body tag in both * {@link Connection#xmlInput|xmlInput} and {@link Connection#xmlOutput|xmlOutput}. * * @property {boolean} [strip=false] */ this.strip = (_Bosh$prototype$strip = Bosh.prototype.strip) !== null && _Bosh$prototype$strip !== void 0 ? _Bosh$prototype$strip : false; this.lastResponseHeaders = null; /** @type {Request[]} */ this._requests = []; } /** * @param {number} m */ static setTimeoutMultiplier(m) { timeoutMultiplier = m; } /** * @returns {number} */ static getTimeoutMultplier() { return timeoutMultiplier; } /** * @param {number} m */ static setSecondaryTimeoutMultiplier(m) { secondaryTimeoutMultiplier = m; } /** * @returns {number} */ static getSecondaryTimeoutMultplier() { return secondaryTimeoutMultiplier; } /** * _Private_ helper function to generate the <body/> wrapper for BOSH. * @private * @return {Builder} - A Builder with a <body/> element. */ _buildBody() { const bodyWrap = $build('body', { 'rid': this.rid++, 'xmlns': NS.HTTPBIND }); if (this.sid !== null) { bodyWrap.attrs({ 'sid': this.sid }); } if (this._conn.options.keepalive && this._conn._sessionCachingSupported()) { this._cacheSession(); } return bodyWrap; } /** * Reset the connection. * This function is called by the reset function of the Connection */ _reset() { this.rid = Math.floor(Math.random() * 4294967295); this.sid = null; this.errors = 0; if (this._conn._sessionCachingSupported()) { sessionStorage.removeItem('strophe-bosh-session'); } this._conn.nextValidRid(this.rid); } /** * _Private_ function that initializes the BOSH connection. * Creates and sends the Request that initializes the BOSH connection. * @param {number} wait - The optional HTTPBIND wait value. This is the * time the server will wait before returning an empty result for * a request. The default setting of 60 seconds is recommended. * Other settings will require tweaks to the Strophe.TIMEOUT value. * @param {number} hold - The optional HTTPBIND hold value. This is the * number of connections the server will hold at one time. This * should almost always be set to 1 (the default). * @param {string} route */ _connect(wait, hold, route) { this.wait = wait || this.wait; this.hold = hold || this.hold; this.errors = 0; const body = this._buildBody().attrs({ 'to': this._conn.domain, 'xml:lang': 'en', 'wait': this.wait, 'hold': this.hold, 'content': 'text/xml; charset=utf-8', 'ver': '1.6', 'xmpp:version': '1.0', 'xmlns:xmpp': NS.BOSH }); if (route) { body.attrs({ route }); } const _connect_cb = this._conn._connect_cb; this._requests.push(new Request(body.tree(), this._onRequestStateChange.bind(this, _connect_cb.bind(this._conn)), Number(body.tree().getAttribute('rid')))); this._throttledRequestHandler(); } /** * Attach to an already created and authenticated BOSH session. * * This function is provided to allow Strophe to attach to BOSH * sessions which have been created externally, perhaps by a Web * application. This is often used to support auto-login type features * without putting user credentials into the page. * * @param {string} jid - The full JID that is bound by the session. * @param {string} sid - The SID of the BOSH session. * @param {number} rid - The current RID of the BOSH session. This RID * will be used by the next request. * @param {Function} callback The connect callback function. * @param {number} wait - The optional HTTPBIND wait value. This is the * time the server will wait before returning an empty result for * a request. The default setting of 60 seconds is recommended. * Other settings will require tweaks to the Strophe.TIMEOUT value. * @param {number} hold - The optional HTTPBIND hold value. This is the * number of connections the server will hold at one time. This * should almost always be set to 1 (the default). * @param {number} wind - The optional HTTBIND window value. This is the * allowed range of request ids that are valid. The default is 5. */ _attach(jid, sid, rid, callback, wait, hold, wind) { this._conn.jid = jid; this.sid = sid; this.rid = rid; this._conn.connect_callback = callback; this._conn.domain = getDomainFromJid(this._conn.jid); this._conn.authenticated = true; this._conn.connected = true; this.wait = wait || this.wait; this.hold = hold || this.hold; this.window = wind || this.window; this._conn._changeConnectStatus(Status.ATTACHED, null); } /** * Attempt to restore a cached BOSH session * * @param {string} jid - The full JID that is bound by the session. * This parameter is optional but recommended, specifically in cases * where prebinded BOSH sessions are used where it's important to know * that the right session is being restored. * @param {Function} callback The connect callback function. * @param {number} wait - The optional HTTPBIND wait value. This is the * time the server will wait before returning an empty result for * a request. The default setting of 60 seconds is recommended. * Other settings will require tweaks to the Strophe.TIMEOUT value. * @param {number} hold - The optional HTTPBIND hold value. This is the * number of connections the server will hold at one time. This * should almost always be set to 1 (the default). * @param {number} wind - The optional HTTBIND window value. This is the * allowed range of request ids that are valid. The default is 5.