diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c6aa5fe..4535849 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -34,3 +34,6 @@ jobs:
- name: Run lint
run: yarn lint
+
+ - name: Run tests
+ run: yarn test
diff --git a/package.json b/package.json
index 301862f..610ccee 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "azdataGraph",
"description": "azdataGraph is a derivative of mxGraph, which is a fully client side JavaScript diagramming library that uses SVG and HTML for rendering.",
- "version": "0.0.139",
+ "version": "0.0.140",
"homepage": "https://github.com/microsoft/azdataGraph",
"author": "Microsoft",
"license": "Apache-2.0",
@@ -19,7 +19,8 @@
"build": "yarn compile-mxgraph && npx tsc && node build.js",
"watch": "yarn compile-mxgraph && node build.js --watch",
"examples": "http-server -p 4000",
- "lint": "eslint"
+ "lint": "eslint",
+ "test": "node --test test/*.test.js"
},
"devDependencies": {
"@jgoz/esbuild-plugin-typecheck": "^4.0.2",
diff --git a/src/js/azdata/view/azDataGraph.js b/src/js/azdata/view/azDataGraph.js
index 3b82259..45cf5e2 100644
--- a/src/js/azdata/view/azDataGraph.js
+++ b/src/js/azdata/view/azDataGraph.js
@@ -87,6 +87,31 @@ azdataGraph.prototype.insertWeightedInvertedEdge = function (parent, id, value,
return this.insertInvertedEdge(parent, id, value, source, target, `strokeWidth=${value.weight.toFixed(1)};` + style);
};
+/**
+ * Function: escapeTooltipText
+ *
+ * Encodes text for safe insertion into tooltip HTML.
+ *
+ * Parameters:
+ * value - Value to encode.
+ */
+azdataGraph.prototype.escapeTooltipText = function (value) {
+ return String(value ?? '').replace(/[&<>"']/g, character => {
+ switch (character) {
+ case '&':
+ return '&';
+ case '<':
+ return '<';
+ case '>':
+ return '>';
+ case '"':
+ return '"';
+ case "'":
+ return ''';
+ }
+ });
+};
+
/**
* Function: getStyledTooltipForCell
*
@@ -113,10 +138,10 @@ azdataGraph.prototype.getStyledTooltipForCell = function (cell) {
// tooltip heading for vertices only
if (!cell.edge) {
- let tooltipTitle = this.truncateTooltipTitle(cell.value.tooltipTitle);
+ let tooltipTitle = this.escapeTooltipText(this.truncateTooltipTitle(cell.value.tooltipTitle));
tooltip += `
${tooltipTitle}
`;
if (cell.value.description) {
- tooltip += `${cell.value.description}
`;
+ tooltip += `${this.escapeTooltipText(cell.value.description)}
`;
}
}
@@ -129,8 +154,8 @@ azdataGraph.prototype.getStyledTooltipForCell = function (cell) {
tooltip += ``;
tooltip += `
`;
- tooltip += `${cell.value.metrics[i].name}`;
- tooltip += `${cell.value.metrics[i].value}`;
+ tooltip += `${this.escapeTooltipText(cell.value.metrics[i].name)}`;
+ tooltip += `${this.escapeTooltipText(cell.value.metrics[i].value)}`;
tooltip += '
';
if (i < cell.value.metrics.length - 1) {
@@ -144,13 +169,13 @@ azdataGraph.prototype.getStyledTooltipForCell = function (cell) {
if (!cell.edge) {
cell.value.metrics.filter(m => m.isLongString).forEach(m => {
tooltip += '
';
- tooltip += `
${m.name}
`;
+ tooltip += `
${this.escapeTooltipText(m.name)}
`;
let metricLabel = m.value.replace(/(\r\n|\n|\r)/gm, " ");
if (metricLabel.length > 103) {
metricLabel = metricLabel.substring(0, 100) + '...';
}
- tooltip += `
${metricLabel}
`; // Removing all line breaks as they look bad in tooltips
+ tooltip += `
${this.escapeTooltipText(metricLabel)}
`; // Removing all line breaks as they look bad in tooltips
})
}
diff --git a/test/tooltipRendering.test.js b/test/tooltipRendering.test.js
new file mode 100644
index 0000000..9c40694
--- /dev/null
+++ b/test/tooltipRendering.test.js
@@ -0,0 +1,72 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const path = require('node:path');
+const test = require('node:test');
+const vm = require('node:vm');
+
+function loadAzdataGraph() {
+ function mxGraph() {}
+ mxGraph.prototype.getTooltipForCell = function () {
+ return '';
+ };
+
+ const context = vm.createContext({ mxGraph });
+ const source = fs.readFileSync(
+ path.join(__dirname, '..', 'src', 'js', 'azdata', 'view', 'azDataGraph.js'),
+ 'utf8'
+ );
+ vm.runInContext(source, context);
+ return context.azdataGraph;
+}
+
+test('escapeTooltipText encodes HTML-significant characters', () => {
+ const AzdataGraph = loadAzdataGraph();
+ const graph = Object.create(AzdataGraph.prototype);
+
+ assert.equal(
+ graph.escapeTooltipText(`&<>"'`),
+ '&<>"''
+ );
+});
+
+test('styled tooltips encode all dynamic text fields', () => {
+ const AzdataGraph = loadAzdataGraph();
+ const graph = Object.create(AzdataGraph.prototype);
+ const tooltip = graph.getStyledTooltipForCell({
+ edge: false,
+ value: {
+ tooltipTitle: '
Title & text',
+ description: '

',
+ metrics: [
+ {
+ name: 'Footer',
+ value: '',
+ isLongString: false,
+ },
+ {
+ name: '
',
+ value: `"