From 5b0d4cb0f92dfb64a996969a0d9f965c0f30ca08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Mon, 8 Aug 2016 18:15:40 -0400 Subject: [PATCH 1/7] svg_text_utils: don't require Plotly --- src/lib/svg_text_utils.js | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/lib/svg_text_utils.js b/src/lib/svg_text_utils.js index fa0382eb54b..a0febf5a5eb 100644 --- a/src/lib/svg_text_utils.js +++ b/src/lib/svg_text_utils.js @@ -11,14 +11,11 @@ /* global MathJax:false */ -var Plotly = require('../plotly'); var d3 = require('d3'); var Lib = require('../lib'); var xmlnsNamespaces = require('../constants/xmlns_namespaces'); -var util = module.exports = {}; - // Append SVG d3.selection.prototype.appendSVG = function(_svgString) { @@ -45,7 +42,7 @@ d3.selection.prototype.appendSVG = function(_svgString) { // Text utilities -util.html_entity_decode = function(s) { +exports.html_entity_decode = function(s) { var hiddenDiv = d3.select('body').append('div').style({display: 'none'}).html(''); var replaced = s.replace(/(&[^;]*;)/gi, function(d) { if(d === '<') { return '<'; } // special handling for brackets @@ -56,7 +53,7 @@ util.html_entity_decode = function(s) { return replaced; }; -util.xml_entity_encode = function(str) { +exports.xml_entity_encode = function(str) { return str.replace(/&(?!\w+;|\#[0-9]+;| \#x[0-9A-F]+;)/g, '&'); }; @@ -66,7 +63,7 @@ function getSize(_selection, _dimension) { return _selection.node().getBoundingClientRect()[_dimension]; } -util.convertToTspans = function(_context, _callback) { +exports.convertToTspans = function(_context, _callback) { var str = _context.text(); var converted = convertToSVG(str); var that = _context; @@ -112,7 +109,7 @@ util.convertToTspans = function(_context, _callback) { } if(tex) { - var td = Plotly.Lib.getPlotDiv(that.node()); + var td = Lib.getPlotDiv(that.node()); ((td && td._promises) || []).push(new Promise(function(resolve) { that.style({visibility: 'hidden'}); var config = {fontSize: parseInt(that.style('font-size'), 10)}; @@ -195,7 +192,7 @@ function cleanEscapesForTex(s) { } function texToSVG(_texString, _config, _callback) { - var randomID = 'math-output-' + Plotly.Lib.randstr([], 64); + var randomID = 'math-output-' + Lib.randstr([], 64); var tmpDiv = d3.select('body').append('div') .attr({id: randomID}) .style({visibility: 'hidden', position: 'absolute'}) @@ -236,7 +233,7 @@ var PROTOCOLS = ['http:', 'https:', 'mailto:']; var STRIP_TAGS = new RegExp(']*)?/?>', 'g'); -util.plainText = function(_str) { +exports.plainText = function(_str) { // strip out our pseudo-html so we have a readable // version to put into text fields return (_str || '').replace(STRIP_TAGS, ' '); @@ -316,7 +313,7 @@ function convertToSVG(_str) { } } else { - return Plotly.util.xml_entity_encode(d).replace(/ Date: Tue, 9 Aug 2016 10:46:39 -0400 Subject: [PATCH 2/7] add string_mappings constants file: - use it in html2unicode and svg_text_utils to translate a limited set of HTML enitity to unicode - list unicode-to-entities mappings (used in svg text style and anchors) --- src/constants/string_mappings.js | 37 ++++++++++++++++++++++++++++ src/lib/html2unicode.js | 11 +++------ src/lib/svg_text_utils.js | 41 +++++++++++++++++++++++++++----- 3 files changed, 75 insertions(+), 14 deletions(-) create mode 100644 src/constants/string_mappings.js diff --git a/src/constants/string_mappings.js b/src/constants/string_mappings.js new file mode 100644 index 00000000000..6953d0ae649 --- /dev/null +++ b/src/constants/string_mappings.js @@ -0,0 +1,37 @@ +/** +* Copyright 2012-2016, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + + +'use strict'; + +// N.B. HTML entities are listed without the leading '&' and trailing ';' + +module.exports = { + + entityToUnicode: { + 'mu': 'μ', + 'amp': '&', + 'lt': '<', + 'gt': '>', + 'quot': '\"', + 'nbsp': ' ', + 'times': '×', + 'plusmn': '±', + 'deg': '°' + }, + + unicodeToEntity: { + '&': 'amp', + '<': 'lt', + '>': 'gt', + '"': 'quot', + '\'': '#x27', + '\/': '#x2F' + } + +}; diff --git a/src/lib/html2unicode.js b/src/lib/html2unicode.js index c01ae754a19..fd76582077f 100644 --- a/src/lib/html2unicode.js +++ b/src/lib/html2unicode.js @@ -10,13 +10,7 @@ 'use strict'; var toSuperScript = require('superscript-text'); - -var ENTITIES = { - 'mu': 'μ', - 'amp': '&', - 'lt': '<', - 'gt': '>' -}; +var stringMappings = require('../constants/string_mappings'); function fixSuperScript(x) { var idx = 0; @@ -40,6 +34,7 @@ function stripTags(x) { } function fixEntities(x) { + var entityToUnicode = stringMappings.entityToUnicode; var idx = 0; while((idx = x.indexOf('&', idx)) >= 0) { @@ -49,7 +44,7 @@ function fixEntities(x) { continue; } - var entity = ENTITIES[x.slice(idx + 1, nidx)]; + var entity = entityToUnicode[x.slice(idx + 1, nidx)]; if(entity) { x = x.slice(0, idx) + entity + x.slice(nidx + 1); } else { diff --git a/src/lib/svg_text_utils.js b/src/lib/svg_text_utils.js index a0febf5a5eb..8b8e37257d8 100644 --- a/src/lib/svg_text_utils.js +++ b/src/lib/svg_text_utils.js @@ -15,6 +15,7 @@ var d3 = require('d3'); var Lib = require('../lib'); var xmlnsNamespaces = require('../constants/xmlns_namespaces'); +var stringMappings = require('../constants/string_mappings'); // Append SVG @@ -67,6 +68,7 @@ exports.convertToTspans = function(_context, _callback) { var str = _context.text(); var converted = convertToSVG(str); var that = _context; + // Until we get tex integrated more fully (so it can be used along with non-tex) // allow some elements to prohibit it by attaching 'data-notex' to the original var tex = (!that.attr('data-notex')) && converted.match(/([^$]*)([$]+[^$]*[$]+)([^$]*)/); @@ -233,22 +235,48 @@ var PROTOCOLS = ['http:', 'https:', 'mailto:']; var STRIP_TAGS = new RegExp(']*)?/?>', 'g'); +var ENTITY_TO_UNICODE = Object.keys(stringMappings.entityToUnicode).map(function(k) { + return { + regExp: new RegExp('&' + k + ';', 'g'), + sub: stringMappings.entityToUnicode[k] + }; +}); + +var UNICODE_TO_ENTITY = Object.keys(stringMappings.unicodeToEntity).map(function(k) { + return { + regExp: new RegExp(k, 'g'), + sub: '&' + stringMappings.unicodeToEntity[k] + ';' + }; +}); + exports.plainText = function(_str) { // strip out our pseudo-html so we have a readable // version to put into text fields return (_str || '').replace(STRIP_TAGS, ' '); }; +function replaceFromMapObject(_str, list) { + var out = _str || ''; + + for(var i = 0; i < list.length; i++) { + var item = list[i]; + out = out.replace(item.regExp, item.sub); + } + + return out; +} + +function convertEntities(_str) { + return replaceFromMapObject(_str, ENTITY_TO_UNICODE); +} + function encodeForHTML(_str) { - return (_str || '').replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, ''') - .replace(/\//g, '/'); + return replaceFromMapObject(_str, UNICODE_TO_ENTITY); } function convertToSVG(_str) { + _str = convertEntities(_str); + var result = _str .split(/(<[^<>]*>)/).map(function(d) { var match = d.match(/<(\/?)([^ >]*)\s*(.*)>/i), @@ -267,6 +295,7 @@ function convertToSVG(_str) { * resurrect it. */ extraStyle = extra.match(/^style\s*=\s*"([^"]+)"\s*/i); + // anchor and br are the only ones that don't turn into a tspan if(tag === 'a') { if(close) return ''; From 010f5a8c0bdde489494e8b023624c72da2c22ebc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 9 Aug 2016 10:47:07 -0400 Subject: [PATCH 3/7] Revert "Update tests to not depend on entity decode" This reverts commit 7649e5d5d294279d844f2c9d36019bb0252fa3bf. --- test/jasmine/tests/svg_text_utils_test.js | 43 +++++++++++------------ 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/test/jasmine/tests/svg_text_utils_test.js b/test/jasmine/tests/svg_text_utils_test.js index 7daa24f4bfb..be4601743c8 100644 --- a/test/jasmine/tests/svg_text_utils_test.js +++ b/test/jasmine/tests/svg_text_utils_test.js @@ -121,34 +121,33 @@ describe('svg+text utils', function() { }); it('wrap XSS attacks in href', function() { - var node = mockTextSVGElement( - 'Subtitle' - ); - - expect(node.text()).toEqual('Subtitle'); - assertAnchorAttrs(node); - assertAnchorLink(node, 'XSS onmouseover=alert(1) style=font-size:300px'); - }); - - it('wrap XSS attacks with quoted entities in href', function() { - var node = mockTextSVGElement( + var textCases = [ + 'Subtitle', 'Subtitle' - ); + ]; - console.log(node.select('a').attr('xlink:href')); - expect(node.text()).toEqual('Subtitle'); - assertAnchorAttrs(node); - assertAnchorLink(node, 'XSS" onmouseover="alert(1)" style="font-size:300px'); + textCases.forEach(function(textCase) { + var node = mockTextSVGElement(textCase); + + expect(node.text()).toEqual('Subtitle'); + assertAnchorAttrs(node); + assertAnchorLink(node, 'XSS onmouseover=alert(1) style=font-size:300px'); + }); }); it('should keep query parameters in href', function() { - var node = mockTextSVGElement( - 'abc.com?shared-key' - ); + var textCases = [ + 'abc.com?shared-key', + 'abc.com?shared-key' + ]; - assertAnchorAttrs(node); - expect(node.text()).toEqual('abc.com?shared-key'); - assertAnchorLink(node, 'https://abc.com/myFeature.jsp?name=abc&pwd=def'); + textCases.forEach(function(textCase) { + var node = mockTextSVGElement(textCase); + + assertAnchorAttrs(node); + expect(node.text()).toEqual('abc.com?shared-key'); + assertAnchorLink(node, 'https://abc.com/myFeature.jsp?name=abc&pwd=def'); + }); }); it('allow basic spans', function() { From 20f7ae5cfaf02edc4c0811b197b57b4c0122e817 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 9 Aug 2016 10:48:32 -0400 Subject: [PATCH 4/7] bring back & in json mock - this commit partly reverts 6cf80dec6b318a0522726351c9eadb9da1f6b637 --- test/image/mocks/axes_enumerated_ticks.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/image/mocks/axes_enumerated_ticks.json b/test/image/mocks/axes_enumerated_ticks.json index cbe53ce1e10..a5016608717 100644 --- a/test/image/mocks/axes_enumerated_ticks.json +++ b/test/image/mocks/axes_enumerated_ticks.json @@ -33,7 +33,7 @@ "xaxis": { "ticktext": [ "green eggs", - "& ham", + "& ham", "H2O", "Gorgonzola" ], From e73ab7035e8437fe1d8bcaa4566f8d4b10a1bf77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 9 Aug 2016 11:32:05 -0400 Subject: [PATCH 5/7] test: add case for supported set of html entities --- test/jasmine/tests/svg_text_utils_test.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/jasmine/tests/svg_text_utils_test.js b/test/jasmine/tests/svg_text_utils_test.js index be4601743c8..d9887165c77 100644 --- a/test/jasmine/tests/svg_text_utils_test.js +++ b/test/jasmine/tests/svg_text_utils_test.js @@ -194,5 +194,14 @@ describe('svg+text utils', function() { expect(node.text()).toEqual('text'); assertTspanStyle(node, 'quoted: yeah&\';;'); }); + + it('decode some HTML entities in text', function() { + var node = mockTextSVGElement( + '100μ & < 10 > 0"  ' + + '100 × 20 ± 0.5 °' + ); + + expect(node.text()).toEqual('100μ & < 10 > 0\"  100 × 20 ± 0.5 °'); + }); }); }); From 73d46fe5b8163ab62d46d368f676eab78d2c6ad8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 9 Aug 2016 11:39:58 -0400 Subject: [PATCH 6/7] lint: make html2unicode require statement consistent --- src/plots/gl2d/convert.js | 4 ++-- src/plots/gl2d/scene2d.js | 4 ++-- src/plots/gl3d/layout/convert.js | 4 ++-- src/plots/gl3d/layout/tick_marks.js | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/plots/gl2d/convert.js b/src/plots/gl2d/convert.js index 2321224bd39..13f584a36d7 100644 --- a/src/plots/gl2d/convert.js +++ b/src/plots/gl2d/convert.js @@ -11,7 +11,7 @@ var Plotly = require('../../plotly'); -var htmlToUnicode = require('../../lib/html2unicode'); +var convertHTMLToUnicode = require('../../lib/html2unicode'); var str2RGBArray = require('../../lib/str2rgbarray'); function Axes2DOptions(scene) { @@ -115,7 +115,7 @@ proto.merge = function(options) { for(j = 0; j <= 2; j += 2) { this.labelEnable[i + j] = false; - this.labels[i + j] = htmlToUnicode(axTitle); + this.labels[i + j] = convertHTMLToUnicode(axTitle); this.labelColor[i + j] = str2RGBArray(ax.titlefont.color); this.labelFont[i + j] = ax.titlefont.family; this.labelSize[i + j] = ax.titlefont.size; diff --git a/src/plots/gl2d/scene2d.js b/src/plots/gl2d/scene2d.js index c1876847538..0d33abebec8 100644 --- a/src/plots/gl2d/scene2d.js +++ b/src/plots/gl2d/scene2d.js @@ -18,7 +18,7 @@ var createSelectBox = require('gl-select-box'); var createOptions = require('./convert'); var createCamera = require('./camera'); -var htmlToUnicode = require('../../lib/html2unicode'); +var convertHTMLToUnicode = require('../../lib/html2unicode'); var showNoWebGlMsg = require('../../lib/show_no_webgl_msg'); var AXES = ['xaxis', 'yaxis']; @@ -231,7 +231,7 @@ proto.computeTickMarks = function() { for(var i = 0; i < nextTicks[j].length; ++i) { // TODO add support for '\n' in gl-plot2d, // For now, replace '\n' with ' ' - nextTicks[j][i].text = htmlToUnicode(nextTicks[j][i].text + '').replace(/\n/g, ' '); + nextTicks[j][i].text = convertHTMLToUnicode(nextTicks[j][i].text + '').replace(/\n/g, ' '); } } diff --git a/src/plots/gl3d/layout/convert.js b/src/plots/gl3d/layout/convert.js index 3d9201e3815..ebdeaa39d8a 100644 --- a/src/plots/gl3d/layout/convert.js +++ b/src/plots/gl3d/layout/convert.js @@ -10,7 +10,7 @@ 'use strict'; var arrtools = require('arraytools'); -var convertHTML = require('../../../lib/html2unicode'); +var convertHTMLToUnicode = require('../../../lib/html2unicode'); var str2RgbaArray = require('../../../lib/str2rgbarray'); var arrayCopy1D = arrtools.copy1D; @@ -77,7 +77,7 @@ proto.merge = function(sceneLayout) { var axes = sceneLayout[AXES_NAMES[i]]; /////// Axes labels // - opts.labels[i] = convertHTML(axes.title); + opts.labels[i] = convertHTMLToUnicode(axes.title); if('titlefont' in axes) { if(axes.titlefont.color) opts.labelColor[i] = str2RgbaArray(axes.titlefont.color); if(axes.titlefont.family) opts.labelFont[i] = axes.titlefont.family; diff --git a/src/plots/gl3d/layout/tick_marks.js b/src/plots/gl3d/layout/tick_marks.js index 46746915388..be602485668 100644 --- a/src/plots/gl3d/layout/tick_marks.js +++ b/src/plots/gl3d/layout/tick_marks.js @@ -14,7 +14,7 @@ module.exports = computeTickMarks; var Plotly = require('../../../plotly'); -var convertHTML = require('../../../lib/html2unicode'); +var convertHTMLToUnicode = require('../../../lib/html2unicode'); var AXES_NAMES = ['xaxis', 'yaxis', 'zaxis']; @@ -70,7 +70,7 @@ function computeTickMarks(scene) { var dataTicks = Plotly.Axes.calcTicks(axes); for(var j = 0; j < dataTicks.length; ++j) { dataTicks[j].x = dataTicks[j].x * scene.dataScale[i]; - dataTicks[j].text = convertHTML(dataTicks[j].text); + dataTicks[j].text = convertHTMLToUnicode(dataTicks[j].text); } ticks[i] = dataTicks; From 9ba61a35747d0fd3220bc6b1541a4255f23b2241 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 9 Aug 2016 12:02:28 -0400 Subject: [PATCH 7/7] do not translate " entities --- src/constants/string_mappings.js | 1 - test/jasmine/tests/svg_text_utils_test.js | 8 ++++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/constants/string_mappings.js b/src/constants/string_mappings.js index 6953d0ae649..de41701e641 100644 --- a/src/constants/string_mappings.js +++ b/src/constants/string_mappings.js @@ -18,7 +18,6 @@ module.exports = { 'amp': '&', 'lt': '<', 'gt': '>', - 'quot': '\"', 'nbsp': ' ', 'times': '×', 'plusmn': '±', diff --git a/test/jasmine/tests/svg_text_utils_test.js b/test/jasmine/tests/svg_text_utils_test.js index d9887165c77..3468f65317c 100644 --- a/test/jasmine/tests/svg_text_utils_test.js +++ b/test/jasmine/tests/svg_text_utils_test.js @@ -122,8 +122,8 @@ describe('svg+text utils', function() { it('wrap XSS attacks in href', function() { var textCases = [ - 'Subtitle', - 'Subtitle' + 'Subtitle', + 'Subtitle' ]; textCases.forEach(function(textCase) { @@ -197,11 +197,11 @@ describe('svg+text utils', function() { it('decode some HTML entities in text', function() { var node = mockTextSVGElement( - '100μ & < 10 > 0"  ' + + '100μ & < 10 > 0  ' + '100 × 20 ± 0.5 °' ); - expect(node.text()).toEqual('100μ & < 10 > 0\"  100 × 20 ± 0.5 °'); + expect(node.text()).toEqual('100μ & < 10 > 0  100 × 20 ± 0.5 °'); }); }); });