From f77539b0c46794e272f1f8354cf46d060c7d0c2d Mon Sep 17 00:00:00 2001 From: Jim O'Donnell Date: Mon, 8 Jan 2024 10:08:17 +0000 Subject: [PATCH] [Security] Remove markdown-it-html5-embed - Add tests for images with audio and video URLs. - Remove `markdown-it-html5-embed`, which is quite old, depends on an outdated version of `markdown-it`, and loads in all of `mime-db` to check for audio or video URLs. - Replace it with `lib/html5-embed`, a modified version of the original plugin, updated to use `mime/lite` to lookup MIME types for audio and video, in the browser. - Bump `jsdom` and `isomorphic-dompurify`. --- package-lock.json | 269 +++++++++++++++++++++------------- package.json | 9 +- src/lib/html5-embed.js | 286 +++++++++++++++++++++++++++++++++++++ src/lib/utils.js | 2 +- test/use-markdownz-test.js | 28 +++- test/utils-test.jsx | 20 ++- 6 files changed, 502 insertions(+), 112 deletions(-) create mode 100644 src/lib/html5-embed.js diff --git a/package-lock.json b/package-lock.json index b220154..fd2cb5c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,18 +10,18 @@ "license": "Apache-2.0", "dependencies": { "@twemoji/api": "~15.0.3", - "isomorphic-dompurify": "~2.0.0", + "isomorphic-dompurify": "~2.2.0", "markdown-it": "~14.0.0", "markdown-it-anchor": "~8.6.5", "markdown-it-container": "~4.0.0", "markdown-it-emoji": "~3.0.0", "markdown-it-footnote": "~4.0.0", - "markdown-it-html5-embed": "~1.0.0", "markdown-it-imsize": "~2.0.1", "markdown-it-sub": "~2.0.0", "markdown-it-sup": "~2.0.0", "markdown-it-table-of-contents": "~0.6.0", "markdown-it-video": "~0.6.3", + "mime": "~3.0.0", "rehype": "~11.0.0", "rehype-react": "~6.2.1" }, @@ -41,7 +41,7 @@ "eslint-plugin-import": "~2.29.0", "eslint-plugin-jsx-a11y": "~6.8.0", "eslint-plugin-react": "~7.33.0", - "jsdom": "~23.0.0", + "jsdom": "~23.2.0", "mocha": "~10.2.0", "prop-types": "~15.8.1", "react": "~16.14", @@ -75,6 +75,16 @@ "node": ">=6.0.0" } }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-2.0.1.tgz", + "integrity": "sha512-QJAJffmCiymkv6YyQ7voyQb5caCth6jzZsQncYCpHXrJ7RqdYG5y43+is8mnFcYubdOkr7cn1+na9BdFMxqw7w==", + "dependencies": { + "bidi-js": "^1.0.3", + "css-tree": "^2.3.1", + "is-potential-custom-element-name": "^1.0.1" + } + }, "node_modules/@babel/cli": { "version": "7.23.4", "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.23.4.tgz", @@ -2364,9 +2374,9 @@ "integrity": "sha512-lh9515BNsvKSNvyUqbj5yFu83iIDQ77SwVcsN/SnEGawczhsKU6qWuogewN1GweTi5Imo5ToQ9s+nNTf97IXvg==" }, "node_modules/@types/dompurify": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.3.tgz", - "integrity": "sha512-odiGr/9/qMqjcBOe5UhcNLOFHSYmKFOyr+bJ/Xu3Qp4k1pNPAlNLUVNNLcLfjQI7+W7ObX58EdD3H+3p3voOvA==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz", + "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==", "dependencies": { "@types/trusted-types": "*" } @@ -2413,9 +2423,9 @@ "integrity": "sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw==" }, "node_modules/@types/trusted-types": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.4.tgz", - "integrity": "sha512-IDaobHimLQhjwsQ/NMwRVfa/yL7L/wriQPMhw1ZJall0KX6E1oxk29XMDeilW5qTIg5aoiqf5Udy8U/51aNoQQ==" + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==" }, "node_modules/@types/unist": { "version": "2.0.9", @@ -2874,6 +2884,14 @@ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -3319,6 +3337,18 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "dependencies": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, "node_modules/css-what": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", @@ -3332,14 +3362,14 @@ } }, "node_modules/cssstyle": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-3.0.0.tgz", - "integrity": "sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.0.1.tgz", + "integrity": "sha512-8ZYiJ3A/3OkDd093CBT/0UKDWry7ak4BdPTFP2+QEP7cmhouyq/Up709ASSj2cK02BbZiMgk7kYjZNS4QP5qrQ==", "dependencies": { "rrweb-cssom": "^0.6.0" }, "engines": { - "node": ">=14" + "node": ">=18" } }, "node_modules/damerau-levenshtein": { @@ -3530,9 +3560,9 @@ } }, "node_modules/dompurify": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.6.tgz", - "integrity": "sha512-ilkD8YEnnGh1zJ240uJsW7AzE+2qpbOUYjacomn3AvJ6J4JhKGSZ2nh4wUIXPZrEPppaCLx5jFe8T89Rk8tQ7w==" + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.8.tgz", + "integrity": "sha512-b7uwreMYL2eZhrSCRC4ahLTeZcPZxSmYfmcQGXGkXiZSNW1X85v+SDM5KsWcpivIiUBH47Ji7NtyUdpLeF5JZQ==" }, "node_modules/domutils": { "version": "3.0.1", @@ -5494,13 +5524,13 @@ } }, "node_modules/isomorphic-dompurify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isomorphic-dompurify/-/isomorphic-dompurify-2.0.0.tgz", - "integrity": "sha512-BJvrSQzg7jleSaySaWyhzGqH9/QxYc3sflm5fvjcXWAQcHQvQPQdCN0ORyqvMqnQDbwFuZXvqh2IcuVa3dG/DA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/isomorphic-dompurify/-/isomorphic-dompurify-2.2.0.tgz", + "integrity": "sha512-r+pTGOcmnWm1qGdB6OzAk6zM4kfb7jVHXFliJOTlnRw5G3+LOR2mSWllE2Um1V23njAluToqOeRqp1jMsMFT0A==", "dependencies": { - "@types/dompurify": "^3.0.3", - "dompurify": "^3.0.6", - "jsdom": "^23.0.0" + "@types/dompurify": "^3.0.5", + "dompurify": "^3.0.8", + "jsdom": "^23.1.0" }, "engines": { "node": ">=18" @@ -5538,11 +5568,12 @@ } }, "node_modules/jsdom": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-23.0.1.tgz", - "integrity": "sha512-2i27vgvlUsGEBO9+/kJQRbtqtm+191b5zAZrU/UezVmnC2dlDAFLgDYJvAEi94T4kjsRKkezEtLQTgsNEsW2lQ==", + "version": "23.2.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-23.2.0.tgz", + "integrity": "sha512-L88oL7D/8ufIES+Zjz7v0aes+oBMh2Xnh3ygWvL0OaICOomKEPKuPnIfBJekiXr+BHbbMjrWn/xqrDQuxFTeyA==", "dependencies": { - "cssstyle": "^3.0.0", + "@asamuzakjp/dom-selector": "^2.0.1", + "cssstyle": "^4.0.1", "data-urls": "^5.0.0", "decimal.js": "^10.4.3", "form-data": "^4.0.0", @@ -5550,7 +5581,6 @@ "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.7", "parse5": "^7.1.2", "rrweb-cssom": "^0.6.0", "saxes": "^6.0.0", @@ -5561,7 +5591,7 @@ "whatwg-encoding": "^3.1.1", "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.0.0", - "ws": "^8.14.2", + "ws": "^8.16.0", "xml-name-validator": "^5.0.0" }, "engines": { @@ -5843,15 +5873,6 @@ "resolved": "https://registry.npmjs.org/markdown-it-footnote/-/markdown-it-footnote-4.0.0.tgz", "integrity": "sha512-WYJ7urf+khJYl3DqofQpYfEYkZKbmXmwxQV8c8mO/hGIhgZ1wOe7R4HLFNwqx7TjILbnC98fuyeSsin19JdFcQ==" }, - "node_modules/markdown-it-html5-embed": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/markdown-it-html5-embed/-/markdown-it-html5-embed-1.0.0.tgz", - "integrity": "sha512-SPgugO/1+/9sZcgxoxijoTHSUpCUgFCNe1MSuTmDxDkV6NQrVzMclhRMFgE/rcHO+2rhIg3U7Oy80XA/E8ytpg==", - "dependencies": { - "markdown-it": "^8.4.0", - "mimoza": "~1.0.0" - } - }, "node_modules/markdown-it-imsize": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/markdown-it-imsize/-/markdown-it-imsize-2.0.1.tgz", @@ -5880,11 +5901,27 @@ "resolved": "https://registry.npmjs.org/markdown-it-video/-/markdown-it-video-0.6.3.tgz", "integrity": "sha512-T4th1kwy0OcvyWSN4u3rqPGxvbDclpucnVSSaH3ZacbGsAts964dxokx9s/I3GYsrDCJs4ogtEeEeVP18DQj0Q==" }, + "node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==" + }, "node_modules/mdurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==" }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -5904,14 +5941,6 @@ "node": ">= 0.6" } }, - "node_modules/mimoza": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/mimoza/-/mimoza-1.0.0.tgz", - "integrity": "sha1-10qk/giTLwBeQwvce/z6lfyrTmI=", - "dependencies": { - "mime-db": "^1.6.0" - } - }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -6208,11 +6237,6 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, - "node_modules/nwsapi": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", - "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==" - }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -6911,6 +6935,14 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", @@ -7175,6 +7207,14 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", @@ -7954,9 +7994,9 @@ "dev": true }, "node_modules/ws": { - "version": "8.14.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", - "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", + "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", "engines": { "node": ">=10.0.0" }, @@ -8104,6 +8144,16 @@ "@jridgewell/trace-mapping": "^0.3.9" } }, + "@asamuzakjp/dom-selector": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-2.0.1.tgz", + "integrity": "sha512-QJAJffmCiymkv6YyQ7voyQb5caCth6jzZsQncYCpHXrJ7RqdYG5y43+is8mnFcYubdOkr7cn1+na9BdFMxqw7w==", + "requires": { + "bidi-js": "^1.0.3", + "css-tree": "^2.3.1", + "is-potential-custom-element-name": "^1.0.1" + } + }, "@babel/cli": { "version": "7.23.4", "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.23.4.tgz", @@ -9716,9 +9766,9 @@ "integrity": "sha512-lh9515BNsvKSNvyUqbj5yFu83iIDQ77SwVcsN/SnEGawczhsKU6qWuogewN1GweTi5Imo5ToQ9s+nNTf97IXvg==" }, "@types/dompurify": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.3.tgz", - "integrity": "sha512-odiGr/9/qMqjcBOe5UhcNLOFHSYmKFOyr+bJ/Xu3Qp4k1pNPAlNLUVNNLcLfjQI7+W7ObX58EdD3H+3p3voOvA==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz", + "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==", "requires": { "@types/trusted-types": "*" } @@ -9765,9 +9815,9 @@ "integrity": "sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw==" }, "@types/trusted-types": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.4.tgz", - "integrity": "sha512-IDaobHimLQhjwsQ/NMwRVfa/yL7L/wriQPMhw1ZJall0KX6E1oxk29XMDeilW5qTIg5aoiqf5Udy8U/51aNoQQ==" + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==" }, "@types/unist": { "version": "2.0.9", @@ -10115,6 +10165,14 @@ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true }, + "bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "requires": { + "require-from-string": "^2.0.2" + } + }, "binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -10432,6 +10490,15 @@ "nth-check": "^2.0.1" } }, + "css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "requires": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + } + }, "css-what": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", @@ -10439,9 +10506,9 @@ "dev": true }, "cssstyle": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-3.0.0.tgz", - "integrity": "sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.0.1.tgz", + "integrity": "sha512-8ZYiJ3A/3OkDd093CBT/0UKDWry7ak4BdPTFP2+QEP7cmhouyq/Up709ASSj2cK02BbZiMgk7kYjZNS4QP5qrQ==", "requires": { "rrweb-cssom": "^0.6.0" } @@ -10582,9 +10649,9 @@ } }, "dompurify": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.6.tgz", - "integrity": "sha512-ilkD8YEnnGh1zJ240uJsW7AzE+2qpbOUYjacomn3AvJ6J4JhKGSZ2nh4wUIXPZrEPppaCLx5jFe8T89Rk8tQ7w==" + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.8.tgz", + "integrity": "sha512-b7uwreMYL2eZhrSCRC4ahLTeZcPZxSmYfmcQGXGkXiZSNW1X85v+SDM5KsWcpivIiUBH47Ji7NtyUdpLeF5JZQ==" }, "domutils": { "version": "3.0.1", @@ -12002,13 +12069,13 @@ "dev": true }, "isomorphic-dompurify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isomorphic-dompurify/-/isomorphic-dompurify-2.0.0.tgz", - "integrity": "sha512-BJvrSQzg7jleSaySaWyhzGqH9/QxYc3sflm5fvjcXWAQcHQvQPQdCN0ORyqvMqnQDbwFuZXvqh2IcuVa3dG/DA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/isomorphic-dompurify/-/isomorphic-dompurify-2.2.0.tgz", + "integrity": "sha512-r+pTGOcmnWm1qGdB6OzAk6zM4kfb7jVHXFliJOTlnRw5G3+LOR2mSWllE2Um1V23njAluToqOeRqp1jMsMFT0A==", "requires": { - "@types/dompurify": "^3.0.3", - "dompurify": "^3.0.6", - "jsdom": "^23.0.0" + "@types/dompurify": "^3.0.5", + "dompurify": "^3.0.8", + "jsdom": "^23.1.0" } }, "iterator.prototype": { @@ -12040,11 +12107,12 @@ } }, "jsdom": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-23.0.1.tgz", - "integrity": "sha512-2i27vgvlUsGEBO9+/kJQRbtqtm+191b5zAZrU/UezVmnC2dlDAFLgDYJvAEi94T4kjsRKkezEtLQTgsNEsW2lQ==", + "version": "23.2.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-23.2.0.tgz", + "integrity": "sha512-L88oL7D/8ufIES+Zjz7v0aes+oBMh2Xnh3ygWvL0OaICOomKEPKuPnIfBJekiXr+BHbbMjrWn/xqrDQuxFTeyA==", "requires": { - "cssstyle": "^3.0.0", + "@asamuzakjp/dom-selector": "^2.0.1", + "cssstyle": "^4.0.1", "data-urls": "^5.0.0", "decimal.js": "^10.4.3", "form-data": "^4.0.0", @@ -12052,7 +12120,6 @@ "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.7", "parse5": "^7.1.2", "rrweb-cssom": "^0.6.0", "saxes": "^6.0.0", @@ -12063,7 +12130,7 @@ "whatwg-encoding": "^3.1.1", "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.0.0", - "ws": "^8.14.2", + "ws": "^8.16.0", "xml-name-validator": "^5.0.0" } }, @@ -12286,15 +12353,6 @@ "resolved": "https://registry.npmjs.org/markdown-it-footnote/-/markdown-it-footnote-4.0.0.tgz", "integrity": "sha512-WYJ7urf+khJYl3DqofQpYfEYkZKbmXmwxQV8c8mO/hGIhgZ1wOe7R4HLFNwqx7TjILbnC98fuyeSsin19JdFcQ==" }, - "markdown-it-html5-embed": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/markdown-it-html5-embed/-/markdown-it-html5-embed-1.0.0.tgz", - "integrity": "sha512-SPgugO/1+/9sZcgxoxijoTHSUpCUgFCNe1MSuTmDxDkV6NQrVzMclhRMFgE/rcHO+2rhIg3U7Oy80XA/E8ytpg==", - "requires": { - "markdown-it": "~14.0.0", - "mimoza": "~1.0.0" - } - }, "markdown-it-imsize": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/markdown-it-imsize/-/markdown-it-imsize-2.0.1.tgz", @@ -12320,11 +12378,21 @@ "resolved": "https://registry.npmjs.org/markdown-it-video/-/markdown-it-video-0.6.3.tgz", "integrity": "sha512-T4th1kwy0OcvyWSN4u3rqPGxvbDclpucnVSSaH3ZacbGsAts964dxokx9s/I3GYsrDCJs4ogtEeEeVP18DQj0Q==" }, + "mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==" + }, "mdurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==" }, + "mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==" + }, "mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -12338,14 +12406,6 @@ "mime-db": "1.52.0" } }, - "mimoza": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/mimoza/-/mimoza-1.0.0.tgz", - "integrity": "sha1-10qk/giTLwBeQwvce/z6lfyrTmI=", - "requires": { - "mime-db": "^1.6.0" - } - }, "minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -12556,11 +12616,6 @@ "boolbase": "^1.0.0" } }, - "nwsapi": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", - "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==" - }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -13087,6 +13142,11 @@ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" + }, "requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", @@ -13279,6 +13339,11 @@ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true }, + "source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==" + }, "source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", @@ -13855,9 +13920,9 @@ "dev": true }, "ws": { - "version": "8.14.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", - "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", + "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", "requires": {} }, "xml-name-validator": { diff --git a/package.json b/package.json index 768451a..15a965d 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "eslint-plugin-import": "~2.29.0", "eslint-plugin-jsx-a11y": "~6.8.0", "eslint-plugin-react": "~7.33.0", - "jsdom": "~23.0.0", + "jsdom": "~23.2.0", "mocha": "~10.2.0", "prop-types": "~15.8.1", "react": "~16.14", @@ -62,22 +62,19 @@ }, "dependencies": { "@twemoji/api": "~15.0.3", - "isomorphic-dompurify": "~2.0.0", + "isomorphic-dompurify": "~2.2.0", "markdown-it": "~14.0.0", "markdown-it-anchor": "~8.6.5", "markdown-it-container": "~4.0.0", "markdown-it-emoji": "~3.0.0", "markdown-it-footnote": "~4.0.0", - "markdown-it-html5-embed": "~1.0.0", "markdown-it-imsize": "~2.0.1", "markdown-it-sub": "~2.0.0", "markdown-it-sup": "~2.0.0", "markdown-it-table-of-contents": "~0.6.0", "markdown-it-video": "~0.6.3", + "mime": "~3.0.0", "rehype": "~11.0.0", "rehype-react": "~6.2.1" - }, - "overrides": { - "markdown-it": "~14.0.0" } } diff --git a/src/lib/html5-embed.js b/src/lib/html5-embed.js new file mode 100644 index 0000000..b2a1f96 --- /dev/null +++ b/src/lib/html5-embed.js @@ -0,0 +1,286 @@ +/* + A fork of markdown-it-html5-embed, replacing mimosa with mime/lite, to look up + the MIME type of image URLs. +*/ +import mime from 'mime/lite'; + +// Default UI messages. You can customize and add simple translations via +// options.messages. The language has to be provided via the markdown-it +// environment, e.g.: +// +// md.render('some text', { language: 'some code' }) +// +// It will default to English if not provided. To use your own i18n framework, +// you have to provide a translation function via options.translateFn. +// +// The "untitled video" / "untitled audio" messages are only relevant to usage +// inside alternative render functions, where you can access the title between [] as +// {{title}}, and this text is used if no title is provided. +var messages = { + en: { + 'video not supported': 'Your browser does not support playing HTML5 video. ' + + 'You can download a copy of the video file instead.', + 'audio not supported': 'Your browser does not support playing HTML5 audio. ' + + 'You can download a copy of the audio file instead.', + 'content description': 'Here is a description of the content: %s', + 'untitled video': 'Untitled video', + 'untitled audio': 'Untitled audio' + } +}; + +function clearTokens(tokens, idx) { + for (var i = idx; i < tokens.length; i++) { + switch (tokens[i].type) { + case 'link_close': + tokens[i].hidden = true; + break; + case 'text': + tokens[i].content = ''; + break; + default: + throw "Unexpected token: " + tokens[i].type; + } + } +} + +function parseToken(tokens, idx, env) { + var parsed = {}; + var token = tokens[idx]; + var description = ''; + + var aIndex = token.attrIndex('src'); + parsed.isLink = aIndex < 0; + if (parsed.isLink) { + aIndex = token.attrIndex('href'); + description = tokens[idx + 1].content; + } else { + description = token.content; + } + + parsed.url = token.attrs[aIndex][1]; + parsed.mimeType = mime.getType(parsed.url); + var RE = /^(audio|video)\/.*/gi; + var mimetype_matches = RE.exec(parsed.mimeType); + if (mimetype_matches === null) { + parsed.mediaType = null; + } else { + parsed.mediaType = mimetype_matches[1]; + } + + if (parsed.mediaType !== null) { + // For use as titles in alternative render functions, we store the description + // in parsed.title. For use as fallback text, we store it in parsed.fallback + // alongside the standard fallback text. + parsed.fallback = translate({ + messageKey: parsed.mediaType + ' not supported', + messageParam: parsed.url, + language: env.language + }); + if (description.trim().length) { + parsed.fallback += '\n' + translate({ + messageKey: 'content description', + messageParam: description, + language: env.language + }); + parsed.title = description; + } else { + parsed.title = translate({ + messageKey: 'untitled ' + parsed.mediaType, + language: env.language + }); + } + } + return parsed; +} + +function isAllowedMimeType(parsed, options) { + return parsed.mediaType !== null && + (!options.isAllowedMimeType || options.isAllowedMimeType([parsed.mimeType, parsed.mediaType])); +} + +function isAllowedSchema(parsed, options) { + if (!options.isAllowedHttp && parsed.url.match('^http://')) { + return false; + } + return true; +} + +function isAllowedToEmbed(parsed, options) { + return isAllowedMimeType(parsed, options) && isAllowedSchema(parsed, options); +} + +function renderMediaEmbed(parsed, mediaAttributes) { + var attributes = mediaAttributes[parsed.mediaType]; + + return ['<' + parsed.mediaType + ' ' + attributes + '>', + '', + parsed.fallback, + '' + ].join('\n'); +} + +function html5EmbedRenderer(tokens, idx, options, env, renderer, defaultRender) { + var parsed = parseToken(tokens, idx, env); + + if (!isAllowedToEmbed(parsed, options.html5embed)) { + return defaultRender(tokens, idx, options, env, renderer); + } + + if (parsed.isLink) { + clearTokens(tokens, idx + 1); + } + + return renderMediaEmbed(parsed, options.html5embed.attributes); +} + +function forEachLinkOpen(state, action) { + state.tokens.forEach(function(token, _idx, _tokens) { + if (token.type === "inline") { + token.children.forEach(function(token, idx, tokens) { + if (token.type === "link_open") { + action(tokens, idx); + } + }); + } + }); +} + +function findDirective(state, startLine, _endLine, silent, regexp, build_token) { + var pos = state.bMarks[startLine] + state.tShift[startLine]; + var max = state.eMarks[startLine]; + + // Detect directive markdown + var currentLine = state.src.substring(pos, max); + var match = regexp.exec(currentLine); + if (match === null || match.length < 1) { + return false; + } + + if (silent) { + return true; + } + + state.line = startLine + 1; + + // Build content + var token = build_token(); + token.map = [startLine, state.line]; + token.markup = currentLine; + + return true; +} + +/** + * Very basic translation function. To translate or customize the UI messages, + * set options.messages. To also customize the translation function itself, set + * option.translateFn to a function that handles the same message object format. + * + * @param {Object} messageObj + * the message object + * @param {String} messageObj.messageKey + * an identifier used for looking up the message in i18n files + * @param {String} messageObj.messageParam + * for substitution of %s for filename and description in the respective + * messages + * @param {String} [messageObj.language='en'] + * a language code, ignored in the default implementation + * @this {Object} + * the built-in default messages, or options.messages if set + */ +function translate(messageObj) { + // Default to English if we don't have this message, or don't support this + // language at all + var language = messageObj.language && this[messageObj.language] && + this[messageObj.language][messageObj.messageKey] ? + messageObj.language : + 'en'; + var rv = this[language][messageObj.messageKey]; + + if (messageObj.messageParam) { + rv = rv.replace('%s', messageObj.messageParam); + } + return rv; +} + +module.exports = function html5_embed_plugin(md, options) { + var gstate; + var defaults = { + attributes: { + audio: 'controls preload="metadata"', + video: 'controls preload="metadata"' + }, + useImageSyntax: true, + inline: true, + autoAppend: false, + embedPlaceDirectiveRegexp: /^\[\[html5media\]\]/im, + messages: messages + }; + var options = md.utils.assign({}, defaults, options.html5embed); + + if (!options.inline) { + md.block.ruler.before("paragraph", "html5embed", function(state, startLine, endLine, silent) { + return findDirective(state, startLine, endLine, silent, options.embedPlaceDirectiveRegexp, function() { + return state.push("html5media", "html5media", 0); + }); + }); + + md.renderer.rules.html5media = function(tokens, index, _, env) { + var result = ""; + forEachLinkOpen(gstate, function(tokens, idx) { + var parsed = parseToken(tokens, idx, env); + + if (!isAllowedToEmbed(parsed, options)) { + return; + } + + result += renderMediaEmbed(parsed, options.attributes); + }); + if (result.length) { + result += "\n"; + } + return result; + }; + + // Catch all the tokens for iteration later + md.core.ruler.push("grab_state", function(state) { + gstate = state; + + if (options.autoAppend) { + var token = new state.Token("html5media", "", 0); + state.tokens.push(token); + } + }); + } + + if (typeof options.isAllowedMimeType === "undefined") { + options.isAllowedMimeType = options.is_allowed_mime_type; + } + + if (options.inline && options.useImageSyntax) { + var defaultRender = md.renderer.rules.image; + md.renderer.rules.image = function(tokens, idx, opt, env, self) { + opt.html5embed = options; + return html5EmbedRenderer(tokens, idx, opt, env, self, defaultRender); + } + } + + if (options.inline && options.useLinkSyntax) { + var defaultRender = md.renderer.rules.link_open || function(tokens, idx, options, env, self) { + return self.renderToken(tokens, idx, options); + }; + md.renderer.rules.link_open = function(tokens, idx, opt, env, self) { + opt.html5embed = options; + return html5EmbedRenderer(tokens, idx, opt, env, self, defaultRender); + }; + } + + // options.messages will be set to built-in messages at the beginning of this + // file if not configured + translate = typeof options.translateFn == 'function' ? + options.translateFn.bind(options.messages) : + translate.bind(options.messages); + + if (typeof options.renderFn == 'function') { + renderMediaEmbed = options.renderFn; + } +}; diff --git a/src/lib/utils.js b/src/lib/utils.js index 6e3535d..e9fde46 100644 --- a/src/lib/utils.js +++ b/src/lib/utils.js @@ -8,7 +8,6 @@ import markdownImsize from 'markdown-it-imsize'; import markdownVideo from 'markdown-it-video'; import markdownTableOfContents from 'markdown-it-table-of-contents'; import markdownAnchor from 'markdown-it-anchor'; -import html5Embed from 'markdown-it-html5-embed'; import twemoji from '@twemoji/api'; import { sanitize } from 'isomorphic-dompurify'; @@ -16,6 +15,7 @@ import { Fragment, createElement } from 'react'; import rehype from 'rehype'; import rehype2react from 'rehype-react'; +import html5Embed from './html5-embed'; import markdownNewTab from './links-in-new-tabs'; import relNofollow from './links-rel-nofollow'; import replaceSymbols from './default-transformer'; diff --git a/test/use-markdownz-test.js b/test/use-markdownz-test.js index 33318ea..d9619f9 100644 --- a/test/use-markdownz-test.js +++ b/test/use-markdownz-test.js @@ -93,7 +93,33 @@ describe('useMarkdownz', () => { expect(markdownDiv.innerHTML).to.equal('Test'); }); - it('embeds YoutTube videos with modified image syntax', function () { + it('embeds HTML5 video with modified image syntax', function () { + const md = TestUtils.renderIntoDocument( + + ![This is a video file.](https://panoptes-uploads.zooniverse.org/someVideo.mp4) + + ); + const markdownDiv = TestUtils.findRenderedDOMComponentWithClass(md, 'testStub'); + expect(markdownDiv.innerHTML).to.equal(``); + }); + + it('embeds HTML5 audio with modified image syntax', function () { + const md = TestUtils.renderIntoDocument( + + ![This is an audio file.](https://panoptes-uploads.zooniverse.org/someAudio.mp3) + + ); + const markdownDiv = TestUtils.findRenderedDOMComponentWithClass(md, 'testStub'); + expect(markdownDiv.innerHTML).to.equal(``); + }); + + it('embeds YouTube videos with modified image syntax', function () { const md = TestUtils.renderIntoDocument( @[youtube](dQw4w9WgXcQ) diff --git a/test/utils-test.jsx b/test/utils-test.jsx index 29c0f75..5ec592a 100644 --- a/test/utils-test.jsx +++ b/test/utils-test.jsx @@ -42,9 +42,25 @@ describe('Utilities', () => { expect(html).to.equal('Test'); }); - it('embeds YoutTube videos with modified image syntax', function () { + it('embeds HTML5 video with modified image syntax', function () { + const html = utils.getHtml({ content: '![This is a video file.](https://panoptes-uploads.zooniverse.org/someVideo.mp4)', inline: true }); + expect(html).to.equal(``); + }); + + it('embeds HTML5 audio with modified image syntax', function () { + const html = utils.getHtml({ content: '![This is an audio file.](https://panoptes-uploads.zooniverse.org/someAudio.mp3)', inline: true }); + expect(html).to.equal(``); + }); + + it('embeds YouTube videos with modified image syntax', function () { const html = utils.getHtml({ content: '@[youtube](dQw4w9WgXcQ)', inline: true }); - expect(html).to.equal('
'); + expect(html).to.equal('
'); }); }); });