diff --git a/.vscode/settings.json b/.vscode/settings.json index 48fb3ba0b..7c5bc5263 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,5 +10,6 @@ "redhat.telemetry.enabled": false, "[xml]": { "editor.defaultFormatter": "redhat.vscode-xml" - } + }, + "githubPullRequests.ignoredPullRequestBranches": ["main"] } diff --git a/package-lock.json b/package-lock.json index 2546a1bca..e5eb2b996 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,6 +46,7 @@ "node-poppler": "^6.2.3", "node-unrtf": "^3.1.3", "pino": "^8.14.1", + "puppeteer": "^20.3.0", "redoc": "^2.0.0", "secure-json-parse": "^2.7.0", "tesseract.js": "^4.0.5", @@ -2293,6 +2294,61 @@ "node": ">=14" } }, + "node_modules/@puppeteer/browsers": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-1.3.0.tgz", + "integrity": "sha512-an3QdbNPkuU6qpxpbssxAbjRLJcF+eP4L8UqIY3+6n0sbaVxw5pz7PiCLy9g32XEZuoamUlV5ZQPnA6FxvkIHA==", + "dependencies": { + "debug": "4.3.4", + "extract-zip": "2.0.1", + "http-proxy-agent": "5.0.0", + "https-proxy-agent": "5.0.1", + "progress": "2.0.3", + "proxy-from-env": "1.1.0", + "tar-fs": "2.1.1", + "unbzip2-stream": "1.4.3", + "yargs": "17.7.1" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "typescript": ">= 4.7.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@puppeteer/browsers/node_modules/yargs": { + "version": "17.7.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz", + "integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@puppeteer/browsers/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + }, "node_modules/@redocly/ajv": { "version": "8.11.0", "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.11.0.tgz", @@ -2572,6 +2628,15 @@ "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", "dev": true }, + "node_modules/@types/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/scope-manager": { "version": "5.59.2", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.2.tgz", @@ -3388,6 +3453,52 @@ "node": ">=8" } }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/bluebird": { "version": "3.4.7", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", @@ -3512,7 +3623,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "engines": { "node": ">=6" } @@ -3650,6 +3760,11 @@ "fsevents": "~2.3.2" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, "node_modules/chrome-trace-event": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", @@ -3659,6 +3774,17 @@ "node": ">=6.0" } }, + "node_modules/chromium-bidi": { + "version": "0.4.9", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.4.9.tgz", + "integrity": "sha512-u3DC6XwgLCA9QJ5ak1voPslCmacQdulZNCPsI3qNXxSnEcZS7DFIbww+5RM2bznMEje7cc0oydavRLRvOIZtHw==", + "dependencies": { + "mitt": "3.0.0" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, "node_modules/ci-info": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", @@ -3937,7 +4063,6 @@ "version": "8.1.3", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.1.3.tgz", "integrity": "sha512-/UkO2JKI18b5jVMJUp0lvKFMpa/Gye+ZgZjKD+DGEN9y7NRcf/nK1A0sp67ONmKtnDCNMS44E6jrk0Yc3bDuUw==", - "dev": true, "dependencies": { "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", @@ -3990,6 +4115,14 @@ "integrity": "sha512-YIaY9TR5Nxeb8SMdtrU8asWVM4jqJDNDYlKV21LxtYcfNJhp1kEsgSa6qXwXgzN0WQWGODps0+TlGp2xQSHwOg==", "dev": true }, + "node_modules/cross-fetch": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.6.tgz", + "integrity": "sha512-riRvo06crlE8HiqOwIpQhxwdOk4fOeR7FVM/wXoxchFEqMNUjvbs3bfo4OTgMEMHzppd4DxFBDbyySj8Cv781g==", + "dependencies": { + "node-fetch": "^2.6.11" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -4216,6 +4349,11 @@ "node": ">=8" } }, + "node_modules/devtools-protocol": { + "version": "0.0.1120988", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1120988.tgz", + "integrity": "sha512-39fCpE3Z78IaIPChJsP6Lhmkbf4dWXOmzLk/KFTdRkNk/0JymRIfUynDVRndV9HoDz8PyalK1UH21ST/ivwW5Q==" + }, "node_modules/dezalgo": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", @@ -4488,7 +4626,6 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, "dependencies": { "is-arrayish": "^0.2.1" } @@ -5196,6 +5333,39 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extract-zip/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/fast-content-type-parse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-1.0.0.tgz", @@ -5584,6 +5754,11 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, "node_modules/fs-extra": { "version": "11.1.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", @@ -6327,7 +6502,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -6343,7 +6517,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "engines": { "node": ">=4" } @@ -6459,8 +6632,7 @@ "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" }, "node_modules/is-bigint": { "version": "1.0.4", @@ -7948,8 +8120,7 @@ "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, "node_modules/loader-runner": { "version": "4.3.0", @@ -8368,6 +8539,11 @@ "node": ">=8" } }, + "node_modules/mitt": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.0.tgz", + "integrity": "sha512-7dX2/10ITVyqh4aOSVI9gdape+t9l2/8QxHrFmUXu4EEUpdlxl6RudZUPZoc+zuY2hk1j7XxVroIVIan/pD/SQ==" + }, "node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", @@ -8380,6 +8556,11 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, "node_modules/mnemonist": { "version": "0.39.5", "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.39.5.tgz", @@ -8478,9 +8659,9 @@ } }, "node_modules/node-fetch": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz", - "integrity": "sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==", + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==", "dependencies": { "whatwg-url": "^5.0.0" }, @@ -9106,7 +9287,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "dependencies": { "callsites": "^3.0.0" }, @@ -9118,7 +9298,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", @@ -9235,7 +9414,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, "engines": { "node": ">=8" } @@ -9677,7 +9855,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, "engines": { "node": ">=0.4.0" } @@ -9722,6 +9899,11 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -9784,6 +9966,41 @@ "node": ">=6" } }, + "node_modules/puppeteer": { + "version": "20.3.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-20.3.0.tgz", + "integrity": "sha512-OJIsxC3PcU6fAWfp1BX0oKj62FFxhxwpakGAcGVbyfqfDrNmkFjpzb0XrMsgnQxCJmoLpTZKHRXDFxEDnLqkow==", + "hasInstallScript": true, + "dependencies": { + "@puppeteer/browsers": "1.3.0", + "cosmiconfig": "8.1.3", + "puppeteer-core": "20.3.0" + } + }, + "node_modules/puppeteer-core": { + "version": "20.3.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-20.3.0.tgz", + "integrity": "sha512-264pBrIui5bO6NJeOcbJrLa0OCwmA4+WK00JMrLIKTfRiqe2gx8KWTzLsjyw/bizErp3TKS7vt/I0i5fTC+mAw==", + "dependencies": { + "@puppeteer/browsers": "1.3.0", + "chromium-bidi": "0.4.9", + "cross-fetch": "3.1.6", + "debug": "4.3.4", + "devtools-protocol": "0.0.1120988", + "ws": "8.13.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "typescript": ">= 4.7.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/pure-rand": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.2.tgz", @@ -11363,6 +11580,45 @@ "node": ">=6" } }, + "node_modules/tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/terser": { "version": "5.17.2", "resolved": "https://registry.npmjs.org/terser/-/terser-5.17.2.tgz", @@ -11572,8 +11828,7 @@ "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "dev": true + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" }, "node_modules/through2": { "version": "4.0.2", @@ -11914,7 +12169,7 @@ "version": "5.0.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", - "dev": true, + "devOptional": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -11938,6 +12193,38 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "dependencies": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "node_modules/unbzip2-stream/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", diff --git a/package.json b/package.json index f1e5f99dc..cc22a828f 100644 --- a/package.json +++ b/package.json @@ -126,6 +126,7 @@ "node-poppler": "^6.2.3", "node-unrtf": "^3.1.3", "pino": "^8.14.1", + "puppeteer": "^20.3.0", "redoc": "^2.0.0", "secure-json-parse": "^2.7.0", "tesseract.js": "^4.0.5", diff --git a/scripts/license-checker.js b/scripts/license-checker.js index 6000d7fb6..891cc174c 100644 --- a/scripts/license-checker.js +++ b/scripts/license-checker.js @@ -17,6 +17,7 @@ async function checkLicenses() { /** * List of deprecated copyleft license identifiers + * * @see https://spdx.org/licenses/ */ const deprecatedLicenseList = [ diff --git a/src/plugins/html-to-pdf/index.js b/src/plugins/html-to-pdf/index.js new file mode 100644 index 000000000..25d865464 --- /dev/null +++ b/src/plugins/html-to-pdf/index.js @@ -0,0 +1,67 @@ +const fp = require("fastify-plugin"); +// eslint-disable-next-line import/no-extraneous-dependencies +const puppeteer = require("puppeteer"); + +/** + * @author Julian Matthews + * @description Pre-handler plugin that uses puppeteer to convert Buffer containing + * HTML in `req.body` to PDF. + * `req` object is decorated with `conversionResults.body` holding the converted document. + * @param {object} server - Fastify instance. + * @param {object} options - Plugin config values. + * @param {object=} options.htmlToPdfOptions - Options to pass to puppeteer + */ +async function plugin(server) { + server.addHook("onRequest", async (req) => { + req.conversionResults = { body: undefined }; + }); + + server.addHook("preHandler", async (req, res) => { + // Define any default settings the plugin should have to get up and running + const config = { + htmlToPdfOptions: { + margin: { + top: "80px", + right: "30px", + bottom: "80px", + left: "30px", + }, + printBackground: true, + format: "A4", + }, + }; + + try { + // Create a headless browser instance + const browser = await puppeteer.launch(); + + // Create new page + const page = await browser.newPage(); + + // Get HTML content from req.body + const html = req.body.toString(); + await page.setContent(html, { waitUntil: "domcontentloaded" }); + + // Convert HTML To PDF (which is basically "printing" to PDF from the headless browser) + const pdf = await page.pdf(config.htmlToPdfOptions); + + // Close the browser instance + await browser.close(); + + req.conversionResults.body = pdf; + + res.type("application/pdf"); + } catch { + /** + * Catch all in case of failure. + */ + throw server.httpErrors.badRequest(); + } + }); +} + +module.exports = fp(plugin, { + fastify: "4.x", + name: "html-to-pdf", + dependencies: ["@fastify/sensible"], +}); diff --git a/src/plugins/html-to-pdf/plugin.test.js b/src/plugins/html-to-pdf/plugin.test.js new file mode 100644 index 000000000..d50f0d97a --- /dev/null +++ b/src/plugins/html-to-pdf/plugin.test.js @@ -0,0 +1,52 @@ +const fs = require("fs/promises"); +const Fastify = require("fastify"); +const sensible = require("@fastify/sensible"); +const plugin = require("."); +const isPdf = require("../../utils/is-pdf/index"); + +describe("HTML-to-PDF conversion plugin", () => { + let server; + + beforeAll(async () => { + server = Fastify(); + + server.addContentTypeParser( + "text/html", + { parseAs: "buffer" }, + async (_req, payload) => payload + ); + + await server.register(sensible).register(plugin); + + server.post("/", (req, res) => { + res.header("content-type", "application/json").send( + req.conversionResults + ); + }); + + await server.ready(); + }); + + afterAll(async () => { + await server.close(); + }); + + test("Should convert HTML file to PDF", async () => { + const response = await server.inject({ + method: "POST", + url: "/", + body: await fs.readFile( + "./test_resources/test_files/valid_html.html" + ), + headers: { + "content-type": "text/html", + }, + }); + + const body = JSON.parse(response.payload); + + expect(isPdf.buff(body.body.data)).toBe(true); + + expect(response.statusCode).toBe(200); + }); +}); diff --git a/src/routes/html/pdf/index.js b/src/routes/html/pdf/index.js new file mode 100644 index 000000000..3b79828cd --- /dev/null +++ b/src/routes/html/pdf/index.js @@ -0,0 +1,57 @@ +// Import plugins +const cors = require("@fastify/cors"); +const htmlToPdf = require("../../../plugins/html-to-pdf"); + +const { htmlToPdfPostSchema } = require("./schema"); + +const accepts = ["text/html"]; + +/** + * @author Julian Matthews + * @description Sets routing options for server. + * @param {object} server - Fastify instance. + * @param {object} options - Route config values. + * @param {*=} options.bearerTokenAuthKeys - Apply `bearerToken` security scheme to route if defined. + * @param {object} options.cors - CORS settings. + */ +async function route(server, options) { + if (options.bearerTokenAuthKeys) { + htmlToPdfPostSchema.security = [{ bearerToken: [] }]; + htmlToPdfPostSchema.response[401] = { + $ref: "responses#/properties/unauthorized", + description: "Unauthorized", + }; + } + + server.addContentTypeParser( + "text/html", + { parseAs: "buffer" }, + async (_req, payload) => payload + ); + + // Register plugins + await server + // Enable CORS if options passed + .register(cors, { + ...options.cors, + methods: ["POST"], + }) + .register(htmlToPdf); + + server.route({ + method: "POST", + url: "/", + schema: htmlToPdfPostSchema, + onRequest: async (req) => { + if ( + // Catch unsupported Accept header media types + !req.accepts().type(accepts) + ) { + throw server.httpErrors.notAcceptable(); + } + }, + handler: async (req) => req.conversionResults.body, + }); +} + +module.exports = route; diff --git a/src/routes/html/pdf/route.test.js b/src/routes/html/pdf/route.test.js new file mode 100644 index 000000000..e976e1020 --- /dev/null +++ b/src/routes/html/pdf/route.test.js @@ -0,0 +1,98 @@ +const accepts = require("@fastify/accepts"); +const fs = require("fs/promises"); +const Fastify = require("fastify"); +const sensible = require("@fastify/sensible"); +const isPdf = require("../../../utils/is-pdf/index"); +const route = require("."); +const getConfig = require("../../../config"); +const sharedSchemas = require("../../../plugins/shared-schemas"); + +const tidyCss = require("../../../plugins/tidy-css"); +const tidyHtml = require("../../../plugins/tidy-html"); + +describe("HTML-to-PDF route", () => { + let config; + let server; + + beforeAll(async () => { + config = await getConfig(); + + server = Fastify({ bodyLimit: 10485760 }); + await server + .register(accepts) + .register(sensible) + .register(sharedSchemas) + .register(tidyCss) + .register(tidyHtml) + .register(route, config) + .ready(); + }); + + afterAll(async () => { + await server.close(); + }); + + test("Should convert HTML file to PDF", async () => { + const response = await server.inject({ + method: "POST", + url: "/", + body: await fs.readFile( + "./test_resources/test_files/valid_html.html" + ), + headers: { + "content-type": "text/html", + }, + }); + + const body = response.payload; + + expect(isPdf.raw(body)).toBe(true); + + expect(response.statusCode).toBe(200); + + return response.statusCode; + }); + + test("Should return HTTP status code 415 if file media type is not supported by route", async () => { + const response = await server.inject({ + method: "POST", + url: "/", + body: await fs.readFile( + "./test_resources/test_files/pdf_1.5_lorem_ipsum.pdf" + ), + headers: { + "content-type": "application/pdf", + }, + }); + + expect(JSON.parse(response.payload)).toEqual({ + error: "Unsupported Media Type", + message: "Unsupported Media Type: application/pdf", + statusCode: 415, + }); + expect(response.statusCode).toBe(415); + + return response.statusCode; + }); + + test("Should return HTTP status code 406 if media type in `Accept` request header is unsupported", async () => { + const response = await server.inject({ + method: "POST", + url: "/", + body: await fs.readFile( + "./test_resources/test_files/valid_html.html" + ), + headers: { + accept: "application/javascript", + "content-type": "application/rtf", + }, + }); + + expect(JSON.parse(response.payload)).toEqual({ + error: "Not Acceptable", + message: "Not Acceptable", + statusCode: 406, + }); + expect(response.statusCode).toBe(406); + }); +}); diff --git a/src/routes/html/pdf/schema.js b/src/routes/html/pdf/schema.js new file mode 100644 index 000000000..35599a17f --- /dev/null +++ b/src/routes/html/pdf/schema.js @@ -0,0 +1,49 @@ +const S = require("fluent-json-schema"); + +const tags = ["HTML"]; + +/** + * Fastify uses AJV for JSON Schema Validation, + * see https://fastify.io/docs/latest/Reference/Validation-and-Serialization/ + * + * Input validation protects against XSS, HPP, prototype pollution, + * and most other injection attacks + */ +const htmlToPdfPostSchema = { + tags, + summary: "Convert HTML to PDF", + description: + "Returns the result of converting a HTML document to PDF format.", + operationId: "postHtmlToPdf", + consumes: ["text/html"], + produces: ["application/pdf"], + query: S.object().additionalProperties(false), + response: { + 200: { + content: { + "application/pdf": { + schema: { + type: "string", + }, + }, + }, + }, + 400: S.ref("responses#/properties/badRequest").description( + "Bad Request" + ), + 406: S.ref("responses#/properties/notAcceptable").description( + "Not Acceptable" + ), + 415: S.ref("responses#/properties/unsupportedMediaType").description( + "Unsupported Media Type" + ), + 429: S.ref("responses#/properties/tooManyRequests").description( + "Too Many Requests" + ), + 503: S.ref("responses#/properties/serviceUnavailable").description( + "Service Unavailable" + ), + }, +}; + +module.exports = { htmlToPdfPostSchema }; diff --git a/src/utils/is-pdf/index.js b/src/utils/is-pdf/index.js new file mode 100644 index 000000000..48ec4e14f --- /dev/null +++ b/src/utils/is-pdf/index.js @@ -0,0 +1,30 @@ +/** + * @author Julian Matthews + * @description Validates buffer as PDF. + * @param {*} buffer - Buffer to validate. + * @returns {*} Boolean. + */ +function buff(buffer) { + if (!buffer || buffer.length < 4) { + return false; + } + return ( + buffer[0] === 37 && + buffer[1] === 80 && + buffer[2] === 68 && + buffer[3] === 70 + ); +} +/** + * @description Validates raw input as PDF. + * @param {*} str - string to validate. + * @returns {*} Boolean. + */ +function raw(str) { + if (!str || str.length < 4) { + return false; + } + return str.startsWith("%PDF"); +} + +module.exports = { buff, raw };