diff --git a/README.md b/README.md index 5921e5d..5d3005b 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,7 @@ On the other hand, the `customTextWrapper` parser function provides the followin - `child`: The HTML string that specifies the child element - `value`: The value passed against the child element + You can use the following customized JSON RTE Serializer code to convert your JSON RTE field data into HTML format. ```javascript @@ -230,6 +231,60 @@ The resulting HTML data will look as follows: ```HTML

This is text.

``` +
+
+ +#####

You can pass the option `skipURLSanitization` as true to bypass the validation checks and sanitization for the src URLs of JSON element types - social embed and embed.

+
By default, this option is set to false.
+ +#### Examples: + + 1. For the following JSON, with src url containing script tags + ```JSON + { + "type": "doc", + "attrs": {}, + "children": [ + { + "type": "social-embeds", + "attrs": { + "src": "https://www.youtube.com/watch?v=Gw7EqoOYC9A\"> +``` + +2. For any JSON containing src urls violating expected protocols, the src attribute will be removed when converted to HTML + + ```JSON + { + "type": "doc", + "attrs": {}, + "children": [ + { + "type": "social-embeds", + "attrs": { + "src": "www.youtube.com/watch?v=Gw7EqoOYC9A\">", + "width": 560, + "height": 320 + }, + } + ] + } +``` +The resulting HTML: +```HTML + +``` + +
### Convert HTML to JSON diff --git a/package-lock.json b/package-lock.json index f88724b..1ee91c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,16 @@ { "name": "@contentstack/json-rte-serializer", - "version": "2.0.7", + "version": "2.0.12", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@contentstack/json-rte-serializer", - "version": "2.0.7", + "version": "2.0.12", "license": "MIT", "dependencies": { "array-flat-polyfill": "^1.0.1", + "dompurify": "^3.2.3", "lodash": "^4.17.21", "lodash.clonedeep": "^4.5.0", "lodash.flatten": "^4.4.0", @@ -1608,6 +1609,12 @@ "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", "dev": true }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "optional": true + }, "node_modules/@types/uuid": { "version": "8.3.4", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", @@ -2243,6 +2250,14 @@ "node": ">=8" } }, + "node_modules/dompurify": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.3.tgz", + "integrity": "sha512-U1U5Hzc2MO0oW3DF+G9qYN0aT7atAou4AgI0XjWz061nyBPbdxkfdhfy5uMgGn6+oLFCfn44ZGbdDqCzVmlOWA==", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.622", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.622.tgz", @@ -6265,6 +6280,12 @@ "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", "dev": true }, + "@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "optional": true + }, "@types/uuid": { "version": "8.3.4", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", @@ -6735,6 +6756,14 @@ } } }, + "dompurify": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.3.tgz", + "integrity": "sha512-U1U5Hzc2MO0oW3DF+G9qYN0aT7atAou4AgI0XjWz061nyBPbdxkfdhfy5uMgGn6+oLFCfn44ZGbdDqCzVmlOWA==", + "requires": { + "@types/trusted-types": "^2.0.7" + } + }, "electron-to-chromium": { "version": "1.4.622", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.622.tgz", diff --git a/package.json b/package.json index 48f0f16..be2705c 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ }, "dependencies": { "array-flat-polyfill": "^1.0.1", + "dompurify": "^3.2.3", "lodash": "^4.17.21", "lodash.clonedeep": "^4.5.0", "lodash.flatten": "^4.4.0", diff --git a/src/toRedactor.tsx b/src/toRedactor.tsx index 36508e1..69670e9 100644 --- a/src/toRedactor.tsx +++ b/src/toRedactor.tsx @@ -1,6 +1,6 @@ import kebbab from 'lodash.kebabcase' import isEmpty from 'lodash.isempty' - +import DOMPurify from 'dompurify' import {IJsonToHtmlElementTags, IJsonToHtmlOptions, IJsonToHtmlTextTags} from './types' import isPlainObject from 'lodash.isplainobject' @@ -494,6 +494,19 @@ export const toRedactor = (jsonValue: any,options?:IJsonToHtmlOptions) : string } figureStyles.fieldsEdited.push(figureStyles.caption) } + + if (!options?.skipURLSanitization && (jsonValue['type'] === 'social-embeds' || jsonValue['type'] === 'embed')) { + const sanitizedHTML = DOMPurify.sanitize(allattrs['src']); + + const urlMatch = sanitizedHTML.match(/https?:\/\/[^\s"'<>()]+/); + + if (urlMatch) { + attrsJson['src'] = decodeURIComponent(urlMatch[0]); + } else { + delete attrsJson['src']; + } + } + if(!(options?.customElementTypes && !isEmpty(options.customElementTypes) && options.customElementTypes[jsonValue['type']])) { delete attrsJson['url'] } diff --git a/src/types.ts b/src/types.ts index adb3785..c531d28 100644 --- a/src/types.ts +++ b/src/types.ts @@ -20,4 +20,5 @@ export interface IJsonToHtmlOptions { customElementTypes?: IJsonToHtmlElementTags, customTextWrapper?: IJsonToHtmlTextTags, allowNonStandardTypes?: boolean, + skipURLSanitization?:boolean } diff --git a/test/expectedJson.ts b/test/expectedJson.ts index 777a02d..8202d8b 100644 --- a/test/expectedJson.ts +++ b/test/expectedJson.ts @@ -1999,6 +1999,137 @@ export default { } ], "htmlUpdated": "

" + }, + "RT-360":{ + "html": [ + ``, + ``, + '', + ], + "json": + [ + { + "type": "doc", + "attrs": {}, + "uid": "18396bf67f1f4b0a9da57643ac0542ca", + "children": [ + { + "uid": "45a850acbeb949db86afe415625ad1ce", + "type": "social-embeds", + "attrs": { + "src": "https://www.youtube.com/watch?v=Gw7EqoOYC9A\">`); }) + + describe("RT-360", () =>{ + it("should remove script and/or other tags from src links in HTML for social-embeds", () => { + const json = expectedValue["RT-360"].json[0] + const html = toRedactor(json); + expect(html).toBe(expectedValue["RT-360"].html[0]); + }) + + it("should handle undefined or null cases",()=>{ + const json = expectedValue["RT-360"].json[1] + const html = toRedactor(json); + expect(html).toBe(expectedValue["RT-360"].html[1]); + }) + + it("should handle src without protocol",()=>{ + const json = expectedValue["RT-360"].json[2] + const html = toRedactor(json); + expect(html).toBe(expectedValue["RT-360"].html[2]); + }) + }) })