api sanitizer improvements (#457)

* api sanitizer improvements

* update

* sanitize recursively

* clear from logs and update comments

* optimize

* update

* perfect recursive method

* update request

* upd

* update docs

* update comments

* update

* update docs

* update last comment

* update

* update docs

* update docs

* update

* upd docs

* add extra condition

* update

* update docs link
This commit is contained in:
Murod Khaydarov 2018-10-01 14:07:51 +03:00 committed by GitHub
parent 972c47e73a
commit ff80ca6e92
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 404 additions and 191 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -6,8 +6,6 @@ Uses lightweight npm package with simple API [html-janitor](https://www.npmjs.co
Sanitizer class implements basic Module class that holds User configuration
and default CodeX Editor instances
You can read more about Module class [here]()
## Properties
Default Editor Sanitizer configuration according to the html-janitor API

View file

@ -205,3 +205,118 @@ static get onPaste() {
```
### Sanitize
CodeX Editor provides [API](sanitizer.md) to clean taint strings.
Use it manually at the `save()` method or or pass `sanitizer` config to do it automatically.
#### Sanitizer Configuration
The example of sanitizer configuration
```javascript
let sanitizerConfig = {
b: true, // leave <b>
p: true, // leave <p>
}
```
Keys of config object is tags and the values is a rules.
##### Rule
Rule can be boolean, object or function. Object is a dictionary of rules for tag's attributes.
You can set `true`, to allow tag with all attributes or `false|{}` to remove all attributes,
but leave tag.
Also you can pass special attributes that you want to leave.
```javascript
a: {
href: true
}
```
If you want to use a custom handler, use should specify a function
that returns a rule.
```javascript
b: function(el) {
return !el.textContent.includes('bad text');
}
```
or
```javascript
a: function(el) {
let anchorHref = el.getAttribute('href');
if (anchorHref && anchorHref.substring(0, 4) === 'http') {
return {
href: true,
target: '_blank'
}
} else {
return {
href: true
}
}
}
```
#### Manual sanitize
Call API method `sanitizer.clean()` at the save method for each field in returned data.
```javascript
save() {
return {
text: this.api.sanitizer.clean(taintString, sanitizerConfig)
}
}
```
#### Automatic sanitize
If you pass the sanitizer config, CodeX Editor will automatically sanitize your saved data.
You can define rules for each field
```javascript
get sanitize() {
return {
text: {},
items: {
b: true, // leave <b>
a: false, // remove <a>
}
}
}
```
Don't forget to set the rule for each embedded subitems otherwise they will
not be sanitized.
if you want to sanitize everything and get data without any tags, use `{}` or just
ignore field in case if you want to get pure HTML
```javascript
get sanitize() {
return {
text: {},
items: {}, // this rules will be used for all properties of this object
// or
items: {
// other objects here won't be sanitized
subitems: {
// leave <a> and <b> in subitems
a: true,
b: true,
}
}
}
}
```

View file

@ -1,6 +1,6 @@
{
"name": "codex.editor",
"version": "2.1.0",
"version": "2.1.1",
"description": "Codex Editor. Native JS, based on API and Open Source",
"main": "build/codex-editor.js",
"scripts": {

View file

@ -17,6 +17,7 @@ type Tool = any;
import MoveUpTune from './block-tunes/block-tune-move-up';
import DeleteTune from './block-tunes/block-tune-delete';
import MoveDownTune from './block-tunes/block-tune-move-down';
import {IAPI} from './interfaces/api';
/**
* @classdesc Abstract Block class that contains Block information, Tool name and Tool class instance
@ -228,7 +229,7 @@ export default class Block {
public settings: object;
public holder: HTMLDivElement;
public tunes: IBlockTune[];
private readonly api: object;
private readonly api: IAPI;
private inputIndex = 0;
/**
@ -239,7 +240,7 @@ export default class Block {
* @param {Object} settings - default settings
* @param {Object} apiMethods - Editor API
*/
constructor(toolName: string, toolInstance: Tool, toolClass: object, settings: object, apiMethods: object) {
constructor(toolName: string, toolInstance: Tool, toolClass: object, settings: object, apiMethods: IAPI) {
this.name = toolName;
this.tool = toolInstance;
this.class = toolClass;
@ -285,8 +286,16 @@ export default class Block {
* Groups Tool's save processing time
* @return {Object}
*/
public save(): Promise<void|{tool: string, data: any, time: number}> {
const extractedBlock = this.tool.save(this.pluginsContent);
public async save(): Promise<void|{tool: string, data: any, time: number}> {
let extractedBlock = await this.tool.save(this.pluginsContent);
/**
* if Tool provides custom sanitizer config
* then use this config
*/
if (this.tool.sanitize && typeof this.tool.sanitize === 'object') {
extractedBlock = this.sanitizeBlock(extractedBlock, this.tool.sanitize);
}
/**
* Measuring execution time
@ -364,6 +373,73 @@ export default class Block {
return tunesElement;
}
/**
* Method recursively reduces Block's data and cleans with passed rules
*
* @param {Object|string} blockData - taint string or object/array that contains taint string
* @param {Object} rules - object with sanitizer rules
*/
private sanitizeBlock(blockData, rules) {
/**
* Case 1: Block data is Array
* Array's in JS can not be enumerated with for..in because result will be Object not Array
* which conflicts with Consistency
*/
if (Array.isArray(blockData)) {
/**
* Create new "cleanData" array and fill in with sanitizer data
*/
return blockData.map((item) => {
return this.sanitizeBlock(item, rules);
});
} else if (typeof blockData === 'object') {
/**
* Create new "cleanData" object and fill with sanitized objects
*/
const cleanData = {};
/**
* Object's may have 3 cases:
* 1. When Data is Array. Then call again itself and recursively clean arrays items
* 2. When Data is Object that can have object's inside. Do the same, call itself and clean recursively
* 3. When Data is base type (string, int, bool, ...). Check if rule is passed
*/
for (const data in blockData) {
if (Array.isArray(blockData[data]) || typeof blockData[data] === 'object') {
/**
* Case 1 & Case 2
*/
if (rules[data]) {
cleanData[data] = this.sanitizeBlock(blockData[data], rules[data]);
} else if (_.isEmpty(rules)) {
cleanData[data] = this.sanitizeBlock(blockData[data], rules);
} else {
cleanData[data] = blockData[data];
}
} else {
/**
* Case 3.
*/
if (rules[data]) {
cleanData[data] = this.api.sanitizer.clean(blockData[data], rules[data]);
} else {
cleanData[data] = this.api.sanitizer.clean(blockData[data], rules);
}
}
}
return cleanData;
} else {
/**
* In case embedded objects use parent rules
*/
return this.api.sanitizer.clean(blockData, rules);
}
}
/**
* Toggle drop target state
* @param {boolean} state

View file

@ -27,6 +27,11 @@ export default interface IBlockTool extends ITool {
*/
toolboxIcon?: string;
/**
* Sanitizer rules description
*/
sanitizer?: object;
/**
* Return Tool's main block-wrapper
* @return {HTMLElement}

View file

@ -45,12 +45,10 @@ export default class Sanitizer extends Module {
constructor({config}) {
super({config});
// default config
this.defaultConfig = null;
this._sanitizerInstance = null;
/** Custom configuration */
this.sanitizerConfig = config.settings ? config.settings.sanitizer : {};
this.sanitizerConfig = config.settings ? config.settings.sanitizer : null;
/** HTML Janitor library */
this.sanitizerInstance = require('html-janitor');
@ -66,30 +64,37 @@ export default class Sanitizer extends Module {
* @param {HTMLJanitor} library - sanitizer extension
*/
set sanitizerInstance(library) {
this._sanitizerInstance = new library(this.defaultConfig);
if (this.sanitizerConfig) {
this._sanitizerInstance = new library(this.sanitizerConfig);
}
return this._sanitizerInstance;
}
/**
* Sets sanitizer configuration. Uses default config if user didn't pass the restriction
* @param {SanitizerConfig} config
*/
set sanitizerConfig(config) {
if (_.isEmpty(config)) {
this.defaultConfig = {
tags: {
p: {},
a: {
href: true,
target: '_blank',
rel: 'nofollow'
},
b: {},
i: {}
}
};
} else {
this.defaultConfig = config;
}
get defaultConfig() {
return {
tags: {
p: {},
a: {
href: true,
target: '_blank',
rel: 'nofollow'
},
b: {},
i: {}
}
};
}
/**
* Return sanitizer instance
* @return {null|library}
*/
get sanitizerInstance() {
return this._sanitizerInstance;
}
/**
@ -98,11 +103,25 @@ export default class Sanitizer extends Module {
* @param {Object} customConfig - custom sanitizer configuration. Method uses default if param is empty
* @return {String} clean HTML
*/
clean(taintString, customConfig = {}) {
if (_.isEmpty(customConfig)) {
return this._sanitizerInstance.clean(taintString);
clean(taintString, customConfig) {
if (customConfig && typeof customConfig === 'object') {
/**
* API client can use custom config to manage sanitize process
*/
let newConfig = {
tags: customConfig
};
return Sanitizer.clean(taintString, newConfig);
} else {
return Sanitizer.clean(taintString, customConfig);
/**
* Ignore sanitizing when nothing passed in config
*/
if (!this.sanitizerInstance) {
return taintString;
} else {
return this.sanitizerInstance.clean(taintString);
}
}
}