strophe.js
Version:
Strophe.js is an XMPP library for JavaScript
1,430 lines (1,352 loc) • 207 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Strophe = {}));
})(this, (function (exports) { 'use strict';
/**
* 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, '&');
text = text.replace(/</g, '<');
text = text.replace(/>/g, '>');
text = text.replace(/'/g, ''');
text = text.replace(/"/g, '"');
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(/\&/g, '&');
text = text.replace(/</g, '<');
text = text.replace(/>/g, '>');
text = text.replace(/'/g, "'");
text = text.replace(/"/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
*/