editor.js/src/components/modules/sanitizer.ts
Peter Savchenko ac93017c70
Release 2.16 (#966)
* 2.16.0

* [Refactor] Separate internal and external settings (#845)

* Enable flipping tools via standalone class (#830)

* Enable flipping tools via standalone class

* use flipper to refactor (#842)

* use flipper to refactor

* save changes

* update

* fix flipper on inline toolbar

* ready for testing

* requested changes

* update doc

* updates

* destroy flippers

* some requested changes

* update

* update

* ready

* update

* last changes

* update docs

* Hghl active button of CT, simplify activate/deactivate

* separate dom iterator

* unhardcode directions

* fixed a link in readme.md (#856)

* Fix Block selection via CMD+A (#829)

* Fix Block selection via CMD+A

* Delete editor.js.map

* update

* update

* Update CHANGELOG.md

* Improve style of selected blocks (#858)

* Cross-block-selection style improved

* Update CHANGELOG.md

* Fix case when property 'observer' in modificationObserver is not defined (#866)

* Bump lodash.template from 4.4.0 to 4.5.0 (#885)

Bumps [lodash.template](https://github.com/lodash/lodash) from 4.4.0 to 4.5.0.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.4.0...4.5.0)

Signed-off-by: dependabot[bot] <support@github.com>

* Bump eslint-utils from 1.3.1 to 1.4.2 (#886)

Bumps [eslint-utils](https://github.com/mysticatea/eslint-utils) from 1.3.1 to 1.4.2.
- [Release notes](https://github.com/mysticatea/eslint-utils/releases)
- [Commits](https://github.com/mysticatea/eslint-utils/compare/v1.3.1...v1.4.2)

Signed-off-by: dependabot[bot] <support@github.com>

* Bump mixin-deep from 1.3.1 to 1.3.2 (#887)

Bumps [mixin-deep](https://github.com/jonschlinkert/mixin-deep) from 1.3.1 to 1.3.2.
- [Release notes](https://github.com/jonschlinkert/mixin-deep/releases)
- [Commits](https://github.com/jonschlinkert/mixin-deep/compare/1.3.1...1.3.2)

Signed-off-by: dependabot[bot] <support@github.com>

* update bundle and readme

* Update README.md

* upd codeowners, fix funding

* Minor Docs Fix according to main Readme (#916)

* Inline Toolbar now contains Conversion Toolbar (#932)

* Block lifecycle hooks (#906)

* [Fix] Arrow selection (#964)

* Fix arrow selection

* Add docs

* [issue-926]: fix dom iterator leafing when items are empty (#958)

* [issue-926]: fix dom iterator leafing when items are empty

* update Changelog

* Issue 869 (#963)

* Fix issue 943 (#965)

* [Draft] Feature/tooltip enhancements (#907)

* initial

* update

* make module standalone

* use tooltips as external module

* update

* build via prod mode

* add tooltips as external module

* add declaration file and options param

* add api tooltip

* update

* removed submodule

* removed due to the incorrect setip

* setup tooltips again

* wip

* update tooltip module

* toolbox, inline toolbar

* Tooltips in block tunes not uses shorthand

* shorthand in a plus and block settings

* fix doc

* Update tools-inline.md

* Delete tooltip.css

* Update CHANGELOG.md

* Update codex.tooltips

* Update api.md

* [issue-779]: Grammarly conflicts (#956)

* grammarly conflicts

* update

* upd bundle

* Submodule Header now on master

* Submodule Marker now on master

* Submodule Paragraph now on master

* Submodule InlineCode now on master

* Submodule Simple Image now on master

* [issue-868]: Deleting multiple blocks triggers back button in Firefox (#967)

* Deleting multiple blocks triggers back button in Firefox

@evgenusov

* Update editor.js

* Update CHANGELOG.md

* pass options on removeEventListener (#904)

* pass options on removeEventListener by removeAll

* rebuild

* Merge branch 'release/2.16' into pr/904

* Update CHANGELOG.md

* Update inline.ts

* [Fix] Selection rangecount (#968)

* Fix #952 (#969)

* Update codex.tooltips

* Selection bugfix (#970)

* Selection bugfix

* fix cross block selection

* close inline toolbar when blocks selected via shift

* remove inline toolbar closing on cross block selection mouse up due to the bug (#972)

* [Feature] Log levels (#971)

* Decrease margins (#973)

* Decrease margins

* Update editor.licenses.txt

* Update src/components/domIterator.ts

Co-Authored-By: Murod Khaydarov <murod.haydarov@gmail.com>

* [Fix] Fix delete blocks api method (#974)

* Update docs/usage.md

Co-Authored-By: Murod Khaydarov <murod.haydarov@gmail.com>

* rm unused

* Update yarn.lock file

* upd bundle, changelog
2019-11-30 23:42:39 +03:00

311 lines
8.5 KiB
TypeScript

/**
* CodeX Sanitizer
*
* @module Sanitizer
* Clears HTML from taint tags
*
* @version 2.0.0
*
* @example
* Module can be used within two ways:
* 1) When you have an instance
* - this.Editor.Sanitizer.clean(yourTaintString);
* 2) As static method
* - EditorJS.Sanitizer.clean(yourTaintString, yourCustomConfiguration);
*
* {@link SanitizerConfig}
*/
import Module from '../__module';
import * as _ from '../utils';
/**
* @typedef {Object} SanitizerConfig
* @property {Object} tags - define tags restrictions
*
* @example
*
* tags : {
* p: true,
* a: {
* href: true,
* rel: "nofollow",
* target: "_blank"
* }
* }
*/
import HTMLJanitor from 'html-janitor';
import {BlockToolData, InlineToolConstructable, SanitizerConfig} from '../../../types';
export default class Sanitizer extends Module {
/**
* Memoize tools config
*/
private configCache: {[toolName: string]: SanitizerConfig} = {};
/**
* Cached inline tools config
*/
private inlineToolsConfigCache: SanitizerConfig | null = null;
/**
* Sanitize Blocks
*
* Enumerate blocks and clean data
*
* @param {{tool, data: BlockToolData}[]} blocksData[]
*/
public sanitizeBlocks(
blocksData: Array<{tool: string, data: BlockToolData}>,
): Array<{tool: string, data: BlockToolData}> {
return blocksData.map((block) => {
const toolConfig = this.composeToolConfig(block.tool);
if (_.isEmpty(toolConfig)) {
return block;
}
block.data = this.deepSanitize(block.data, toolConfig);
return block;
});
}
/**
* 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
*/
public deepSanitize(dataToSanitize: any, rules: SanitizerConfig): any {
/**
* BlockData It may contain 3 types:
* - Array
* - Object
* - Primitive
*/
if (Array.isArray(dataToSanitize)) {
/**
* Array: call sanitize for each item
*/
return this.cleanArray(dataToSanitize, rules);
} else if (typeof dataToSanitize === 'object') {
/**
* Objects: just clean object deeper.
*/
return this.cleanObject(dataToSanitize, rules);
} else {
/**
* Primitives (number|string|boolean): clean this item
*
* Clean only strings
*/
if (typeof dataToSanitize === 'string') {
return this.cleanOneItem(dataToSanitize, rules);
}
return dataToSanitize;
}
}
/**
* Cleans string from unwanted tags
* Method allows to use default config
*
* @param {string} taintString - taint string
* @param {SanitizerConfig} customConfig - allowed tags
*
* @return {string} clean HTML
*/
public clean(taintString: string, customConfig: SanitizerConfig = {} as SanitizerConfig): string {
const sanitizerConfig = {
tags: customConfig,
};
/**
* API client can use custom config to manage sanitize process
*/
const sanitizerInstance = this.createHTMLJanitorInstance(sanitizerConfig);
return sanitizerInstance.clean(taintString);
}
/**
* Merge with inline tool config
*
* @param {string} toolName
* @param {SanitizerConfig} toolRules
* @return {SanitizerConfig}
*/
public composeToolConfig(toolName: string): SanitizerConfig {
/**
* If cache is empty, then compose tool config and put it to the cache object
*/
if (this.configCache[toolName]) {
return this.configCache[toolName];
}
const sanitizeGetter = this.Editor.Tools.INTERNAL_SETTINGS.SANITIZE_CONFIG;
const toolClass = this.Editor.Tools.available[toolName];
const baseConfig = this.getInlineToolsConfig(toolName);
/**
* If Tools doesn't provide sanitizer config or it is empty
*/
if (!toolClass.sanitize || (toolClass[sanitizeGetter] && _.isEmpty(toolClass[sanitizeGetter]))) {
return baseConfig;
}
const toolRules = toolClass.sanitize;
const toolConfig = {} as SanitizerConfig;
for (const fieldName in toolRules) {
if (toolRules.hasOwnProperty(fieldName)) {
const rule = toolRules[fieldName];
if (typeof rule === 'object') {
toolConfig[fieldName] = Object.assign({}, baseConfig, rule);
} else {
toolConfig[fieldName] = rule;
}
}
}
this.configCache[toolName] = toolConfig;
return toolConfig;
}
/**
* Returns Sanitizer config
* When Tool's "inlineToolbar" value is True, get all sanitizer rules from all tools,
* otherwise get only enabled
*/
public getInlineToolsConfig(name: string): SanitizerConfig {
const {Tools} = this.Editor;
const toolsConfig = Tools.getToolSettings(name);
const enableInlineTools = toolsConfig.inlineToolbar || [];
let config = {} as SanitizerConfig;
if (typeof enableInlineTools === 'boolean' && enableInlineTools) {
/**
* getting all tools sanitizer rule
*/
config = this.getAllInlineToolsConfig();
} else {
/**
* getting only enabled
*/
(enableInlineTools as string[]).map( (inlineToolName) => {
config = Object.assign(
config,
Tools.inline[inlineToolName][Tools.INTERNAL_SETTINGS.SANITIZE_CONFIG],
) as SanitizerConfig;
});
}
return config;
}
/**
* Return general config for all inline tools
*/
public getAllInlineToolsConfig(): SanitizerConfig {
const {Tools} = this.Editor;
if (this.inlineToolsConfigCache) {
return this.inlineToolsConfigCache;
}
const config: SanitizerConfig = {} as SanitizerConfig;
Object.entries(Tools.inline)
.forEach( ([name, inlineTool]: [string, InlineToolConstructable]) => {
Object.assign(config, inlineTool[Tools.INTERNAL_SETTINGS.SANITIZE_CONFIG]);
});
this.inlineToolsConfigCache = config;
return this.inlineToolsConfigCache;
}
/**
* Clean array
* @param {array} array - [1, 2, {}, []]
* @param {object} ruleForItem
*/
private cleanArray(array: any[], ruleForItem: SanitizerConfig): any[] {
return array.map( (arrayItem) => this.deepSanitize(arrayItem, ruleForItem));
}
/**
* Clean object
* @param {object} object - {level: 0, text: 'adada', items: [1,2,3]}}
* @param {object} rules - { b: true } or true|false
* @return {object}
*/
private cleanObject(object: any, rules: SanitizerConfig|{[field: string]: SanitizerConfig}): any {
const cleanData = {};
for (const fieldName in object) {
if (!object.hasOwnProperty(fieldName)) {
continue;
}
const currentIterationItem = object[fieldName];
/**
* Get object from config by field name
* - if it is a HTML Janitor rule, call with this rule
* - otherwise, call with parent's config
*/
const ruleForItem = this.isRule(rules[fieldName] as SanitizerConfig) ? rules[fieldName] : rules;
cleanData[fieldName] = this.deepSanitize(currentIterationItem, ruleForItem as SanitizerConfig);
}
return cleanData;
}
/**
* @param {string} taintString
* @param {SanitizerConfig|boolean} rule
* @return {string}
*/
private cleanOneItem(taintString: string, rule: SanitizerConfig|boolean): string {
if (typeof rule === 'object') {
return this.clean(taintString, rule);
} else if (rule === false) {
return this.clean(taintString, {} as SanitizerConfig);
} else {
return taintString;
}
}
/**
* Check if passed item is a HTML Janitor rule:
* { a : true }, {}, false, true, function(){} — correct rules
* undefined, null, 0, 1, 2 — not a rules
* @param config
*/
private isRule(config: SanitizerConfig): boolean {
return typeof config === 'object' || typeof config === 'boolean' || typeof config === 'function';
}
/**
* If developer uses editor's API, then he can customize sanitize restrictions.
* Or, sanitizing config can be defined globally in editors initialization. That config will be used everywhere
* At least, if there is no config overrides, that API uses Default configuration
*
* @uses https://www.npmjs.com/package/html-janitor
* @license https://github.com/guardian/html-janitor/blob/master/LICENSE
*
* @param {SanitizerConfig} config - sanitizer extension
*/
private createHTMLJanitorInstance(config: {tags: SanitizerConfig}): HTMLJanitor|null {
if (config) {
return new HTMLJanitor(config);
}
return null;
}
}