Просмотр исходного кода

Merge branch 'master' into support/148121-replace-tests-with-playwright

Shun Miyazawa 1 год назад
Родитель
Сommit
fa3461fce5
80 измененных файлов с 589 добавлено и 853 удалено
  1. 5 0
      .changeset/tasty-baboons-burn.md
  2. 5 1
      .github/dependabot.yml
  3. 29 1
      CHANGELOG.md
  4. 1 1
      apps/app/docker/README.md
  5. 16 0
      apps/app/next.config.js
  6. 10 11
      apps/app/package.json
  7. 6 6
      apps/app/src/client/services/renderer/renderer.tsx
  8. 0 96
      apps/app/src/components/Admin/MarkdownSetting/WhitelistInput.jsx
  9. 79 0
      apps/app/src/components/Admin/MarkdownSetting/WhitelistInput.tsx
  10. 10 10
      apps/app/src/components/Admin/MarkdownSetting/XssForm.jsx
  11. 4 6
      apps/app/src/components/Admin/UserGroupDetail/UserGroupDetailPage.tsx
  12. 3 7
      apps/app/src/components/Admin/UserGroupDetail/UserGroupUserFormByInput.tsx
  13. 1 1
      apps/app/src/components/Common/PageViewLayout.tsx
  14. 9 1
      apps/app/src/components/Common/SubmittableInput/use-submittable.ts
  15. 1 1
      apps/app/src/components/CreateTemplateModal.tsx
  16. 3 1
      apps/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  17. 1 1
      apps/app/src/components/PageEditor/EditorNavbar/EditingUserList.tsx
  18. 19 6
      apps/app/src/components/PageEditor/OptionsSelector.tsx
  19. 6 2
      apps/app/src/components/Script/DrawioViewerScript/DrawioViewerScript.tsx
  20. 2 31
      apps/app/src/components/Script/DrawioViewerScript/use-viewer-min-js-url.spec.ts
  21. 4 10
      apps/app/src/components/Script/DrawioViewerScript/use-viewer-min-js-url.ts
  22. 0 6
      apps/app/src/interfaces/rehype.ts
  23. 15 0
      apps/app/src/interfaces/services/rehype-sanitize.ts
  24. 2 2
      apps/app/src/interfaces/services/renderer.ts
  25. 4 4
      apps/app/src/pages/[[...path]].page.tsx
  26. 4 4
      apps/app/src/pages/_private-legacy-pages.page.tsx
  27. 4 4
      apps/app/src/pages/_search.page.tsx
  28. 3 3
      apps/app/src/pages/me/[[...path]].page.tsx
  29. 5 5
      apps/app/src/pages/share/[[...path]].page.tsx
  30. 1 15
      apps/app/src/server/crowi/index.js
  31. 4 4
      apps/app/src/server/models/config.ts
  32. 3 20
      apps/app/src/server/routes/apiv3/page/update-page.ts
  33. 3 2
      apps/app/src/server/routes/apiv3/user-group.js
  34. 0 13
      apps/app/src/server/routes/page.js
  35. 1 1
      apps/app/src/server/service/config-loader.ts
  36. 1 28
      apps/app/src/server/service/customize.ts
  37. 8 7
      apps/app/src/server/service/page/index.ts
  38. 2 1
      apps/app/src/server/service/slack-command-handler/create-page-service.js
  39. 0 73
      apps/app/src/server/service/xss.js
  40. 39 0
      apps/app/src/services/general-xss-filter/general-xss-filter.spec.ts
  41. 37 0
      apps/app/src/services/general-xss-filter/general-xss-filter.ts
  42. 1 0
      apps/app/src/services/general-xss-filter/index.ts
  43. 38 0
      apps/app/src/services/renderer/recommended-whitelist.spec.ts
  44. 29 0
      apps/app/src/services/renderer/recommended-whitelist.ts
  45. 12 23
      apps/app/src/services/renderer/renderer.tsx
  46. 0 42
      apps/app/src/services/xss/commonmark-spec.js
  47. 0 63
      apps/app/src/services/xss/index.js
  48. 0 21
      apps/app/src/services/xss/recommended-whitelist.js
  49. 0 32
      apps/app/src/services/xss/xssOption.ts
  50. 0 10
      apps/app/src/stores/xss.ts
  51. 1 1
      apps/app/src/styles/organisms/_wiki.scss
  52. 2 2
      apps/app/test/cypress/tsconfig.json
  53. 0 3
      apps/app/test/integration/service/page-grant.test.ts
  54. 9 8
      apps/app/test/integration/service/page.test.js
  55. 10 9
      apps/app/test/integration/service/v5.non-public-page.test.ts
  56. 0 2
      apps/app/test/integration/service/v5.page.test.ts
  57. 17 16
      apps/app/test/integration/service/v5.public-page.test.ts
  58. 0 1
      apps/app/test/integration/setup-crowi.ts
  59. 6 0
      apps/app/test/integration/tsconfig.json
  60. 0 7
      apps/app/test/tsconfig.json
  61. 0 1
      apps/app/tsconfig.build.client.json
  62. 0 11
      apps/app/tsconfig.build.server-tsc-alias.json
  63. 7 0
      apps/app/tsconfig.build.server.json
  64. 14 4
      apps/app/tsconfig.json
  65. 4 2
      apps/app/turbo.json
  66. 4 7
      apps/slackbot-proxy/package.json
  67. 9 1
      apps/slackbot-proxy/tsconfig.json
  68. 3 1
      package.json
  69. 1 1
      packages/editor/package.json
  70. 1 2
      packages/editor/tsconfig.json
  71. 0 11
      packages/editor/tsconfig.node.json
  72. 1 0
      packages/editor/vite.config.ts
  73. 0 1
      packages/pluginkit/tsconfig.json
  74. 1 1
      packages/remark-growi-directive/package.json
  75. 0 12
      packages/remark-growi-directive/tsconfig.base.json
  76. 2 6
      packages/remark-growi-directive/tsconfig.build.json
  77. 7 8
      packages/remark-growi-directive/tsconfig.json
  78. 1 2
      packages/slack/package.json
  79. 3 3
      tsconfig.base.json
  80. 56 154
      yarn.lock

+ 5 - 0
.changeset/tasty-baboons-burn.md

@@ -0,0 +1,5 @@
+---
+'@growi/pluginkit': patch
+---
+
+Update tsconfig.json module setting

+ 5 - 1
.github/dependabot.yml

@@ -4,7 +4,8 @@ updates:
     directory: '/'
     open-pull-requests-limit: 3
     schedule:
-      interval: monthly
+      interval: weekly
+      day: saturday
     labels:
       - "type/dependencies"
     commit-message:
@@ -16,6 +17,7 @@ updates:
     open-pull-requests-limit: 3
     schedule:
       interval: weekly
+      day: saturday
     labels:
       - "type/dependencies"
     commit-message:
@@ -27,4 +29,6 @@ updates:
       - dependency-name: "@handsontable/react"
       - dependency-name: handsontable
       - dependency-name: typeorm
+      - dependency-name: mysql2
+
 

+ 29 - 1
CHANGELOG.md

@@ -1,9 +1,37 @@
 # Changelog
 
-## [Unreleased](https://github.com/weseek/growi/compare/v7.0.9...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v7.0.10...HEAD)
 
 *Please do not manually update this file. We've automated the process.*
 
+## [v7.0.10](https://github.com/weseek/growi/compare/v7.0.9...v7.0.10) - 2024-06-13
+
+### 💎 Features
+
+* imprv: Autofocus on PageTitleHeader when edigin untitled page (#8813) @WNomunomu
+* imprv: Autofocus on PageTitleHeader when creating untitled page (#8813) @WNomunomu
+
+### 🚀 Improvement
+
+* imprv: DrawioViewerScript should respect the base path in DRAWIO_URI 2 (#8889) @yuki-takei
+* imprv: Styling icon on the side of header (#8833) @reiji-h
+* imprv: DrawioViewerScript should respect the base path in DRAWIO_URI (#8878) @yuki-takei
+* imprv: Behavior when clicking on a label in the dropdown (#8857) @maeshinshin
+* imprv(plugin): Support github tag in githuburl.ts (#8868) @reiji-h
+* imprv: Display selected tree item in page select modal as active (#8802) @WNomunomu
+
+### 🐛 Bug Fixes
+
+* fix: Match width of video tag to img tag (#8836) @TatsuyaIse
+* fix: Behaviour of input during Japanese input (#8880) @miya
+* fix: Supress warning of checkbox 2 (#8871) @yuki-takei
+
+### 🧰 Maintenance
+
+* support: Watch with nodemon (#8877) @yuki-takei
+* support: Add playwright test for installer (#8874) @yuki-takei
+* support: Upgrade turbo to v2 (#8875) @yuki-takei
+
 ## [v7.0.9](https://github.com/weseek/growi/compare/v7.0.8...v7.0.9) - 2024-05-30
 
 ### 🐛 Bug Fixes

+ 1 - 1
apps/app/docker/README.md

@@ -10,7 +10,7 @@ GROWI Official docker image
 Supported tags and respective Dockerfile links
 ------------------------------------------------
 
-* [`7.0.9`, `7.0`, `7`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v7.0.9/apps/app/docker/Dockerfile)
+* [`7.0.10`, `7.0`, `7`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v7.0.10/apps/app/docker/Dockerfile)
 * [`6.3.2`, `6.3`, `6` (Dockerfile)](https://github.com/weseek/growi/blob/v6.3.2/apps/app/docker/Dockerfile)
 * [`6.2.4`, `6.2` (Dockerfile)](https://github.com/weseek/growi/blob/v6.2.4/apps/app/docker/Dockerfile)
 * [`6.1.15`, `6.1` (Dockerfile)](https://github.com/weseek/growi/blob/v6.1.15/apps/app/docker/Dockerfile)

+ 16 - 0
apps/app/next.config.js

@@ -62,6 +62,19 @@ const getTranspilePackages = () => {
   return packages;
 };
 
+const optimizePackageImports = [
+  '@growi/core',
+  '@growi/editor',
+  '@growi/pluginkit',
+  '@growi/presentation',
+  '@growi/preset-themes',
+  '@growi/remark-attachment-refs',
+  '@growi/remark-drawio',
+  '@growi/remark-growi-directive',
+  '@growi/remark-lsx',
+  '@growi/slack',
+  '@growi/ui',
+];
 
 module.exports = async(phase, { defaultConfig }) => {
 
@@ -85,6 +98,9 @@ module.exports = async(phase, { defaultConfig }) => {
     transpilePackages: phase !== PHASE_PRODUCTION_SERVER
       ? getTranspilePackages()
       : undefined,
+    experimental: {
+      optimizePackageImports,
+    },
 
     /** @param config {import('next').NextConfig} */
     webpack(config, options) {

+ 10 - 11
apps/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/app",
-  "version": "7.0.10-RC.0",
+  "version": "7.0.11-RC.0",
   "license": "MIT",
   "private": "true",
   "scripts": {
@@ -8,7 +8,7 @@
     "build": "run-p build:*",
     "start": "yarn next start",
     "build:client": "yarn next build",
-    "build:server": "yarn cross-env NODE_ENV=production tsc -p tsconfig.build.server.json && tsc-alias -p tsconfig.build.server-tsc-alias.json",
+    "build:server": "yarn cross-env NODE_ENV=production tspc -p tsconfig.build.server.json",
     "postbuild:server": "shx echo \"Listing files under transpiled\" && shx ls transpiled && shx rm -rf dist && shx mv transpiled/src dist && shx rm -rf transpiled",
     "clean": "shx rm -rf dist transpiled",
     "server": "yarn cross-env NODE_ENV=production node -r dotenv-flow/config dist/server/app.js",
@@ -28,16 +28,16 @@
     "cy:run": "cypress run --browser chromium",
     "//// for CI": "",
     "dev:ci": "yarn cross-env NODE_ENV=development yarn ts-node src/server/app.ts --ci",
-    "lint:typecheck": "npx -y tsc",
+    "lint:typecheck": "npx -y tspc",
     "lint:eslint": "yarn eslint --quiet \"**/*.{js,jsx,ts,tsx}\"",
     "lint:styles": "stylelint \"src/**/*.scss\"",
     "lint:swagger2openapi": "node node_modules/.bin/oas-validate tmp/swagger.json",
     "lint": "run-p lint:*",
     "prelint:swagger2openapi": "yarn openapi:v3",
     "test": "run-p test:*",
-    "test:jest": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=4096\" jest --logHeapUsage",
+    "test:jest": "cross-env NODE_ENV=test TS_NODE_PROJECT=test/integration/tsconfig.json jest",
     "test:vitest": "run-p vitest:run vitest:run:integ vitest:run:components",
-    "jest:run": "cross-env NODE_ENV=test jest --passWithNoTests -- ",
+    "jest:run": "cross-env NODE_ENV=test TS_NODE_PROJECT=test/integration/tsconfig.json jest --passWithNoTests -- ",
     "reg:run": "reg-suit run",
     "vitest:run": "vitest run config src --coverage",
     "vitest:run:integ": "vitest run -c vitest.config.integ.ts src --coverage",
@@ -133,7 +133,7 @@
     "md5": "^2.2.1",
     "mermaid": "^10.1.0",
     "method-override": "^3.0.0",
-    "migrate-mongo": "^8.2.3",
+    "migrate-mongo": "^11.0.0",
     "mkdirp": "^1.0.3",
     "mongoose": "^6.11.3",
     "mongoose-gridfs": "^1.2.42",
@@ -165,8 +165,6 @@
     "react-card-flip": "^1.0.10",
     "react-datepicker": "^4.7.0",
     "react-disable": "^0.1.1",
-    "react-dnd": "^14.0.5",
-    "react-dnd-html5-backend": "^14.1.0",
     "react-dom": "^18.2.0",
     "react-error-boundary": "^3.1.4",
     "react-i18next": "^14.1.0",
@@ -176,7 +174,6 @@
     "react-scroll": "^1.8.7",
     "react-stickynode": "^4.1.1",
     "react-syntax-highlighter": "^15.5.0",
-    "react-toastify": "^9.1.3",
     "react-use-ripple": "^1.5.2",
     "reactstrap": "^9.2.2",
     "reconnecting-websocket": "^4.4.0",
@@ -272,9 +269,12 @@
     "pretty-bytes": "^6.1.1",
     "react-codemirror2": "^6.0.0",
     "react-copy-to-clipboard": "^5.0.1",
+    "react-dnd": "^14.0.5",
+    "react-dnd-html5-backend": "^14.1.0",
     "react-dropzone": "^14.2.3",
     "react-hotkeys": "^2.0.0",
     "react-input-autosize": "^3.0.0",
+    "react-toastify": "^9.1.3",
     "rehype-rewrite": "^3.0.6",
     "replacestream": "^4.0.3",
     "sass": "^1.53.0",
@@ -282,7 +282,6 @@
     "simplebar-react": "^2.3.6",
     "socket.io-client": "^4.7.5",
     "source-map-loader": "^4.0.1",
-    "swagger2openapi": "^7.0.8",
-    "tsc-alias": "^1.2.9"
+    "swagger2openapi": "^7.0.8"
   }
 }

+ 6 - 6
apps/app/src/client/services/renderer/renderer.tsx

@@ -3,7 +3,6 @@ import assert from 'assert';
 import { isClient } from '@growi/core/dist/utils/browser-utils';
 import * as refsGrowiDirective from '@growi/remark-attachment-refs/dist/client';
 import * as drawio from '@growi/remark-drawio';
-// eslint-disable-next-line import/extensions
 import * as lsxGrowiDirective from '@growi/remark-lsx/dist/client';
 import katex from 'rehype-katex';
 import sanitize from 'rehype-sanitize';
@@ -20,8 +19,8 @@ import { LightBox } from '~/components/ReactMarkdownComponents/LightBox';
 import { RichAttachment } from '~/components/ReactMarkdownComponents/RichAttachment';
 import { TableWithEditButton } from '~/components/ReactMarkdownComponents/TableWithEditButton';
 import * as mermaid from '~/features/mermaid';
-import { RehypeSanitizeOption } from '~/interfaces/rehype';
 import type { RendererOptions } from '~/interfaces/renderer-options';
+import { RehypeSanitizeType } from '~/interfaces/services/rehype-sanitize';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import * as addLineNumberAttribute from '~/services/renderer/rehype-plugins/add-line-number-attribute';
 import * as keywordHighlighter from '~/services/renderer/rehype-plugins/keyword-highlighter';
@@ -36,6 +35,7 @@ import loggerFactory from '~/utils/logger';
 
 // import EasyGrid from './PreProcessor/EasyGrid';
 
+
 import '@growi/remark-lsx/dist/client/style.css';
 import '@growi/remark-attachment-refs/dist/client/style.css';
 
@@ -71,7 +71,7 @@ export const generateViewOptions = (
     remarkPlugins.push(breaks);
   }
 
-  if (config.xssOption === RehypeSanitizeOption.CUSTOM) {
+  if (config.sanitizeType === RehypeSanitizeType.CUSTOM) {
     injectCustomSanitizeOption(config);
   }
 
@@ -132,7 +132,7 @@ export const generateTocOptions = (config: RendererConfig, tocNode: HtmlElementN
   // add remark plugins
   // remarkPlugins.push();
 
-  if (config.xssOption === RehypeSanitizeOption.CUSTOM) {
+  if (config.sanitizeType === RehypeSanitizeType.CUSTOM) {
     injectCustomSanitizeOption(config);
   }
 
@@ -184,7 +184,7 @@ export const generateSimpleViewOptions = (
     remarkPlugins.push(breaks);
   }
 
-  if (config.xssOption === RehypeSanitizeOption.CUSTOM) {
+  if (config.sanitizeType === RehypeSanitizeType.CUSTOM) {
     injectCustomSanitizeOption(config);
   }
 
@@ -277,7 +277,7 @@ export const generatePreviewOptions = (config: RendererConfig, pagePath: string)
     remarkPlugins.push(breaks);
   }
 
-  if (config.xssOption === RehypeSanitizeOption.CUSTOM) {
+  if (config.sanitizeType === RehypeSanitizeType.CUSTOM) {
     injectCustomSanitizeOption(config);
   }
 

+ 0 - 96
apps/app/src/components/Admin/MarkdownSetting/WhitelistInput.jsx

@@ -1,96 +0,0 @@
-import React from 'react';
-
-import { useTranslation } from 'next-i18next';
-import PropTypes from 'prop-types';
-import { defaultSchema as sanitizeDefaultSchema } from 'rehype-sanitize';
-
-import AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
-
-import { withUnstatedContainers } from '../../UnstatedUtils';
-
-class WhitelistInput extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.tagWhitelist = React.createRef();
-    this.attrWhitelist = React.createRef();
-
-    this.tags = sanitizeDefaultSchema.tagNames;
-    this.attrs = JSON.stringify(sanitizeDefaultSchema.attributes);
-
-    this.onClickRecommendTagButton = this.onClickRecommendTagButton.bind(this);
-    this.onClickRecommendAttrButton = this.onClickRecommendAttrButton.bind(this);
-  }
-
-  onClickRecommendTagButton() {
-    this.tagWhitelist.current.value = this.tags;
-    this.props.adminMarkDownContainer.setState({ tagWhitelist: this.tags });
-  }
-
-  onClickRecommendAttrButton() {
-    this.attrWhitelist.current.value = this.attrs;
-    this.props.adminMarkDownContainer.setState({ attrWhitelist: this.attrs });
-  }
-
-  render() {
-    const { t, adminMarkDownContainer } = this.props;
-
-    return (
-      <>
-        <div className="mt-4">
-          <div className="d-flex justify-content-between">
-            {t('markdown_settings.xss_options.tag_names')}
-            <p id="btn-import-tags" className="btn btn-sm btn-primary" onClick={this.onClickRecommendTagButton}>
-              {t('markdown_settings.xss_options.import_recommended', { target: 'Tags' })}
-            </p>
-          </div>
-          <textarea
-            className="form-control xss-list"
-            name="recommendedTags"
-            rows="6"
-            cols="40"
-            ref={this.tagWhitelist}
-            defaultValue={adminMarkDownContainer.state.tagWhitelist}
-            onChange={(e) => { adminMarkDownContainer.setState({ tagWhitelist: e.target.value }) }}
-          />
-        </div>
-        <div className="mt-4">
-          <div className="d-flex justify-content-between">
-            {t('markdown_settings.xss_options.tag_attributes')}
-            <p id="btn-import-tags" className="btn btn-sm btn-primary" onClick={this.onClickRecommendAttrButton}>
-              {t('markdown_settings.xss_options.import_recommended', { target: 'Attrs' })}
-            </p>
-          </div>
-          <textarea
-            className="form-control xss-list"
-            name="recommendedAttrs"
-            rows="6"
-            cols="40"
-            ref={this.attrWhitelist}
-            defaultValue={adminMarkDownContainer.state.attrWhitelist}
-            onChange={(e) => { adminMarkDownContainer.setState({ attrWhitelist: e.target.value }) }}
-          />
-        </div>
-      </>
-    );
-  }
-
-}
-
-
-WhitelistInput.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired,
-
-};
-
-const PresentationFormWrapperFC = (props) => {
-  const { t } = useTranslation('admin');
-
-  return <WhitelistInput t={t} {...props} />;
-};
-
-const WhitelistWrapper = withUnstatedContainers(PresentationFormWrapperFC, [AdminMarkDownContainer]);
-
-export default WhitelistWrapper;

+ 79 - 0
apps/app/src/components/Admin/MarkdownSetting/WhitelistInput.tsx

@@ -0,0 +1,79 @@
+import { useCallback, useRef } from 'react';
+
+import { useTranslation } from 'next-i18next';
+
+import type AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
+import { tagNames as recommendedTagNames, attributes as recommendedAttributes } from '~/services/renderer/recommended-whitelist';
+
+type Props ={
+  adminMarkDownContainer: AdminMarkDownContainer
+}
+
+export const WhitelistInput = (props: Props): JSX.Element => {
+
+  const { t } = useTranslation('admin');
+  const { adminMarkDownContainer } = props;
+
+  const tagNamesRef = useRef<HTMLTextAreaElement>(null);
+  const attrsRef = useRef<HTMLTextAreaElement>(null);
+
+  const clickRecommendTagButtonHandler = useCallback(() => {
+    if (tagNamesRef.current == null) {
+      return;
+    }
+
+    const tagWhitelist = recommendedTagNames.join(',');
+    tagNamesRef.current.value = tagWhitelist;
+    adminMarkDownContainer.setState({ tagWhitelist });
+  }, [adminMarkDownContainer]);
+
+  const clickRecommendAttrButtonHandler = useCallback(() => {
+    if (attrsRef.current == null) {
+      return;
+    }
+
+    const attrWhitelist = JSON.stringify(recommendedAttributes);
+    attrsRef.current.value = attrWhitelist;
+    adminMarkDownContainer.setState({ attrWhitelist });
+  }, [adminMarkDownContainer]);
+
+  return (
+    <>
+      <div className="mt-4">
+        <div className="d-flex justify-content-between">
+          {t('markdown_settings.xss_options.tag_names')}
+          <p id="btn-import-tags" className="btn btn-sm btn-primary" onClick={clickRecommendTagButtonHandler}>
+            {t('markdown_settings.xss_options.import_recommended', { target: 'Tags' })}
+          </p>
+        </div>
+        <textarea
+          ref={tagNamesRef}
+          className="form-control xss-list"
+          name="recommendedTags"
+          rows={6}
+          cols={40}
+          defaultValue={adminMarkDownContainer.state.tagWhitelist}
+          onChange={(e) => { adminMarkDownContainer.setState({ tagWhitelist: e.target.value }) }}
+        />
+      </div>
+      <div className="mt-4">
+        <div className="d-flex justify-content-between">
+          {t('markdown_settings.xss_options.tag_attributes')}
+          <p id="btn-import-tags" className="btn btn-sm btn-primary" onClick={clickRecommendAttrButtonHandler}>
+            {t('markdown_settings.xss_options.import_recommended', { target: 'Attrs' })}
+          </p>
+        </div>
+        <textarea
+          ref={attrsRef}
+          className="form-control xss-list"
+          name="recommendedAttrs"
+          rows={6}
+          cols={40}
+          defaultValue={adminMarkDownContainer.state.attrWhitelist}
+          onChange={(e) => { adminMarkDownContainer.setState({ attrWhitelist: e.target.value }) }}
+        />
+      </div>
+    </>
+  );
+
+};

+ 10 - 10
apps/app/src/components/Admin/MarkdownSetting/XssForm.jsx

@@ -2,17 +2,17 @@ import React from 'react';
 
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
-import { defaultSchema as sanitizeDefaultSchema } from 'rehype-sanitize';
 
 import AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
 import { toastSuccess, toastError } from '~/client/util/toastr';
-import { RehypeSanitizeOption } from '~/interfaces/rehype';
+import { RehypeSanitizeType } from '~/interfaces/services/rehype-sanitize';
+import { tagNames as recommendedTagNames, attributes as recommendedAttributes } from '~/services/renderer/recommended-whitelist';
 import loggerFactory from '~/utils/logger';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 
-import WhitelistInput from './WhitelistInput';
+import { WhitelistInput } from './WhitelistInput';
 
 const logger = loggerFactory('growi:importer');
 
@@ -41,8 +41,8 @@ class XssForm extends React.Component {
     const { t, adminMarkDownContainer } = this.props;
     const { xssOption } = adminMarkDownContainer.state;
 
-    const rehypeRecommendedTags = sanitizeDefaultSchema.tagNames;
-    const rehypeRecommendedAttributes = JSON.stringify(sanitizeDefaultSchema.attributes);
+    const rehypeRecommendedTags = recommendedTagNames.join(',');
+    const rehypeRecommendedAttributes = JSON.stringify(recommendedAttributes);
 
     return (
       <div className="col-12 mt-3">
@@ -55,8 +55,8 @@ class XssForm extends React.Component {
                 className="form-check-input"
                 id="xssOption1"
                 name="XssOption"
-                checked={xssOption === RehypeSanitizeOption.RECOMMENDED}
-                onChange={() => { adminMarkDownContainer.setState({ xssOption: RehypeSanitizeOption.RECOMMENDED }) }}
+                checked={xssOption === RehypeSanitizeType.RECOMMENDED}
+                onChange={() => { adminMarkDownContainer.setState({ xssOption: RehypeSanitizeType.RECOMMENDED }) }}
               />
               <label className="form-label form-check-label w-100" htmlFor="xssOption1">
                 <p className="fw-bold">{t('markdown_settings.xss_options.recommended_setting')}</p>
@@ -97,12 +97,12 @@ class XssForm extends React.Component {
                 className="form-check-input"
                 id="xssOption2"
                 name="XssOption"
-                checked={xssOption === RehypeSanitizeOption.CUSTOM}
-                onChange={() => { adminMarkDownContainer.setState({ xssOption: RehypeSanitizeOption.CUSTOM }) }}
+                checked={xssOption === RehypeSanitizeType.CUSTOM}
+                onChange={() => { adminMarkDownContainer.setState({ xssOption: RehypeSanitizeType.CUSTOM }) }}
               />
               <label className="form-label form-check-label w-100" htmlFor="xssOption2">
                 <p className="fw-bold">{t('markdown_settings.xss_options.custom_whitelist')}</p>
-                <WhitelistInput customizable />
+                <WhitelistInput adminMarkDownContainer={adminMarkDownContainer} />
               </label>
             </div>
           </div>

+ 4 - 6
apps/app/src/components/Admin/UserGroupDetail/UserGroupDetailPage.tsx

@@ -1,5 +1,5 @@
 import React, {
-  useState, useCallback, useEffect, useMemo,
+  useState, useCallback, useEffect,
 } from 'react';
 
 import {
@@ -18,7 +18,6 @@ import { toastSuccess, toastError } from '~/client/util/toastr';
 import type { IExternalUserGroupHasId } from '~/features/external-user-group/interfaces/external-user-group';
 import type { PageActionOnGroupDelete, SearchType } from '~/interfaces/user-group';
 import { SearchTypes } from '~/interfaces/user-group';
-import Xss from '~/services/xss';
 import { useIsAclEnabled } from '~/stores/context';
 import { useUpdateUserGroupConfirmModal } from '~/stores/modal';
 import { useSWRxUserGroupPages, useSWRxSelectableParentUserGroups, useSWRxSelectableChildUserGroups } from '~/stores/user-group';
@@ -54,7 +53,6 @@ type Props = {
 const UserGroupDetailPage = (props: Props): JSX.Element => {
   const { t } = useTranslation('admin');
   const router = useRouter();
-  const xss = useMemo(() => new Xss(), []);
   const { userGroupId: currentUserGroupId, isExternalGroup } = props;
 
   const { data: currentUserGroup } = useUserGroup(currentUserGroupId, isExternalGroup);
@@ -221,13 +219,13 @@ const UserGroupDetailPage = (props: Props): JSX.Element => {
   const removeUserByUsername = useCallback(async(username: string) => {
     try {
       await apiv3Delete(`/user-groups/${currentUserGroupId}/users/${username}`);
-      toastSuccess(`Removed "${xss.process(username)}" from "${xss.process(currentUserGroup?.name)}"`);
+      toastSuccess(`Removed "${username}" from "${currentUserGroup?.name}"`);
       mutateUserGroupRelationList();
     }
     catch (err) {
-      toastError(new Error(`Unable to remove "${xss.process(username)}" from "${xss.process(currentUserGroup?.name)}"`));
+      toastError(new Error(`Unable to remove "${username}" from "${currentUserGroup?.name}"`));
     }
-  }, [currentUserGroup?.name, currentUserGroupId, mutateUserGroupRelationList, xss]);
+  }, [currentUserGroup?.name, currentUserGroupId, mutateUserGroupRelationList]);
 
   const showUpdateModal = useCallback((group: IUserGroupHasId) => {
     setUpdateModalShown(true);

+ 3 - 7
apps/app/src/components/Admin/UserGroupDetail/UserGroupUserFormByInput.tsx

@@ -1,5 +1,5 @@
 import type { FC, KeyboardEvent } from 'react';
-import React, { useState, useRef } from 'react';
+import React, { useState } from 'react';
 
 import type { IUserGroupHasId, IUserHasId } from '@growi/core';
 import { UserPicture } from '@growi/ui/dist/components';
@@ -8,7 +8,6 @@ import { AsyncTypeahead } from 'react-bootstrap-typeahead';
 
 import { toastSuccess, toastError } from '~/client/util/toastr';
 import type { SearchType } from '~/interfaces/user-group';
-import Xss from '~/services/xss';
 
 type Props = {
   userGroup: IUserGroupHasId,
@@ -25,25 +24,22 @@ export const UserGroupUserFormByInput: FC<Props> = (props) => {
   } = props;
 
   const { t } = useTranslation();
-  const typeaheadRef = useRef(null);
   const [inputUser, setInputUser] = useState<IUserHasId[]>([]);
   const [applicableUsers, setApplicableUsers] = useState<IUserHasId[]>([]);
   const [isLoading, setIsLoading] = useState(false);
   const [isSearchError, setIsSearchError] = useState(false);
 
-  const xss = new Xss();
-
   const addUserBySubmit = async() => {
     if (inputUser.length === 0) { return }
     const userName = inputUser[0].username;
 
     try {
       await onClickAddUserBtn(userName);
-      toastSuccess(`Added "${xss.process(userName)}" to "${xss.process(userGroup.name)}"`);
+      toastSuccess(`Added "${userName}" to "${userGroup.name}"`);
       setInputUser([]);
     }
     catch (err) {
-      toastError(new Error(`Unable to add "${xss.process(userName)}" to "${xss.process(userGroup.name)}"`));
+      toastError(new Error(`Unable to add "${userName}" to "${userGroup.name}"`));
     }
   };
 

+ 1 - 1
apps/app/src/components/Common/PageViewLayout.tsx

@@ -23,7 +23,7 @@ export const PageViewLayout = (props: Props): JSX.Element => {
 
   return (
     <>
-      <div className={`main ${pageViewLayoutClass} ${fluidLayoutClass} flex-expand-vert ps-sidebar`}>
+      <div className={`main ${pageViewLayoutClass} ${fluidLayoutClass} flex-expand-vert ps-sidebar position-relative z-0`}>
         <div className="container-lg wide-gutter-x-lg grw-container-convertible flex-expand-vert">
           { headerContents != null && headerContents }
           { sideContents != null

+ 9 - 1
apps/app/src/components/Common/SubmittableInput/use-submittable.ts

@@ -18,6 +18,7 @@ export const useSubmittable = (props: SubmittableInputProps): Partial<React.Inpu
   } = props;
 
   const [inputText, setInputText] = useState(value ?? '');
+  const [lastSubmittedInputText, setLastSubmittedInputText] = useState<string|undefined>(value ?? '');
   const [isComposing, setComposing] = useState(false);
 
   const changeHandler = useCallback(async(e: React.ChangeEvent<HTMLInputElement>) => {
@@ -34,6 +35,7 @@ export const useSubmittable = (props: SubmittableInputProps): Partial<React.Inpu
         if (isComposing) {
           return;
         }
+        setLastSubmittedInputText(inputText);
         onSubmit?.(inputText.trim());
         break;
       case 'Escape':
@@ -46,10 +48,16 @@ export const useSubmittable = (props: SubmittableInputProps): Partial<React.Inpu
   }, [inputText, isComposing, onCancel, onSubmit]);
 
   const blurHandler = useCallback((e) => {
+    // suppress continuous calls to submit by blur event
+    if (lastSubmittedInputText === inputText) {
+      return;
+    }
+
     // submit on blur
+    setLastSubmittedInputText(inputText);
     onSubmit?.(inputText.trim());
     onBlur?.(e);
-  }, [inputText, onSubmit, onBlur]);
+  }, [inputText, lastSubmittedInputText, onSubmit, onBlur]);
 
   const compositionStartHandler = useCallback((e: CompositionEvent<HTMLInputElement>) => {
     setComposing(true);

+ 1 - 1
apps/app/src/components/CreateTemplateModal.tsx

@@ -65,7 +65,7 @@ export const CreateTemplateModal: React.FC<CreateTemplateModalProps> = ({
     catch (err) {
       toastError(t('toaster.create_failed', { target: path }));
     }
-  }, [createTemplate, path, t]);
+  }, [createTemplate, onClose, path, t]);
 
   const parentPath = pathUtils.addTrailingSlash(path);
 

+ 3 - 1
apps/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -35,7 +35,6 @@ import {
   useIsDeviceLargerThanMd,
 } from '~/stores/ui';
 
-import { CreateTemplateModal } from '../CreateTemplateModal';
 import { NotAvailable } from '../NotAvailable';
 import { Skeleton } from '../Skeleton';
 
@@ -44,6 +43,9 @@ import { GroundGlassBar } from './GroundGlassBar';
 import styles from './GrowiContextualSubNavigation.module.scss';
 import PageEditorModeManagerStyles from './PageEditorModeManager.module.scss';
 
+
+const CreateTemplateModal = dynamic(() => import('../CreateTemplateModal').then(mod => mod.CreateTemplateModal), { ssr: false });
+
 const PageEditorModeManager = dynamic(
   () => import('./PageEditorModeManager').then(mod => mod.PageEditorModeManager),
   { ssr: false, loading: () => <Skeleton additionalClass={`${PageEditorModeManagerStyles['grw-page-editor-mode-manager-skeleton']}`} /> },

+ 1 - 1
apps/app/src/components/PageEditor/EditorNavbar/EditingUserList.tsx

@@ -30,7 +30,7 @@ export const EditingUserList: FC<Props> = ({ userList }) => {
     <div className="d-flex flex-column justify-content-start justify-content-sm-end">
       <div className="d-flex justify-content-start justify-content-sm-end">
         {firstFourUsers.map(user => (
-          <div className="ms-1">
+          <div key={user._id} className="ms-1">
             <UserPicture
               user={user}
               noLink

+ 19 - 6
apps/app/src/components/PageEditor/OptionsSelector.tsx

@@ -175,16 +175,19 @@ IndentSizeSelector.displayName = 'IndentSizeSelector';
 
 
 type SwitchItemProps = {
-  onClick: () => void,
+  inputId: string,
+  onChange: () => void,
   checked: boolean,
   text: string,
 };
 const SwitchItem = memo((props: SwitchItemProps): JSX.Element => {
-  const { onClick, checked, text } = props;
+  const {
+    inputId, onChange, checked, text,
+  } = props;
   return (
     <FormGroup switch>
-      <Input type="switch" checked={checked} onClick={onClick} />
-      <label>{text}</label>
+      <Input id={inputId} type="switch" checked={checked} onChange={onChange} />
+      <label htmlFor={inputId}>{text}</label>
     </FormGroup>
 
   );
@@ -203,7 +206,12 @@ const ConfigurationSelector = memo((): JSX.Element => {
     const isActive = editorSettings.styleActiveLine;
 
     return (
-      <SwitchItem onClick={() => update({ styleActiveLine: !isActive })} checked={isActive} text={t('page_edit.Show active line')} />
+      <SwitchItem
+        inputId="switchActiveLine"
+        onChange={() => update({ styleActiveLine: !isActive })}
+        checked={isActive}
+        text={t('page_edit.Show active line')}
+      />
     );
   }, [editorSettings, update, t]);
 
@@ -215,7 +223,12 @@ const ConfigurationSelector = memo((): JSX.Element => {
     const isActive = editorSettings.autoFormatMarkdownTable;
 
     return (
-      <SwitchItem onClick={() => update({ autoFormatMarkdownTable: !isActive })} checked={isActive} text={t('page_edit.auto_format_table')} />
+      <SwitchItem
+        inputId="switchTableAutoFormatting"
+        onChange={() => update({ autoFormatMarkdownTable: !isActive })}
+        checked={isActive}
+        text={t('page_edit.auto_format_table')}
+      />
     );
   }, [editorSettings, t, update]);
 

+ 6 - 2
apps/app/src/components/Script/DrawioViewerScript/DrawioViewerScript.tsx

@@ -10,8 +10,12 @@ declare global {
   var GraphViewer: IGraphViewerGlobal;
 }
 
-export const DrawioViewerScript = (): JSX.Element => {
-  const viewerMinJsSrc = useViewerMinJsUrl();
+type Props = {
+  drawioUri: string;
+}
+
+export const DrawioViewerScript = ({ drawioUri }: Props): JSX.Element => {
+  const viewerMinJsSrc = useViewerMinJsUrl(drawioUri);
 
   const loadedHandler = useCallback(() => {
     // disable useResizeSensor and checkVisibleState

+ 2 - 31
apps/app/src/components/Script/DrawioViewerScript/use-viewer-min-js-url.spec.ts

@@ -1,44 +1,15 @@
 import { useViewerMinJsUrl } from './use-viewer-min-js-url';
 
-const mocks = vi.hoisted(() => {
-  return {
-    useRendererConfigMock: vi.fn(),
-  };
-});
-
-vi.mock('~/stores/context', () => ({
-  useRendererConfig: mocks.useRendererConfigMock,
-}));
-
 describe('useViewerMinJsUrl', () => {
-  it('should return the URL when rendererConfig is undefined', () => {
-    // Arrange
-    mocks.useRendererConfigMock.mockImplementation(() => {
-      return { data: undefined };
-    });
-
-    // Act
-    const url = useViewerMinJsUrl();
-
-    // Assert
-    expect(url).toBe('http://localhost/js/viewer.min.js');
-  });
-
   it.each`
     drawioUri                                     | expected
-    ${undefined}                                  | ${'http://localhost/js/viewer.min.js'}
     ${'http://localhost:8080'}                    | ${'http://localhost:8080/js/viewer.min.js'}
     ${'http://example.com'}                       | ${'http://example.com/js/viewer.min.js'}
     ${'http://example.com/drawio'}                | ${'http://example.com/drawio/js/viewer.min.js'}
     ${'http://example.com/?offline=1&https=0'}    | ${'http://example.com/js/viewer.min.js?offline=1&https=0'}
-  `('should return the expected URL "$expected" when drawioUri is "$drawioUrk"', ({ drawioUri, expected }: {drawioUri: string|undefined, expected: string}) => {
-    // Arrange
-    mocks.useRendererConfigMock.mockImplementation(() => {
-      return { data: { drawioUri } };
-    });
-
+  `('should return the expected URL "$expected" when drawioUri is "$drawioUrk"', ({ drawioUri, expected }: {drawioUri: string, expected: string}) => {
     // Act
-    const url = useViewerMinJsUrl();
+    const url = useViewerMinJsUrl(drawioUri);
 
     // Assert
     expect(url).toBe(expected);

+ 4 - 10
apps/app/src/components/Script/DrawioViewerScript/use-viewer-min-js-url.ts

@@ -1,15 +1,9 @@
 import urljoin from 'url-join';
 
-import { useRendererConfig } from '~/stores/context';
-
-export const useViewerMinJsUrl = (): string => {
-  const { data: rendererConfig } = useRendererConfig();
-
-  const { drawioUri: _drawioUriStr = 'http://localhost' } = rendererConfig ?? {};
-
+export const useViewerMinJsUrl = (drawioUri: string): string => {
   // extract search from URL
-  const drawioUri = new URL(_drawioUriStr);
-  const pathname = urljoin(drawioUri.pathname, '/js/viewer.min.js');
+  const url = new URL(drawioUri);
+  const pathname = urljoin(url.pathname, '/js/viewer.min.js');
 
-  return `${drawioUri.origin}${pathname}${drawioUri.search}`;
+  return `${url.origin}${pathname}${url.search}`;
 };

+ 0 - 6
apps/app/src/interfaces/rehype.ts

@@ -1,6 +0,0 @@
-export const RehypeSanitizeOption = {
-  RECOMMENDED: 'Recommended',
-  CUSTOM: 'Custom',
-} as const;
-
-export type RehypeSanitizeOption = typeof RehypeSanitizeOption[keyof typeof RehypeSanitizeOption];

+ 15 - 0
apps/app/src/interfaces/services/rehype-sanitize.ts

@@ -0,0 +1,15 @@
+import type { Attributes } from 'hast-util-sanitize/lib';
+
+export const RehypeSanitizeType = {
+  RECOMMENDED: 'Recommended',
+  CUSTOM: 'Custom',
+} as const;
+
+export type RehypeSanitizeType = typeof RehypeSanitizeType[keyof typeof RehypeSanitizeType];
+
+export type RehypeSanitizeConfiguration = {
+  isEnabledXssPrevention: boolean,
+  sanitizeType: RehypeSanitizeType,
+  customTagWhitelist?: Array<string> | null,
+  customAttrWhitelist?: Attributes | null,
+}

+ 2 - 2
apps/app/src/interfaces/services/renderer.ts

@@ -1,4 +1,4 @@
-import { XssOptionConfig } from '~/services/xss/xssOption';
+import type { RehypeSanitizeConfiguration } from './rehype-sanitize';
 
 export type RendererConfig = {
   isSharedPage?: boolean
@@ -11,4 +11,4 @@ export type RendererConfig = {
 
   drawioUri: string,
   plantumlUri: string,
-} & XssOptionConfig;
+} & RehypeSanitizeConfiguration;

+ 4 - 4
apps/app/src/pages/[[...path]].page.tsx

@@ -371,7 +371,7 @@ Page.getLayout = function getLayout(page: React.ReactElement<Props>) {
   return (
     <>
       <GrowiPluginsActivator />
-      <DrawioViewerScript />
+      <DrawioViewerScript drawioUri={page.props.rendererConfig.drawioUri} />
 
       <Layout {...page.props}>
         {page}
@@ -566,9 +566,9 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
 
     // XSS Options
     isEnabledXssPrevention: configManager.getConfig('markdown', 'markdown:rehypeSanitize:isEnabledPrevention'),
-    xssOption: configManager.getConfig('markdown', 'markdown:rehypeSanitize:option'),
-    attrWhitelist: JSON.parse(crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:attributes')),
-    tagWhitelist: crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:tagNames'),
+    sanitizeType: configManager.getConfig('markdown', 'markdown:rehypeSanitize:option'),
+    customAttrWhitelist: JSON.parse(crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:attributes')),
+    customTagWhitelist: crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:tagNames'),
     highlightJsStyleBorder: crowi.configManager.getConfig('crowi', 'customize:highlightJsStyleBorder'),
   };
 

+ 4 - 4
apps/app/src/pages/_private-legacy-pages.page.tsx

@@ -76,7 +76,7 @@ const PrivateLegacyPage: NextPage<Props> = (props: Props) => {
         <title>{title}</title>
       </Head>
 
-      <DrawioViewerScript />
+      <DrawioViewerScript drawioUri={props.rendererConfig.drawioUri} />
 
       <SearchResultLayout>
         <div id="private-regacy-pages">
@@ -114,9 +114,9 @@ async function injectServerConfigurations(context: GetServerSidePropsContext, pr
 
     // XSS Options
     isEnabledXssPrevention: configManager.getConfig('markdown', 'markdown:rehypeSanitize:isEnabledPrevention'),
-    xssOption: configManager.getConfig('markdown', 'markdown:rehypeSanitize:option'),
-    attrWhitelist: JSON.parse(crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:attributes')),
-    tagWhitelist: crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:tagNames'),
+    sanitizeType: configManager.getConfig('markdown', 'markdown:rehypeSanitize:option'),
+    customAttrWhitelist: JSON.parse(crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:attributes')),
+    customTagWhitelist: crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:tagNames'),
     highlightJsStyleBorder: crowi.configManager.getConfig('crowi', 'customize:highlightJsStyleBorder'),
   };
 }

+ 4 - 4
apps/app/src/pages/_search.page.tsx

@@ -108,7 +108,7 @@ const Layout = ({ children, ...props }: LayoutProps): JSX.Element => {
 SearchResultPage.getLayout = function getLayout(page) {
   return (
     <>
-      <DrawioViewerScript />
+      <DrawioViewerScript drawioUri={page.props.rendererConfig.drawioUri} />
       <Layout {...page.props}>{page}</Layout>
     </>
   );
@@ -141,9 +141,9 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
 
     // XSS Options
     isEnabledXssPrevention: configManager.getConfig('markdown', 'markdown:rehypeSanitize:isEnabledPrevention'),
-    xssOption: configManager.getConfig('markdown', 'markdown:rehypeSanitize:option'),
-    attrWhitelist: JSON.parse(crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:attributes')),
-    tagWhitelist: crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:tagNames'),
+    sanitizeType: configManager.getConfig('markdown', 'markdown:rehypeSanitize:option'),
+    customAttrWhitelist: JSON.parse(crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:attributes')),
+    customTagWhitelist: crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:tagNames'),
     highlightJsStyleBorder: crowi.configManager.getConfig('crowi', 'customize:highlightJsStyleBorder'),
   };
 

+ 3 - 3
apps/app/src/pages/me/[[...path]].page.tsx

@@ -196,9 +196,9 @@ async function injectServerConfigurations(context: GetServerSidePropsContext, pr
 
     // XSS Options
     isEnabledXssPrevention: configManager.getConfig('markdown', 'markdown:rehypeSanitize:isEnabledPrevention'),
-    xssOption: configManager.getConfig('markdown', 'markdown:rehypeSanitize:option'),
-    attrWhitelist: JSON.parse(crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:attributes')),
-    tagWhitelist: crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:tagNames'),
+    sanitizeType: configManager.getConfig('markdown', 'markdown:rehypeSanitize:option'),
+    customAttrWhitelist: JSON.parse(crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:attributes')),
+    customTagWhitelist: crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:tagNames'),
     highlightJsStyleBorder: crowi.configManager.getConfig('crowi', 'customize:highlightJsStyleBorder'),
   };
 }

+ 5 - 5
apps/app/src/pages/share/[[...path]].page.tsx

@@ -141,7 +141,7 @@ const SharedPage: NextPageWithLayout<Props> = (props: Props) => {
 SharedPage.getLayout = function getLayout(page) {
   return (
     <>
-      <DrawioViewerScript />
+      <DrawioViewerScript drawioUri={page.props.rendererConfig.drawioUri} />
       <ShareLinkLayout>{page}</ShareLinkLayout>
     </>
   );
@@ -173,10 +173,10 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
 
     // XSS Options
     isEnabledXssPrevention: configManager.getConfig('markdown', 'markdown:rehypeSanitize:isEnabledPrevention'),
-    xssOption: configManager.getConfig('markdown', 'markdown:rehypeSanitize:option'),
-    attrWhitelist: JSON.parse(crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:attributes')),
-    tagWhitelist: crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:tagNames'),
-    highlightJsStyleBorder: configManager.getConfig('crowi', 'customize:highlightJsStyleBorder'),
+    sanitizeType: configManager.getConfig('markdown', 'markdown:rehypeSanitize:option'),
+    customAttrWhitelist: JSON.parse(crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:attributes')),
+    customTagWhitelist: crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:tagNames'),
+    highlightJsStyleBorder: crowi.configManager.getConfig('crowi', 'customize:highlightJsStyleBorder'),
   };
 
   props.ssrMaxRevisionBodyLength = configManager.getConfig('crowi', 'app:ssrMaxRevisionBodyLength');

+ 1 - 15
apps/app/src/server/crowi/index.js

@@ -14,7 +14,6 @@ import { KeycloakUserGroupSyncService } from '~/features/external-user-group/ser
 import { LdapUserGroupSyncService } from '~/features/external-user-group/server/service/ldap-user-group-sync';
 import QuestionnaireService from '~/features/questionnaire/server/service/questionnaire';
 import QuestionnaireCronService from '~/features/questionnaire/server/service/questionnaire-cron';
-import Xss from '~/services/xss';
 import loggerFactory from '~/utils/logger';
 import { projectRoot } from '~/utils/project-dir-utils';
 
@@ -81,7 +80,6 @@ class Crowi {
     this.mailService = null;
     this.passportService = null;
     this.globalNotificationService = null;
-    this.xssService = null;
     this.aclService = null;
     this.appService = null;
     this.fileUploadService = null;
@@ -97,7 +95,6 @@ class Crowi {
     this.inAppNotificationService = null;
     this.activityService = null;
     this.commentService = null;
-    this.xss = new Xss();
     this.questionnaireService = null;
     this.questionnaireCronService = null;
 
@@ -133,12 +130,11 @@ Crowi.prototype.init = async function() {
   await this.setupS2sMessagingService();
   await this.setupSocketIoService();
 
-  // customizeService depends on AppService and XssService
+  // customizeService depends on AppService
   // passportService depends on appService
   // export and import depends on setUpGrowiBridge
   await Promise.all([
     this.setUpApp(),
-    this.setUpXss(),
     this.setUpGrowiBridge(),
   ]);
 
@@ -597,16 +593,6 @@ Crowi.prototype.setUpUserNotification = async function() {
   }
 };
 
-/**
- * setup XssService
- */
-Crowi.prototype.setUpXss = async function() {
-  const XssService = require('../service/xss');
-  if (this.xssService == null) {
-    this.xssService = new XssService(this.configManager);
-  }
-};
-
 /**
  * setup AclService
  */

+ 4 - 4
apps/app/src/server/models/config.ts

@@ -1,9 +1,9 @@
-import { PresetThemes } from '@growi/preset-themes';
 import type { Types } from 'mongoose';
 import { Schema } from 'mongoose';
 import uniqueValidator from 'mongoose-unique-validator';
 
-import { RehypeSanitizeOption } from '../../interfaces/rehype';
+import { RehypeSanitizeType } from '~/interfaces/services/rehype-sanitize';
+
 import { getOrCreateModel } from '../util/mongoose-utils';
 
 
@@ -123,7 +123,7 @@ export const defaultCrowiConfigs: { [key: string]: any } = {
   'customize:title' : undefined,
   'customize:highlightJsStyle' : 'github',
   'customize:highlightJsStyleBorder' : false,
-  'customize:theme' : PresetThemes.DEFAULT,
+  'customize:theme' : 'default', // PresetThemes.DEFAULT
   'customize:theme:forcedColorScheme' : null,
   'customize:isContainerFluid' : false,
   'customize:isEnabledTimeline' : true,
@@ -161,7 +161,7 @@ export const defaultMarkdownConfigs: { [key: string]: any } = {
   'markdown:xss:attrWhitelist': [],
 
   'markdown:rehypeSanitize:isEnabledPrevention': true,
-  'markdown:rehypeSanitize:option': RehypeSanitizeOption.RECOMMENDED,
+  'markdown:rehypeSanitize:option': RehypeSanitizeType.RECOMMENDED,
   'markdown:rehypeSanitize:tagNames': [],
   'markdown:rehypeSanitize:attributes': '{}',
   'markdown:isEnabledLinebreaks': false,

+ 3 - 20
apps/app/src/server/routes/apiv3/page/update-page.ts

@@ -11,18 +11,15 @@ import mongoose from 'mongoose';
 import { SupportedAction, SupportedTargetModel } from '~/interfaces/activity';
 import { type IApiv3PageUpdateParams, PageUpdateErrorCode } from '~/interfaces/apiv3';
 import type { IOptionsForUpdate } from '~/interfaces/page';
-import { RehypeSanitizeOption } from '~/interfaces/rehype';
 import type Crowi from '~/server/crowi';
 import { generateAddActivityMiddleware } from '~/server/middlewares/add-activity';
 import {
   GlobalNotificationSettingEvent, serializePageSecurely, serializeRevisionSecurely, serializeUserSecurely,
 } from '~/server/models';
 import type { PageDocument, PageModel } from '~/server/models/page';
-import { configManager } from '~/server/service/config-manager';
 import { preNotifyService } from '~/server/service/pre-notify';
 import { getYjsConnectionManager } from '~/server/service/yjs-connection-manager';
-import Xss from '~/services/xss';
-import XssOption from '~/services/xss/xssOption';
+import { generalXssFilter } from '~/services/general-xss-filter';
 import loggerFactory from '~/utils/logger';
 
 import { apiV3FormValidator } from '../../../middlewares/apiv3-form-validator';
@@ -47,20 +44,6 @@ export const updatePageHandlersFactory: UpdatePageHandlersFactory = (crowi) => {
   const accessTokenParser = require('../../../middlewares/access-token-parser')(crowi);
   const loginRequiredStrictly = require('../../../middlewares/login-required')(crowi);
 
-
-  const xss = (() => {
-    const initializedConfig = {
-      isEnabledXssPrevention: configManager.getConfig('markdown', 'markdown:xss:isEnabledPrevention'),
-      tagWhitelist: crowi.xssService.getTagWhitelist(),
-      attrWhitelist: crowi.xssService.getAttrWhitelist(),
-      // TODO: Omit rehype related property from XssOptionConfig type
-      //  Server side xss implementation does not require it.
-      xssOption: RehypeSanitizeOption.CUSTOM,
-    };
-    const xssOption = new XssOption(initializedConfig);
-    return new Xss(xssOption);
-  })();
-
   // define validators for req.body
   const validator: ValidationChain[] = [
     body('pageId').exists().not().isEmpty({ ignore_whitespace: true })
@@ -138,7 +121,7 @@ export const updatePageHandlersFactory: UpdatePageHandlersFactory = (crowi) => {
         pageId, revisionId, body, origin,
       } = req.body;
 
-      const sanitizeRevisionId = revisionId == null ? undefined : xss.process(revisionId);
+      const sanitizeRevisionId = revisionId == null ? undefined : generalXssFilter.process(revisionId);
 
       // check page existence
       const isExist = await Page.count({ _id: pageId }) > 0;
@@ -153,7 +136,7 @@ export const updatePageHandlersFactory: UpdatePageHandlersFactory = (crowi) => {
         const latestRevision = await Revision.findById(currentPage.revision).populate('author');
         const returnLatestRevision = {
           revisionId: latestRevision?._id.toString(),
-          revisionBody: xss.process(latestRevision?.body),
+          revisionBody: latestRevision?.body,
           createdAt: latestRevision?.createdAt,
           user: serializeUserSecurely(latestRevision?.author),
         };

+ 3 - 2
apps/app/src/server/routes/apiv3/user-group.js

@@ -7,6 +7,7 @@ import { serializeUserGroupRelationSecurely } from '~/server/models/serializers/
 import UserGroup from '~/server/models/user-group';
 import UserGroupRelation from '~/server/models/user-group-relation';
 import { excludeTestIdsFromTargetIds } from '~/server/util/compare-objectId';
+import { generalXssFilter } from '~/services/general-xss-filter';
 import loggerFactory from '~/utils/logger';
 
 import { generateAddActivityMiddleware } from '../../middlewares/add-activity';
@@ -230,8 +231,8 @@ module.exports = (crowi) => {
     const { name, description = '', parentId } = req.body;
 
     try {
-      const userGroupName = crowi.xss.process(name);
-      const userGroupDescription = crowi.xss.process(description);
+      const userGroupName = generalXssFilter.process(name);
+      const userGroupDescription = generalXssFilter.process(description);
       const userGroup = await UserGroup.createGroup(userGroupName, userGroupDescription, parentId);
 
       const parameters = { action: SupportedAction.ACTION_ADMIN_USER_GROUP_CREATE };

+ 0 - 13
apps/app/src/server/routes/page.js

@@ -1,14 +1,12 @@
 import { body } from 'express-validator';
 import mongoose from 'mongoose';
 
-import XssOption from '~/services/xss/xssOption';
 import loggerFactory from '~/utils/logger';
 
 import { GlobalNotificationSettingEvent } from '../models';
 import { PathAlreadyExistsError } from '../models/errors';
 import PageTagRelation from '../models/page-tag-relation';
 import UpdatePost from '../models/update-post';
-import { configManager } from '../service/config-manager';
 
 /**
  * @swagger
@@ -146,19 +144,8 @@ module.exports = function(crowi, app) {
 
   const ApiResponse = require('../util/apiResponse');
 
-  const { xssService } = crowi;
   const globalNotificationService = crowi.getGlobalNotificationService();
 
-  const Xss = require('~/services/xss/index');
-  const initializedConfig = {
-    isEnabledXssPrevention: configManager.getConfig('markdown', 'markdown:xss:isEnabledPrevention'),
-    tagWhitelist: xssService.getTagWhitelist(),
-    attrWhitelist: xssService.getAttrWhitelist(),
-  };
-  const xssOption = new XssOption(initializedConfig);
-  const xss = new Xss(xssOption);
-
-
   const actions = {};
 
   // async function showPageForPresentation(req, res, next) {

+ 1 - 1
apps/app/src/server/service/config-loader.ts

@@ -715,7 +715,7 @@ const ENV_VAR_NAME_TO_CONFIG_INFO = {
     ns: 'crowi',
     key: 'app:ssrMaxRevisionBodyLength',
     type: ValueType.NUMBER,
-    default: 30000,
+    default: 3000,
   },
   WIP_PAGE_EXPIRATION_SECONDS: {
     ns: 'crowi',

+ 1 - 28
apps/app/src/server/service/customize.ts

@@ -1,7 +1,6 @@
 import path from 'path';
 
 import type { ColorScheme } from '@growi/core';
-import { DevidedPagePath } from '@growi/core/dist/models';
 import { getForcedColorScheme } from '@growi/core/dist/utils';
 import { DefaultThemeMetadata, PresetThemesMetadatas, manifestPath } from '@growi/preset-themes';
 import uglifycss from 'uglifycss';
@@ -11,6 +10,7 @@ import loggerFactory from '~/utils/logger';
 
 import S2sMessage from '../models/vo/s2s-message';
 
+
 import type { ConfigManager } from './config-manager';
 import type { S2sMessageHandlable } from './s2s-messaging/handlable';
 
@@ -29,8 +29,6 @@ class CustomizeService implements S2sMessageHandlable {
 
   appService: any;
 
-  xssService: any;
-
   lastLoadedAt?: Date;
 
   customCss?: string;
@@ -47,7 +45,6 @@ class CustomizeService implements S2sMessageHandlable {
     this.configManager = crowi.configManager;
     this.s2sMessagingService = crowi.s2sMessagingService;
     this.appService = crowi.appService;
-    this.xssService = crowi.xssService;
   }
 
   /**
@@ -126,30 +123,6 @@ class CustomizeService implements S2sMessageHandlable {
     this.lastLoadedAt = new Date();
   }
 
-  generateCustomTitle(pageOrPath) {
-    const path = pageOrPath.path || pageOrPath;
-    const dPagePath = new DevidedPagePath(path, true, true);
-
-    const customTitle = this.customTitleTemplate
-      .replace('{{sitename}}', this.appService.getAppTitle())
-      .replace('{{pagepath}}', path)
-      .replace('{{page}}', dPagePath.latter) // for backward compatibility
-      .replace('{{pagename}}', dPagePath.latter);
-
-    return this.xssService.process(customTitle);
-  }
-
-  generateCustomTitleForFixedPageName(title) {
-    // replace
-    const customTitle = this.customTitleTemplate
-      .replace('{{sitename}}', this.appService.getAppTitle())
-      .replace('{{page}}', title)
-      .replace('{{pagepath}}', title)
-      .replace('{{pagename}}', title);
-
-    return this.xssService.process(customTitle);
-  }
-
   async initGrowiTheme(): Promise<void> {
     const theme = this.configManager.getConfig('crowi', 'customize:theme');
 

+ 8 - 7
apps/app/src/server/service/page/index.ts

@@ -44,6 +44,7 @@ import type { UserGroupDocument } from '~/server/models/user-group';
 import { getYjsConnectionManager } from '~/server/service/yjs-connection-manager';
 import { createBatchStream } from '~/server/util/batch-stream';
 import { collectAncestorPaths } from '~/server/util/collect-ancestor-paths';
+import { generalXssFilter } from '~/services/general-xss-filter';
 import loggerFactory from '~/utils/logger';
 import { prepareDeleteConfigValuesForCalc } from '~/utils/page-delete-config';
 
@@ -610,7 +611,7 @@ class PageService implements IPageService {
 
     const updateMetadata = options.updateMetadata || false;
     // sanitize path
-    newPagePath = this.crowi.xss.process(newPagePath); // eslint-disable-line no-param-reassign
+    newPagePath = generalXssFilter.process(newPagePath); // eslint-disable-line no-param-reassign
 
     // UserGroup & Owner validation
     // use the parent's grant when target page is an empty page
@@ -839,7 +840,7 @@ class PageService implements IPageService {
     } = options;
 
     // sanitize path
-    newPagePath = this.crowi.xss.process(newPagePath); // eslint-disable-line no-param-reassign
+    newPagePath = generalXssFilter.process(newPagePath); // eslint-disable-line no-param-reassign
 
     // create descendants first
     if (isRecursively) {
@@ -1104,7 +1105,7 @@ class PageService implements IPageService {
       throw Error('Page not found.');
     }
 
-    newPagePath = this.crowi.xss.process(newPagePath); // eslint-disable-line no-param-reassign
+    newPagePath = generalXssFilter.process(newPagePath); // eslint-disable-line no-param-reassign
 
     // 1. Separate v4 & v5 process
     const isShouldUseV4Process = shouldUseV4Process(page);
@@ -1278,7 +1279,7 @@ class PageService implements IPageService {
     options.grantUserGroupIds = page.grantedGroups;
     options.grantedUserIds = page.grantedUsers;
 
-    newPagePath = this.crowi.xss.process(newPagePath); // eslint-disable-line no-param-reassign
+    newPagePath = generalXssFilter.process(newPagePath); // eslint-disable-line no-param-reassign
 
     const createdPage = await this.create(
       newPagePath, page.revision.body, user, options,
@@ -3777,7 +3778,7 @@ class PageService implements IPageService {
     }
 
     // Values
-    const path: string = this.crowi.xss.process(_path); // sanitize path
+    const path: string = generalXssFilter.process(_path); // sanitize path
 
     // Retrieve closest ancestor document
     const Page = mongoose.model<PageDocument, PageModel>('Page');
@@ -3907,7 +3908,7 @@ class PageService implements IPageService {
     const expandContentWidth = this.crowi.configManager.getConfig('crowi', 'customize:isContainerFluid');
 
     // sanitize path
-    path = this.crowi.xss.process(path); // eslint-disable-line no-param-reassign
+    path = generalXssFilter.process(path); // eslint-disable-line no-param-reassign
 
     let grant = options.grant;
     // force public
@@ -3988,7 +3989,7 @@ class PageService implements IPageService {
 
     // Values
     // eslint-disable-next-line no-param-reassign
-    path = this.crowi.xss.process(path); // sanitize path
+    path = generalXssFilter.process(path); // sanitize path
 
     const {
       grantUserGroupIds, grantUserIds,

+ 2 - 1
apps/app/src/server/service/slack-command-handler/create-page-service.js

@@ -1,6 +1,7 @@
 import { markdownSectionBlock } from '@growi/slack/dist/utils/block-kit-builder';
 import { reshapeContentsBody } from '@growi/slack/dist/utils/reshape-contents-body';
 
+import { generalXssFilter } from '~/services/general-xss-filter';
 import loggerFactory from '~/utils/logger';
 
 // eslint-disable-next-line no-unused-vars
@@ -19,7 +20,7 @@ class CreatePageService {
     const reshapedContentsBody = reshapeContentsBody(contentsBody);
 
     // sanitize path
-    const sanitizedPath = this.crowi.xss.process(path);
+    const sanitizedPath = generalXssFilter.process(path);
     const normalizedPath = pathUtils.normalizePath(sanitizedPath);
 
     // Since an ObjectId is required for creating a page, if a user does not exist, a dummy user will be generated

+ 0 - 73
apps/app/src/server/service/xss.js

@@ -1,73 +0,0 @@
-import loggerFactory from '~/utils/logger';
-
-const logger = loggerFactory('growi:service:XssSerivce'); // eslint-disable-line no-unused-vars
-
-const Xss = require('~/services/xss');
-const { tags, attrs } = require('~/services/xss/recommended-whitelist');
-
-/**
- * the service class of XssSerivce
- */
-class XssSerivce {
-
-  constructor(configManager) {
-    this.configManager = configManager;
-
-    this.xss = new Xss();
-  }
-
-  process(value) {
-    return this.xss.process(value);
-  }
-
-  getTagWhitelist() {
-    const isEnabledXssPrevention = this.configManager.getConfig('markdown', 'markdown:xss:isEnabledPrevention');
-    const xssOpiton = this.configManager.getConfig('markdown', 'markdown:xss:option');
-
-    if (isEnabledXssPrevention) {
-      switch (xssOpiton) {
-        case 1: // ignore all: use default option
-          return [];
-
-        case 2: // recommended
-          return tags;
-
-        case 3: // custom whitelist
-          return this.configManager.getConfig('markdown', 'markdown:xss:tagWhitelist');
-
-        default:
-          return [];
-      }
-    }
-    else {
-      return [];
-    }
-  }
-
-  getAttrWhitelist() {
-    const isEnabledXssPrevention = this.configManager.getConfig('markdown', 'markdown:xss:isEnabledPrevention');
-    const xssOpiton = this.configManager.getConfig('markdown', 'markdown:xss:option');
-
-    if (isEnabledXssPrevention) {
-      switch (xssOpiton) {
-        case 1: // ignore all: use default option
-          return [];
-
-        case 2: // recommended
-          return attrs;
-
-        case 3: // custom whitelist
-          return this.configManager.getConfig('markdown', 'markdown:xss:attrWhitelist');
-
-        default:
-          return [];
-      }
-    }
-    else {
-      return [];
-    }
-  }
-
-}
-
-module.exports = XssSerivce;

+ 39 - 0
apps/app/src/services/general-xss-filter/general-xss-filter.spec.ts

@@ -0,0 +1,39 @@
+import { generalXssFilter } from './general-xss-filter';
+
+describe('generalXssFilter', () => {
+
+  test('should be sanitize script tag', () => {
+    // Act
+    const result = generalXssFilter.process('<script>alert("XSS")</script>');
+
+    // Assert
+    expect(result).toBe('alert("XSS")');
+  });
+
+  test('should be sanitize nested script tag recursively', () => {
+    // Act
+    const result = generalXssFilter.process('<scr<script>ipt>alert("XSS")</scr<script>ipt>');
+
+    // Assert
+    expect(result).toBe('alert("XSS")');
+  });
+
+  // for https://github.com/weseek/growi/issues/221
+  test('should not be sanitize blockquote', () => {
+    // Act
+    const result = generalXssFilter.process('> foo\n> bar');
+
+    // Assert
+    expect(result).toBe('> foo\n> bar');
+  });
+
+  // https://github.com/weseek/growi/pull/505
+  test('should not be sanitize next closing-tag', () => {
+    // Act
+    const result = generalXssFilter.process('<evil /><span>text</span>');
+
+    // Assert
+    expect(result).toBe('<span>text</span>');
+  });
+
+});

+ 37 - 0
apps/app/src/services/general-xss-filter/general-xss-filter.ts

@@ -0,0 +1,37 @@
+import type { IFilterXSSOptions } from 'xss';
+import { FilterXSS } from 'xss';
+
+const REPETITIONS_NUM = 50;
+
+const option: IFilterXSSOptions = {
+  stripIgnoreTag: true,
+  stripIgnoreTagBody: false, // see https://github.com/weseek/growi/pull/505
+  css: false,
+  escapeHtml: (html) => { return html }, // resolve https://github.com/weseek/growi/issues/221
+};
+
+class GeneralXssFilter extends FilterXSS {
+
+  override process(document: string | undefined): string {
+    let count = 0;
+    let currDoc = document;
+    let prevDoc = document;
+
+    do {
+      count += 1;
+      // stop running infinitely
+      if (count > REPETITIONS_NUM) {
+        return '--filtered--';
+      }
+
+      prevDoc = currDoc;
+      currDoc = super.process(currDoc ?? '');
+    }
+    while (currDoc !== prevDoc);
+
+    return currDoc;
+  }
+
+}
+
+export const generalXssFilter = new GeneralXssFilter(option);

+ 1 - 0
apps/app/src/services/general-xss-filter/index.ts

@@ -0,0 +1 @@
+export * from './general-xss-filter';

+ 38 - 0
apps/app/src/services/renderer/recommended-whitelist.spec.ts

@@ -0,0 +1,38 @@
+import { tagNames, attributes } from './recommended-whitelist';
+
+describe('recommended-whitelist', () => {
+
+  test('.tagNames should return iframe tag', () => {
+    expect(tagNames).not.toBeNull();
+    expect(tagNames).includes('iframe');
+  });
+
+  test('.tagNames should return video tag', () => {
+    expect(tagNames).not.toBeNull();
+    expect(tagNames).includes('video');
+  });
+
+  test('.attributes should return data attributes', () => {
+    expect(attributes).not.toBeNull();
+    expect(Object.keys(attributes)).includes('*');
+    expect(attributes['*']).includes('alt');
+    expect(attributes['*']).includes('align');
+    expect(attributes['*']).includes('width');
+    expect(attributes['*']).includes('height');
+    expect(attributes['*']).includes('className');
+    expect(attributes['*']).includes('data*');
+  });
+
+  test('.attributes should return iframe attributes', () => {
+    expect(attributes).not.toBeNull();
+    expect(Object.keys(attributes)).includes('iframe');
+    expect(attributes.iframe).includes('src');
+  });
+
+  test('.attributes should return video attributes', () => {
+    expect(attributes).not.toBeNull();
+    expect(Object.keys(attributes)).includes('video');
+    expect(attributes.iframe).includes('src');
+  });
+
+});

+ 29 - 0
apps/app/src/services/renderer/recommended-whitelist.ts

@@ -0,0 +1,29 @@
+import { defaultSchema } from 'hast-util-sanitize';
+import type { Attributes } from 'hast-util-sanitize/lib';
+import deepmerge from 'ts-deepmerge';
+
+/**
+ * reference: https://meta.stackexchange.com/questions/1777/what-html-tags-are-allowed-on-stack-exchange-sites,
+ *            https://github.com/jch/html-pipeline/blob/70b6903b025c668ff3c02a6fa382031661182147/lib/html/pipeline/sanitization_filter.rb#L41
+ */
+
+export const tagNames: Array<string> = [
+  ...defaultSchema.tagNames ?? [],
+  '-', 'bdi',
+  'col', 'colgroup',
+  'data',
+  'iframe',
+  'video',
+  'rb', 'u',
+];
+
+export const attributes: Attributes = deepmerge(
+  defaultSchema.attributes ?? {},
+  {
+    iframe: ['allow', 'referrerpolicy', 'sandbox', 'src', 'srcdoc'],
+    video: ['controls', 'src', 'muted', 'preload', 'width', 'height', 'autoplay'],
+    // The special value 'data*' as a property name can be used to allow all data properties.
+    // see: https://github.com/syntax-tree/hast-util-sanitize/
+    '*': ['key', 'class', 'className', 'style', 'data*'],
+  },
+);

+ 12 - 23
apps/app/src/services/renderer/renderer.tsx

@@ -2,7 +2,7 @@ import growiDirective from '@growi/remark-growi-directive';
 import type { Schema as SanitizeOption } from 'hast-util-sanitize';
 import katex from 'rehype-katex';
 import raw from 'rehype-raw';
-import sanitize, { defaultSchema as rehypeSanitizeDefaultSchema } from 'rehype-sanitize';
+import sanitize from 'rehype-sanitize';
 import slug from 'rehype-slug';
 import breaks from 'remark-breaks';
 import emoji from 'remark-emoji';
@@ -16,17 +16,19 @@ import type { Pluggable, PluginTuple } from 'unified';
 
 import { CodeBlock } from '~/components/ReactMarkdownComponents/CodeBlock';
 import { NextLink } from '~/components/ReactMarkdownComponents/NextLink';
-import { RehypeSanitizeOption } from '~/interfaces/rehype';
 import type { RendererOptions } from '~/interfaces/renderer-options';
+import { RehypeSanitizeType } from '~/interfaces/services/rehype-sanitize';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import loggerFactory from '~/utils/logger';
 
+import { tagNames as recommendedTagNames, attributes as recommendedAttributes } from './recommended-whitelist';
 import * as addClass from './rehype-plugins/add-class';
 import { relativeLinks } from './rehype-plugins/relative-links';
 import { relativeLinksByPukiwikiLikeLinker } from './rehype-plugins/relative-links-by-pukiwiki-like-linker';
 import { pukiwikiLikeLinker } from './remark-plugins/pukiwiki-like-linker';
 import * as xsvToTable from './remark-plugins/xsv-to-table';
 
+
 // import EasyGrid from './PreProcessor/EasyGrid';
 
 
@@ -36,31 +38,18 @@ const logger = loggerFactory('growi:services:renderer');
 
 type SanitizePlugin = PluginTuple<[SanitizeOption]>;
 
-const baseSanitizeSchema = {
-  tagNames: ['iframe', 'section', 'video'],
-  attributes: {
-    iframe: ['allow', 'referrerpolicy', 'sandbox', 'src', 'srcdoc'],
-    video: ['controls', 'src', 'muted', 'preload', 'width', 'height', 'autoplay'],
-    // The special value 'data*' as a property name can be used to allow all data properties.
-    // see: https://github.com/syntax-tree/hast-util-sanitize/
-    '*': ['key', 'class', 'className', 'style', 'data*'],
-  },
+export const commonSanitizeOption: SanitizeOption = {
+  tagNames: recommendedTagNames,
+  attributes: recommendedAttributes,
+  clobberPrefix: '', // remove clobber prefix
 };
 
-export const commonSanitizeOption: SanitizeOption = deepmerge(
-  rehypeSanitizeDefaultSchema,
-  baseSanitizeSchema,
-  {
-    clobberPrefix: '', // remove clobber prefix
-  },
-);
-
 let isInjectedCustomSanitaizeOption = false;
 
 export const injectCustomSanitizeOption = (config: RendererConfig): void => {
-  if (!isInjectedCustomSanitaizeOption && config.isEnabledXssPrevention && config.xssOption === RehypeSanitizeOption.CUSTOM) {
-    commonSanitizeOption.tagNames = baseSanitizeSchema.tagNames.concat(config.tagWhitelist ?? []);
-    commonSanitizeOption.attributes = deepmerge(baseSanitizeSchema.attributes, config.attrWhitelist ?? {});
+  if (!isInjectedCustomSanitaizeOption && config.isEnabledXssPrevention && config.sanitizeType === RehypeSanitizeType.CUSTOM) {
+    commonSanitizeOption.tagNames = config.customTagWhitelist ?? recommendedTagNames;
+    commonSanitizeOption.attributes = config.customAttrWhitelist ?? recommendedAttributes;
     isInjectedCustomSanitaizeOption = true;
   }
 };
@@ -142,7 +131,7 @@ export const generateSSRViewOptions = (
     remarkPlugins.push(breaks);
   }
 
-  if (config.xssOption === RehypeSanitizeOption.CUSTOM) {
+  if (config.sanitizeType === RehypeSanitizeType.CUSTOM) {
     injectCustomSanitizeOption(config);
   }
 

+ 0 - 42
apps/app/src/services/xss/commonmark-spec.js

@@ -1,42 +0,0 @@
-/**
- * Valid schemes
- * @see https://spec.commonmark.org/0.16/#autolinks
- */
-const schemesForAutolink = [
-  'coap', 'doi', 'javascript', 'aaa', 'aaas', 'about', 'acap', 'cap', 'cid', 'crid', 'data', 'dav', 'dict', 'dns',
-  'file', 'ftp', 'geo', 'go', 'gopher', 'h323', 'http', 'https', 'iax', 'icap', 'im', 'imap', 'info', 'ipp', 'iris',
-  'iris.beep', 'iris.xpc', 'iris.xpcs', 'iris.lwz', 'ldap', 'mailto', 'mid', 'msrp', 'msrps', 'mtqp', 'mupdate',
-  'news', 'nfs', 'ni', 'nih', 'nntp', 'opaquelocktoken', 'pop', 'pres', 'rtsp', 'service', 'session', 'shttp',
-  'sieve', 'sip', 'sips', 'sms', 'snmp,soap.beep', 'soap.beeps', 'tag', 'tel', 'telnet', 'tftp', 'thismessage',
-  'tn3270', 'tip', 'tv', 'urn', 'vemmi', 'ws', 'wss', 'xcon', 'xcon-userid', 'xmlrpc.beep', 'xmlrpc.beeps', 'xmpp',
-  'z39.50r', 'z39.50s', 'adiumxtra', 'afp', 'afs', 'aim', 'apt,attachment', 'aw', 'beshare', 'bitcoin', 'bolo',
-  'callto', 'chrome,chrome-extension', 'com-eventbrite-attendee', 'content', 'cvs,dlna-playsingle', 'dlna-playcontainer',
-  'dtn', 'dvb', 'ed2k', 'facetime', 'feed', 'finger', 'fish', 'gg', 'git', 'gizmoproject', 'gtalk', 'hcp', 'icon',
-  'ipn', 'irc', 'irc6', 'ircs', 'itms', 'jar', 'jms', 'keyparc', 'lastfm', 'ldaps', 'magnet', 'maps', 'market,message',
-  'mms', 'ms-help', 'msnim', 'mumble', 'mvn', 'notes', 'oid', 'palm', 'paparazzi', 'platform', 'proxy', 'psyc',
-  'query', 'res', 'resource', 'rmi', 'rsync', 'rtmp', 'secondlife', 'sftp', 'sgn', 'skype', 'smb', 'soldat', 'spotify',
-  'ssh', 'steam', 'svn', 'teamspeak', 'things', 'udp', 'unreal', 'ut2004', 'ventrilo', 'view-source', 'webcal',
-  'wtai', 'wyciwyg', 'xfire', 'xri', 'ymsgr',
-];
-const schemesCondition = schemesForAutolink.join('|');
-
-/**
- * RegExp for URI
- * @type {RegExp}
- * @see https://spec.commonmark.org/0.16/#autolinks
- */
-const uriAutolinkRegexp = new RegExp(`^(${schemesCondition}):\\/\\/.+$`);
-
-/**
- * RegExp for email
- * @type {RegExp}
- * @see https://spec.commonmark.org/0.16/#autolinks
- */
-// eslint-disable-next-line max-len
-const emailAutolinkRegexp = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
-
-
-module.exports = {
-  uriAutolinkRegexp,
-  emailAutolinkRegexp,
-};

+ 0 - 63
apps/app/src/services/xss/index.js

@@ -1,63 +0,0 @@
-const xss = require('xss');
-const commonmarkSpec = require('./commonmark-spec');
-
-
-const REPETITIONS_NUM = 50;
-
-class Xss {
-
-  constructor(xssOption) {
-
-    xssOption = xssOption || {}; // eslint-disable-line no-param-reassign
-
-    const tagWhitelist = xssOption.tagWhitelist || [];
-    const attrWhitelist = xssOption.attrWhitelist || [];
-
-    const whitelistContent = {};
-
-    // default
-    const option = {
-      stripIgnoreTag: true,
-      stripIgnoreTagBody: false, // see https://github.com/weseek/growi/pull/505
-      css: false,
-      whitelist: whitelistContent,
-      escapeHtml: (html) => { return html }, // resolve https://github.com/weseek/growi/issues/221
-      onTag: (tag, html, options) => {
-        // pass autolink
-        if (tag.match(commonmarkSpec.uriAutolinkRegexp) || tag.match(commonmarkSpec.emailAutolinkRegexp)) {
-          return html;
-        }
-      },
-    };
-
-    tagWhitelist.forEach((tag) => {
-      whitelistContent[tag] = attrWhitelist;
-    });
-
-    // create the XSS Filter instance
-    this.myxss = new xss.FilterXSS(option);
-  }
-
-  process(document) {
-    let count = 0;
-    let currDoc = document;
-    let prevDoc = document;
-
-    do {
-      count += 1;
-      // stop running infinitely
-      if (count > REPETITIONS_NUM) {
-        return '--filtered--';
-      }
-
-      prevDoc = currDoc;
-      currDoc = this.myxss.process(currDoc);
-    }
-    while (currDoc !== prevDoc);
-
-    return currDoc;
-  }
-
-}
-
-module.exports = Xss;

+ 0 - 21
apps/app/src/services/xss/recommended-whitelist.js

@@ -1,21 +0,0 @@
-/**
- * reference: https://meta.stackexchange.com/questions/1777/what-html-tags-are-allowed-on-stack-exchange-sites,
- *            https://github.com/jch/html-pipeline/blob/70b6903b025c668ff3c02a6fa382031661182147/lib/html/pipeline/sanitization_filter.rb#L41
- */
-
-const tags = [
-  '-', 'a', 'abbr', 'b', 'bdi', 'bdo', 'blockquote', 'br', 'caption', 'cite',
-  'code', 'col', 'colgroup', 'data', 'dd', 'del', 'details', 'dfn', 'div', 'dl',
-  'dt', 'em', 'figcaption', 'figure', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'h7',
-  'h8', 'hr', 'i', 'iframe', 'img', 'ins', 'kbd', 'li', 'mark', 'ol', 'p',
-  'pre', 'q', 'rb', 'rp', 'rt', 'ruby', 's', 'samp', 'small', 'span', 'strike',
-  'strong', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'tfoot', 'th',
-  'thead', 'time', 'tr', 'tt', 'u', 'ul', 'var', 'wbr',
-];
-
-const attrs = ['src', 'href', 'class', 'id', 'width', 'height', 'alt', 'title', 'style'];
-
-module.exports = {
-  tags,
-  attrs,
-};

+ 0 - 32
apps/app/src/services/xss/xssOption.ts

@@ -1,32 +0,0 @@
-import { defaultSchema as sanitizeDefaultSchema } from 'rehype-sanitize';
-
-import type { RehypeSanitizeOption } from '~/interfaces/rehype';
-
-type tagWhitelist = typeof sanitizeDefaultSchema.tagNames;
-type attrWhitelist = typeof sanitizeDefaultSchema.attributes;
-
-export type XssOptionConfig = {
-  isEnabledXssPrevention: boolean,
-  xssOption: RehypeSanitizeOption,
-  tagWhitelist: tagWhitelist,
-  attrWhitelist: attrWhitelist,
-}
-
-export default class XssOption {
-
-  isEnabledXssPrevention: boolean;
-
-  tagWhitelist: any[];
-
-  attrWhitelist: any[];
-
-  constructor(config: XssOptionConfig) {
-    const recommendedWhitelist = require('~/services/xss/recommended-whitelist');
-    const initializedConfig: Partial<XssOptionConfig> = (config != null) ? config : {};
-
-    this.isEnabledXssPrevention = initializedConfig.isEnabledXssPrevention || true;
-    this.tagWhitelist = initializedConfig.tagWhitelist || recommendedWhitelist.tags;
-    this.attrWhitelist = initializedConfig.attrWhitelist || recommendedWhitelist.attrs;
-  }
-
-}

+ 0 - 10
apps/app/src/stores/xss.ts

@@ -1,10 +0,0 @@
-
-import { SWRResponse } from 'swr';
-
-import Xss from '~/services/xss';
-
-import { useStaticSWR } from './use-static-swr';
-
-export const useXss = (initialData?: Xss): SWRResponse<Xss, Error> => {
-  return useStaticSWR<Xss, Error>('xss', initialData);
-};

+ 1 - 1
apps/app/src/styles/organisms/_wiki.scss

@@ -130,7 +130,7 @@
     border-left: 0.3rem solid #ddd;
   }
 
-  img {
+  img,video {
     max-width: 100%;
     margin: 5px 0;
   }

+ 2 - 2
apps/app/test/cypress/tsconfig.json

@@ -1,10 +1,10 @@
 {
-  "extends": "../tsconfig.json",
+  "extends": "../../tsconfig.json",
   "compilerOptions": {
     "noEmit": true,
     // be explicit about types included
     // to avoid clashing with Jest types
-    "types": ["cypress"],
+    "types": ["cypress", "cypress-real-events"],
     // turn off sourceMap
     // see: https://github.com/cypress-io/cypress/issues/26203
     "sourceMap": false

+ 0 - 3
apps/app/test/integration/service/page-grant.test.ts

@@ -26,7 +26,6 @@ describe('PageGrantService', () => {
    */
   let crowi;
   let pageGrantService: IPageGrantService;
-  let xssSpy;
 
   let user1;
   let user2;
@@ -489,8 +488,6 @@ describe('PageGrantService', () => {
 
     await createDocumentsToTestIsGrantNormalized();
     await createDocumentsToTestGetPageGroupGrantData();
-
-    xssSpy = jest.spyOn(crowi.xss, 'process').mockImplementation(path => path);
   });
 
   describe('Test isGrantNormalized method with shouldCheckDescendants false', () => {

+ 9 - 8
apps/app/test/integration/service/page.test.js

@@ -8,6 +8,7 @@ import Tag from '~/server/models/tag';
 import UserGroup from '~/server/models/user-group';
 import UserGroupRelation from '~/server/models/user-group-relation';
 
+import { generalXssFilter } from '../../../src/services/general-xss-filter';
 
 const mongoose = require('mongoose');
 
@@ -66,7 +67,7 @@ describe('PageService', () => {
   let Bookmark;
   let Comment;
   let ShareLink;
-  let xssSpy;
+  let generalXssFilterProcessSpy;
 
   beforeAll(async() => {
     crowi = await getInstance();
@@ -346,7 +347,7 @@ describe('PageService', () => {
       },
     ]);
 
-    xssSpy = jest.spyOn(crowi.xss, 'process').mockImplementation(path => path);
+    generalXssFilterProcessSpy = jest.spyOn(generalXssFilter, 'process');
 
     /**
      * getParentAndFillAncestors
@@ -494,7 +495,7 @@ describe('PageService', () => {
         const resultPage = await crowi.pageService.renamePage(parentForRename1,
           '/renamed1', testUser2, {}, { ip: '::ffff:127.0.0.1', endpoint: '/_api/v3/pages/rename' });
 
-        expect(xssSpy).toHaveBeenCalled();
+        expect(generalXssFilterProcessSpy).toHaveBeenCalled();
 
         expect(pageEventSpy).toHaveBeenCalledWith('rename');
 
@@ -508,7 +509,7 @@ describe('PageService', () => {
         const resultPage = await crowi.pageService.renamePage(parentForRename2, '/renamed2', testUser2, { updateMetadata: true },
           { ip: '::ffff:127.0.0.1', endpoint: '/_api/v3/pages/rename' });
 
-        expect(xssSpy).toHaveBeenCalled();
+        expect(generalXssFilterProcessSpy).toHaveBeenCalled();
 
         expect(pageEventSpy).toHaveBeenCalledWith('rename');
 
@@ -522,7 +523,7 @@ describe('PageService', () => {
         const resultPage = await crowi.pageService.renamePage(parentForRename3, '/renamed3', testUser2, { createRedirectPage: true },
           { ip: '::ffff:127.0.0.1', endpoint: '/_api/v3/pages/rename' });
 
-        expect(xssSpy).toHaveBeenCalled();
+        expect(generalXssFilterProcessSpy).toHaveBeenCalled();
         expect(pageEventSpy).toHaveBeenCalledWith('rename');
 
         expect(resultPage.path).toBe('/renamed3');
@@ -535,7 +536,7 @@ describe('PageService', () => {
         const resultPage = await crowi.pageService.renamePage(parentForRename4, '/renamed4', testUser2, { isRecursively: true },
           { ip: '::ffff:127.0.0.1', endpoint: '/_api/v3/pages/rename' });
 
-        expect(xssSpy).toHaveBeenCalled();
+        expect(generalXssFilterProcessSpy).toHaveBeenCalled();
         expect(renameDescendantsWithStreamSpy).toHaveBeenCalled();
         expect(pageEventSpy).toHaveBeenCalledWith('rename');
 
@@ -625,7 +626,7 @@ describe('PageService', () => {
       const resultPage = await crowi.pageService.duplicate(parentForDuplicate, '/newParentDuplicate', testUser2, false);
       const duplicatedToPageRevision = await Revision.findOne({ pageId: resultPage._id });
 
-      expect(xssSpy).toHaveBeenCalled();
+      expect(generalXssFilterProcessSpy).toHaveBeenCalled();
       expect(duplicateDescendantsWithStreamSpy).not.toHaveBeenCalled();
       // TODO https://redmine.weseek.co.jp/issues/87537 : activate outer module mockImplementation
       // expect(serializePageSecurely).toHaveBeenCalled();
@@ -646,7 +647,7 @@ describe('PageService', () => {
       const resultPageRecursivly = await crowi.pageService.duplicate(parentForDuplicate, '/newParentDuplicateRecursively', testUser2, true);
       const duplicatedRecursivelyToPageRevision = await Revision.findOne({ pageId: resultPageRecursivly._id });
 
-      expect(xssSpy).toHaveBeenCalled();
+      expect(generalXssFilterProcessSpy).toHaveBeenCalled();
       expect(duplicateDescendantsWithStreamSpy).toHaveBeenCalled();
       // TODO https://redmine.weseek.co.jp/issues/87537 : activate outer module mockImplementation
       // expect(serializePageSecurely).toHaveBeenCalled();

+ 10 - 9
apps/app/test/integration/service/v5.non-public-page.test.ts

@@ -10,6 +10,7 @@ import PageTagRelation from '../../../src/server/models/page-tag-relation';
 import Tag from '../../../src/server/models/tag';
 import UserGroup from '../../../src/server/models/user-group';
 import UserGroupRelation from '../../../src/server/models/user-group-relation';
+import { generalXssFilter } from '../../../src/services/general-xss-filter';
 import { getInstance } from '../setup-crowi';
 
 describe('PageService page operations with non-public pages', () => {
@@ -31,7 +32,7 @@ describe('PageService page operations with non-public pages', () => {
   let Page;
   let Revision;
   let User;
-  let xssSpy;
+  let generalXssFilterProcessSpy;
 
   let rootPage;
 
@@ -290,7 +291,7 @@ describe('PageService page operations with non-public pages', () => {
       },
     ]);
 
-    xssSpy = jest.spyOn(crowi.xss, 'process').mockImplementation(path => path);
+    generalXssFilterProcessSpy = jest.spyOn(generalXssFilter, 'process');
 
     dummyUser1 = await User.findOne({ username: 'v5DummyUser1' });
     dummyUser2 = await User.findOne({ username: 'v5DummyUser2' });
@@ -1139,7 +1140,7 @@ describe('PageService page operations with non-public pages', () => {
       expect(page3Renamed.parent).toStrictEqual(page2Renamed._id);
       expect(normalizeGrantedGroups(page2Renamed.grantedGroups)).toStrictEqual(normalizeGrantedGroups(_page2.grantedGroups));
       expect(normalizeGrantedGroups(page3Renamed.grantedGroups)).toStrictEqual(normalizeGrantedGroups(_page3.grantedGroups));
-      expect(xssSpy).toHaveBeenCalled();
+      expect(generalXssFilterProcessSpy).toHaveBeenCalled();
     });
     test('Should throw with NOT grant normalized pages', async() => {
       const _pathD = '/np_rename4_destination';
@@ -1206,7 +1207,7 @@ describe('PageService page operations with non-public pages', () => {
       expect(page2Renamed).toBeTruthy();
       expect(page3Renamed).toBeNull();
       expect(page2Renamed.parent).toBeNull();
-      expect(xssSpy).toHaveBeenCalled();
+      expect(generalXssFilterProcessSpy).toHaveBeenCalled();
     });
   });
   describe('Duplicate', () => {
@@ -1240,7 +1241,7 @@ describe('PageService page operations with non-public pages', () => {
 
       const duplicatedPage = await Page.findOne({ path: newPagePath });
       const duplicatedRevision = await Revision.findOne({ pageId: duplicatedPage._id });
-      expect(xssSpy).toHaveBeenCalled();
+      expect(generalXssFilterProcessSpy).toHaveBeenCalled();
       expect(duplicatedPage).toBeTruthy();
       expect(duplicatedPage._id).not.toStrictEqual(_page._id);
       expect(duplicatedPage.grant).toBe(_page.grant);
@@ -1271,7 +1272,7 @@ describe('PageService page operations with non-public pages', () => {
       const duplicatedPage2 = await Page.findOne({ path: '/dup_np_duplicate2/np_duplicate3' }).populate({ path: 'revision', model: 'Revision' });
       const duplicatedRevision1 = duplicatedPage1.revision;
       const duplicatedRevision2 = duplicatedPage2.revision;
-      expect(xssSpy).toHaveBeenCalled();
+      expect(generalXssFilterProcessSpy).toHaveBeenCalled();
       expect(duplicatedPage1).toBeTruthy();
       expect(duplicatedPage2).toBeTruthy();
       expect(duplicatedRevision1).toBeTruthy();
@@ -1316,7 +1317,7 @@ describe('PageService page operations with non-public pages', () => {
       const duplicatedPage3 = await Page.findOne({ path: '/dup_np_duplicate4/np_duplicate6' }).populate({ path: 'revision', model: 'Revision' });
       const duplicatedRevision1 = duplicatedPage1.revision;
       const duplicatedRevision3 = duplicatedPage3.revision;
-      expect(xssSpy).toHaveBeenCalled();
+      expect(generalXssFilterProcessSpy).toHaveBeenCalled();
       expect(duplicatedPage1).toBeTruthy();
       expect(duplicatedPage2).toBeNull();
       expect(duplicatedPage3).toBeTruthy();
@@ -1352,7 +1353,7 @@ describe('PageService page operations with non-public pages', () => {
       const duplicatedPage2 = await Page.findOne({ path: '/dup_np_duplicate7/np_duplicate8' }).populate({ path: 'revision', model: 'Revision' });
       const duplicatedPage3 = await Page.findOne({ path: '/dup_np_duplicate7/np_duplicate9' }).populate({ path: 'revision', model: 'Revision' });
       const duplicatedRevision1 = duplicatedPage1.revision;
-      expect(xssSpy).toHaveBeenCalled();
+      expect(generalXssFilterProcessSpy).toHaveBeenCalled();
       expect(duplicatedPage1).toBeTruthy();
       expect(duplicatedPage2).toBeFalsy();
       expect(duplicatedPage3).toBeFalsy();
@@ -1394,7 +1395,7 @@ describe('PageService page operations with non-public pages', () => {
       const duplicatedRevision1 = duplicatedPage1.revision;
       const duplicatedRevision2 = duplicatedPage2.revision;
       const duplicatedRevision3 = duplicatedPage3.revision;
-      expect(xssSpy).toHaveBeenCalled();
+      expect(generalXssFilterProcessSpy).toHaveBeenCalled();
       expect(duplicatedPage1).toBeTruthy();
       expect(duplicatedPage2).toBeTruthy();
       expect(duplicatedPage3).toBeTruthy();

+ 0 - 2
apps/app/test/integration/service/v5.page.test.ts

@@ -16,7 +16,6 @@ describe('Test page service methods', () => {
   let ShareLink;
   let PageRedirect;
   let PageOperation;
-  let xssSpy;
 
   let rootPage;
 
@@ -50,7 +49,6 @@ describe('Test page service methods', () => {
     /*
      * Common
      */
-    xssSpy = jest.spyOn(crowi.xss, 'process').mockImplementation(path => path);
 
     // ***********************************************************************************************************
     // * Do NOT change properties of globally used documents. Otherwise, it might cause some errors in other tests

+ 17 - 16
apps/app/test/integration/service/v5.public-page.test.ts

@@ -5,6 +5,7 @@ import { PageActionType, PageActionStage } from '../../../src/interfaces/page-op
 import type { IPageTagRelation } from '../../../src/interfaces/page-tag-relation';
 import PageTagRelation from '../../../src/server/models/page-tag-relation';
 import Tag from '../../../src/server/models/tag';
+import { generalXssFilter } from '../../../src/services/general-xss-filter';
 import { getInstance } from '../setup-crowi';
 
 describe('PageService page operations with only public pages', () => {
@@ -21,7 +22,7 @@ describe('PageService page operations with only public pages', () => {
   let ShareLink;
   let PageRedirect;
   let PageOperation;
-  let xssSpy;
+  let generalXssFilterProcessSpy;
 
   let rootPage;
 
@@ -62,7 +63,7 @@ describe('PageService page operations with only public pages', () => {
     dummyUser1 = await User.findOne({ username: 'v5DummyUser1' });
     dummyUser2 = await User.findOne({ username: 'v5DummyUser2' });
 
-    xssSpy = jest.spyOn(crowi.xss, 'process').mockImplementation(path => path);
+    generalXssFilterProcessSpy = jest.spyOn(generalXssFilter, 'process');
 
     rootPage = await Page.findOne({ path: '/' });
     if (rootPage == null) {
@@ -1254,7 +1255,7 @@ describe('PageService page operations with only public pages', () => {
       });
       const childPageBeforeRename = await Page.findOne({ path: '/v5_ChildForRename1' });
 
-      expect(xssSpy).toHaveBeenCalled();
+      expect(generalXssFilterProcessSpy).toHaveBeenCalled();
       expect(renamedPage.path).toBe(newPath);
       expect(renamedPage.parent).toStrictEqual(parentPage._id);
       expect(childPageBeforeRename).toBeNull();
@@ -1275,7 +1276,7 @@ describe('PageService page operations with only public pages', () => {
       });
       const childPageBeforeRename = await Page.findOne({ path: '/v5_ChildForRename2' });
 
-      expect(xssSpy).toHaveBeenCalled();
+      expect(generalXssFilterProcessSpy).toHaveBeenCalled();
       expect(renamedPage.path).toBe(newPath);
       expect(parentPage.isEmpty).toBe(true);
       expect(renamedPage.parent).toStrictEqual(parentPage._id);
@@ -1296,7 +1297,7 @@ describe('PageService page operations with only public pages', () => {
         endpoint: '/_api/v3/pages/rename',
       });
 
-      expect(xssSpy).toHaveBeenCalled();
+      expect(generalXssFilterProcessSpy).toHaveBeenCalled();
       expect(renamedPage.path).toBe(newPath);
       expect(renamedPage.parent).toStrictEqual(parentPage._id);
       expect(renamedPage.lastUpdateUser).toStrictEqual(dummyUser2._id);
@@ -1317,7 +1318,7 @@ describe('PageService page operations with only public pages', () => {
       });
       const pageRedirect = await PageRedirect.findOne({ fromPath: oldPath, toPath: renamedPage.path });
 
-      expect(xssSpy).toHaveBeenCalled();
+      expect(generalXssFilterProcessSpy).toHaveBeenCalled();
       expect(renamedPage.path).toBe(newPath);
       expect(renamedPage.parent).toStrictEqual(parentPage._id);
       expect(pageRedirect).toBeTruthy();
@@ -1342,7 +1343,7 @@ describe('PageService page operations with only public pages', () => {
       const childPageBeforeRename = await Page.findOne({ path: '/v5_ChildForRename5' });
       const grandchildBeforeRename = await Page.findOne({ path: grandchild.path });
 
-      expect(xssSpy).toHaveBeenCalled();
+      expect(generalXssFilterProcessSpy).toHaveBeenCalled();
       expect(renamedPage.path).toBe(newPath);
       expect(renamedPage.parent).toStrictEqual(parentPage._id);
       expect(childPageBeforeRename).toBeNull();
@@ -1369,7 +1370,7 @@ describe('PageService page operations with only public pages', () => {
       const grandchildAfterRename = await Page.findOne({ parent: renamedPage._id });
       const grandchildBeforeRename = await Page.findOne({ path: '/v5_ChildForRename7/v5_GrandchildForRename7' });
 
-      expect(xssSpy).toHaveBeenCalled();
+      expect(generalXssFilterProcessSpy).toHaveBeenCalled();
       expect(renamedPage.path).toBe(newPath);
       expect(renamedPage.isEmpty).toBe(true);
       expect(renamedPage.parent).toStrictEqual(parentPage._id);
@@ -1409,7 +1410,7 @@ describe('PageService page operations with only public pages', () => {
         endpoint: '/_api/v3/pages/rename',
       });
 
-      expect(xssSpy).toHaveBeenCalled();
+      expect(generalXssFilterProcessSpy).toHaveBeenCalled();
       expect(renamedPage.path).toBe(newPath);
       expect(renamedPage.isEmpty).toBe(false);
       expect(renamedPage._id).toStrictEqual(page._id);
@@ -1715,7 +1716,7 @@ describe('PageService page operations with only public pages', () => {
       const baseRevision = await Revision.findOne({ pageId: page._id });
 
       // new path
-      expect(xssSpy).toHaveBeenCalled();
+      expect(generalXssFilterProcessSpy).toHaveBeenCalled();
       expect(duplicatedPage.path).toBe(newPagePath);
       expect(duplicatedPage._id).not.toStrictEqual(page._id);
       expect(duplicatedPage.revision).toStrictEqual(duplicatedRevision._id);
@@ -1751,7 +1752,7 @@ describe('PageService page operations with only public pages', () => {
       const baseRevision = await Revision.findOne({ pageId: page._id });
 
       // new path
-      expect(xssSpy).toHaveBeenCalled();
+      expect(generalXssFilterProcessSpy).toHaveBeenCalled();
       expect(duplicatedPage.path).toBe(newPagePath);
       expect(duplicatedPage._id).not.toStrictEqual(page._id);
       expect(duplicatedPage.revision).toStrictEqual(duplicatedRevision._id);
@@ -1789,7 +1790,7 @@ describe('PageService page operations with only public pages', () => {
       expect(revisionBodyForDupChild1).toBeTruthy();
       expect(revisionBodyForDupChild2).toBeTruthy();
 
-      expect(xssSpy).toHaveBeenCalled();
+      expect(generalXssFilterProcessSpy).toHaveBeenCalled();
       expect(duplicatedPage.path).toBe(newPagePath);
       expect(duplicatedChildPage1.path).toBe('/duplicatedv5PageForDuplicate3/v5_Child_1_ForDuplicate3');
       expect(duplicatedChildPage2.path).toBe('/duplicatedv5PageForDuplicate3/v5_Child_2_ForDuplicate3');
@@ -1809,7 +1810,7 @@ describe('PageService page operations with only public pages', () => {
       const duplicatedChild = await Page.findOne({ parent: duplicatedPage._id });
       const duplicatedGrandchild = await Page.findOne({ parent: duplicatedChild._id });
 
-      expect(xssSpy).toHaveBeenCalled();
+      expect(generalXssFilterProcessSpy).toHaveBeenCalled();
       expect(duplicatedPage).toBeTruthy();
       expect(duplicatedGrandchild).toBeTruthy();
       expect(duplicatedPage.path).toBe(newPagePath);
@@ -1837,7 +1838,7 @@ describe('PageService page operations with only public pages', () => {
       const duplicatedPage = await duplicate(basePage, newPagePath, dummyUser1, false);
       const duplicatedTagRelations = await PageTagRelation.find({ relatedPage: duplicatedPage._id });
 
-      expect(xssSpy).toHaveBeenCalled();
+      expect(generalXssFilterProcessSpy).toHaveBeenCalled();
       expect(duplicatedPage.path).toBe(newPagePath);
       expect(duplicatedTagRelations.length).toBeGreaterThanOrEqual(2);
     });
@@ -1852,7 +1853,7 @@ describe('PageService page operations with only public pages', () => {
       const duplicatedPage = await duplicate(basePage, newPagePath, dummyUser1, false);
       const duplicatedComments = await Comment.find({ page: duplicatedPage._id });
 
-      expect(xssSpy).toHaveBeenCalled();
+      expect(generalXssFilterProcessSpy).toHaveBeenCalled();
       expect(duplicatedPage.path).toBe(newPagePath);
       expect(basePageComments.length).not.toBe(duplicatedComments.length);
     });
@@ -1876,7 +1877,7 @@ describe('PageService page operations with only public pages', () => {
       expect(duplicatedGrandchild).toBeTruthy();
       expect(duplicatedChild.revision).toBeTruthy();
       expect(duplicatedGrandchild.revision).toBeTruthy();
-      expect(xssSpy).toHaveBeenCalled();
+      expect(generalXssFilterProcessSpy).toHaveBeenCalled();
       expect(duplicatedPage.path).toBe(newPagePath);
       expect(duplicatedPage.isEmpty).toBe(true);
       expect(duplicatedChild.revision.body).toBe(basePageChild.revision.body);

+ 0 - 1
apps/app/test/integration/setup-crowi.ts

@@ -13,7 +13,6 @@ const initCrowi = async(crowi) => {
 
   await Promise.all([
     crowi.setUpApp(),
-    crowi.setUpXss(),
   ]);
 
   await Promise.all([

+ 6 - 0
apps/app/test/integration/tsconfig.json

@@ -0,0 +1,6 @@
+{
+  "extends": "../../tsconfig.json",
+  "compilerOptions": {
+    "module": "CommonJS"
+  },
+}

+ 0 - 7
apps/app/test/tsconfig.json

@@ -1,7 +0,0 @@
-{
-  "extends": "../tsconfig.json",
-  "compilerOptions": {
-    "isolatedModules": false,
-    "types": ["cypress", "cypress-real-events"],
-  },
-}

+ 0 - 1
apps/app/tsconfig.build.client.json

@@ -5,7 +5,6 @@
     ".next/types/**/*.ts"
   ],
   "compilerOptions": {
-    "module": "ESNext",
     "strict": false,
     "strictNullChecks": true,
 

+ 0 - 11
apps/app/tsconfig.build.server-tsc-alias.json

@@ -1,11 +0,0 @@
-{
-  "$schema": "http://json.schemastore.org/tsconfig",
-  "extends": "./tsconfig.build.server.json",
-  "compilerOptions": {
-    "paths": {
-      "~/*": ["./src/*"],
-      "^/*": ["./*"],
-      "debug": ["./src/utils/logger/alias-for-debug"]
-    }
-  }
-}

+ 7 - 0
apps/app/tsconfig.build.server.json

@@ -3,6 +3,7 @@
   "extends": "./tsconfig.json",
   "compilerOptions": {
     "module": "CommonJS",
+    "moduleResolution": "Node",
     "outDir": "transpiled",
     "declaration": true,
     "noResolve": false,
@@ -21,8 +22,14 @@
     "resource",
     "src/client",
     "src/components",
+    "src/components-universal",
     "src/linter-checker",
     "src/stores",
+    "src/stores-universal",
     "src/styles",
+    "src/**/*.jsx",
+    "src/**/*.tsx",
+    "src/**/*.spec.ts",
+    "src/**/*.integ.ts"
   ]
 }

+ 14 - 4
apps/app/tsconfig.json

@@ -2,8 +2,6 @@
   "$schema": "http://json.schemastore.org/tsconfig",
   "extends": "../../tsconfig.base.json",
   "compilerOptions": {
-    "module": "CommonJS",
-
     "jsx": "preserve",
     "resolveJsonModule": true,
     "types": [
@@ -22,7 +20,15 @@
     "strictNullChecks": true,
     "strictBindCallApply": true,
     "noImplicitAny": false,
-    "noImplicitOverride": true
+    "noImplicitOverride": true,
+
+    // Note: To transform paths for both the output .js and .d.ts files, you need both of the below entries
+    "plugins": [
+      // Transform paths in output .js files
+      { "transform": "typescript-transform-paths" },
+      // Transform paths in output .d.ts files (Include this line if you output declarations files)
+      { "transform": "typescript-transform-paths", "afterDeclarations": true }
+    ]
   },
   "include": [
     "next-env.d.ts",
@@ -31,6 +37,10 @@
   ],
   "ts-node": {
     "transpileOnly": true,
-    "swc": true
+    "swc": true,
+    "compilerOptions": {
+      "module": "CommonJS",
+      "moduleResolution": "Node"
+    }
   }
 }

+ 4 - 2
apps/app/turbo.json

@@ -7,6 +7,7 @@
       "dependsOn": ["@growi/ui#build"],
       "outputs": ["src/styles/prebuilt/**"],
       "inputs": [
+        "vite.styles-prebuilt.config.ts",
         "src/styles/**/*.scss",
         "../../packages/core/scss/**"
       ],
@@ -16,12 +17,12 @@
       "dependsOn": ["^build", "styles-prebuilt"],
       "outputs": [".next/**", "!.next/cache/**", "dist/**"],
       "inputs": [
+        "next.config.js",
         "config/**",
         "public/**",
         "resource/**",
         "src/**",
-        "tsconfig*.json",
-        "vite*.ts"
+        "tsconfig*.json"
       ],
       "outputLogs": "new-only"
     },
@@ -36,6 +37,7 @@
       "dependsOn": ["@growi/ui#dev"],
       "outputs": ["src/styles/prebuilt/**"],
       "inputs": [
+        "vite.styles-prebuilt.config.ts",
         "src/styles/**/*.scss",
         "!src/styles/prebuilt/**",
         "../../packages/core/scss/**"

+ 4 - 7
apps/slackbot-proxy/package.json

@@ -1,17 +1,15 @@
 {
   "name": "@growi/slackbot-proxy",
-  "version": "7.0.10-slackbot-proxy.0",
+  "version": "7.0.11-slackbot-proxy.0",
   "license": "MIT",
   "private": "true",
   "scripts": {
-    "build": "yarn tsc && tsc-alias -p tsconfig.build.json",
+    "build": "tspc -p tsconfig.build.json",
     "clean": "shx rm -rf dist",
     "cp:public": "cp -RT ./src/public ./dist/public",
     "cp:views": "cp -RT ./src/views ./dist/views",
     "cp:bootstrap": "cp -RT ../../node_modules/bootstrap/dist ./dist/public/bootstrap",
     "cp:bootstrap:dev": "cp -RT ../../node_modules/bootstrap/dist ./src/public/bootstrap",
-    "tsc": "tsc -p tsconfig.build.json",
-    "tsc:w": "yarn tsc -w",
     "dev:ci": "cross-env NODE_ENV=development yarn ts-node src/index.ts --ci",
     "dev": "cross-env NODE_ENV=development nodemon --exec yarn ts-node --inspect src/index.ts",
     "start:prod:ci": "yarn start:prod --ci",
@@ -20,7 +18,7 @@
     "predev": "yarn cp:bootstrap:dev",
     "lint:js": "yarn eslint src/**/*.{js,ts}",
     "lint:styles": "stylelint --allow-empty-input \"src/**/*.scss\" \"src/**/*.css\"",
-    "lint:typecheck": "tsc",
+    "lint:typecheck": "tspc",
     "lint": "run-p lint:*",
     "version": "yarn version --no-git-tag-version --preid=slackbot-proxy",
     "ts-node": "node -r ts-node/register/transpile-only -r tsconfig-paths/register -r dotenv-flow/config"
@@ -67,7 +65,6 @@
     "bootstrap": "=5.3.2",
     "browser-bunyan": "^1.6.3",
     "eslint-plugin-regex": "^1.8.0",
-    "morgan": "^1.10.0",
-    "tsc-alias": "^1.2.9"
+    "morgan": "^1.10.0"
   }
 }

+ 9 - 1
apps/slackbot-proxy/tsconfig.json

@@ -14,7 +14,15 @@
     "strictNullChecks": true,
     "strictBindCallApply": true,
     "noImplicitAny": false,
-    "noImplicitOverride": true
+    "noImplicitOverride": true,
+
+    // Note: To transform paths for both the output .js and .d.ts files, you need both of the below entries
+    "plugins": [
+      // Transform paths in output .js files
+      { "transform": "typescript-transform-paths" },
+      // Transform paths in output .d.ts files (Include this line if you output declarations files)
+      { "transform": "typescript-transform-paths", "afterDeclarations": true }
+    ]
   },
   "include": [
     "src"

+ 3 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "7.0.10-RC.0",
+  "version": "7.0.11-RC.0",
   "description": "Team collaboration software using markdown",
   "license": "MIT",
   "private": "true",
@@ -98,8 +98,10 @@
     "stylelint-config-recess-order": "^5.0.1",
     "stylelint-config-recommended-scss": "^14.0.0",
     "ts-node": "^10.9.2",
+    "ts-patch": "^3.2.0",
     "tsconfig-paths": "^4.2.0",
     "typescript": "~5.0.0",
+    "typescript-transform-paths": "^3.4.7",
     "vite": "^5.2.9",
     "vite-plugin-dts": "^3.8.3",
     "vite-tsconfig-paths": "^4.3.2",

+ 1 - 1
packages/editor/package.json

@@ -5,8 +5,8 @@
   "license": "MIT",
   "private": "true",
   "type": "module",
+  "main": "dist/index.js",
   "module": "dist/index.js",
-  "types": "dist/index.d.ts",
   "scripts": {
     "build": "tsc && vite build",
     "clean": "shx rm -rf dist",

+ 1 - 2
packages/editor/tsconfig.json

@@ -26,6 +26,5 @@
       "/*": ["./public/*"]
     }
   },
-  "include": ["src"],
-  "references": [{ "path": "./tsconfig.node.json" }]
+  "include": ["src"]
 }

+ 0 - 11
packages/editor/tsconfig.node.json

@@ -1,11 +0,0 @@
-{
-  "$schema": "http://json.schemastore.org/tsconfig",
-  "compilerOptions": {
-    "composite": true,
-    "skipLibCheck": true,
-    "module": "ESNext",
-    "moduleResolution": "bundler",
-    "allowSyntheticDefaultImports": true
-  },
-  "include": ["vite.config.ts"]
-}

+ 1 - 0
packages/editor/vite.config.ts

@@ -18,6 +18,7 @@ export default defineConfig({
   plugins: [
     react(),
     dts({
+      entryRoot: 'src',
       exclude: [
         ...excludeFiles,
       ],

+ 0 - 1
packages/pluginkit/tsconfig.json

@@ -2,7 +2,6 @@
   "$schema": "http://json.schemastore.org/tsconfig",
   "extends": "../../tsconfig.base.json",
   "compilerOptions": {
-    "module": "CommonJS",
     "types": [
       "vitest/globals"
     ]

+ 1 - 1
packages/remark-growi-directive/package.json

@@ -17,7 +17,7 @@
   "main": "dist/index.js",
   "typings": "dist/index.d.ts",
   "scripts": {
-    "build": "yarn tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json",
+    "build": "yarn tsc -p tsconfig.build.json",
     "clean": "shx rm -rf dist",
     "dev": "yarn build",
     "watch": "yarn tsc -w",

+ 0 - 12
packages/remark-growi-directive/tsconfig.base.json

@@ -1,12 +0,0 @@
-{
-  "$schema": "http://json.schemastore.org/tsconfig",
-  "extends": "../../tsconfig.base.json",
-  "compilerOptions": {
-  },
-  "include": [
-    "src"
-  ],
-  "exclude": [
-    "test"
-  ]
-}

+ 2 - 6
packages/remark-growi-directive/tsconfig.build.json

@@ -1,6 +1,6 @@
 {
   "$schema": "http://json.schemastore.org/tsconfig",
-  "extends": "./tsconfig.base.json",
+  "extends": "./tsconfig.json",
   "compilerOptions": {
     "rootDir": "./src",
     "outDir": "dist",
@@ -8,10 +8,6 @@
     "noResolve": false,
     "preserveConstEnums": true,
     "sourceMap": false,
-    "noEmit": false,
-
-    "baseUrl": ".",
-    "paths": {
-    }
+    "noEmit": false
   }
 }

+ 7 - 8
packages/remark-growi-directive/tsconfig.json

@@ -1,11 +1,10 @@
 {
   "$schema": "http://json.schemastore.org/tsconfig",
-  "extends": "./tsconfig.base.json",
-  "compilerOptions": {
-    "baseUrl": ".",
-    "paths": {
-      "~/*": ["./src/*"],
-      "@growi/*": ["../*/src"]
-    }
-  }
+  "extends": "../../tsconfig.base.json",
+  "include": [
+    "src"
+  ],
+  "exclude": [
+    "test"
+  ]
 }

+ 1 - 2
packages/slack/package.json

@@ -67,7 +67,6 @@
   },
   "devDependencies": {
     "@types/express": "^4.17.11",
-    "eslint-plugin-regex": "^1.8.0",
-    "tsc-alias": "^1.2.9"
+    "eslint-plugin-regex": "^1.8.0"
   }
 }

+ 3 - 3
tsconfig.base.json

@@ -1,8 +1,8 @@
 {
   "$schema": "http://json.schemastore.org/tsconfig",
   "compilerOptions": {
-    "target": "es2019",
-    "module": "esnext",
+    "target": "ESNext",
+    "module": "ESNext",
     "allowJs": true,
     "skipLibCheck": true,
     "importHelpers": true,
@@ -20,7 +20,7 @@
     "noUnusedParameters": false,
 
     /* Module Resolution Options */
-    "moduleResolution": "node",
+    "moduleResolution": "Bundler",
     "allowSyntheticDefaultImports": true,
     "esModuleInterop": true,
 

+ 56 - 154
yarn.lock

@@ -1176,10 +1176,10 @@
     core-js-pure "^3.20.2"
     regenerator-runtime "^0.13.4"
 
-"@babel/runtime@^7.0.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.8", "@babel/runtime@^7.14.6", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.18.6", "@babel/runtime@^7.20.1", "@babel/runtime@^7.22.15", "@babel/runtime@^7.23.2", "@babel/runtime@^7.23.9", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
-  version "7.24.5"
-  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.5.tgz#230946857c053a36ccc66e1dd03b17dd0c4ed02c"
-  integrity sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==
+"@babel/runtime@^7.0.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.8", "@babel/runtime@^7.14.6", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.18.6", "@babel/runtime@^7.20.1", "@babel/runtime@^7.21.0", "@babel/runtime@^7.22.15", "@babel/runtime@^7.23.2", "@babel/runtime@^7.23.9", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
+  version "7.24.7"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.7.tgz#f4f0d5530e8dbdf59b3451b9b3e594b6ba082e12"
+  integrity sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==
   dependencies:
     regenerator-runtime "^0.14.0"
 
@@ -2469,22 +2469,6 @@
     "@types/yargs" "^17.0.8"
     chalk "^4.0.0"
 
-"@jfonx/console-utils@^1.0.3":
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/@jfonx/console-utils/-/console-utils-1.0.3.tgz#cbb7f911e4191a4a2fe1ba4807d29f100b5d099f"
-  integrity sha512-/XbnqjWc7yNZVLAJJO9rimfIz9DYte+cj3EF9hwhIv7vw6ok2t3cjl0huYsmD89srKH03vWjeqAcIH86CuYj3g==
-  dependencies:
-    colors "^1.3.3"
-
-"@jfonx/file-utils@^3.0.1":
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/@jfonx/file-utils/-/file-utils-3.0.1.tgz#8d3d6e931a283420fe29802ea71c28dd397cd8d3"
-  integrity sha512-qwH0CuzWmghtTHGMyuPHj6SJPQgWeiXFJBfrxCWMbzxVCa3aLZPEfzSdlSnC/UABsk6feRkNdHXw59rVshNPqw==
-  dependencies:
-    "@jfonx/console-utils" "^1.0.3"
-    comment-json "^4.1.0"
-    find-up "^4.1.0"
-
 "@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2", "@jridgewell/gen-mapping@^0.3.5":
   version "0.3.5"
   resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36"
@@ -5424,11 +5408,6 @@ array-includes@^3.1.4, array-includes@^3.1.5:
     get-intrinsic "^1.1.1"
     is-string "^1.0.7"
 
-array-timsort@^1.0.3:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/array-timsort/-/array-timsort-1.0.3.tgz#3c9e4199e54fb2b9c3fe5976396a21614ef0d926"
-  integrity sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==
-
 array-union@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
@@ -6491,7 +6470,7 @@ cheerio@~1.0.0-rc.12:
     parse5 "^7.0.0"
     parse5-htmlparser2-tree-adapter "^7.0.0"
 
-"chokidar@>=3.0.0 <4.0.0", chokidar@^3.5.0, chokidar@^3.5.2:
+"chokidar@>=3.0.0 <4.0.0", chokidar@^3.5.2:
   version "3.6.0"
   resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b"
   integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==
@@ -6577,10 +6556,10 @@ cli-spinners@^2.5.0:
   resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.6.1.tgz#adc954ebe281c37a6319bfa401e6dd2488ffb70d"
   integrity sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==
 
-cli-table3@^0.6.0, cli-table3@~0.6.1:
-  version "0.6.3"
-  resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.3.tgz#61ab765aac156b52f222954ffc607a6f01dbeeb2"
-  integrity sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==
+cli-table3@^0.6.1, cli-table3@~0.6.1:
+  version "0.6.5"
+  resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.5.tgz#013b91351762739c16a9567c21a04632e449bf2f"
+  integrity sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==
   dependencies:
     string-width "^4.2.0"
   optionalDependencies:
@@ -6784,27 +6763,16 @@ commander@^6.2.1:
   resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c"
   integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==
 
-commander@^8.0.0, commander@^8.1.0, commander@^8.3.0:
+commander@^8.0.0, commander@^8.3.0:
   version "8.3.0"
   resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66"
   integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==
 
-commander@^9.3.0, commander@^9.4.1:
+commander@^9.1.0, commander@^9.3.0, commander@^9.4.1:
   version "9.5.0"
   resolved "https://registry.yarnpkg.com/commander/-/commander-9.5.0.tgz#bc08d1eb5cedf7ccb797a96199d41c7bc3e60d30"
   integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==
 
-comment-json@^4.1.0:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/comment-json/-/comment-json-4.1.0.tgz#09d08f0fbc4ad5eeccbac20f469adbb967dcbd2c"
-  integrity sha512-WEghmVYaNq9NlWbrkzQTSsya9ycLyxJxpTQfZEan6a5Jomnjw18zS3Podf8q1Zf9BvonvQd/+Z7Z39L7KKzzdQ==
-  dependencies:
-    array-timsort "^1.0.3"
-    core-util-is "^1.0.2"
-    esprima "^4.0.1"
-    has-own-prop "^2.0.0"
-    repeat-string "^1.6.1"
-
 common-tags@^1.8.0:
   version "1.8.2"
   resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.2.tgz#94ebb3c076d26032745fd54face7f688ef5ac9c6"
@@ -7045,7 +7013,7 @@ core-js@^3, core-js@^3.0.1, core-js@^3.2.1, core-js@^3.6.5:
   resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.33.0.tgz#70366dbf737134761edb017990cf5ce6c6369c40"
   integrity sha512-HoZr92+ZjFEKar5HS6MC776gYslNOKHt75mEBKWKnPeFDpZ6nH5OeF3S6HFT1mUAUZKrzkez05VboaX8myjSuw==
 
-core-util-is@1.0.2, core-util-is@^1.0.2, core-util-is@~1.0.0:
+core-util-is@1.0.2, core-util-is@~1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
   integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
@@ -7684,10 +7652,12 @@ date-and-time@^2.0.0:
   resolved "https://registry.yarnpkg.com/date-and-time/-/date-and-time-2.0.1.tgz#bc8b72704980e8a0979bb186118d30d02059ef04"
   integrity sha512-O7Xe5dLaqvY/aF/MFWArsAM1J4j7w1CSZlPCX9uHgmb+6SbkPd8Q4YOvfvH/cZGvFlJFfHOZKxQtmMUOoZhc/w==
 
-date-fns@^2.23.0, date-fns@^2.24.0:
-  version "2.28.0"
-  resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.28.0.tgz#9570d656f5fc13143e50c975a3b6bbeb46cd08b2"
-  integrity sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==
+date-fns@^2.24.0, date-fns@^2.28.0:
+  version "2.30.0"
+  resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0"
+  integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==
+  dependencies:
+    "@babel/runtime" "^7.21.0"
 
 date-fns@^3.6.0:
   version "3.6.0"
@@ -7962,10 +7932,6 @@ destroy@1.2.0:
   resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015"
   integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==
 
-detect-file@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7"
-
 detect-indent@^6.0.0:
   version "6.1.0"
   resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6"
@@ -8834,7 +8800,7 @@ espree@^9.0.0, espree@^9.5.2:
     acorn-jsx "^5.3.2"
     eslint-visitor-keys "^3.4.1"
 
-esprima@^4.0.0, esprima@^4.0.1:
+esprima@^4.0.0:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
   integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
@@ -8986,12 +8952,6 @@ expand-brackets@^2.1.4:
     snapdragon "^0.8.1"
     to-regex "^3.0.1"
 
-expand-tilde@^2.0.0, expand-tilde@^2.0.2:
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502"
-  dependencies:
-    homedir-polyfill "^1.0.1"
-
 expect@^29.0.0, expect@^29.5.0:
   version "29.5.0"
   resolved "https://registry.yarnpkg.com/expect/-/expect-29.5.0.tgz#68c0509156cb2a0adb8865d413b137eeaae682f7"
@@ -9340,14 +9300,6 @@ find-cache-dir@^3.3.1, find-cache-dir@^3.3.2:
     make-dir "^3.0.2"
     pkg-dir "^4.1.0"
 
-find-node-modules@^2.1.0:
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/find-node-modules/-/find-node-modules-2.1.2.tgz#57565a3455baf671b835bc6b2134a9b938b9c53c"
-  integrity sha512-x+3P4mbtRPlSiVE1Qco0Z4YLU8WFiFcuWTf3m75OV9Uzcfs2Bg+O9N+r/K0AnmINBW06KpfqKwYJbFlFq4qNug==
-  dependencies:
-    findup-sync "^4.0.0"
-    merge "^2.1.0"
-
 find-up@^1.0.0:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f"
@@ -9386,16 +9338,6 @@ find-yarn-workspace-root2@1.2.16:
     micromatch "^4.0.2"
     pkg-dir "^4.2.0"
 
-findup-sync@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-4.0.0.tgz#956c9cdde804052b881b428512905c4a5f2cdef0"
-  integrity sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ==
-  dependencies:
-    detect-file "^1.0.0"
-    is-glob "^4.0.0"
-    micromatch "^4.0.2"
-    resolve-dir "^1.0.1"
-
 flat-cache@^3.0.4:
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11"
@@ -9525,7 +9467,7 @@ fs-constants@^1.0.0:
   resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
   integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==
 
-fs-extra@^10.0.0:
+fs-extra@^10.0.1:
   version "10.1.0"
   resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf"
   integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==
@@ -9876,14 +9818,6 @@ global-dirs@^3.0.0:
   dependencies:
     ini "2.0.0"
 
-global-modules@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea"
-  dependencies:
-    global-prefix "^1.0.1"
-    is-windows "^1.0.1"
-    resolve-dir "^1.0.0"
-
 global-modules@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780"
@@ -9891,16 +9825,6 @@ global-modules@^2.0.0:
   dependencies:
     global-prefix "^3.0.0"
 
-global-prefix@^1.0.1:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-1.0.2.tgz#dbf743c6c14992593c655568cb66ed32c0122ebe"
-  dependencies:
-    expand-tilde "^2.0.2"
-    homedir-polyfill "^1.0.1"
-    ini "^1.3.4"
-    is-windows "^1.0.1"
-    which "^1.2.14"
-
 global-prefix@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-3.0.0.tgz#fc85f73064df69f50421f47f883fe5b913ba9b97"
@@ -9947,7 +9871,7 @@ globby@11.0.1:
     merge2 "^1.3.0"
     slash "^3.0.0"
 
-globby@^11.0.0, globby@^11.0.1, globby@^11.0.2, globby@^11.1.0:
+globby@^11.0.0, globby@^11.0.1, globby@^11.1.0:
   version "11.1.0"
   resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b"
   integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==
@@ -10110,11 +10034,6 @@ has-flag@^4.0.0:
   resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
   integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
 
-has-own-prop@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/has-own-prop/-/has-own-prop-2.0.0.tgz#f0f95d58f65804f5d218db32563bb85b8e0417af"
-  integrity sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==
-
 has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854"
@@ -10405,12 +10324,6 @@ hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
   dependencies:
     react-is "^16.7.0"
 
-homedir-polyfill@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz#4c2bbc8a758998feebf5ed68580f76d46768b4bc"
-  dependencies:
-    parse-passwd "^1.0.0"
-
 hosted-git-info@^2.1.4:
   version "2.8.9"
   resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
@@ -10747,7 +10660,7 @@ ini@2.0.0:
   resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5"
   integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==
 
-ini@^1.3.4, ini@^1.3.5, ini@~1.3.0:
+ini@^1.3.5, ini@~1.3.0:
   version "1.3.8"
   resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
   integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
@@ -10922,7 +10835,7 @@ is-ci@^3.0.0:
   dependencies:
     ci-info "^3.1.1"
 
-is-core-module@^2.1.0, is-core-module@^2.11.0, is-core-module@^2.2.0, is-core-module@^2.8.1, is-core-module@^2.9.0:
+is-core-module@^2.1.0, is-core-module@^2.13.0, is-core-module@^2.2.0, is-core-module@^2.8.1, is-core-module@^2.9.0:
   version "2.13.1"
   resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384"
   integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==
@@ -11236,7 +11149,7 @@ is-what@^4.1.6:
   resolved "https://registry.yarnpkg.com/is-what/-/is-what-4.1.7.tgz#c41dc1d2d2d6a9285c624c2505f61849c8b1f9cc"
   integrity sha512-DBVOQNiPKnGMxRMLIYSwERAS5MVY1B7xYiGnpgctsOFvVDz9f9PFXXxMcTOHuoqYp4NK9qFYQaIC1NRRxLMpBQ==
 
-is-windows@^1.0.0, is-windows@^1.0.1, is-windows@^1.0.2:
+is-windows@^1.0.0, is-windows@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
 
@@ -13020,11 +12933,6 @@ merge2@^1.3.0, merge2@^1.4.1:
   resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
   integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
 
-merge@^2.1.0:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/merge/-/merge-2.1.1.tgz#59ef4bf7e0b3e879186436e8481c06a6c162ca98"
-  integrity sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w==
-
 mermaid@^10.1.0:
   version "10.1.0"
   resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-10.1.0.tgz#6e40d5250174f4750ca6548e4ee00f6ae210855a"
@@ -13412,18 +13320,17 @@ micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5:
     braces "^3.0.2"
     picomatch "^2.3.1"
 
-migrate-mongo@^8.2.3:
-  version "8.2.3"
-  resolved "https://registry.yarnpkg.com/migrate-mongo/-/migrate-mongo-8.2.3.tgz#76786e62e942f35ff17762fd440d28f888c87882"
-  integrity sha512-ezaxBdWRSljXkxDZQ6/2TrkNsL1TYbtKg7f7QfPIhFL3kWtv2G3Vv4XNXItgChFAEbVUX/LUvJ6fKCJpnTMFaQ==
+migrate-mongo@^11.0.0:
+  version "11.0.0"
+  resolved "https://registry.yarnpkg.com/migrate-mongo/-/migrate-mongo-11.0.0.tgz#d1b2291624fe8e134a0666ca77ad2fa18f42e337"
+  integrity sha512-GB/gHzUwp/fL1w6ksNGihTyb+cSrm6NbVLlz1OSkQKaLlzAXMwH7iKK2ZS7W5v+I8vXiY2rL58WTUZSAL6QR+A==
   dependencies:
-    cli-table3 "^0.6.0"
-    commander "^8.1.0"
-    date-fns "^2.23.0"
+    cli-table3 "^0.6.1"
+    commander "^9.1.0"
+    date-fns "^2.28.0"
     fn-args "^5.0.0"
-    fs-extra "^10.0.0"
+    fs-extra "^10.0.1"
     lodash "^4.17.21"
-    mongodb "^4.0.1"
     p-each-series "^2.2.0"
 
 mime-db@1.52.0, "mime-db@>= 1.43.0 < 2":
@@ -13668,7 +13575,7 @@ mongodb-memory-server-core@^9.1.1:
     tslib "^2.6.2"
     yauzl "^2.10.0"
 
-mongodb@4.16.0, mongodb@^4.0.1:
+mongodb@4.16.0:
   version "4.16.0"
   resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-4.16.0.tgz#8b0043de7b577c6a7e0ce44a2ca7315b9c0a7927"
   integrity sha512-0EB113Fsucaq1wsY0dOhi1fmZOwFtLOtteQkiqOXGklvWMnSH3g2QS53f0KTP+/6qOkuoXE2JksubSZNmxeI+g==
@@ -14693,10 +14600,6 @@ parse-ms@>=2.1.0:
   resolved "https://registry.yarnpkg.com/parse-ms/-/parse-ms-2.1.0.tgz#348565a753d4391fa524029956b172cb7753097d"
   integrity sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==
 
-parse-passwd@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6"
-
 parse5-htmlparser2-tree-adapter@^6.0.0:
   version "6.0.1"
   resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6"
@@ -16303,13 +16206,6 @@ resolve-cwd@^3.0.0:
   dependencies:
     resolve-from "^5.0.0"
 
-resolve-dir@^1.0.0, resolve-dir@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-1.0.1.tgz#79a40644c362be82f26effe739c9bb5382046f43"
-  dependencies:
-    expand-tilde "^2.0.0"
-    global-modules "^1.0.0"
-
 resolve-from@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
@@ -16329,12 +16225,12 @@ resolve.exports@^2.0.0:
   resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.2.tgz#f8c934b8e6a13f539e38b7098e2e36134f01e800"
   integrity sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==
 
-resolve@^1.1.6, resolve@^1.10.0, resolve@^1.20.0, resolve@^1.22.0, resolve@~1.22.1:
-  version "1.22.2"
-  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.2.tgz#0ed0943d4e301867955766c9f3e1ae6d01c6845f"
-  integrity sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==
+resolve@^1.1.6, resolve@^1.10.0, resolve@^1.20.0, resolve@^1.22.0, resolve@^1.22.2, resolve@~1.22.1:
+  version "1.22.8"
+  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d"
+  integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==
   dependencies:
-    is-core-module "^2.11.0"
+    is-core-module "^2.13.0"
     path-parse "^1.0.7"
     supports-preserve-symlinks-flag "^1.0.0"
 
@@ -18263,18 +18159,17 @@ ts-node@^10.9.2:
     v8-compile-cache-lib "^3.0.1"
     yn "3.1.1"
 
-tsc-alias@^1.2.9:
-  version "1.2.9"
-  resolved "https://registry.yarnpkg.com/tsc-alias/-/tsc-alias-1.2.9.tgz#9fbf38e5eb1bd89c7f4fc26ef0712e22a6ef8939"
-  integrity sha512-/ec9t/EIhW7h1oQ/mbezNlHsYipDsJY6IUi2SNRvzvnu8Iamp4nSUDUIdpx9jaoq2QZPKm63Je6bQZBGqWS4jA==
+ts-patch@^3.2.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/ts-patch/-/ts-patch-3.2.0.tgz#537b0e19aa273da4a34e42be68240ef062646dd3"
+  integrity sha512-fUGMkjGIlD4BFibDM+6pLYLXRguzCUY6fhP1KQzSnFJfAtTDT7DKyX0yHn3CJqfBv4mia/o3ZRte31UVf9Dl1A==
   dependencies:
-    "@jfonx/console-utils" "^1.0.3"
-    "@jfonx/file-utils" "^3.0.1"
-    chokidar "^3.5.0"
-    commander "^6.2.1"
-    find-node-modules "^2.1.0"
-    globby "^11.0.2"
-    normalize-path "^3.0.0"
+    chalk "^4.1.2"
+    global-prefix "^3.0.0"
+    minimist "^1.2.8"
+    resolve "^1.22.2"
+    semver "^7.5.4"
+    strip-ansi "^6.0.1"
 
 tsconfck@^3.0.3:
   version "3.0.3"
@@ -18528,6 +18423,13 @@ typeorm@=0.2.45:
     yargs "^17.0.1"
     zen-observable-ts "^1.0.0"
 
+typescript-transform-paths@^3.4.7:
+  version "3.4.7"
+  resolved "https://registry.yarnpkg.com/typescript-transform-paths/-/typescript-transform-paths-3.4.7.tgz#1deaf976fb1b7a70fb26b541356017057c0c17a5"
+  integrity sha512-1Us1kdkdfKd2unbkBAOV2HHRmbRBYpSuk9nJ7cLD2hP4QmfToiM/VpxNlhJc1eezVwVqSKSBjNSzZsK/fWR/9A==
+  dependencies:
+    minimatch "^3.0.4"
+
 typescript@5.4.2:
   version "5.4.2"
   resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.2.tgz#0ae9cebcfae970718474fe0da2c090cad6577372"
@@ -19263,7 +19165,7 @@ which-typed-array@^1.1.14, which-typed-array@^1.1.15, which-typed-array@^1.1.2:
     gopd "^1.0.1"
     has-tostringtag "^1.0.2"
 
-which@^1.2.14, which@^1.2.9, which@^1.3.1:
+which@^1.2.9, which@^1.3.1:
   version "1.3.1"
   resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
   integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==