mirror of
https://github.com/codex-team/editor.js
synced 2026-03-16 07:35:48 +01:00
test: add tests for sanitisation
This commit is contained in:
parent
d14b971774
commit
bd40b0ff69
13 changed files with 1308 additions and 113 deletions
|
|
@ -74,8 +74,8 @@ export default class MoveUpTune implements BlockTune {
|
|||
* - when previous block is visible and has offset from the window,
|
||||
* than we scroll window to the difference between this offsets.
|
||||
*/
|
||||
const currentBlockCoords = currentBlockElement.getBoundingClientRect(),
|
||||
previousBlockCoords = previousBlockElement.getBoundingClientRect();
|
||||
const currentBlockCoords = currentBlockElement.getBoundingClientRect();
|
||||
const previousBlockCoords = previousBlockElement.getBoundingClientRect();
|
||||
|
||||
let scrollUpOffset;
|
||||
|
||||
|
|
|
|||
|
|
@ -155,11 +155,7 @@ export default class Core {
|
|||
};
|
||||
|
||||
this.config.placeholder = this.config.placeholder ?? false;
|
||||
this.config.sanitizer = this.config.sanitizer || {
|
||||
p: true,
|
||||
b: true,
|
||||
a: true,
|
||||
} as SanitizerConfig;
|
||||
this.config.sanitizer = this.config.sanitizer ?? {} as SanitizerConfig;
|
||||
|
||||
this.config.hideToolbar = this.config.hideToolbar ?? false;
|
||||
this.config.tools = this.config.tools || {};
|
||||
|
|
|
|||
|
|
@ -132,8 +132,8 @@ export default class Dom {
|
|||
*/
|
||||
public static swap(el1: HTMLElement, el2: HTMLElement): void {
|
||||
// create marker element and insert it where el1 is
|
||||
const temp = document.createElement('div'),
|
||||
parent = el1.parentNode;
|
||||
const temp = document.createElement('div');
|
||||
const parent = el1.parentNode;
|
||||
|
||||
parent?.insertBefore(temp, el1);
|
||||
|
||||
|
|
@ -231,8 +231,8 @@ export default class Dom {
|
|||
*
|
||||
* @type {string}
|
||||
*/
|
||||
const child = atLast ? 'lastChild' : 'firstChild',
|
||||
sibling = atLast ? 'previousSibling' : 'nextSibling';
|
||||
const child = atLast ? 'lastChild' : 'firstChild';
|
||||
const sibling = atLast ? 'previousSibling' : 'nextSibling';
|
||||
|
||||
if (node === null || node.nodeType !== Node.ELEMENT_NODE) {
|
||||
return node;
|
||||
|
|
|
|||
|
|
@ -657,10 +657,10 @@ export default class BlockEvents extends Module {
|
|||
* @param {KeyboardEvent} event - keyboard event
|
||||
*/
|
||||
private needToolbarClosing(event: KeyboardEvent): boolean {
|
||||
const toolboxItemSelected = (event.keyCode === _.keyCodes.ENTER && this.Editor.Toolbar.toolbox.opened),
|
||||
blockSettingsItemSelected = (event.keyCode === _.keyCodes.ENTER && this.Editor.BlockSettings.opened),
|
||||
inlineToolbarItemSelected = (event.keyCode === _.keyCodes.ENTER && this.Editor.InlineToolbar.opened),
|
||||
flippingToolbarItems = event.keyCode === _.keyCodes.TAB;
|
||||
const toolboxItemSelected = (event.keyCode === _.keyCodes.ENTER && this.Editor.Toolbar.toolbox.opened);
|
||||
const blockSettingsItemSelected = (event.keyCode === _.keyCodes.ENTER && this.Editor.BlockSettings.opened);
|
||||
const inlineToolbarItemSelected = (event.keyCode === _.keyCodes.ENTER && this.Editor.InlineToolbar.opened);
|
||||
const flippingToolbarItems = event.keyCode === _.keyCodes.TAB;
|
||||
|
||||
/**
|
||||
* Do not close Toolbar in cases:
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import Module from '../__module';
|
|||
import $ from '../dom';
|
||||
import * as _ from '../utils';
|
||||
import Blocks from '../blocks';
|
||||
import type { BlockToolData, PasteEvent } from '../../../types';
|
||||
import type { BlockToolData, PasteEvent, SanitizerConfig } from '../../../types';
|
||||
import type { BlockTuneData } from '../../../types/block-tunes/block-tune-data';
|
||||
import BlockAPI from '../block/api';
|
||||
import type { BlockMutationEventMap, BlockMutationType } from '../../../types/events/block';
|
||||
|
|
@ -18,7 +18,7 @@ import { BlockAddedMutationType } from '../../../types/events/block/BlockAdded';
|
|||
import { BlockMovedMutationType } from '../../../types/events/block/BlockMoved';
|
||||
import { BlockChangedMutationType } from '../../../types/events/block/BlockChanged';
|
||||
import { BlockChanged } from '../events';
|
||||
import { clean, sanitizeBlocks } from '../utils/sanitizer';
|
||||
import { clean, composeSanitizerConfig, sanitizeBlocks } from '../utils/sanitizer';
|
||||
import { convertStringToBlockData, isBlockConvertable } from '../utils/blocks';
|
||||
import PromiseQueue from '../utils/promise-queue';
|
||||
|
||||
|
|
@ -493,7 +493,11 @@ export default class BlockManager extends Module {
|
|||
return;
|
||||
}
|
||||
|
||||
const [ cleanData ] = sanitizeBlocks([ blockToMergeDataRaw ], targetBlock.tool.sanitizeConfig);
|
||||
const [ cleanData ] = sanitizeBlocks(
|
||||
[ blockToMergeDataRaw ],
|
||||
targetBlock.tool.sanitizeConfig,
|
||||
this.config.sanitizer as SanitizerConfig
|
||||
);
|
||||
|
||||
blockToMergeData = cleanData;
|
||||
|
||||
|
|
@ -689,9 +693,9 @@ export default class BlockManager extends Module {
|
|||
element = element.parentNode as HTMLElement;
|
||||
}
|
||||
|
||||
const nodes = this._blocks.nodes,
|
||||
firstLevelBlock = element.closest(`.${Block.CSS.wrapper}`),
|
||||
index = nodes.indexOf(firstLevelBlock as HTMLElement);
|
||||
const nodes = this._blocks.nodes;
|
||||
const firstLevelBlock = element.closest(`.${Block.CSS.wrapper}`);
|
||||
const index = nodes.indexOf(firstLevelBlock as HTMLElement);
|
||||
|
||||
if (index >= 0) {
|
||||
return this._blocks[index];
|
||||
|
|
@ -857,7 +861,7 @@ export default class BlockManager extends Module {
|
|||
*/
|
||||
const cleanData: string = clean(
|
||||
exportedData,
|
||||
replacingTool.sanitizeConfig
|
||||
composeSanitizerConfig(this.config.sanitizer as SanitizerConfig, replacingTool.sanitizeConfig)
|
||||
);
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import Shortcuts from '../utils/shortcuts';
|
|||
|
||||
import SelectionUtils from '../selection';
|
||||
import type { SanitizerConfig } from '../../../types/configs';
|
||||
import { clean } from '../utils/sanitizer';
|
||||
import { clean, composeSanitizerConfig } from '../utils/sanitizer';
|
||||
|
||||
/**
|
||||
*
|
||||
|
|
@ -33,7 +33,7 @@ export default class BlockSelection extends Module {
|
|||
* @returns {SanitizerConfig}
|
||||
*/
|
||||
private get sanitizerConfig(): SanitizerConfig {
|
||||
return {
|
||||
const baseConfig: SanitizerConfig = {
|
||||
p: {},
|
||||
h1: {},
|
||||
h2: {},
|
||||
|
|
@ -57,6 +57,8 @@ export default class BlockSelection extends Module {
|
|||
i: {},
|
||||
u: {},
|
||||
};
|
||||
|
||||
return composeSanitizerConfig(this.config.sanitizer as SanitizerConfig, baseConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import type {
|
|||
} from '../../../types';
|
||||
import type Block from '../block';
|
||||
import type { SavedData } from '../../../types/data-formats';
|
||||
import { clean, sanitizeBlocks } from '../utils/sanitizer';
|
||||
import { clean, composeSanitizerConfig, sanitizeBlocks } from '../utils/sanitizer';
|
||||
import type BlockToolAdapter from '../tools/block';
|
||||
|
||||
/**
|
||||
|
|
@ -214,7 +214,13 @@ export default class Paste extends Module {
|
|||
return result;
|
||||
}, {} as SanitizerConfig);
|
||||
|
||||
const customConfig = Object.assign({}, toolsTags, Tools.getAllInlineToolsSanitizeConfig(), { br: {} });
|
||||
const inlineSanitizeConfig = Tools.getAllInlineToolsSanitizeConfig();
|
||||
const customConfig = composeSanitizerConfig(
|
||||
this.config.sanitizer as SanitizerConfig,
|
||||
toolsTags,
|
||||
inlineSanitizeConfig,
|
||||
{ br: {} }
|
||||
);
|
||||
const cleanData = clean(htmlData, customConfig);
|
||||
|
||||
/** If there is no HTML or HTML string is equal to plain one, process it as plain text */
|
||||
|
|
@ -608,7 +614,7 @@ export default class Paste extends Module {
|
|||
|
||||
return nodes
|
||||
.map((node) => {
|
||||
let content: HTMLElement | null | undefined, tool = Tools.defaultTool, isBlock = false;
|
||||
let content: HTMLElement | null | undefined; let tool = Tools.defaultTool; let isBlock = false;
|
||||
|
||||
switch (node.nodeType) {
|
||||
/** If node is a document fragment, use temp wrapper to get innerHTML */
|
||||
|
|
@ -897,8 +903,10 @@ export default class Paste extends Module {
|
|||
*/
|
||||
private insertEditorJSData(blocks: Pick<SavedData, 'id' | 'data' | 'tool'>[]): void {
|
||||
const { BlockManager, Caret, Tools } = this.Editor;
|
||||
const sanitizedBlocks = sanitizeBlocks(blocks, (name) =>
|
||||
Tools.blockTools.get(name)?.sanitizeConfig ?? {}
|
||||
const sanitizedBlocks = sanitizeBlocks(
|
||||
blocks,
|
||||
(name) => Tools.blockTools.get(name)?.sanitizeConfig ?? {},
|
||||
this.config.sanitizer as SanitizerConfig
|
||||
);
|
||||
|
||||
sanitizedBlocks.forEach(({ tool, data }, i) => {
|
||||
|
|
|
|||
|
|
@ -480,7 +480,7 @@ export default class RectangleSelection extends Module {
|
|||
private trySelectNextBlock(index): void {
|
||||
const sameBlock = this.stackOfSelected[this.stackOfSelected.length - 1] === index;
|
||||
const sizeStack = this.stackOfSelected.length;
|
||||
const down = 1, up = -1, undef = 0;
|
||||
const down = 1; const up = -1; const undef = 0;
|
||||
|
||||
if (sameBlock) {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
* @version 2.0.0
|
||||
*/
|
||||
import Module from '../__module';
|
||||
import type { OutputData } from '../../../types';
|
||||
import type { OutputData, SanitizerConfig } from '../../../types';
|
||||
import type { SavedData, ValidatedData } from '../../../types/data-formats';
|
||||
import type Block from '../block';
|
||||
import * as _ from '../utils';
|
||||
|
|
@ -28,8 +28,8 @@ export default class Saver extends Module {
|
|||
*/
|
||||
public async save(): Promise<OutputData> {
|
||||
const { BlockManager, Tools } = this.Editor;
|
||||
const blocks = BlockManager.blocks,
|
||||
chainData = [];
|
||||
const blocks = BlockManager.blocks;
|
||||
const chainData = [];
|
||||
|
||||
try {
|
||||
blocks.forEach((block: Block) => {
|
||||
|
|
@ -37,9 +37,11 @@ export default class Saver extends Module {
|
|||
});
|
||||
|
||||
const extractedData = await Promise.all(chainData) as Array<Pick<SavedData, 'data' | 'tool'>>;
|
||||
const sanitizedData = await sanitizeBlocks(extractedData, (name) => {
|
||||
return Tools.blockTools.get(name).sanitizeConfig;
|
||||
});
|
||||
const sanitizedData = await sanitizeBlocks(
|
||||
extractedData,
|
||||
(name) => Tools.blockTools.get(name).sanitizeConfig,
|
||||
this.config.sanitizer as SanitizerConfig
|
||||
);
|
||||
|
||||
return this.makeOutput(sanitizedData);
|
||||
} catch (e) {
|
||||
|
|
|
|||
|
|
@ -130,14 +130,14 @@ if (typeof Element.prototype.scrollIntoViewIfNeeded === 'undefined') {
|
|||
return;
|
||||
}
|
||||
|
||||
const parentComputedStyle = window.getComputedStyle(parent, null),
|
||||
parentBorderTopWidth = parseInt(parentComputedStyle.getPropertyValue('border-top-width')),
|
||||
parentBorderLeftWidth = parseInt(parentComputedStyle.getPropertyValue('border-left-width')),
|
||||
overTop = this.offsetTop - parent.offsetTop < parent.scrollTop,
|
||||
overBottom = (this.offsetTop - parent.offsetTop + this.clientHeight - parentBorderTopWidth) > (parent.scrollTop + parent.clientHeight),
|
||||
overLeft = this.offsetLeft - parent.offsetLeft < parent.scrollLeft,
|
||||
overRight = (this.offsetLeft - parent.offsetLeft + this.clientWidth - parentBorderLeftWidth) > (parent.scrollLeft + parent.clientWidth),
|
||||
alignWithTop = overTop && !overBottom;
|
||||
const parentComputedStyle = window.getComputedStyle(parent, null);
|
||||
const parentBorderTopWidth = parseInt(parentComputedStyle.getPropertyValue('border-top-width'));
|
||||
const parentBorderLeftWidth = parseInt(parentComputedStyle.getPropertyValue('border-left-width'));
|
||||
const overTop = this.offsetTop - parent.offsetTop < parent.scrollTop;
|
||||
const overBottom = (this.offsetTop - parent.offsetTop + this.clientHeight - parentBorderTopWidth) > (parent.scrollTop + parent.clientHeight);
|
||||
const overLeft = this.offsetLeft - parent.offsetLeft < parent.scrollLeft;
|
||||
const overRight = (this.offsetLeft - parent.offsetLeft + this.clientWidth - parentBorderLeftWidth) > (parent.scrollLeft + parent.clientWidth);
|
||||
const alignWithTop = overTop && !overBottom;
|
||||
|
||||
if ((overTop || overBottom) && centerIfNeeded) {
|
||||
parent.scrollTop = this.offsetTop - parent.offsetTop - parent.clientHeight / 2 - parentBorderTopWidth + this.clientHeight / 2;
|
||||
|
|
|
|||
|
|
@ -234,8 +234,8 @@ export default class SelectionUtils {
|
|||
* @returns {DOMRect}
|
||||
*/
|
||||
public static get rect(): DOMRect {
|
||||
let sel: Selection | MSSelection | undefined | null = (document as Document).selection,
|
||||
range: TextRange | Range;
|
||||
let sel: Selection | MSSelection | undefined | null = (document as Document).selection;
|
||||
let range: TextRange | Range;
|
||||
|
||||
let rect = {
|
||||
x: 0,
|
||||
|
|
@ -414,6 +414,7 @@ export default class SelectionUtils {
|
|||
public removeFakeBackground(): void {
|
||||
if (!this.fakeBackgroundElements.length) {
|
||||
this.isFakeBackgroundEnabled = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -30,9 +30,13 @@ import * as _ from '../utils';
|
|||
*/
|
||||
|
||||
import HTMLJanitor from 'html-janitor';
|
||||
import type { BlockToolData, SanitizerConfig } from '../../../types';
|
||||
import type { BlockToolData, SanitizerConfig, SanitizerRule } from '../../../types';
|
||||
import type { SavedData } from '../../../types/data-formats';
|
||||
|
||||
type DeepSanitizerRule = SanitizerConfig | SanitizerRule | boolean;
|
||||
|
||||
const UNSAFE_URL_ATTR_PATTERN = /\s*(href|src)\s*=\s*(["']?)\s*(?:javascript:|data:text\/html)[^"' >]*\2/gi;
|
||||
|
||||
/**
|
||||
* Sanitize Blocks
|
||||
*
|
||||
|
|
@ -40,23 +44,27 @@ import type { SavedData } from '../../../types/data-formats';
|
|||
*
|
||||
* @param blocksData - blocks' data to sanitize
|
||||
* @param sanitizeConfig — sanitize config to use or function to get config for Tool
|
||||
* @param globalSanitizer — global sanitizer config defined on editor level
|
||||
*/
|
||||
export function sanitizeBlocks(
|
||||
export const sanitizeBlocks = (
|
||||
blocksData: Array<Pick<SavedData, 'data' | 'tool'>>,
|
||||
sanitizeConfig: SanitizerConfig | ((toolName: string) => SanitizerConfig)
|
||||
): Array<Pick<SavedData, 'data' | 'tool'>> {
|
||||
sanitizeConfig: SanitizerConfig | ((toolName: string) => SanitizerConfig | undefined),
|
||||
globalSanitizer: SanitizerConfig = {} as SanitizerConfig
|
||||
): Array<Pick<SavedData, 'data' | 'tool'>> => {
|
||||
return blocksData.map((block) => {
|
||||
const toolConfig = _.isFunction(sanitizeConfig) ? sanitizeConfig(block.tool) : sanitizeConfig;
|
||||
const rules = toolConfig ?? ({} as SanitizerConfig);
|
||||
|
||||
if (_.isEmpty(toolConfig)) {
|
||||
if (_.isEmpty(rules) && _.isEmpty(globalSanitizer)) {
|
||||
return block;
|
||||
}
|
||||
|
||||
block.data = deepSanitize(block.data, toolConfig) as BlockToolData;
|
||||
|
||||
return block;
|
||||
return {
|
||||
...block,
|
||||
data: deepSanitize(block.data, rules, globalSanitizer) as BlockToolData,
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Cleans string from unwanted tags
|
||||
* Method allows to use default config
|
||||
|
|
@ -65,7 +73,7 @@ export function sanitizeBlocks(
|
|||
* @param {SanitizerConfig} customConfig - allowed tags
|
||||
* @returns {string} clean HTML
|
||||
*/
|
||||
export function clean(taintString: string, customConfig: SanitizerConfig = {} as SanitizerConfig): string {
|
||||
export const clean = (taintString: string, customConfig: SanitizerConfig = {} as SanitizerConfig): string => {
|
||||
const sanitizerConfig = {
|
||||
tags: customConfig,
|
||||
};
|
||||
|
|
@ -76,15 +84,20 @@ export function clean(taintString: string, customConfig: SanitizerConfig = {} as
|
|||
const sanitizerInstance = new HTMLJanitor(sanitizerConfig);
|
||||
|
||||
return sanitizerInstance.clean(taintString);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Method recursively reduces Block's data and cleans with passed rules
|
||||
*
|
||||
* @param {BlockToolData|object|*} dataToSanitize - taint string or object/array that contains taint string
|
||||
* @param {SanitizerConfig} rules - object with sanitizer rules
|
||||
* @param {SanitizerConfig} globalRules - global sanitizer config
|
||||
*/
|
||||
function deepSanitize(dataToSanitize: object | string, rules: SanitizerConfig): object | string {
|
||||
const deepSanitize = (
|
||||
dataToSanitize: object | string,
|
||||
rules: DeepSanitizerRule,
|
||||
globalRules: SanitizerConfig
|
||||
): object | string => {
|
||||
/**
|
||||
* BlockData It may contain 3 types:
|
||||
* - Array
|
||||
|
|
@ -95,12 +108,12 @@ function deepSanitize(dataToSanitize: object | string, rules: SanitizerConfig):
|
|||
/**
|
||||
* Array: call sanitize for each item
|
||||
*/
|
||||
return cleanArray(dataToSanitize, rules);
|
||||
return cleanArray(dataToSanitize, rules, globalRules);
|
||||
} else if (_.isObject(dataToSanitize)) {
|
||||
/**
|
||||
* Objects: just clean object deeper.
|
||||
*/
|
||||
return cleanObject(dataToSanitize, rules);
|
||||
return cleanObject(dataToSanitize, rules, globalRules);
|
||||
} else {
|
||||
/**
|
||||
* Primitives (number|string|boolean): clean this item
|
||||
|
|
@ -108,32 +121,42 @@ function deepSanitize(dataToSanitize: object | string, rules: SanitizerConfig):
|
|||
* Clean only strings
|
||||
*/
|
||||
if (_.isString(dataToSanitize)) {
|
||||
return cleanOneItem(dataToSanitize, rules);
|
||||
return cleanOneItem(dataToSanitize, rules, globalRules);
|
||||
}
|
||||
|
||||
return dataToSanitize;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Clean array
|
||||
*
|
||||
* @param {Array} array - [1, 2, {}, []]
|
||||
* @param {SanitizerConfig} ruleForItem - sanitizer config for array
|
||||
* @param globalRules
|
||||
*/
|
||||
function cleanArray(array: Array<object | string>, ruleForItem: SanitizerConfig): Array<object | string> {
|
||||
return array.map((arrayItem) => deepSanitize(arrayItem, ruleForItem));
|
||||
}
|
||||
const cleanArray = (
|
||||
array: Array<object | string>,
|
||||
ruleForItem: DeepSanitizerRule,
|
||||
globalRules: SanitizerConfig
|
||||
): Array<object | string> => {
|
||||
return array.map((arrayItem) => deepSanitize(arrayItem, ruleForItem, globalRules));
|
||||
};
|
||||
|
||||
/**
|
||||
* Clean object
|
||||
*
|
||||
* @param {object} object - {level: 0, text: 'adada', items: [1,2,3]}}
|
||||
* @param {object} rules - { b: true } or true|false
|
||||
* @param globalRules
|
||||
* @returns {object}
|
||||
*/
|
||||
function cleanObject(object: object, rules: SanitizerConfig|{[field: string]: SanitizerConfig}): object {
|
||||
const cleanData = {};
|
||||
const cleanObject = (
|
||||
object: object,
|
||||
rules: DeepSanitizerRule | Record<string, DeepSanitizerRule>,
|
||||
globalRules: SanitizerConfig
|
||||
): object => {
|
||||
const cleanData: Record<string, unknown> = {};
|
||||
|
||||
for (const fieldName in object) {
|
||||
if (!Object.prototype.hasOwnProperty.call(object, fieldName)) {
|
||||
|
|
@ -147,30 +170,47 @@ function cleanObject(object: object, rules: SanitizerConfig|{[field: string]: Sa
|
|||
* - if it is a HTML Janitor rule, call with this rule
|
||||
* - otherwise, call with parent's config
|
||||
*/
|
||||
const ruleForItem = isRule(rules[fieldName] as SanitizerConfig) ? rules[fieldName] : rules;
|
||||
const rulesRecord = _.isObject(rules) ? (rules as Record<string, DeepSanitizerRule>) : undefined;
|
||||
const ruleCandidate = rulesRecord?.[fieldName];
|
||||
const ruleForItem = ruleCandidate !== undefined && isRule(ruleCandidate)
|
||||
? ruleCandidate
|
||||
: rules;
|
||||
|
||||
cleanData[fieldName] = deepSanitize(currentIterationItem, ruleForItem as SanitizerConfig);
|
||||
cleanData[fieldName] = deepSanitize(currentIterationItem, ruleForItem as DeepSanitizerRule, globalRules);
|
||||
}
|
||||
|
||||
return cleanData;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Clean primitive value
|
||||
*
|
||||
* @param {string} taintString - string to clean
|
||||
* @param {SanitizerConfig|boolean} rule - sanitizer rule
|
||||
* @param globalRules
|
||||
* @returns {string}
|
||||
*/
|
||||
function cleanOneItem(taintString: string, rule: SanitizerConfig|boolean): string {
|
||||
if (_.isObject(rule)) {
|
||||
return clean(taintString, rule);
|
||||
} else if (rule === false) {
|
||||
return clean(taintString, {} as SanitizerConfig);
|
||||
} else {
|
||||
return taintString;
|
||||
const cleanOneItem = (
|
||||
taintString: string,
|
||||
rule: DeepSanitizerRule,
|
||||
globalRules: SanitizerConfig
|
||||
): string => {
|
||||
const effectiveRule = getEffectiveRuleForString(rule, globalRules);
|
||||
|
||||
if (effectiveRule) {
|
||||
const cleaned = clean(taintString, effectiveRule);
|
||||
|
||||
return stripUnsafeUrls(applyAttributeOverrides(cleaned, effectiveRule));
|
||||
}
|
||||
}
|
||||
|
||||
if (!_.isEmpty(globalRules)) {
|
||||
const cleaned = clean(taintString, globalRules);
|
||||
|
||||
return stripUnsafeUrls(applyAttributeOverrides(cleaned, globalRules));
|
||||
}
|
||||
|
||||
return stripUnsafeUrls(taintString);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if passed item is a HTML Janitor rule:
|
||||
|
|
@ -179,6 +219,219 @@ function cleanOneItem(taintString: string, rule: SanitizerConfig|boolean): strin
|
|||
*
|
||||
* @param {SanitizerConfig} config - config to check
|
||||
*/
|
||||
function isRule(config: SanitizerConfig): boolean {
|
||||
const isRule = (config: DeepSanitizerRule): boolean => {
|
||||
return _.isObject(config) || _.isBoolean(config) || _.isFunction(config);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param value
|
||||
*/
|
||||
const stripUnsafeUrls = (value: string): string => {
|
||||
if (!value || value.indexOf('<') === -1) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return value.replace(UNSAFE_URL_ATTR_PATTERN, '');
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param config
|
||||
*/
|
||||
const cloneSanitizerConfig = (config: SanitizerConfig): SanitizerConfig => {
|
||||
if (_.isEmpty(config)) {
|
||||
return {} as SanitizerConfig;
|
||||
}
|
||||
|
||||
return _.deepMerge({}, config);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param rule
|
||||
*/
|
||||
const cloneTagConfig = (rule: SanitizerRule): SanitizerRule => {
|
||||
if (rule === true) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (rule === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_.isFunction(rule) || _.isString(rule)) {
|
||||
return rule;
|
||||
}
|
||||
|
||||
if (_.isObject(rule)) {
|
||||
return _.deepMerge({}, rule as Record<string, unknown>) as SanitizerRule;
|
||||
}
|
||||
|
||||
return rule;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param globalRules
|
||||
* @param fieldRules
|
||||
*/
|
||||
const mergeTagRules = (globalRules: SanitizerConfig, fieldRules: SanitizerConfig): SanitizerConfig => {
|
||||
if (_.isEmpty(globalRules)) {
|
||||
return cloneSanitizerConfig(fieldRules);
|
||||
}
|
||||
|
||||
const merged: SanitizerConfig = {} as SanitizerConfig;
|
||||
|
||||
for (const tag in globalRules) {
|
||||
if (!Object.prototype.hasOwnProperty.call(globalRules, tag)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const globalValue = globalRules[tag];
|
||||
const fieldValue = fieldRules ? fieldRules[tag] : undefined;
|
||||
|
||||
if (_.isFunction(globalValue)) {
|
||||
merged[tag] = globalValue;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_.isFunction(fieldValue)) {
|
||||
merged[tag] = fieldValue;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_.isObject(globalValue) && _.isObject(fieldValue)) {
|
||||
merged[tag] = _.deepMerge({}, fieldValue as SanitizerConfig, globalValue as SanitizerConfig);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (fieldValue !== undefined) {
|
||||
merged[tag] = cloneTagConfig(fieldValue as SanitizerRule);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
merged[tag] = cloneTagConfig(globalValue as SanitizerRule);
|
||||
}
|
||||
|
||||
return merged;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param rule
|
||||
* @param globalRules
|
||||
*/
|
||||
const getEffectiveRuleForString = (
|
||||
rule: DeepSanitizerRule,
|
||||
globalRules: SanitizerConfig
|
||||
): SanitizerConfig | null => {
|
||||
if (_.isObject(rule) && !_.isFunction(rule)) {
|
||||
return mergeTagRules(globalRules, rule as SanitizerConfig);
|
||||
}
|
||||
|
||||
if (rule === false) {
|
||||
return {} as SanitizerConfig;
|
||||
}
|
||||
|
||||
if (_.isEmpty(globalRules)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return cloneSanitizerConfig(globalRules);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param globalConfig
|
||||
* @param {...any} configs
|
||||
*/
|
||||
export const composeSanitizerConfig = (
|
||||
globalConfig: SanitizerConfig,
|
||||
...configs: SanitizerConfig[]
|
||||
): SanitizerConfig => {
|
||||
if (_.isEmpty(globalConfig)) {
|
||||
return Object.assign({}, ...configs) as SanitizerConfig;
|
||||
}
|
||||
|
||||
const base = cloneSanitizerConfig(globalConfig);
|
||||
|
||||
configs.forEach((config) => {
|
||||
if (!config) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const tag in config) {
|
||||
if (!Object.prototype.hasOwnProperty.call(config, tag)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!Object.prototype.hasOwnProperty.call(base, tag)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const sourceValue = config[tag];
|
||||
const targetValue = base[tag];
|
||||
|
||||
if (_.isFunction(sourceValue)) {
|
||||
base[tag] = sourceValue;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_.isObject(sourceValue) && _.isObject(targetValue)) {
|
||||
base[tag] = _.deepMerge({}, targetValue as SanitizerConfig, sourceValue as SanitizerConfig);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
base[tag] = cloneTagConfig(sourceValue as SanitizerRule);
|
||||
}
|
||||
});
|
||||
|
||||
return base;
|
||||
};
|
||||
|
||||
const applyAttributeOverrides = (html: string, rules: SanitizerConfig): string => {
|
||||
if (typeof document === 'undefined' || !html || html.indexOf('<') === -1) {
|
||||
return html;
|
||||
}
|
||||
|
||||
const entries = Object.entries(rules).filter(([, value]) => _.isFunction(value));
|
||||
|
||||
if (entries.length === 0) {
|
||||
return html;
|
||||
}
|
||||
|
||||
const template = document.createElement('template');
|
||||
|
||||
template.innerHTML = html;
|
||||
|
||||
entries.forEach(([tag, rule]) => {
|
||||
const elements = template.content.querySelectorAll(tag);
|
||||
|
||||
elements.forEach((element) => {
|
||||
const ruleResult = (rule as (el: Element) => SanitizerRule)(element);
|
||||
|
||||
if (_.isBoolean(ruleResult) || _.isFunction(ruleResult) || ruleResult == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [attr, attrRule] of Object.entries(ruleResult)) {
|
||||
if (attrRule === false) {
|
||||
element.removeAttribute(attr);
|
||||
} else if (attrRule === true) {
|
||||
continue;
|
||||
} else if (_.isString(attrRule)) {
|
||||
element.setAttribute(attr, attrRule);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return template.innerHTML;
|
||||
};
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue