Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Decode some html entities #835

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

Merged
merged 7 commits into from
Aug 9, 2016
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
36 changes: 36 additions & 0 deletions src/constants/string_mappings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* 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': '>',
'nbsp': ' ',
'times': '×',
'plusmn': '±',
'deg': '°'
},

unicodeToEntity: {
'&': 'amp',
'<': 'lt',
'>': 'gt',
'"': 'quot',
'\'': '#x27',
'\/': '#x2F'
}

};
11 changes: 3 additions & 8 deletions src/lib/html2unicode.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -40,6 +34,7 @@ function stripTags(x) {
}

function fixEntities(x) {
var entityToUnicode = stringMappings.entityToUnicode;
var idx = 0;

while((idx = x.indexOf('&', idx)) >= 0) {
Expand All @@ -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 {
Expand Down
62 changes: 44 additions & 18 deletions src/lib/svg_text_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +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 = {};
var stringMappings = require('../constants/string_mappings');

// Append SVG

Expand Down Expand Up @@ -45,7 +43,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 === '&lt;') { return '&#60;'; } // special handling for brackets
Expand All @@ -56,7 +54,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, '&amp;');
};

Expand All @@ -66,10 +64,11 @@ 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;

// 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(/([^$]*)([$]+[^$]*[$]+)([^$]*)/);
Expand Down Expand Up @@ -112,7 +111,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)};
Expand Down Expand Up @@ -195,7 +194,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'})
Expand Down Expand Up @@ -236,22 +235,48 @@ var PROTOCOLS = ['http:', 'https:', 'mailto:'];

var STRIP_TAGS = new RegExp('</?(' + Object.keys(TAG_STYLES).join('|') + ')( [^>]*)?/?>', 'g');

util.plainText = function(_str) {
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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#x27;')
.replace(/\//g, '&#x2F;');
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),
Expand All @@ -270,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 '</a>';
Expand Down Expand Up @@ -316,7 +342,7 @@ function convertToSVG(_str) {
}
}
else {
return Plotly.util.xml_entity_encode(d).replace(/</g, '&lt;');
return exports.xml_entity_encode(d).replace(/</g, '&lt;');
}
});

Expand Down Expand Up @@ -397,7 +423,7 @@ function alignHTMLWith(_base, container, options) {

// Editable title

util.makeEditable = function(context, _delegate, options) {
exports.makeEditable = function(context, _delegate, options) {
if(!options) options = {};
var that = this;
var dispatch = d3.dispatch('edit', 'input', 'cancel');
Expand Down Expand Up @@ -431,7 +457,7 @@ util.makeEditable = function(context, _delegate, options) {
}

function appendEditable() {
var plotDiv = d3.select(Plotly.Lib.getPlotDiv(that.node())),
var plotDiv = d3.select(Lib.getPlotDiv(that.node())),
container = plotDiv.select('.svg-container'),
div = container.append('div');
div.classed('plugin-editable editable', true)
Expand Down
4 changes: 2 additions & 2 deletions src/plots/gl2d/convert.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions src/plots/gl2d/scene2d.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
Expand Down Expand Up @@ -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, ' ');
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/plots/gl3d/layout/convert.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions src/plots/gl3d/layout/tick_marks.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'];

Expand Down Expand Up @@ -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;

Expand Down
2 changes: 1 addition & 1 deletion test/image/mocks/axes_enumerated_ticks.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"xaxis": {
"ticktext": [
"<span style=\"fill:green\">green</span> eggs",
"& ham",
"&amp; ham",
"H<sub>2</sub>O",
"Gorgonzola"
],
Expand Down
50 changes: 29 additions & 21 deletions test/jasmine/tests/svg_text_utils_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,34 +121,33 @@ describe('svg+text utils', function() {
});

it('wrap XSS attacks in href', function() {
var node = mockTextSVGElement(
var textCases = [
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reverting a commit from PR #804, as now &lt;, &gt; and &quot; are now back in play.

'<a href="XSS\" onmouseover="alert(1)\" style="font-size:300px">Subtitle</a>',
'<a href="XSS" onmouseover="alert(1)" style="font-size:300px">Subtitle</a>'
);

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(
'<a href="XSS&quot; onmouseover=&quot;alert(1)&quot; style=&quot;font-size:300px">Subtitle</a>'
);
textCases.forEach(function(textCase) {
var node = mockTextSVGElement(textCase);

console.log(node.select('a').attr('xlink:href'));
expect(node.text()).toEqual('Subtitle');
assertAnchorAttrs(node);
assertAnchorLink(node, 'XSS&quot; onmouseover=&quot;alert(1)&quot; style=&quot;font-size:300px');
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(
'<a href="https://abc.com/myFeature.jsp?name=abc&pwd=def">abc.com?shared-key</a>'
);
var textCases = [
'<a href="https://abc.com/myFeature.jsp?name=abc&pwd=def">abc.com?shared-key</a>',
'<a href="https://abc.com/myFeature.jsp?name=abc&amp;pwd=def">abc.com?shared-key</a>'
];

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() {
Expand Down Expand Up @@ -195,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&mu; &amp; &lt; 10 &gt; 0 &nbsp;' +
'100 &times; 20 &plusmn; 0.5 &deg;'
);

expect(node.text()).toEqual('100μ & < 10 > 0  100 × 20 ± 0.5 °');
});
});
});