Skip to content

Commit

Permalink
Merge pull request #480 from lineupjs/sgratzl/sanitize
Browse files Browse the repository at this point in the history
sanitize column labels, ...
  • Loading branch information
sgratzl committed Sep 14, 2021
2 parents 6a6a724 + 73db26a commit ae31245
Show file tree
Hide file tree
Showing 56 changed files with 433 additions and 247 deletions.
48 changes: 48 additions & 0 deletions demo/injection.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8" />
<title>LineUp Builder Test</title>

<link href="./LineUpJS.css" rel="stylesheet" />
<link href="./demo.css" rel="stylesheet" />
</head>
<body>
<script src="./LineUpJS.js"></script>

<script>
window.onload = function () {
const arr = [];
const cats = ['c1', 'c2', 'c3'];
for (let i = 0; i < 100; ++i) {
arr.push({
s: 'Row ' + i,
a: Math.random() * 10,
cat: cats[Math.floor(Math.random() * 3)],
d: new Date(Date.now() - Math.floor(Math.random() * 1000000000000)),
});
}
const b = LineUpJS.builder(arr)
.deriveColumns()
.column(
LineUpJS.buildStringColumn('s').label(
`<img src="empty.gif" onerror="alert();this.parentNode.removeChild(this);" />'`
)
)
.column(
LineUpJS.buildNumberColumn('a').label(
`<img src="empty.gif" onerror="alert();this.parentNode.removeChild(this);" />'`
)
)
.column(
LineUpJS.buildCategoricalColumn('cat', [
{ name: 'c1', label: `<img src="empty.gif" onerror="alert();this.parentNode.removeChild(this);" />'` },
{ name: 'c2', label: 'C2' },
{ name: 'c3', label: 'C3' },
]).label(`<img src="empty.gif" onerror="alert();this.parentNode.removeChild(this);" />'`)
);
const lineup = b.build(document.body);
};
</script>
</body>
</html>
16 changes: 13 additions & 3 deletions src/renderer/ActionRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,16 @@ export default class ActionRenderer implements ICellRendererFactory {
return col instanceof ActionColumn && mode !== ERenderMode.SUMMARY;
}

create(col: ActionColumn): ICellRenderer {
create(col: ActionColumn, context: IRenderContext): ICellRenderer {
const actions = col.actions;
return {
template: `<div class="${cssClass('actions')} ${cssClass('hover-only')}">${actions
.map((a) => `<span title='${a.name}' class='${a.className || ''}'>${a.icon || ''}</span>`)
.map(
(a) =>
`<span title='${context.sanitize(a.name)}' class='${context.sanitize(
a.className || ''
)}'>${context.sanitize(a.icon || '')}</span>`
)
.join('')}</div>`,
update: (n: HTMLElement, d: IDataRow) => {
forEachChild(n, (ni: HTMLSpanElement, i: number) => {
Expand All @@ -39,7 +44,12 @@ export default class ActionRenderer implements ICellRendererFactory {
const actions = col.groupActions;
return {
template: `<div class="${cssClass('actions')} ${cssClass('hover-only')}">${actions
.map((a) => `<span title='${a.name}' class='${a.className || ''}'>${a.icon || ''}</span>`)
.map(
(a) =>
`<span title='${context.sanitize(a.name)}' class='${context.sanitize(
a.className || ''
)}'>${context.sanitize(a.icon || '')}</span>`
)
.join('')}</div>`,
update: (n: HTMLElement, group: IOrderedGroup) => {
forEachChild(n, (ni: HTMLSpanElement, i: number) => {
Expand Down
12 changes: 6 additions & 6 deletions src/renderer/CategoricalCellRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export default class CategoricalCellRenderer implements ICellRendererFactory {
}

createGroup(col: ICategoricalLikeColumn, context: IRenderContext): IGroupCellRenderer {
const { template, update } = hist(col, false);
const { template, update } = hist(col, false, context.sanitize);
return {
template: `${template}</div>`,
update: (n: HTMLElement, group: IOrderedGroup) => {
Expand Down Expand Up @@ -93,7 +93,7 @@ export default class CategoricalCellRenderer implements ICellRendererFactory {
}

function staticSummary(col: ICategoricalLikeColumn, context: IRenderContext, interactive: boolean) {
const { template, update } = hist(col, interactive);
const { template, update } = hist(col, interactive, context.sanitize);
return {
template: `${template}</div>`,
update: (n: HTMLElement) => {
Expand All @@ -113,7 +113,7 @@ function staticSummary(col: ICategoricalLikeColumn, context: IRenderContext, int
}

function interactiveSummary(col: HasCategoricalFilter, context: IRenderContext, interactive: boolean) {
const { template, update } = hist(col, interactive || wideEnough(col));
const { template, update } = hist(col, interactive || wideEnough(col), context.sanitize);
let filterUpdate: (missing: number, col: HasCategoricalFilter) => void;
return {
template: `${template}${interactive ? filterMissingNumberMarkup(false, 0) : ''}</div>`,
Expand Down Expand Up @@ -141,13 +141,13 @@ function interactiveSummary(col: HasCategoricalFilter, context: IRenderContext,
};
}

function hist(col: ICategoricalLikeColumn, showLabels: boolean) {
function hist(col: ICategoricalLikeColumn, showLabels: boolean, sanitize: (v: string) => string) {
const mapping = col.getColorMapping();
const bins = col.categories
.map(
(c) =>
`<div class="${cssClass('histogram-bin')}" title="${c.label}: 0" data-cat="${c.name}" ${
showLabels ? `data-title="${c.label}"` : ''
`<div class="${cssClass('histogram-bin')}" title="${sanitize(c.label)}: 0" data-cat="${sanitize(c.name)}" ${
showLabels ? `data-title="${sanitize(c.label)}"` : ''
}><div style="height: 0; background-color: ${mapping.apply(c)}"></div></div>`
)
.join('');
Expand Down
6 changes: 4 additions & 2 deletions src/renderer/CategoricalHeatmapCellRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,14 +101,16 @@ export default class CategoricalHeatmapCellRenderer implements ICellRendererFact
};
}

createSummary(col: ICategoricalsColumn): ISummaryRenderer {
createSummary(col: ICategoricalsColumn, context: IRenderContext): ISummaryRenderer {
let labels = col.labels.slice();
while (labels.length > 0 && !wideEnough(col, labels.length)) {
labels = labels.filter((_, i) => i % 2 === 0); // even
}
let templateRows = `<div class="${cssClass('heatmap')}">`;
for (const label of labels) {
templateRows += `<div class="${cssClass('heatmap-cell')}" title="${label}" data-title="${label}"></div>`;
templateRows += `<div class="${cssClass('heatmap-cell')}" title="${context.sanitize(
label
)}" data-title="${context.sanitize(label)}"></div>`;
}
templateRows += '</div>';
return {
Expand Down
15 changes: 8 additions & 7 deletions src/renderer/CategoricalStackedDistributionlCellRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export default class CategoricalStackedDistributionlCellRenderer implements ICel
}

createGroup(col: ICategoricalColumn, context: IRenderContext): IGroupCellRenderer {
const { template, update } = stackedBar(col);
const { template, update } = stackedBar(col, context.sanitize);
return {
template: `${template}</div>`,
update: (n: HTMLElement, group: IOrderedGroup) => {
Expand All @@ -62,7 +62,7 @@ export default class CategoricalStackedDistributionlCellRenderer implements ICel
}

function staticSummary(col: ICategoricalColumn, context: IRenderContext) {
const { template, update } = stackedBar(col);
const { template, update } = stackedBar(col, context.sanitize);
return {
template: `${template}</div>`,
update: (n: HTMLElement) => {
Expand All @@ -82,7 +82,7 @@ function staticSummary(col: ICategoricalColumn, context: IRenderContext) {
}

function interactiveSummary(col: HasCategoricalFilter, context: IRenderContext, interactive: boolean) {
const { template, update } = stackedBar(col);
const { template, update } = stackedBar(col, context.sanitize);
let filterUpdate: (missing: number, col: HasCategoricalFilter) => void;
return {
template: `${template}${interactive ? filterMissingNumberMarkup(false, 0) : ''}</div>`,
Expand Down Expand Up @@ -116,7 +116,8 @@ function selectedCol(value: string) {
return c.toString();
}

function stackedBar(col: ISetColumn) {
function stackedBar(col: ISetColumn, sanitize: (v: string) => string) {
const s = sanitize;
const mapping = col.getColorMapping();
const cats = col.categories.map((c) => ({
label: c.label,
Expand All @@ -129,11 +130,11 @@ function stackedBar(col: ISetColumn) {
const bins = cats
.map(
(c) =>
`<div class="${cssClass('distribution-bar')}" style="background-color: ${
`<div class="${cssClass('distribution-bar')}" style="background-color: ${s(
c.color
}; color: ${adaptTextColorToBgColor(c.color)}" title="${c.label}: 0" data-cat="${c.name}"><span>${
)}; color: ${adaptTextColorToBgColor(c.color)}" title="${s(c.label)}: 0" data-cat="${s(c.name)}"><span>${s(
c.label
}</span></div>`
)}</span></div>`
)
.join('');

Expand Down
5 changes: 3 additions & 2 deletions src/renderer/DateHistogramCellRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ function interactiveSummary(
export function createDateFilter(
col: IDateColumn,
parent: HTMLElement,
context: { idPrefix: string; dialogManager: DialogManager; tasks: IRenderTasks },
context: { idPrefix: string; dialogManager: DialogManager; tasks: IRenderTasks; sanitize: (v: string) => string },
livePreviews: boolean
) {
const renderer = getHistDOMRenderer(col);
Expand Down Expand Up @@ -286,7 +286,7 @@ function createFilterInfo(col: IDateColumn, domain: [number, number], filter = c

function createFilterContext(
col: IDateColumn,
context: { idPrefix: string; dialogManager: DialogManager },
context: { idPrefix: string; dialogManager: DialogManager; sanitize: (v: string) => string },
domain: [number, number]
): IFilterContext<number> {
const clamp = (v: number) => Math.max(0, Math.min(100, v));
Expand All @@ -312,6 +312,7 @@ function createFilterContext(
manager: context.dialogManager,
level: context.dialogManager.maxLevel + 1,
idPrefix: context.idPrefix,
sanitize: context.sanitize,
};
const dialog = new InputDateDialog(
dialogCtx,
Expand Down
6 changes: 3 additions & 3 deletions src/renderer/DotCellRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export default class DotCellRenderer implements ICellRendererFactory {
};
}

private static getDOMRenderer(col: INumberColumn) {
private static getDOMRenderer(col: INumberColumn, sanitize: (v: string) => string) {
const dots = isNumbersColumn(col) ? col.dataLength! : 1;
let tmp = '';
for (let i = 0; i < dots; ++i) {
Expand All @@ -68,7 +68,7 @@ export default class DotCellRenderer implements ICellRendererFactory {
const l = data.length;
if (n.children.length !== l) {
n.innerHTML = data.reduce((tmp, r) => {
return `${tmp}<div style='background-color: ${r.color}' title='${r.label}'></div>`;
return `${tmp}<div style='background-color: ${sanitize(r.color)}' title='${sanitize(r.label)}'></div>`;
}, '');
}
const children = n.children;
Expand Down Expand Up @@ -97,7 +97,7 @@ export default class DotCellRenderer implements ICellRendererFactory {
}

create(col: INumberColumn, context: IRenderContext, imposer?: IImposer): ICellRenderer {
const { template, render, update } = DotCellRenderer.getDOMRenderer(col);
const { template, render, update } = DotCellRenderer.getDOMRenderer(col, context.sanitize);
const width = context.colWidth(col);
const formatter = col.getNumberFormat();
return {
Expand Down
6 changes: 4 additions & 2 deletions src/renderer/HeatmapCellRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,14 +103,16 @@ export default class HeatmapCellRenderer implements ICellRendererFactory {
};
}

createSummary(col: INumbersColumn): ISummaryRenderer {
createSummary(col: INumbersColumn, context: IRenderContext): ISummaryRenderer {
let labels = col.labels.slice();
while (labels.length > 0 && !wideEnough(col, labels.length)) {
labels = labels.filter((_, i) => i % 2 === 0); // even
}
let templateRows = `<div class="${cssClass('heatmap')}">`;
for (const label of labels) {
templateRows += `<div class="${cssClass('heatmap-cell')}" title="${label}" data-title="${label}"></div>`;
templateRows += `<div class="${cssClass('heatmap-cell')}" title="${context.sanitize(
label
)}" data-title="${context.sanitize(label)}"></div>`;
}
templateRows += '</div>';
return {
Expand Down
5 changes: 3 additions & 2 deletions src/renderer/HistogramCellRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ function interactiveSummary(
export function createNumberFilter(
col: INumberColumn & IMapAbleColumn,
parent: HTMLElement,
context: { idPrefix: string; dialogManager: DialogManager; tasks: IRenderTasks },
context: { idPrefix: string; dialogManager: DialogManager; tasks: IRenderTasks; sanitize: (v: string) => string },
livePreviews: boolean
) {
const renderer = getHistDOMRenderer(col);
Expand Down Expand Up @@ -276,7 +276,7 @@ function createFilterInfo(col: IMapAbleColumn, filter = col.getFilter()): IFilte

function createFilterContext(
col: IMapAbleColumn,
context: { idPrefix: string; dialogManager: DialogManager }
context: { idPrefix: string; dialogManager: DialogManager; sanitize: (v: string) => string }
): IFilterContext<number> {
const domain = col.getMapping().domain;
const format = col.getNumberFormat();
Expand All @@ -303,6 +303,7 @@ function createFilterContext(
manager: context.dialogManager,
level: context.dialogManager.maxLevel + 1,
idPrefix: context.idPrefix,
sanitize: context.sanitize,
};
const dialog = new InputNumberDialog(dialogCtx, resolve, {
value,
Expand Down
45 changes: 32 additions & 13 deletions src/renderer/LinkCellRenderer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { LinkColumn, Column, IDataRow, IOrderedGroup } from '../model';
import { LinkColumn, Column, IDataRow, IOrderedGroup, ILink } from '../model';
import {
IRenderContext,
ERenderMode,
Expand All @@ -10,7 +10,7 @@ import {
import { renderMissingDOM } from './missing';
import { noRenderer, setText } from './utils';
import { cssClass } from '../styles';
import type { ISequence } from '../internal';
import { clear, ISequence } from '../internal';

export default class LinkCellRenderer implements ICellRendererFactory {
readonly title: string = 'Link';
Expand All @@ -19,8 +19,8 @@ export default class LinkCellRenderer implements ICellRendererFactory {
return col instanceof LinkColumn && mode !== ERenderMode.SUMMARY;
}

create(col: LinkColumn): ICellRenderer {
const align = col.alignment || 'left';
create(col: LinkColumn, context: IRenderContext): ICellRenderer {
const align = context.sanitize(col.alignment || 'left');
return {
template: `<a${
align !== 'left' ? ` class="${cssClass(align)}"` : ''
Expand All @@ -38,21 +38,21 @@ export default class LinkCellRenderer implements ICellRendererFactory {
};
}

private static exampleText(col: LinkColumn, rows: ISequence<IDataRow>) {
private static exampleText(col: LinkColumn, rows: ISequence<IDataRow>): [ILink[], boolean] {
const numExampleRows = 5;
const examples: string[] = [];
const examples: ILink[] = [];
rows.every((row) => {
const v = col.getLink(row);
if (!v) {
return true;
}
examples.push(`<a target="_blank" rel="noopener" href="${v.href}">${v.alt}</a>`);
examples.push(v);
return examples.length < numExampleRows;
});
if (examples.length === 0) {
return '';
return [[], false];
}
return `${examples.join(', ')}${examples.length < rows.length ? ', &hellip;' : ''}`;
return [examples, examples.length < rows.length];
}

createGroup(col: LinkColumn, context: IRenderContext): IGroupCellRenderer {
Expand All @@ -61,12 +61,12 @@ export default class LinkCellRenderer implements ICellRendererFactory {
update: (n: HTMLDivElement, group: IOrderedGroup) => {
return context.tasks
.groupExampleRows(col, group, 'link', (rows) => LinkCellRenderer.exampleText(col, rows))
.then((text) => {
if (typeof text === 'symbol') {
.then((out) => {
if (typeof out === 'symbol') {
return;
}
n.classList.toggle(cssClass('missing'), !text);
n.innerHTML = text;
const [links, more] = out;
updateLinkList(n, links, more);
});
},
};
Expand All @@ -76,3 +76,22 @@ export default class LinkCellRenderer implements ICellRendererFactory {
return noRenderer;
}
}

export function updateLinkList(n: HTMLElement, links: ILink[], more: boolean) {
n.classList.toggle(cssClass('missing'), links.length === 0);
clear(n);
links.forEach((l, i) => {
if (i > 0) {
n.appendChild(n.ownerDocument.createTextNode(', '));
}
const a = n.ownerDocument.createElement('a');
a.href = l.href;
a.textContent = l.alt;
a.target = '_blank';
a.rel = 'noopener';
n.appendChild(a);
});
if (more) {
n.insertAdjacentText('beforeend', ', …');
}
}
Loading

0 comments on commit ae31245

Please sign in to comment.