Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Let the Stanza class extend the Builder class. #769

Merged
merged 7 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

## Version 3.0.2 - (Unreleased)

- **Security Fix**: Escape values passed to the `stx` tagged template literal
- Allow `Stanza` and `Builder` objects to be passed as values the `stx`.
- Avoid inserting commas when nesting array values in `stx` templates.
- Replace [xmldom](https://github.com/xmldom/xmldom) with [jsdom](https://github.com/jsdom/jsdom).
- Avoid inserting commas when nesting lists of `stx` templates.
- Remove deprecated `abab` package
- Make sure `ConnectionOptions` type is exportable
- fix: invert default and types exports
Expand Down Expand Up @@ -300,8 +302,7 @@ import { Strophe, $build, stx } from strophe.js;

## Version 1.0.2 - 2011-06-19

* Fix security bug where DIGEST-MD5 client nonce was not properly
randomized.
* Fix security bug where DIGEST-MD5 client nonce was not properly randomized.
* Fix double escaping in copyElement.
* Fix IE errors related to importNode.
* Add ability to pass text into Builder.c().
Expand Down
1 change: 0 additions & 1 deletion karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ module.exports = function (config) {
'node_modules/sinon/pkg/sinon.js',
'dist/strophe.umd.js',
'tests/tests.js',
'tests/stx.js'
],

// list of files to exclude
Expand Down
60 changes: 54 additions & 6 deletions src/builder.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ElementType, NS } from './constants.js';
import { copyElement, createHtml, xmlElement, xmlGenerator, xmlTextNode, xmlescape } from './utils.js';
import { copyElement, createHtml, toElement, xmlElement, xmlGenerator, xmlTextNode, xmlescape } from './utils.js';

/**
* Create a {@link Strophe.Builder}
Expand Down Expand Up @@ -73,6 +73,15 @@ class Builder {
* @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.
Expand All @@ -89,10 +98,48 @@ class Builder {
attrs = { xmlns: NS.CLIENT };
}
}
// Holds the tree being built.
this.nodeTree = xmlElement(name, attrs);
// Points to the current operation node.
this.node = this.nodeTree;

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="[email protected]/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;
}

/**
Expand Down Expand Up @@ -254,7 +301,8 @@ class Builder {
const xmlGen = xmlGenerator();
try {
impNode = xmlGen.importNode !== undefined;
} catch (e) { // eslint-disable-line no-unused-vars
// eslint-disable-next-line no-unused-vars
} catch (e) {
impNode = false;
}

Expand Down
9 changes: 4 additions & 5 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import Websocket from './websocket.js';
import WorkerWebsocket from './worker-websocket.js';
import log from './log.js';
import { ElementType, ErrorCondition, LOG_LEVELS, NS, Status, XHTML } from './constants.js';
import { stx, toStanzaElement, Stanza } from './stanza.js';
import { stx, Stanza } from './stanza.js';

/**
* A container for all Strophe library functions.
Expand Down Expand Up @@ -185,10 +185,9 @@ globalThis.$iq = $iq;
globalThis.$msg = $msg;
globalThis.$pres = $pres;
globalThis.Strophe = Strophe;
globalThis.toStanzaElement = toStanzaElement;
globalThis.stx = stx;

globalThis.toStanza = toStanzaElement; // Deprecated
const toStanza = Stanza.toElement;
globalThis.toStanza = Stanza.toElement; // Deprecated

export { Builder, $build, $iq, $msg, $pres, Strophe, Stanza, stx, toStanzaElement, Request };
export { toStanzaElement as toStanza }
export { Builder, $build, $iq, $msg, $pres, Strophe, Stanza, stx, toStanza, Request };
32 changes: 29 additions & 3 deletions src/shims.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,38 @@ function getWebSocketImplementation() {
if (typeof globalThis.WebSocket === 'undefined') {
try {
return require('ws');
} catch (e) { // eslint-disable-line no-unused-vars
// eslint-disable-next-line no-unused-vars
} catch (e) {
throw new Error('You must install the "ws" package to use Strophe in nodejs.');
}
}
return globalThis.WebSocket;
}
export const WebSocket = getWebSocketImplementation();

/**
* Retrieves the XMLSerializer implementation for the current environment.
*
* In browser environments, it uses the built-in XMLSerializer.
* In Node.js environments, it attempts to load the 'jsdom' package
* to create a compatible XMLSerializer.
*/
function getXMLSerializerImplementation() {
if (typeof globalThis.XMLSerializer === 'undefined') {
let JSDOM;
try {
JSDOM = require('jsdom').JSDOM;
// eslint-disable-next-line no-unused-vars
} catch (e) {
throw new Error('You must install the "ws" package to use Strophe in nodejs.');
}
const dom = new JSDOM('');
return dom.window.XMLSerializer;
}
return globalThis.XMLSerializer;
}
export const XMLSerializer = getXMLSerializerImplementation();

/**
* DOMParser
* https://w3c.github.io/DOM-Parsing/#the-domparser-interface
Expand All @@ -52,7 +76,8 @@ function getDOMParserImplementation() {
let JSDOM;
try {
JSDOM = require('jsdom').JSDOM;
} catch (e) { // eslint-disable-line no-unused-vars
// eslint-disable-next-line no-unused-vars
} catch (e) {
throw new Error('You must install the "jsdom" package to use Strophe in nodejs.');
}
const dom = new JSDOM('');
Expand All @@ -75,7 +100,8 @@ export function getDummyXMLDOMDocument() {
let JSDOM;
try {
JSDOM = require('jsdom').JSDOM;
} catch (e) { // eslint-disable-line no-unused-vars
// eslint-disable-next-line no-unused-vars
} catch (e) {
throw new Error('You must install the "jsdom" package to use Strophe in nodejs.');
}
const dom = new JSDOM('');
Expand Down
138 changes: 92 additions & 46 deletions src/stanza.js
Original file line number Diff line number Diff line change
@@ -1,67 +1,113 @@
import Builder from './builder.js';
import log from './log.js';
import { getFirstElementChild, getParserError, xmlHtmlNode } from './utils.js';

/**
* @param {string} string
* @param {boolean} [throwErrorIfInvalidNS]
* @returns {Element}
*/
export function toStanzaElement(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;
}
import { getFirstElementChild, getParserError, xmlHtmlNode, xmlescape } from './utils.js';

/**
* A Stanza represents a XML element used in XMPP (commonly referred to as stanzas).
*/
export class Stanza {
export class Stanza extends Builder {
/** @type {string} */
#string;
/** @type {Array<string>} */
#strings;
/**
* @typedef {Array<string|Stanza|Builder>} StanzaValue
* @type {StanzaValue|Array<StanzaValue>}
*/
#values;

/**
* @param {string[]} strings
* @param {any[]} values
*/
constructor(strings, values) {
this.strings = strings;
this.values = values;
super('stanza');
this.#strings = strings;
this.#values = values;
}

/**
* @return {string}
* A directive which can be used to pass a string of XML as a value to the
* stx tagged template literal.
*
* It's considered "unsafe" because it can pose a security risk if used with
* untrusted input.
*
* @param {string} string
* @returns {Builder}
* @example
* const status = '<status>I am busy!</status>';
* const pres = stx`
* <presence from='[email protected]/chamber' id='pres1'>
* <show>dnd</show>
* ${unsafeXML(status)}
* </presence>`;
* connection.send(pres);
*/
toString() {
this.string =
this.string ||
this.strings.reduce((acc, str) => {
const idx = this.strings.indexOf(str);
const value = this.values.length > idx ? this.values[idx] : '';
return acc + str + (Array.isArray(value) ? value.join('') : value.toString());
}, '');
return this.string;
static unsafeXML(string) {
return Builder.fromString(string);
}

/**
* @return {Element}
* Turns the passed-in string into an XML Element.
* @param {string} string
* @param {boolean} [throwErrorIfInvalidNS]
* @returns {Element}
*/
tree() {
this.node = this.node ?? toStanzaElement(this.toString(), true);
return this.node;
static 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;
}

buildTree() {
return Stanza.toElement(this.toString(), true);
}

/**
* @return {string}
*/
toString() {
this.#string =
this.#string ||
this.#strings
.reduce((acc, str) => {
const idx = this.#strings.indexOf(str);
const value = this.#values.length > idx ? this.#values[idx] : '';
return (
acc +
str +
(Array.isArray(value)
? value
.map((v) =>
v instanceof Stanza || v instanceof Builder ? v : xmlescape(v.toString())
)
.join('')
: value instanceof Stanza || value instanceof Builder
? value
: xmlescape(value.toString()))
);
}, '')
.trim();

return this.#string;
}
}

Expand Down
Loading
Loading