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

Merge branch 'master' into feat/enhanced-link-edit-modal-for-master-merge

yusuketk 5 лет назад
Родитель
Сommit
a58ea598d1
72 измененных файлов с 1103 добавлено и 761 удалено
  1. 18 3
      CHANGES.md
  2. 4 4
      README.md
  3. 2 2
      bin/github-actions/update-readme.sh
  4. 1 1
      config/env.dev.js
  5. 1 1
      config/logger/config.dev.js
  6. 4 2
      docker/README.md
  7. 6 5
      package.json
  8. 1 1
      resource/cdn-manifests.js
  9. 3 2
      src/client/js/admin.jsx
  10. 2 2
      src/client/js/app.jsx
  11. 2 2
      src/client/js/base.jsx
  12. 7 7
      src/client/js/components/Admin/ElasticsearchManagement/ElasticsearchManagement.jsx
  13. 12 8
      src/client/js/components/Admin/ElasticsearchManagement/RebuildIndexControls.jsx
  14. 4 4
      src/client/js/components/Admin/ExportArchiveDataPage.jsx
  15. 5 5
      src/client/js/components/Admin/ImportData/GrowiArchive/ImportForm.jsx
  16. 2 2
      src/client/js/components/Navbar/PersonalDropdown.jsx
  17. 1 1
      src/client/js/components/Page.jsx
  18. 7 3
      src/client/js/components/Page/CopyDropdown.jsx
  19. 3 3
      src/client/js/components/PageEditor/MarkdownDrawioUtil.js
  20. 1 1
      src/client/js/components/PageStatusAlert.jsx
  21. 0 1
      src/client/js/hackmd-agent.js
  22. 19 0
      src/client/js/services/AdminSocketIoContainer.js
  23. 16 16
      src/client/js/services/PageContainer.js
  24. 9 5
      src/client/js/services/SocketIoContainer.js
  25. 0 16
      src/client/js/util/interceptor/drawio-interceptor.js
  26. 1 6
      src/client/styles/scss/_layout_kibela.scss
  27. 8 0
      src/client/styles/scss/_me.scss
  28. 12 0
      src/client/styles/scss/_override-bootstrap-variables.scss
  29. 4 4
      src/client/styles/scss/_search.scss
  30. 11 11
      src/client/styles/scss/theme/_apply-colors-light.scss
  31. 8 8
      src/client/styles/scss/theme/_apply-colors.scss
  32. 15 14
      src/client/styles/scss/theme/default.scss
  33. 2 2
      src/server/crowi/express-init.js
  34. 57 29
      src/server/crowi/index.js
  35. 0 11
      src/server/events/search.js
  36. 0 11
      src/server/middlewares/auto-reconnect-to-config-pubsub.js
  37. 11 0
      src/server/middlewares/auto-reconnect-to-s2s-msg-server.js
  38. 33 28
      src/server/models/page.js
  39. 6 3
      src/server/models/vo/s2s-message.js
  40. 0 13
      src/server/routes/admin.js
  41. 2 2
      src/server/routes/apiv3/app-settings.js
  42. 4 4
      src/server/routes/apiv3/export.js
  43. 4 4
      src/server/routes/apiv3/import.js
  44. 1 1
      src/server/routes/apiv3/security-setting.js
  45. 0 5
      src/server/routes/hackmd.js
  46. 3 35
      src/server/routes/page.js
  47. 17 16
      src/server/service/app.js
  48. 14 8
      src/server/service/config-loader.js
  49. 18 16
      src/server/service/config-manager.js
  50. 0 14
      src/server/service/config-pubsub/handlable.js
  51. 0 195
      src/server/service/config-pubsub/nchan.js
  52. 13 13
      src/server/service/customize.js
  53. 13 13
      src/server/service/mail.js
  54. 29 0
      src/server/service/page.js
  55. 13 13
      src/server/service/passport.js
  56. 13 11
      src/server/service/s2s-messaging/base.js
  57. 18 0
      src/server/service/s2s-messaging/handlable.js
  58. 7 4
      src/server/service/s2s-messaging/index.js
  59. 194 0
      src/server/service/s2s-messaging/nchan.js
  60. 1 1
      src/server/service/s2s-messaging/redis.js
  61. 7 7
      src/server/service/search-delegator/elasticsearch.js
  62. 2 4
      src/server/service/search.js
  63. 37 0
      src/server/service/socket-io.js
  64. 110 0
      src/server/service/system-events/sync-page-status.js
  65. 2 0
      src/server/views/layout/layout.html
  66. 2 0
      src/server/views/search.html
  67. 31 0
      src/server/views/widget/headers/drawio.html
  68. 17 12
      src/server/views/widget/headers/mathjax.html
  69. 1 1
      src/server/views/widget/page_list.html
  70. 125 0
      src/test/models/page.test.js
  71. 1 1
      src/test/service/config-manager.test.js
  72. 106 144
      yarn.lock

+ 18 - 3
CHANGES.md

@@ -1,6 +1,10 @@
 # CHANGES
 # CHANGES
 
 
-## v4.1.0-RC
+## v4.1.1-RC
+
+* Fix: "Append params" switch of CopyDropdown does not work when multiple CopyDropdown instance exists
+
+## v4.1.0
 
 
 ### BREAKING CHANGES
 ### BREAKING CHANGES
 
 
@@ -11,21 +15,32 @@ Upgrading Guide: <https://docs.growi.org/en/admin-guide/upgrading/41x.html>
 
 
 ### Updates
 ### Updates
 
 
-* Feature: Config synchronization for multiple GROWI Apps
+* Feature: Server settings synchronization for multiple GROWI Apps
+* Feature: Page status alert synchronization for multiple GROWI Apps
 * Feature: Smooth scroll for anchor links
 * Feature: Smooth scroll for anchor links
 * Feature: Mirror Mode with [Konami Code](https://en.wikipedia.org/wiki/Konami_Code)
 * Feature: Mirror Mode with [Konami Code](https://en.wikipedia.org/wiki/Konami_Code)
 * Improvement: Determine whether the "In Use" badge is displayed or not by attachment ID
 * Improvement: Determine whether the "In Use" badge is displayed or not by attachment ID
+* Improvement: draw.io under NO_CDN environment
+* Fix: Deleting/renaming with recursive option affects pages that are inaccessible to active users
+* Fix: DrawioModal cuts without beginning/ending line
 * Fix: New settings of SMTP and AWS SES are not reflected when server is running
 * Fix: New settings of SMTP and AWS SES are not reflected when server is running
+* Fix: Sidebar layout broken when using Kibela layout
 * Support: Support Node.js v14
 * Support: Support Node.js v14
+* Support: Update libs
+    * mathjax
 
 
+## v4.0.11
 
 
+* Fix: Fab on search result page does not displayed
+* Fix: Adjust margin/padding for search result page
+* Fix: PageAlert broken
+    * Introduced by v4.0.9
 
 
 ## v4.0.10
 ## v4.0.10
 
 
 * Improvement: Adjust ToC height
 * Improvement: Adjust ToC height
 * Fix: Fail to rename/delete a page set as "Anyone with the link"
 * Fix: Fail to rename/delete a page set as "Anyone with the link"
 
 
-
 ## v4.0.9
 ## v4.0.9
 
 
 * Feature: Detailed configurations for OpenID Connect
 * Feature: Detailed configurations for OpenID Connect

+ 4 - 4
README.md

@@ -111,11 +111,11 @@ See [confirmed versions](https://docs.growi.org/en/dev/startup/dev-env.html#set-
 
 
 |command|desc|
 |command|desc|
 |--|--|
 |--|--|
-|`npm run build:prod`|Build the client|
-|`npm run server:prod`|Launch the server|
-|`npm start`|Invoke `npm run build:prod` and `npm run server:prod`|
+|`yarn run build:prod`|Build the client|
+|`yarn run server:prod`|Launch the server|
+|`yarn start`|Invoke `yarn run build:prod` and `yarn run server:prod`|
 
 
-For more info, see [GROWI Docs: List of npm Commands](https://docs.growi.org/en/dev/startup/launch.html#list-of-npm-commands).
+For more info, see [GROWI Docs: List of npm Commands](https://docs.growi.org/en/dev/startup-v2/launch.html#list-of-npm-commands).
 
 
 
 
 Documentation
 Documentation

+ 2 - 2
bin/github-actions/update-readme.sh

@@ -2,5 +2,5 @@
 
 
 cd docker
 cd docker
 
 
-sed -i -e "s/^\([*] \[\`\)[^\`]\+\(\`, \`4\.0\`, .\+\]\)\(.\+\/blob\/v\).\+\(\/docker\/Dockerfile.\+\)$/\1${RELEASE_VERSION}\2\3${RELEASE_VERSION}\4/" README.md
-sed -i -e "s/^\([*] \[\`\)[^\`]\+\(\`, \`4\.0-nocdn\`, .\+\]\)\(.\+\/blob\/v\).\+\(\/docker\/Dockerfile.\+\)$/\1${RELEASE_VERSION}-nocdn\2\3${RELEASE_VERSION}\4/" README.md
+sed -i -e "s/^\([*] \[\`\)[^\`]\+\(\`, \`4\.1\`, .\+\]\)\(.\+\/blob\/v\).\+\(\/docker\/Dockerfile.\+\)$/\1${RELEASE_VERSION}\2\3${RELEASE_VERSION}\4/" README.md
+sed -i -e "s/^\([*] \[\`\)[^\`]\+\(\`, \`4\.1-nocdn\`, .\+\]\)\(.\+\/blob\/v\).\+\(\/docker\/Dockerfile.\+\)$/\1${RELEASE_VERSION}-nocdn\2\3${RELEASE_VERSION}\4/" README.md

+ 1 - 1
config/env.dev.js

@@ -11,7 +11,7 @@ module.exports = {
   HACKMD_URI: 'http://localhost:3010',
   HACKMD_URI: 'http://localhost:3010',
   HACKMD_URI_FOR_SERVER: 'http://hackmd:3000',
   HACKMD_URI_FOR_SERVER: 'http://hackmd:3000',
   // DRAWIO_URI: 'http://localhost:8080/?offline=1&https=0',
   // DRAWIO_URI: 'http://localhost:8080/?offline=1&https=0',
-  // CONFIG_PUBSUB_SERVER_TYPE: 'nchan',
+  // S2SMSG_PUBSUB_SERVER_TYPE: 'nchan',
   PLUGIN_NAMES_TOBE_LOADED: [
   PLUGIN_NAMES_TOBE_LOADED: [
     // 'growi-plugin-lsx',
     // 'growi-plugin-lsx',
     // 'growi-plugin-pukiwiki-like-linker',
     // 'growi-plugin-pukiwiki-like-linker',

+ 1 - 1
config/logger/config.dev.js

@@ -16,7 +16,7 @@ module.exports = {
   'growi:routes:login-passport': 'debug',
   'growi:routes:login-passport': 'debug',
   'growi:middleware:safe-redirect': 'debug',
   'growi:middleware:safe-redirect': 'debug',
   'growi:service:PassportService': 'debug',
   'growi:service:PassportService': 'debug',
-  'growi:service:config-pubsub:*': 'debug',
+  'growi:service:s2s-messaging:*': 'debug',
   // 'growi:service:ConfigManager': 'debug',
   // 'growi:service:ConfigManager': 'debug',
   // 'growi:service:mail': 'debug',
   // 'growi:service:mail': 'debug',
   'growi:lib:search': 'debug',
   'growi:lib:search': 'debug',

+ 4 - 2
docker/README.md

@@ -10,8 +10,10 @@ GROWI Official docker image
 Supported tags and respective Dockerfile links
 Supported tags and respective Dockerfile links
 ------------------------------------------------
 ------------------------------------------------
 
 
-* [`4.0.0`, `4.0`, `4`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v4.0.0/docker/Dockerfile)
-* [`4.0.0-nocdn`, `4.0-nocdn`, `4-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.0.0/docker/Dockerfile)
+* [`4.1.0`, `4.1`, `4`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v4.1.0/docker/Dockerfile)
+* [`4.1.0-nocdn`, `4.1-nocdn`, `4-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.1.0/docker/Dockerfile)
+* [`4.0.11`, `4.0`(Dockerfile)](https://github.com/weseek/growi/blob/v4.0.11/docker/Dockerfile)
+* [`4.0.11-nocdn`, `4.0-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.0.11/docker/Dockerfile)
 * [`3.8.0`, `3.8`, `3` (Dockerfile)](https://github.com/weseek/growi/blob/v3.8.0/docker/Dockerfile)
 * [`3.8.0`, `3.8`, `3` (Dockerfile)](https://github.com/weseek/growi/blob/v3.8.0/docker/Dockerfile)
 * [`3.8.0-nocdn`, `3.8-nocdn`, `3-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v3.8.0/docker/Dockerfile)
 * [`3.8.0-nocdn`, `3.8-nocdn`, `3-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v3.8.0/docker/Dockerfile)
 
 

+ 6 - 5
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "growi",
   "name": "growi",
-  "version": "4.1.0-RC",
+  "version": "4.1.1-RC",
   "description": "Team collaboration software using markdown",
   "description": "Team collaboration software using markdown",
   "tags": [
   "tags": [
     "wiki",
     "wiki",
@@ -137,10 +137,11 @@
     "passport-twitter": "^1.0.4",
     "passport-twitter": "^1.0.4",
     "react-card-flip": "^1.0.10",
     "react-card-flip": "^1.0.10",
     "react-image-crop": "^8.3.0",
     "react-image-crop": "^8.3.0",
+    "reconnecting-websocket": "^4.4.0",
     "redis": "^3.0.2",
     "redis": "^3.0.2",
     "rimraf": "^3.0.0",
     "rimraf": "^3.0.0",
     "slack-node": "^0.1.8",
     "slack-node": "^0.1.8",
-    "socket.io": "^2.0.3",
+    "socket.io": "^2.3.0",
     "stream-to-promise": "^2.2.0",
     "stream-to-promise": "^2.2.0",
     "string-width": "^4.1.0",
     "string-width": "^4.1.0",
     "swig-templates": "^2.0.2",
     "swig-templates": "^2.0.2",
@@ -148,7 +149,7 @@
     "unzipper": "^0.10.5",
     "unzipper": "^0.10.5",
     "url-join": "^4.0.0",
     "url-join": "^4.0.0",
     "validator": "^12.0.0",
     "validator": "^12.0.0",
-    "websocket": "^1.0.31",
+    "ws": "^7.3.1",
     "xss": "^1.0.6"
     "xss": "^1.0.6"
   },
   },
   "devDependencies": {
   "devDependencies": {
@@ -203,7 +204,7 @@
     "lodash-webpack-plugin": "^0.11.5",
     "lodash-webpack-plugin": "^0.11.5",
     "markdown-it": "^10.0.0",
     "markdown-it": "^10.0.0",
     "markdown-it-blockdiag": "^1.1.1",
     "markdown-it-blockdiag": "^1.1.1",
-    "markdown-it-drawio-viewer": "^1.2.0",
+    "markdown-it-drawio-viewer": "^1.3.0",
     "markdown-it-emoji": "^1.4.0",
     "markdown-it-emoji": "^1.4.0",
     "markdown-it-footnote": "^3.0.1",
     "markdown-it-footnote": "^3.0.1",
     "markdown-it-mathjax": "^2.0.0",
     "markdown-it-mathjax": "^2.0.0",
@@ -240,7 +241,7 @@
     "rs-i18n": "^0.0.9",
     "rs-i18n": "^0.0.9",
     "sass-loader": "^8.0.0",
     "sass-loader": "^8.0.0",
     "simple-load-script": "^1.0.2",
     "simple-load-script": "^1.0.2",
-    "socket.io-client": "^2.0.3",
+    "socket.io-client": "^2.3.0",
     "sticky-events": "^3.1.3",
     "sticky-events": "^3.1.3",
     "style-loader": "^1.0.0",
     "style-loader": "^1.0.0",
     "styled-components": "^5.0.1",
     "styled-components": "^5.0.1",

+ 1 - 1
resource/cdn-manifests.js

@@ -39,7 +39,7 @@ module.exports = {
     },
     },
     {
     {
       name: 'mathjax',
       name: 'mathjax',
-      url: 'https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.2/MathJax.js',
+      url: 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js',
       args: {
       args: {
         async: true,
         async: true,
         integrity: '',
         integrity: '',

+ 3 - 2
src/client/js/admin.jsx

@@ -25,6 +25,7 @@ import AdminNavigation from './components/Admin/Common/AdminNavigation';
 
 
 import NavigationContainer from './services/NavigationContainer';
 import NavigationContainer from './services/NavigationContainer';
 
 
+import AdminSocketIoContainer from './services/AdminSocketIoContainer';
 import AdminHomeContainer from './services/AdminHomeContainer';
 import AdminHomeContainer from './services/AdminHomeContainer';
 import AdminCustomizeContainer from './services/AdminCustomizeContainer';
 import AdminCustomizeContainer from './services/AdminCustomizeContainer';
 import AdminUserGroupDetailContainer from './services/AdminUserGroupDetailContainer';
 import AdminUserGroupDetailContainer from './services/AdminUserGroupDetailContainer';
@@ -50,11 +51,11 @@ const logger = loggerFactory('growi:admin');
 appContainer.initContents();
 appContainer.initContents();
 
 
 const { i18n } = appContainer;
 const { i18n } = appContainer;
-const websocketContainer = appContainer.getContainer('WebsocketContainer');
 
 
 // create unstated container instance
 // create unstated container instance
 const navigationContainer = new NavigationContainer(appContainer);
 const navigationContainer = new NavigationContainer(appContainer);
 const adminAppContainer = new AdminAppContainer(appContainer);
 const adminAppContainer = new AdminAppContainer(appContainer);
+const adminSocketIoContainer = new AdminSocketIoContainer(appContainer);
 const adminHomeContainer = new AdminHomeContainer(appContainer);
 const adminHomeContainer = new AdminHomeContainer(appContainer);
 const adminCustomizeContainer = new AdminCustomizeContainer(appContainer);
 const adminCustomizeContainer = new AdminCustomizeContainer(appContainer);
 const adminUsersContainer = new AdminUsersContainer(appContainer);
 const adminUsersContainer = new AdminUsersContainer(appContainer);
@@ -64,9 +65,9 @@ const adminMarkDownContainer = new AdminMarkDownContainer(appContainer);
 const adminUserGroupDetailContainer = new AdminUserGroupDetailContainer(appContainer);
 const adminUserGroupDetailContainer = new AdminUserGroupDetailContainer(appContainer);
 const injectableContainers = [
 const injectableContainers = [
   appContainer,
   appContainer,
-  websocketContainer,
   navigationContainer,
   navigationContainer,
   adminAppContainer,
   adminAppContainer,
+  adminSocketIoContainer,
   adminHomeContainer,
   adminHomeContainer,
   adminCustomizeContainer,
   adminCustomizeContainer,
   adminUsersContainer,
   adminUsersContainer,

+ 2 - 2
src/client/js/app.jsx

@@ -45,7 +45,7 @@ const logger = loggerFactory('growi:cli:app');
 appContainer.initContents();
 appContainer.initContents();
 
 
 const { i18n } = appContainer;
 const { i18n } = appContainer;
-const websocketContainer = appContainer.getContainer('WebsocketContainer');
+const socketIoContainer = appContainer.getContainer('SocketIoContainer');
 
 
 // create unstated container instance
 // create unstated container instance
 const navigationContainer = new NavigationContainer(appContainer);
 const navigationContainer = new NavigationContainer(appContainer);
@@ -55,7 +55,7 @@ const editorContainer = new EditorContainer(appContainer, defaultEditorOptions,
 const tagContainer = new TagContainer(appContainer);
 const tagContainer = new TagContainer(appContainer);
 const personalContainer = new PersonalContainer(appContainer);
 const personalContainer = new PersonalContainer(appContainer);
 const injectableContainers = [
 const injectableContainers = [
-  appContainer, websocketContainer, navigationContainer, pageContainer, commentContainer, editorContainer, tagContainer, personalContainer,
+  appContainer, socketIoContainer, navigationContainer, pageContainer, commentContainer, editorContainer, tagContainer, personalContainer,
 ];
 ];
 
 
 logger.info('unstated containers have been initialized');
 logger.info('unstated containers have been initialized');

+ 2 - 2
src/client/js/base.jsx

@@ -10,7 +10,7 @@ import HotkeysManager from './components/Hotkeys/HotkeysManager';
 import Fab from './components/Fab';
 import Fab from './components/Fab';
 
 
 import AppContainer from './services/AppContainer';
 import AppContainer from './services/AppContainer';
-import WebsocketContainer from './services/WebsocketContainer';
+import SocketIoContainer from './services/SocketIoContainer';
 import PageCreateModal from './components/PageCreateModal';
 import PageCreateModal from './components/PageCreateModal';
 
 
 const logger = loggerFactory('growi:cli:app');
 const logger = loggerFactory('growi:cli:app');
@@ -26,7 +26,7 @@ window.xss = xss;
 // create unstated container instance
 // create unstated container instance
 const appContainer = new AppContainer();
 const appContainer = new AppContainer();
 // eslint-disable-next-line no-unused-vars
 // eslint-disable-next-line no-unused-vars
-const websocketContainer = new WebsocketContainer(appContainer);
+const socketIoContainer = new SocketIoContainer(appContainer);
 
 
 appContainer.initApp();
 appContainer.initApp();
 
 

+ 7 - 7
src/client/js/components/Admin/ElasticsearchManagement/ElasticsearchManagement.jsx

@@ -4,7 +4,7 @@ import { withTranslation } from 'react-i18next';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import AppContainer from '../../../services/AppContainer';
 import AppContainer from '../../../services/AppContainer';
-import WebsocketContainer from '../../../services/WebsocketContainer';
+import AdminSocketIoContainer from '../../../services/AdminSocketIoContainer';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 
 
 import StatusTable from './StatusTable';
 import StatusTable from './StatusTable';
@@ -45,22 +45,22 @@ class ElasticsearchManagement extends React.Component {
   }
   }
 
 
   initWebSockets() {
   initWebSockets() {
-    const socket = this.props.websocketContainer.getWebSocket();
+    const socket = this.props.adminSocketIoContainer.getSocket();
 
 
-    socket.on('admin:addPageProgress', (data) => {
+    socket.on('addPageProgress', (data) => {
       this.setState({
       this.setState({
         isRebuildingProcessing: true,
         isRebuildingProcessing: true,
       });
       });
     });
     });
 
 
-    socket.on('admin:finishAddPage', (data) => {
+    socket.on('finishAddPage', (data) => {
       this.setState({
       this.setState({
         isRebuildingProcessing: false,
         isRebuildingProcessing: false,
         isRebuildingCompleted: true,
         isRebuildingCompleted: true,
       });
       });
     });
     });
 
 
-    socket.on('admin:rebuildingFailed', (data) => {
+    socket.on('rebuildingFailed', (data) => {
       toastError(new Error(data.error), 'Rebuilding Index has failed.');
       toastError(new Error(data.error), 'Rebuilding Index has failed.');
     });
     });
   }
   }
@@ -224,12 +224,12 @@ class ElasticsearchManagement extends React.Component {
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
-const ElasticsearchManagementWrapper = withUnstatedContainers(ElasticsearchManagement, [AppContainer, WebsocketContainer]);
+const ElasticsearchManagementWrapper = withUnstatedContainers(ElasticsearchManagement, [AppContainer, AdminSocketIoContainer]);
 
 
 ElasticsearchManagement.propTypes = {
 ElasticsearchManagement.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  websocketContainer: PropTypes.instanceOf(WebsocketContainer).isRequired,
+  adminSocketIoContainer: PropTypes.instanceOf(AdminSocketIoContainer).isRequired,
 };
 };
 
 
 export default withTranslation()(ElasticsearchManagementWrapper);
 export default withTranslation()(ElasticsearchManagementWrapper);

+ 12 - 8
src/client/js/components/Admin/ElasticsearchManagement/RebuildIndexControls.jsx

@@ -4,7 +4,7 @@ import { withTranslation } from 'react-i18next';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import AppContainer from '../../../services/AppContainer';
 import AppContainer from '../../../services/AppContainer';
-import WebsocketContainer from '../../../services/WebsocketContainer';
+import AdminSocketIoContainer from '../../../services/AdminSocketIoContainer';
 
 
 import ProgressBar from '../Common/ProgressBar';
 import ProgressBar from '../Common/ProgressBar';
 
 
@@ -25,17 +25,21 @@ class RebuildIndexControls extends React.Component {
   }
   }
 
 
   initWebSockets() {
   initWebSockets() {
-    const socket = this.props.websocketContainer.getWebSocket();
+    const socket = this.props.adminSocketIoContainer.getSocket();
 
 
-    socket.on('admin:addPageProgress', (data) => {
+    socket.on('addPageProgress', (data) => {
       this.setState({
       this.setState({
-        ...data,
+        total: data.totalCount,
+        current: data.count,
+        skip: data.skipped,
       });
       });
     });
     });
 
 
-    socket.on('admin:finishAddPage', (data) => {
+    socket.on('finishAddPage', (data) => {
       this.setState({
       this.setState({
-        ...data,
+        total: data.totalCount,
+        current: data.count,
+        skip: data.skipped,
       });
       });
     });
     });
 
 
@@ -97,12 +101,12 @@ class RebuildIndexControls extends React.Component {
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
-const RebuildIndexControlsWrapper = withUnstatedContainers(RebuildIndexControls, [AppContainer, WebsocketContainer]);
+const RebuildIndexControlsWrapper = withUnstatedContainers(RebuildIndexControls, [AppContainer, AdminSocketIoContainer]);
 
 
 RebuildIndexControls.propTypes = {
 RebuildIndexControls.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  websocketContainer: PropTypes.instanceOf(WebsocketContainer).isRequired,
+  adminSocketIoContainer: PropTypes.instanceOf(AdminSocketIoContainer).isRequired,
 
 
   isRebuildingProcessing: PropTypes.bool.isRequired,
   isRebuildingProcessing: PropTypes.bool.isRequired,
   isRebuildingCompleted: PropTypes.bool.isRequired,
   isRebuildingCompleted: PropTypes.bool.isRequired,

+ 4 - 4
src/client/js/components/Admin/ExportArchiveDataPage.jsx

@@ -8,7 +8,7 @@ import { withUnstatedContainers } from '../UnstatedUtils';
 // import { toastSuccess, toastError } from '../../../util/apiNotification';
 // import { toastSuccess, toastError } from '../../../util/apiNotification';
 
 
 import AppContainer from '../../services/AppContainer';
 import AppContainer from '../../services/AppContainer';
-import WebsocketContainer from '../../services/WebsocketContainer';
+import AdminSocketIoContainer from '../../services/AdminSocketIoContainer';
 
 
 import ProgressBar from './Common/ProgressBar';
 import ProgressBar from './Common/ProgressBar';
 
 
@@ -67,7 +67,7 @@ class ExportArchiveDataPage extends React.Component {
   }
   }
 
 
   setupWebsocketEventHandler() {
   setupWebsocketEventHandler() {
-    const socket = this.props.websocketContainer.getWebSocket();
+    const socket = this.props.adminSocketIoContainer.getSocket();
 
 
     // websocket event
     // websocket event
     socket.on('admin:onProgressForExport', ({ currentCount, totalCount, progressList }) => {
     socket.on('admin:onProgressForExport', ({ currentCount, totalCount, progressList }) => {
@@ -248,12 +248,12 @@ class ExportArchiveDataPage extends React.Component {
 ExportArchiveDataPage.propTypes = {
 ExportArchiveDataPage.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  websocketContainer: PropTypes.instanceOf(WebsocketContainer).isRequired,
+  adminSocketIoContainer: PropTypes.instanceOf(AdminSocketIoContainer).isRequired,
 };
 };
 
 
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
-const ExportArchiveDataPageWrapper = withUnstatedContainers(ExportArchiveDataPage, [AppContainer, WebsocketContainer]);
+const ExportArchiveDataPageWrapper = withUnstatedContainers(ExportArchiveDataPage, [AppContainer, AdminSocketIoContainer]);
 
 
 export default withTranslation()(ExportArchiveDataPageWrapper);
 export default withTranslation()(ExportArchiveDataPageWrapper);

+ 5 - 5
src/client/js/components/Admin/ImportData/GrowiArchive/ImportForm.jsx

@@ -8,7 +8,7 @@ import ImportOptionForRevisions from '@commons/models/admin/import-option-for-re
 
 
 import { withUnstatedContainers } from '../../../UnstatedUtils';
 import { withUnstatedContainers } from '../../../UnstatedUtils';
 import AppContainer from '../../../../services/AppContainer';
 import AppContainer from '../../../../services/AppContainer';
-import WebsocketContainer from '../../../../services/WebsocketContainer';
+import AdminSocketIoContainer from '../../../../services/AdminSocketIoContainer';
 import { toastSuccess, toastError } from '../../../../util/apiNotification';
 import { toastSuccess, toastError } from '../../../../util/apiNotification';
 
 
 
 
@@ -102,7 +102,7 @@ class ImportForm extends React.Component {
   }
   }
 
 
   setupWebsocketEventHandler() {
   setupWebsocketEventHandler() {
-    const socket = this.props.websocketContainer.getWebSocket();
+    const socket = this.props.adminSocketIoContainer.getSocket();
 
 
     // websocket event
     // websocket event
     // eslint-disable-next-line object-curly-newline
     // eslint-disable-next-line object-curly-newline
@@ -142,7 +142,7 @@ class ImportForm extends React.Component {
   }
   }
 
 
   teardownWebsocketEventHandler() {
   teardownWebsocketEventHandler() {
-    const socket = this.props.websocketContainer.getWebSocket();
+    const socket = this.props.adminSocketIoContainer.getSocket();
 
 
     socket.removeAllListeners('admin:onProgressForImport');
     socket.removeAllListeners('admin:onProgressForImport');
     socket.removeAllListeners('admin:onTerminateForImport');
     socket.removeAllListeners('admin:onTerminateForImport');
@@ -493,7 +493,7 @@ class ImportForm extends React.Component {
 ImportForm.propTypes = {
 ImportForm.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  websocketContainer: PropTypes.instanceOf(WebsocketContainer).isRequired,
+  adminSocketIoContainer: PropTypes.instanceOf(AdminSocketIoContainer).isRequired,
 
 
   fileName: PropTypes.string,
   fileName: PropTypes.string,
   innerFileStats: PropTypes.arrayOf(PropTypes.object).isRequired,
   innerFileStats: PropTypes.arrayOf(PropTypes.object).isRequired,
@@ -504,6 +504,6 @@ ImportForm.propTypes = {
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
-const ImportFormWrapper = withUnstatedContainers(ImportForm, [AppContainer, WebsocketContainer]);
+const ImportFormWrapper = withUnstatedContainers(ImportForm, [AppContainer, AdminSocketIoContainer]);
 
 
 export default withTranslation()(ImportFormWrapper);
 export default withTranslation()(ImportFormWrapper);

+ 2 - 2
src/client/js/components/Navbar/PersonalDropdown.jsx

@@ -118,10 +118,10 @@ const PersonalDropdown = (props) => {
           </div>
           </div>
 
 
           <div className="btn-group btn-block mt-2" role="group">
           <div className="btn-group btn-block mt-2" role="group">
-            <a className="btn btn-sm btn-outline-secondary" href={`/user/${user.username}`}>
+            <a className="btn btn-sm btn-outline-secondary col" href={`/user/${user.username}`}>
               <i className="icon-fw icon-home"></i>{ t('personal_dropdown.home') }
               <i className="icon-fw icon-home"></i>{ t('personal_dropdown.home') }
             </a>
             </a>
-            <a className="btn btn-sm btn-outline-secondary" href="/me">
+            <a className="btn btn-sm btn-outline-secondary col" href="/me">
               <i className="icon-fw icon-wrench"></i>{ t('personal_dropdown.settings') }
               <i className="icon-fw icon-wrench"></i>{ t('personal_dropdown.settings') }
             </a>
             </a>
           </div>
           </div>

+ 1 - 1
src/client/js/components/Page.jsx

@@ -61,7 +61,7 @@ class Page extends React.Component {
    */
    */
   launchDrawioModal(beginLineNumber, endLineNumber) {
   launchDrawioModal(beginLineNumber, endLineNumber) {
     const markdown = this.props.pageContainer.state.markdown;
     const markdown = this.props.pageContainer.state.markdown;
-    const drawioMarkdownArray = markdown.split(/\r\n|\r|\n/).slice(beginLineNumber, endLineNumber);
+    const drawioMarkdownArray = markdown.split(/\r\n|\r|\n/).slice(beginLineNumber - 1, endLineNumber);
     const drawioData = drawioMarkdownArray.slice(1, drawioMarkdownArray.length - 1).join('\n').trim();
     const drawioData = drawioMarkdownArray.slice(1, drawioMarkdownArray.length - 1).join('\n').trim();
     this.setState({ currentTargetDrawioArea: { beginLineNumber, endLineNumber } });
     this.setState({ currentTargetDrawioArea: { beginLineNumber, endLineNumber } });
     this.drawioModal.current.show(drawioData);
     this.drawioModal.current.show(drawioData);

+ 7 - 3
src/client/js/components/Page/CopyDropdown.jsx

@@ -21,6 +21,8 @@ class CopyDropdown extends React.Component {
       isParamsAppended: true,
       isParamsAppended: true,
     };
     };
 
 
+    this.id = (Math.random() * 1000).toString();
+
     this.toggle = this.toggle.bind(this);
     this.toggle = this.toggle.bind(this);
     this.showToolTip = this.showToolTip.bind(this);
     this.showToolTip = this.showToolTip.bind(this);
     this.generatePagePathWithParams = this.generatePagePathWithParams.bind(this);
     this.generatePagePathWithParams = this.generatePagePathWithParams.bind(this);
@@ -97,7 +99,9 @@ class CopyDropdown extends React.Component {
     const pagePathUrl = this.generatePagePathUrl();
     const pagePathUrl = this.generatePagePathUrl();
     const permalink = this.generatePermalink();
     const permalink = this.generatePermalink();
 
 
-    const { DropdownItemContents } = this;
+    const { id, DropdownItemContents } = this;
+
+    const customSwitchForParamsId = `customSwitchForParams_${id}`;
 
 
     return (
     return (
       <>
       <>
@@ -120,12 +124,12 @@ class CopyDropdown extends React.Component {
               <div className="px-3 custom-control custom-switch custom-switch-sm">
               <div className="px-3 custom-control custom-switch custom-switch-sm">
                 <input
                 <input
                   type="checkbox"
                   type="checkbox"
-                  id="customSwitchForParams"
+                  id={customSwitchForParamsId}
                   className="custom-control-input"
                   className="custom-control-input"
                   checked={isParamsAppended}
                   checked={isParamsAppended}
                   onChange={e => this.setState({ isParamsAppended: !isParamsAppended })}
                   onChange={e => this.setState({ isParamsAppended: !isParamsAppended })}
                 />
                 />
-                <label className="custom-control-label small" htmlFor="customSwitchForParams">Append params</label>
+                <label className="custom-control-label small" htmlFor={customSwitchForParamsId}>Append params</label>
               </div>
               </div>
             </div>
             </div>
 
 

+ 3 - 3
src/client/js/components/PageEditor/MarkdownDrawioUtil.js

@@ -139,17 +139,17 @@ class MarkdownDrawioUtil {
    */
    */
   replaceDrawioInMarkdown(drawioData, markdown, beginLineNumber, endLineNumber) {
   replaceDrawioInMarkdown(drawioData, markdown, beginLineNumber, endLineNumber) {
     const splitMarkdown = markdown.split(/\r\n|\r|\n/);
     const splitMarkdown = markdown.split(/\r\n|\r|\n/);
-    const markdownBeforeDrawio = splitMarkdown.slice(0, beginLineNumber);
+    const markdownBeforeDrawio = splitMarkdown.slice(0, beginLineNumber - 1);
     const markdownAfterDrawio = splitMarkdown.slice(endLineNumber);
     const markdownAfterDrawio = splitMarkdown.slice(endLineNumber);
 
 
     let newMarkdown = '';
     let newMarkdown = '';
     if (markdownBeforeDrawio.length > 0) {
     if (markdownBeforeDrawio.length > 0) {
       newMarkdown += `${markdownBeforeDrawio.join('\n')}\n`;
       newMarkdown += `${markdownBeforeDrawio.join('\n')}\n`;
-      newMarkdown += '::: drawio\n';
     }
     }
+    newMarkdown += '::: drawio\n';
     newMarkdown += drawioData;
     newMarkdown += drawioData;
+    newMarkdown += '\n:::';
     if (markdownAfterDrawio.length > 0) {
     if (markdownAfterDrawio.length > 0) {
-      newMarkdown += '\n:::';
       newMarkdown += `\n${markdownAfterDrawio.join('\n')}`;
       newMarkdown += `\n${markdownAfterDrawio.join('\n')}`;
     }
     }
 
 

+ 1 - 1
src/client/js/components/PageStatusAlert.jsx

@@ -94,7 +94,7 @@ class PageStatusAlert extends React.Component {
 
 
     // when remote revision is newer than both
     // when remote revision is newer than both
     if (isHackmdDocumentOutdated && isRevisionOutdated) {
     if (isHackmdDocumentOutdated && isRevisionOutdated) {
-      getContentsFunc = this.getContentsFunc;
+      getContentsFunc = this.getContentsForUpdatedAlert;
     }
     }
     // when someone editing with HackMD
     // when someone editing with HackMD
     else if (isHackmdDraftUpdatingInRealtime) {
     else if (isHackmdDraftUpdatingInRealtime) {

+ 0 - 1
src/client/js/hackmd-agent.js

@@ -143,7 +143,6 @@ function connectToParentWithPenpal() {
   console.log('[HackMD] Loading GROWI agent for HackMD...');
   console.log('[HackMD] Loading GROWI agent for HackMD...');
 
 
   window.addEventListener('load', (event) => {
   window.addEventListener('load', (event) => {
-    console.log('loaded');
     addEventListenersToCodemirror();
     addEventListenersToCodemirror();
   });
   });
 
 

+ 19 - 0
src/client/js/services/AdminSocketIoContainer.js

@@ -0,0 +1,19 @@
+import SocketIoContainer from './SocketIoContainer';
+
+/**
+ * A subclass of SocketIoContainer for /admin namespace
+ */
+export default class AdminSocketIoContainer extends SocketIoContainer {
+
+  constructor(appContainer) {
+    super(appContainer, '/admin');
+  }
+
+  /**
+   * Workaround for the mangling in production build to break constructor.name
+   */
+  static getClassName() {
+    return 'AdminSocketIoContainer';
+  }
+
+}

+ 16 - 16
src/client/js/services/PageContainer.js

@@ -314,11 +314,11 @@ export default class PageContainer extends Container {
   }
   }
 
 
   async createPage(pagePath, markdown, tmpParams) {
   async createPage(pagePath, markdown, tmpParams) {
-    const websocketContainer = this.appContainer.getContainer('WebsocketContainer');
+    const socketIoContainer = this.appContainer.getContainer('SocketIoContainer');
 
 
     // clone
     // clone
     const params = Object.assign(tmpParams, {
     const params = Object.assign(tmpParams, {
-      socketClientId: websocketContainer.getSocketClientId(),
+      socketClientId: socketIoContainer.getSocketClientId(),
       path: pagePath,
       path: pagePath,
       body: markdown,
       body: markdown,
     });
     });
@@ -331,11 +331,11 @@ export default class PageContainer extends Container {
   }
   }
 
 
   async updatePage(pageId, revisionId, markdown, tmpParams) {
   async updatePage(pageId, revisionId, markdown, tmpParams) {
-    const websocketContainer = this.appContainer.getContainer('WebsocketContainer');
+    const socketIoContainer = this.appContainer.getContainer('SocketIoContainer');
 
 
     // clone
     // clone
     const params = Object.assign(tmpParams, {
     const params = Object.assign(tmpParams, {
-      socketClientId: websocketContainer.getSocketClientId(),
+      socketClientId: socketIoContainer.getSocketClientId(),
       page_id: pageId,
       page_id: pageId,
       revision_id: revisionId,
       revision_id: revisionId,
       body: markdown,
       body: markdown,
@@ -349,7 +349,7 @@ export default class PageContainer extends Container {
   }
   }
 
 
   deletePage(isRecursively, isCompletely) {
   deletePage(isRecursively, isCompletely) {
-    const websocketContainer = this.appContainer.getContainer('WebsocketContainer');
+    const socketIoContainer = this.appContainer.getContainer('SocketIoContainer');
 
 
     // control flag
     // control flag
     const completely = isCompletely ? true : null;
     const completely = isCompletely ? true : null;
@@ -360,13 +360,13 @@ export default class PageContainer extends Container {
       completely,
       completely,
       page_id: this.state.pageId,
       page_id: this.state.pageId,
       revision_id: this.state.revisionId,
       revision_id: this.state.revisionId,
-      socketClientId: websocketContainer.getSocketClientId(),
+      socketClientId: socketIoContainer.getSocketClientId(),
     });
     });
 
 
   }
   }
 
 
   revertRemove(isRecursively) {
   revertRemove(isRecursively) {
-    const websocketContainer = this.appContainer.getContainer('WebsocketContainer');
+    const socketIoContainer = this.appContainer.getContainer('SocketIoContainer');
 
 
     // control flag
     // control flag
     const recursively = isRecursively ? true : null;
     const recursively = isRecursively ? true : null;
@@ -374,12 +374,12 @@ export default class PageContainer extends Container {
     return this.appContainer.apiPost('/pages.revertRemove', {
     return this.appContainer.apiPost('/pages.revertRemove', {
       recursively,
       recursively,
       page_id: this.state.pageId,
       page_id: this.state.pageId,
-      socketClientId: websocketContainer.getSocketClientId(),
+      socketClientId: socketIoContainer.getSocketClientId(),
     });
     });
   }
   }
 
 
   rename(pageNameInput, isRenameRecursively, isRenameRedirect, isRenameMetadata) {
   rename(pageNameInput, isRenameRecursively, isRenameRedirect, isRenameMetadata) {
-    const websocketContainer = this.appContainer.getContainer('WebsocketContainer');
+    const socketIoContainer = this.appContainer.getContainer('SocketIoContainer');
     const isRecursively = isRenameRecursively ? true : null;
     const isRecursively = isRenameRecursively ? true : null;
     const isRedirect = isRenameRedirect ? true : null;
     const isRedirect = isRenameRedirect ? true : null;
     const isRemain = isRenameMetadata ? true : null;
     const isRemain = isRenameMetadata ? true : null;
@@ -391,7 +391,7 @@ export default class PageContainer extends Container {
       new_path: pageNameInput,
       new_path: pageNameInput,
       create_redirect: isRedirect,
       create_redirect: isRedirect,
       remain_metadata: isRemain,
       remain_metadata: isRemain,
-      socketClientId: websocketContainer.getSocketClientId(),
+      socketClientId: socketIoContainer.getSocketClientId(),
     });
     });
   }
   }
 
 
@@ -420,12 +420,12 @@ export default class PageContainer extends Container {
 
 
   addWebSocketEventHandlers() {
   addWebSocketEventHandlers() {
     const pageContainer = this;
     const pageContainer = this;
-    const websocketContainer = this.appContainer.getContainer('WebsocketContainer');
-    const socket = websocketContainer.getWebSocket();
+    const socketIoContainer = this.appContainer.getContainer('SocketIoContainer');
+    const socket = socketIoContainer.getSocket();
 
 
     socket.on('page:create', (data) => {
     socket.on('page:create', (data) => {
       // skip if triggered myself
       // skip if triggered myself
-      if (data.socketClientId != null && data.socketClientId === websocketContainer.getSocketClientId()) {
+      if (data.socketClientId != null && data.socketClientId === socketIoContainer.getSocketClientId()) {
         return;
         return;
       }
       }
 
 
@@ -439,7 +439,7 @@ export default class PageContainer extends Container {
 
 
     socket.on('page:update', (data) => {
     socket.on('page:update', (data) => {
       // skip if triggered myself
       // skip if triggered myself
-      if (data.socketClientId != null && data.socketClientId === websocketContainer.getSocketClientId()) {
+      if (data.socketClientId != null && data.socketClientId === socketIoContainer.getSocketClientId()) {
         return;
         return;
       }
       }
 
 
@@ -460,7 +460,7 @@ export default class PageContainer extends Container {
 
 
     socket.on('page:delete', (data) => {
     socket.on('page:delete', (data) => {
       // skip if triggered myself
       // skip if triggered myself
-      if (data.socketClientId != null && data.socketClientId === websocketContainer.getSocketClientId()) {
+      if (data.socketClientId != null && data.socketClientId === socketIoContainer.getSocketClientId()) {
         return;
         return;
       }
       }
 
 
@@ -474,7 +474,7 @@ export default class PageContainer extends Container {
 
 
     socket.on('page:editingWithHackmd', (data) => {
     socket.on('page:editingWithHackmd', (data) => {
       // skip if triggered myself
       // skip if triggered myself
-      if (data.socketClientId != null && data.socketClientId === websocketContainer.getSocketClientId()) {
+      if (data.socketClientId != null && data.socketClientId === socketIoContainer.getSocketClientId()) {
         return;
         return;
       }
       }
 
 

+ 9 - 5
src/client/js/services/WebsocketContainer.js → src/client/js/services/SocketIoContainer.js

@@ -6,15 +6,19 @@ import io from 'socket.io-client';
  * Service container related to options for WebSocket
  * Service container related to options for WebSocket
  * @extends {Container} unstated Container
  * @extends {Container} unstated Container
  */
  */
-export default class WebsocketContainer extends Container {
+export default class SocketIoContainer extends Container {
 
 
-  constructor(appContainer) {
+  constructor(appContainer, namespace) {
     super();
     super();
 
 
     this.appContainer = appContainer;
     this.appContainer = appContainer;
     this.appContainer.registerContainer(this);
     this.appContainer.registerContainer(this);
 
 
-    this.socket = io();
+    const ns = namespace || '/';
+
+    this.socket = io(ns, {
+      transports: ['websocket'],
+    });
     this.socketClientId = Math.floor(Math.random() * 100000);
     this.socketClientId = Math.floor(Math.random() * 100000);
 
 
     this.state = {
     this.state = {
@@ -26,10 +30,10 @@ export default class WebsocketContainer extends Container {
    * Workaround for the mangling in production build to break constructor.name
    * Workaround for the mangling in production build to break constructor.name
    */
    */
   static getClassName() {
   static getClassName() {
-    return 'WebsocketContainer';
+    return 'SocketIoContainer';
   }
   }
 
 
-  getWebSocket() {
+  getSocket() {
     return this.socket;
     return this.socket;
   }
   }
 
 

+ 0 - 16
src/client/js/util/interceptor/drawio-interceptor.js

@@ -18,22 +18,6 @@ export class DrawioInterceptor extends BasicInterceptor {
 
 
     this.previousPreviewContext = null;
     this.previousPreviewContext = null;
     this.appContainer = appContainer;
     this.appContainer = appContainer;
-
-    // define callback function invoked by viewer.min.js of draw.io
-    // refs: https://github.com/jgraph/drawio/blob/v12.9.1/etc/build/build.xml#L219-L232
-    window.onDrawioViewerLoad = function() {
-      const DrawioViewer = window.GraphViewer;
-
-      if (DrawioViewer != null) {
-        // disable useResizeSensor and checkVisibleState
-        //   for preventing resize event by viewer.min.js
-        DrawioViewer.useResizeSensor = false;
-        DrawioViewer.prototype.checkVisibleState = false;
-
-        // initialize
-        DrawioViewer.processElements();
-      }
-    };
   }
   }
 
 
   /**
   /**

+ 1 - 6
src/client/styles/scss/_layout_kibela.scss

@@ -5,11 +5,6 @@ body.kibela {
     padding-top: 10px !important;
     padding-top: 10px !important;
   }
   }
 
 
-  /* navbar for kibela */
-  #page-wrapper {
-    margin-top: $grw-navbar-height + $grw-navbar-border-width;
-  }
-
   /* Logo */
   /* Logo */
   .logo {
   .logo {
     .logo-mark {
     .logo-mark {
@@ -51,7 +46,7 @@ body.kibela {
 
 
   .kibela-block {
   .kibela-block {
     position: relative;
     position: relative;
-    top: 10px;
+    top: 30px;
     right: 100px;
     right: 100px;
     bottom: 0px;
     bottom: 0px;
     left: 0px;
     left: 0px;

+ 8 - 0
src/client/styles/scss/_me.scss

@@ -1,2 +1,10 @@
 .user-settings-page {
 .user-settings-page {
+  .title {
+    padding: 0.5rem 15px;
+
+    line-height: 1em;
+
+    @include variable-font-size(28px);
+    line-height: 1.1em;
+  }
 }
 }

+ 12 - 0
src/client/styles/scss/_override-bootstrap-variables.scss

@@ -12,6 +12,18 @@ $warning: #ffa32b !default;
 $danger: #ff0a54 !default;
 $danger: #ff0a54 !default;
 $light: #e4e7ea !default;
 $light: #e4e7ea !default;
 $dark: #343a40 !default;
 $dark: #343a40 !default;
+$gray-50: lighten($light, 7%) !default;
+$gray-100: lighten($light, 4%) !default;
+$gray-200: $light !default;
+$gray-300: darken($light, 5%) !default;
+$gray-400: darken($light, 20%) !default;
+$gray-500: darken($light, 30%) !default;
+$gray-600: lighten($dark, 10%) !default;
+$gray-700: lighten($dark, 5%) !default;
+$gray-800: $dark !default;
+$gray-900: darken($dark, 5%) !default;
+$grays: ("50": $gray-50) !default;
+$red: #ff0a54 !default;
 
 
 //== Typography
 //== Typography
 //
 //

+ 4 - 4
src/client/styles/scss/_search.scss

@@ -193,12 +193,12 @@
   }
   }
 
 
   .search-result-content {
   .search-result-content {
-    padding-bottom: 32px;
+    padding-bottom: 36px;
 
 
     .search-result-page {
     .search-result-page {
-      padding-top: 48px;
+      padding-top: 64px;
       // adjust for anchor links by the height of fixed .search-page-input
       // adjust for anchor links by the height of fixed .search-page-input
-      margin-top: -48px;
+      margin-top: -64px;
 
 
       > h2 {
       > h2 {
         margin-right: 10px;
         margin-right: 10px;
@@ -221,7 +221,7 @@
 
 
 .search-page-input {
 .search-page-input {
   position: sticky;
   position: sticky;
-  top: 65px;
+  top: 15px;
   // placed at front-most
   // placed at front-most
   z-index: 15;
   z-index: 15;
 
 

+ 11 - 11
src/client/styles/scss/theme/_apply-colors-light.scss

@@ -8,15 +8,15 @@ $bgcolor-list-active: $primary !default;
 $bgcolor-subnav: darken($bgcolor-global, 3%) !default;
 $bgcolor-subnav: darken($bgcolor-global, 3%) !default;
 $color-table: $color-global !default;
 $color-table: $color-global !default;
 $bgcolor-table: null !default;
 $bgcolor-table: null !default;
-$border-color-table: #dee2e6 !default;
+$border-color-table: $gray-200 !default;
 $color-table-hover: $color-table !default;
 $color-table-hover: $color-table !default;
 $bgcolor-table-hover: rgba(black, 0.075) !default;
 $bgcolor-table-hover: rgba(black, 0.075) !default;
 $bgcolor-sidebar-list-group: $bgcolor-list !default;
 $bgcolor-sidebar-list-group: $bgcolor-list !default;
-$color-tags: #949494 !default;
-$bgcolor-tags: #ebebeb !default;
+$color-tags: $gray-500 !default;
+$bgcolor-tags: $gray-200 !default;
 
 
 // override bootstrap variables
 // override bootstrap variables
-$border-color: #dee2e6;
+$border-color: $gray-200;
 $table-color: $color-table;
 $table-color: $color-table;
 $table-bg: $bgcolor-table;
 $table-bg: $bgcolor-table;
 $table-border-color: $border-color-table;
 $table-border-color: $border-color-table;
@@ -72,28 +72,28 @@ $table-hover-bg: $bgcolor-table-hover;
   .dropdown-with-icon {
   .dropdown-with-icon {
     .dropdown-toggle {
     .dropdown-toggle {
       color: white;
       color: white;
-      background-color: rgba(#505050, 0.7);
+      background-color: rgba($gray-600, 0.7);
       box-shadow: unset;
       box-shadow: unset;
       &:focus {
       &:focus {
         color: white;
         color: white;
-        background-color: rgba(#505050, 0.7);
+        background-color: rgba($gray-600, 0.7);
       }
       }
     }
     }
     i {
     i {
       color: darken(white, 30%);
       color: darken(white, 30%);
-      background-color: rgba(#444, 0.7);
+      background-color: rgba($gray-700, 0.7);
     }
     }
   }
   }
 
 
   .input-group {
   .input-group {
     .input-group-text {
     .input-group-text {
       color: darken(white, 30%);
       color: darken(white, 30%);
-      background-color: rgba(#444, 0.7);
+      background-color: rgba($gray-700, 0.7);
     }
     }
 
 
     .form-control {
     .form-control {
       color: white;
       color: white;
-      background-color: rgba(#505050, 0.7);
+      background-color: rgba($gray-600, 0.7);
       box-shadow: unset;
       box-shadow: unset;
 
 
       &::placeholder {
       &::placeholder {
@@ -134,7 +134,7 @@ $table-hover-bg: $bgcolor-table-hover;
 
 
 .grw-drawer-toggler {
 .grw-drawer-toggler {
   @extend .btn-light;
   @extend .btn-light;
-  color: #999;
+  color: $gray-500;
 }
 }
 
 
 /*
 /*
@@ -176,7 +176,7 @@ $table-hover-bg: $bgcolor-table-hover;
 
 
 .wiki {
 .wiki {
   h1 {
   h1 {
-    border-color: darken($border-color-theme, 10%);
+    border-color: $border-color-theme;
   }
   }
   h2 {
   h2 {
     border-color: $border-color-theme;
     border-color: $border-color-theme;

+ 8 - 8
src/client/styles/scss/theme/_apply-colors.scss

@@ -3,16 +3,16 @@
 //
 //
 
 
 // determine optional variables
 // determine optional variables
-$border-image-navbar: linear-gradient(to right, #ccc 0%, #ccc 100%) !default;
+$border-image-navbar: linear-gradient(to right, $gray-300 0%, $gray-300 100%) !default;
 $bgcolor-search-top-dropdown: $secondary !default;
 $bgcolor-search-top-dropdown: $secondary !default;
 $bgcolor-sidebar-nav-item-active: darken($bgcolor-sidebar, 10%) !default;
 $bgcolor-sidebar-nav-item-active: darken($bgcolor-sidebar, 10%) !default;
 $text-shadow-sidebar-nav-item-active: 1px 1px 2px $primary !default;
 $text-shadow-sidebar-nav-item-active: 1px 1px 2px $primary !default;
-$bgcolor-inline-code: #f0f0f0 !default;
-$color-inline-code: #c7254e !default;
-$bordercolor-inline-code: #ccc8c8 !default;
-$bordercolor-nav-tabs: #dee2e6 !default;
-$bordercolor-nav-tabs-hover: #e9ecef #e9ecef $bordercolor-nav-tabs !default;
-$color-nav-tabs-link-active: #495057 !default;
+$bgcolor-inline-code: $gray-100 !default;
+$color-inline-code: darken($red, 15%) !default;
+$bordercolor-inline-code: $gray-400 !default;
+$bordercolor-nav-tabs: $gray-300 !default;
+$bordercolor-nav-tabs-hover: $gray-200 $gray-200 $bordercolor-nav-tabs !default;
+$color-nav-tabs-link-active: $gray-600 !default;
 $bordercolor-nav-tabs-active: $bordercolor-nav-tabs $bordercolor-nav-tabs $bgcolor-global !default;
 $bordercolor-nav-tabs-active: $bordercolor-nav-tabs $bordercolor-nav-tabs $bgcolor-global !default;
 
 
 // override bootstrap variables
 // override bootstrap variables
@@ -264,7 +264,7 @@ pre:not(.hljs):not(.CodeMirror-line) {
  */
  */
 .admin-page {
 .admin-page {
   span.slider {
   span.slider {
-    background-color: #ccc;
+    background-color: $gray-300;
 
 
     &:before {
     &:before {
       background-color: white;
       background-color: white;

+ 15 - 14
src/client/styles/scss/theme/default.scss

@@ -16,22 +16,23 @@
 //
 //
 html[light] {
 html[light] {
   $primary: #122c55;
   $primary: #122c55;
+  $accent: #209fd8;
 
 
   // Background colors
   // Background colors
   $bgcolor-global: white;
   $bgcolor-global: white;
-  $bgcolor-inline-code: #f0f0f0; //optional
-  $bgcolor-card: #f5f5f5;
+  $bgcolor-inline-code: $gray-100; //optional
+  $bgcolor-card: $gray-50;
 
 
   // Font colors
   // Font colors
   $color-global: #112744;
   $color-global: #112744;
-  $color-reversal: #eeeeee;
+  $color-reversal: $light;
   // $color-header: #2b2b2b;
   // $color-header: #2b2b2b;
   $color-link: #1938ba;
   $color-link: #1938ba;
   $color-link-hover: lighten($color-link, 20%);
   $color-link-hover: lighten($color-link, 20%);
   $color-link-wiki: $color-link;
   $color-link-wiki: $color-link;
   $color-link-wiki-hover: lighten($color-link-wiki, 20%);
   $color-link-wiki-hover: lighten($color-link-wiki, 20%);
-  $color-link-nabvar: #a7a7a7;
-  $color-inline-code: #c7254e; // optional
+  $color-link-nabvar: $gray-500;
+  $color-inline-code: darken($red, 15%); // optional
 
 
   // List Group colors
   // List Group colors
   // $color-list: $color-global; // optional
   // $color-list: $color-global; // optional
@@ -50,8 +51,8 @@ html[light] {
   // $bgcolor-table-hover: #; // optional
   // $bgcolor-table-hover: #; // optional
 
 
   // Navbar
   // Navbar
-  $bgcolor-navbar: #2a2929;
-  $bgcolor-search-top-dropdown: #209fd8;
+  $bgcolor-navbar: $gray-900;
+  $bgcolor-search-top-dropdown: $accent;
   $border-image-navbar: linear-gradient(to right, #36c9ff 0%, #36c9ff 33%, #7926ff 66%, #ff2eff 100%);
   $border-image-navbar: linear-gradient(to right, #36c9ff 0%, #36c9ff 33%, #7926ff 66%, #ff2eff 100%);
 
 
   // Logo colors
   // Logo colors
@@ -59,19 +60,19 @@ html[light] {
   $fillcolor-logo-mark: lighten(desaturate($bgcolor-navbar, 10%), 15%);
   $fillcolor-logo-mark: lighten(desaturate($bgcolor-navbar, 10%), 15%);
 
 
   // Sidebar
   // Sidebar
-  $bgcolor-sidebar: #122c55;
-  $bgcolor-sidebar-nav-item-active: rgba(#000000, 0.37); // optional
+  $bgcolor-sidebar: $primary;
+  $bgcolor-sidebar-nav-item-active: rgba(black, 0.37); // optional
   $text-shadow-sidebar-nav-item-active: 0px 0px 10px #0099ff; // optional
   $text-shadow-sidebar-nav-item-active: 0px 0px 10px #0099ff; // optional
   // Sidebar resize button
   // Sidebar resize button
   $color-resize-button: $color-reversal;
   $color-resize-button: $color-reversal;
-  $bgcolor-resize-button: #209fd8;
+  $bgcolor-resize-button: $accent;
   $color-resize-button-hover: $color-reversal;
   $color-resize-button-hover: $color-reversal;
   $bgcolor-resize-button-hover: lighten($bgcolor-resize-button, 5%);
   $bgcolor-resize-button-hover: lighten($bgcolor-resize-button, 5%);
   // Sidebar contents
   // Sidebar contents
   $color-sidebar-context: $color-global;
   $color-sidebar-context: $color-global;
-  $bgcolor-sidebar-context: #f4f6fc;
+  $bgcolor-sidebar-context: $gray-100;
   // Sidebar list group
   // Sidebar list group
-  $bgcolor-sidebar-list-group: #fafbff; // optional
+  $bgcolor-sidebar-list-group: $gray-50; // optional
 
 
   // Subnavigation
   // Subnavigation
   // $bgcolor-subnav: #fafafa; // optional
   // $bgcolor-subnav: #fafafa; // optional
@@ -89,8 +90,8 @@ html[light] {
   $color-editor-icons: $color-global;
   $color-editor-icons: $color-global;
 
 
   // Border colors
   // Border colors
-  $border-color-theme: #ccc;
-  $bordercolor-inline-code: #ccc8c8; // optional
+  $border-color-theme: $gray-400;
+  $bordercolor-inline-code: $gray-400; // optional
 
 
   // Dropdown colors
   // Dropdown colors
   $bgcolor-dropdown-link-active: $growi-blue;
   $bgcolor-dropdown-link-active: $growi-blue;

+ 2 - 2
src/server/crowi/express-init.js

@@ -20,7 +20,7 @@ module.exports = function(crowi, app) {
 
 
   const registerSafeRedirect = require('../middlewares/safe-redirect')();
   const registerSafeRedirect = require('../middlewares/safe-redirect')();
   const injectCurrentuserToLocalvars = require('../middlewares/inject-currentuser-to-localvars')();
   const injectCurrentuserToLocalvars = require('../middlewares/inject-currentuser-to-localvars')();
-  const autoReconnectToConfigPubsub = require('../middlewares/auto-reconnect-to-config-pubsub')(crowi);
+  const autoReconnectToS2sMsgServer = require('../middlewares/auto-reconnect-to-s2s-msg-server')(crowi);
   const { listLocaleIds } = require('@commons/util/locale-utils');
   const { listLocaleIds } = require('@commons/util/locale-utils');
 
 
   const avoidSessionRoutes = require('../routes/avoid-session-routes');
   const avoidSessionRoutes = require('../routes/avoid-session-routes');
@@ -118,7 +118,7 @@ module.exports = function(crowi, app) {
 
 
   app.use(registerSafeRedirect);
   app.use(registerSafeRedirect);
   app.use(injectCurrentuserToLocalvars);
   app.use(injectCurrentuserToLocalvars);
-  app.use(autoReconnectToConfigPubsub);
+  app.use(autoReconnectToS2sMsgServer);
 
 
   const middlewares = require('../util/middlewares')(crowi, app);
   const middlewares = require('../util/middlewares')(crowi, app);
   app.use(middlewares.swigFilters(swig));
   app.use(middlewares.swigFilters(swig));

+ 57 - 29
src/server/crowi/index.js

@@ -1,5 +1,3 @@
-
-
 const debug = require('debug')('growi:crowi');
 const debug = require('debug')('growi:crowi');
 const logger = require('@alias/logger')('growi:crowi');
 const logger = require('@alias/logger')('growi:crowi');
 const pkg = require('@root/package.json');
 const pkg = require('@root/package.json');
@@ -10,14 +8,14 @@ const { getMongoUri, mongoOptions } = require('@commons/util/mongoose-utils');
 
 
 const path = require('path');
 const path = require('path');
 
 
-const sep = path.sep;
-
 const mongoose = require('mongoose');
 const mongoose = require('mongoose');
 
 
 const models = require('../models');
 const models = require('../models');
 
 
 const PluginService = require('../plugins/plugin.service');
 const PluginService = require('../plugins/plugin.service');
 
 
+const sep = path.sep;
+
 function Crowi(rootdir) {
 function Crowi(rootdir) {
   const self = this;
   const self = this;
 
 
@@ -39,6 +37,7 @@ function Crowi(rootdir) {
 
 
   this.config = {};
   this.config = {};
   this.configManager = null;
   this.configManager = null;
+  this.s2sMessagingService = null;
   this.mailService = null;
   this.mailService = null;
   this.passportService = null;
   this.passportService = null;
   this.globalNotificationService = null;
   this.globalNotificationService = null;
@@ -52,6 +51,9 @@ function Crowi(rootdir) {
   this.exportService = null;
   this.exportService = null;
   this.importService = null;
   this.importService = null;
   this.searchService = null;
   this.searchService = null;
+  this.socketIoService = null;
+  this.pageService = null;
+  this.syncPageStatusService = null;
   this.cdnResourcesService = new CdnResourcesService();
   this.cdnResourcesService = new CdnResourcesService();
   this.interceptorManager = new InterceptorManager();
   this.interceptorManager = new InterceptorManager();
   this.xss = new Xss();
   this.xss = new Xss();
@@ -68,7 +70,6 @@ function Crowi(rootdir) {
   this.events = {
   this.events = {
     user: new (require(`${self.eventsDir}user`))(this),
     user: new (require(`${self.eventsDir}user`))(this),
     page: new (require(`${self.eventsDir}page`))(this),
     page: new (require(`${self.eventsDir}page`))(this),
-    search: new (require(`${self.eventsDir}search`))(this),
     bookmark: new (require(`${self.eventsDir}bookmark`))(this),
     bookmark: new (require(`${self.eventsDir}bookmark`))(this),
     tag: new (require(`${self.eventsDir}tag`))(this),
     tag: new (require(`${self.eventsDir}tag`))(this),
     admin: new (require(`${self.eventsDir}admin`))(this),
     admin: new (require(`${self.eventsDir}admin`))(this),
@@ -81,6 +82,10 @@ Crowi.prototype.init = async function() {
   await this.setupSessionConfig();
   await this.setupSessionConfig();
   await this.setupConfigManager();
   await this.setupConfigManager();
 
 
+  // setup messaging services
+  await this.setupS2sMessagingService();
+  await this.setupSocketIoService();
+
   // customizeService depends on AppService and XssService
   // customizeService depends on AppService and XssService
   // passportService depends on appService
   // passportService depends on appService
   // slack depends on setUpSlacklNotification
   // slack depends on setUpSlacklNotification
@@ -106,6 +111,8 @@ Crowi.prototype.init = async function() {
     this.setupUserGroup(),
     this.setupUserGroup(),
     this.setupExport(),
     this.setupExport(),
     this.setupImport(),
     this.setupImport(),
+    this.setupPageService(),
+    this.setupSyncPageStatusService(),
   ]);
   ]);
 
 
   // globalNotification depends on slack and mailer
   // globalNotification depends on slack and mailer
@@ -251,15 +258,25 @@ Crowi.prototype.setupSessionConfig = async function() {
 Crowi.prototype.setupConfigManager = async function() {
 Crowi.prototype.setupConfigManager = async function() {
   const ConfigManager = require('../service/config-manager');
   const ConfigManager = require('../service/config-manager');
   this.configManager = new ConfigManager(this.model('Config'));
   this.configManager = new ConfigManager(this.model('Config'));
-  await this.configManager.loadConfigs();
+  return this.configManager.loadConfigs();
+};
 
 
-  // setup pubsub
-  this.configPubsub = require('../service/config-pubsub')(this);
-  if (this.configPubsub != null) {
-    this.configPubsub.subscribe();
-    this.configManager.setPubsub(this.configPubsub);
+Crowi.prototype.setupS2sMessagingService = async function() {
+  const s2sMessagingService = require('../service/s2s-messaging')(this);
+  if (s2sMessagingService != null) {
+    s2sMessagingService.subscribe();
+    this.configManager.setS2sMessagingService(s2sMessagingService);
     // add as a message handler
     // add as a message handler
-    this.configPubsub.addMessageHandler(this.configManager);
+    s2sMessagingService.addMessageHandler(this.configManager);
+
+    this.s2sMessagingService = s2sMessagingService;
+  }
+};
+
+Crowi.prototype.setupSocketIoService = async function() {
+  const SocketIoService = require('../service/socket-io');
+  if (this.socketIoService == null) {
+    this.socketIoService = new SocketIoService();
   }
   }
 };
 };
 
 
@@ -269,10 +286,6 @@ Crowi.prototype.setupModels = async function() {
   });
   });
 };
 };
 
 
-Crowi.prototype.getIo = function() {
-  return this.io;
-};
-
 Crowi.prototype.scanRuntimeVersions = async function() {
 Crowi.prototype.scanRuntimeVersions = async function() {
   const self = this;
   const self = this;
 
 
@@ -329,8 +342,8 @@ Crowi.prototype.setupPassport = async function() {
   }
   }
 
 
   // add as a message handler
   // add as a message handler
-  if (this.configPubsub != null) {
-    this.configPubsub.addMessageHandler(this.passportService);
+  if (this.s2sMessagingService != null) {
+    this.s2sMessagingService.addMessageHandler(this.passportService);
   }
   }
 
 
   return Promise.resolve();
   return Promise.resolve();
@@ -346,8 +359,8 @@ Crowi.prototype.setupMailer = async function() {
   this.mailService = new MailService(this);
   this.mailService = new MailService(this);
 
 
   // add as a message handler
   // add as a message handler
-  if (this.configPubsub != null) {
-    this.configPubsub.addMessageHandler(this.mailService);
+  if (this.s2sMessagingService != null) {
+    this.s2sMessagingService.addMessageHandler(this.mailService);
   }
   }
 };
 };
 
 
@@ -398,11 +411,7 @@ Crowi.prototype.start = async function() {
     }
     }
   });
   });
 
 
-  // setup WebSocket
-  const io = require('socket.io')(serverListening);
-  io.sockets.on('connection', (socket) => {
-  });
-  this.io = io;
+  this.socketIoService.attachServer(serverListening);
 
 
   // setup Express Routes
   // setup Express Routes
   this.setupRoutesAtLast();
   this.setupRoutesAtLast();
@@ -510,8 +519,8 @@ Crowi.prototype.setUpCustomize = async function() {
     this.customizeService.initCustomTitle();
     this.customizeService.initCustomTitle();
 
 
     // add as a message handler
     // add as a message handler
-    if (this.configPubsub != null) {
-      this.configPubsub.addMessageHandler(this.customizeService);
+    if (this.s2sMessagingService != null) {
+      this.s2sMessagingService.addMessageHandler(this.customizeService);
     }
     }
   }
   }
 };
 };
@@ -526,8 +535,8 @@ Crowi.prototype.setUpApp = async function() {
 
 
     // add as a message handler
     // add as a message handler
     const isInstalled = this.configManager.getConfig('crowi', 'app:installed');
     const isInstalled = this.configManager.getConfig('crowi', 'app:installed');
-    if (this.configPubsub != null && !isInstalled) {
-      this.configPubsub.addMessageHandler(this.appService);
+    if (this.s2sMessagingService != null && !isInstalled) {
+      this.s2sMessagingService.addMessageHandler(this.appService);
     }
     }
   }
   }
 };
 };
@@ -580,4 +589,23 @@ Crowi.prototype.setupImport = async function() {
   }
   }
 };
 };
 
 
+Crowi.prototype.setupPageService = async function() {
+  const PageEventService = require('../service/page');
+  if (this.pageService == null) {
+    this.pageService = new PageEventService(this);
+  }
+};
+
+Crowi.prototype.setupSyncPageStatusService = async function() {
+  const SyncPageStatusService = require('../service/system-events/sync-page-status');
+  if (this.syncPageStatusService == null) {
+    this.syncPageStatusService = new SyncPageStatusService(this, this.s2sMessagingService, this.socketIoService);
+
+    // add as a message handler
+    if (this.s2sMessagingService != null) {
+      this.s2sMessagingService.addMessageHandler(this.syncPageStatusService);
+    }
+  }
+};
+
 module.exports = Crowi;
 module.exports = Crowi;

+ 0 - 11
src/server/events/search.js

@@ -1,11 +0,0 @@
-const util = require('util');
-const events = require('events');
-
-function SearchEvent(crowi) {
-  this.crowi = crowi;
-
-  events.EventEmitter.call(this);
-}
-util.inherits(SearchEvent, events.EventEmitter);
-
-module.exports = SearchEvent;

+ 0 - 11
src/server/middlewares/auto-reconnect-to-config-pubsub.js

@@ -1,11 +0,0 @@
-module.exports = (crowi) => {
-  const { configPubsub } = crowi;
-
-  return (req, res, next) => {
-    if (configPubsub != null && configPubsub.shouldResubscribe()) {
-      configPubsub.subscribe();
-    }
-
-    return next();
-  };
-};

+ 11 - 0
src/server/middlewares/auto-reconnect-to-s2s-msg-server.js

@@ -0,0 +1,11 @@
+module.exports = (crowi) => {
+  const { s2sMessagingService } = crowi;
+
+  return (req, res, next) => {
+    if (s2sMessagingService != null && s2sMessagingService.shouldResubscribe()) {
+      s2sMessagingService.subscribe();
+    }
+
+    return next();
+  };
+};

+ 33 - 28
src/server/models/page.js

@@ -702,6 +702,32 @@ module.exports = function(crowi) {
     return await findListFromBuilderAndViewer(builder, user, false, option);
     return await findListFromBuilderAndViewer(builder, user, false, option);
   };
   };
 
 
+  /**
+   * find pages that is match with `path` and its descendants whitch user is able to manage
+   */
+  pageSchema.statics.findManageableListWithDescendants = async function(page, user, option = {}) {
+    if (user == null) {
+      return null;
+    }
+
+    const builder = new PageQueryBuilder(this.find());
+    builder.addConditionToListWithDescendants(page.path, option);
+    builder.addConditionToExcludeRedirect();
+
+    // add grant conditions
+    await addConditionToFilteringByViewerToEdit(builder, user);
+
+    const { pages } = await findListFromBuilderAndViewer(builder, user, false, option);
+
+    // add page if 'grant' is GRANT_RESTRICTED
+    // because addConditionToListWithDescendants excludes GRANT_RESTRICTED pages
+    if (page.grant === GRANT_RESTRICTED) {
+      pages.push(page);
+    }
+
+    return pages;
+  };
+
   /**
   /**
    * find pages that start with `path`
    * find pages that start with `path`
    */
    */
@@ -1096,14 +1122,8 @@ module.exports = function(crowi) {
       throw new Error('This method does NOT supports deleting trashed pages.');
       throw new Error('This method does NOT supports deleting trashed pages.');
     }
     }
 
 
-    // find descendants (this array does not include GRANT_RESTRICTED)
-    const result = await this.findListWithDescendants(targetPage.path, user);
-    const pages = result.pages;
-    // add targetPage if 'grant' is GRANT_RESTRICTED
-    //  because findListWithDescendants excludes GRANT_RESTRICTED pages
-    if (targetPage.grant === GRANT_RESTRICTED) {
-      pages.push(targetPage);
-    }
+    // find manageable descendants (this array does not include GRANT_RESTRICTED)
+    const pages = await this.findManageableListWithDescendants(targetPage, user, options);
 
 
     await Promise.all(pages.map((page) => {
     await Promise.all(pages.map((page) => {
       return this.deletePage(page, user, options);
       return this.deletePage(page, user, options);
@@ -1135,8 +1155,7 @@ module.exports = function(crowi) {
 
 
   pageSchema.statics.revertDeletedPageRecursively = async function(targetPage, user, options = {}) {
   pageSchema.statics.revertDeletedPageRecursively = async function(targetPage, user, options = {}) {
     const findOpts = { includeTrashed: true };
     const findOpts = { includeTrashed: true };
-    const result = await this.findListWithDescendants(targetPage.path, user, findOpts);
-    const pages = result.pages;
+    const pages = await this.findManageableListWithDescendants(targetPage, user, findOpts);
 
 
     let updatedPage = null;
     let updatedPage = null;
     await Promise.all(pages.map((page) => {
     await Promise.all(pages.map((page) => {
@@ -1185,18 +1204,10 @@ module.exports = function(crowi) {
    * Delete Bookmarks, Attachments, Revisions, Pages and emit delete
    * Delete Bookmarks, Attachments, Revisions, Pages and emit delete
    */
    */
   pageSchema.statics.completelyDeletePageRecursively = async function(targetPage, user, options = {}) {
   pageSchema.statics.completelyDeletePageRecursively = async function(targetPage, user, options = {}) {
-    const pagePath = targetPage.path;
-
     const findOpts = { includeTrashed: true };
     const findOpts = { includeTrashed: true };
 
 
-    // find descendants (this array does not include GRANT_RESTRICTED)
-    const result = await this.findListWithDescendants(pagePath, user, findOpts);
-    const pages = result.pages;
-    // add targetPage if 'grant' is GRANT_RESTRICTED
-    //  because findListWithDescendants excludes GRANT_RESTRICTED pages
-    if (targetPage.grant === GRANT_RESTRICTED) {
-      pages.push(targetPage);
-    }
+    // find manageable descendants (this array does not include GRANT_RESTRICTED)
+    const pages = await this.findManageableListWithDescendants(targetPage, user, findOpts);
 
 
     await Promise.all(pages.map((page) => {
     await Promise.all(pages.map((page) => {
       return this.completelyDeletePage(page, user, options);
       return this.completelyDeletePage(page, user, options);
@@ -1277,14 +1288,8 @@ module.exports = function(crowi) {
     // sanitize path
     // sanitize path
     newPagePathPrefix = crowi.xss.process(newPagePathPrefix); // eslint-disable-line no-param-reassign
     newPagePathPrefix = crowi.xss.process(newPagePathPrefix); // eslint-disable-line no-param-reassign
 
 
-    // find descendants (this array does not include GRANT_RESTRICTED)
-    const result = await this.findListWithDescendants(path, user, options);
-    const pages = result.pages;
-    // add targetPage if 'grant' is GRANT_RESTRICTED
-    //  because findListWithDescendants excludes GRANT_RESTRICTED pages
-    if (targetPage.grant === GRANT_RESTRICTED) {
-      pages.push(targetPage);
-    }
+    // find manageable descendants
+    const pages = await this.findManageableListWithDescendants(targetPage, user, options);
 
 
     await Promise.all(pages.map((page) => {
     await Promise.all(pages.map((page) => {
       const newPagePath = page.path.replace(pathRegExp, newPagePathPrefix);
       const newPagePath = page.path.replace(pathRegExp, newPagePathPrefix);

+ 6 - 3
src/server/models/vo/config-pubsub-message.js → src/server/models/vo/s2s-message.js

@@ -1,4 +1,7 @@
-class ConfigPubsubMessage {
+/**
+ * Server-to-server message VO
+ */
+class S2sMessage {
 
 
   constructor(eventName, body = {}) {
   constructor(eventName, body = {}) {
     this.eventName = eventName;
     this.eventName = eventName;
@@ -18,9 +21,9 @@ class ConfigPubsubMessage {
       throw new Error('message body must contain \'eventName\'');
       throw new Error('message body must contain \'eventName\'');
     }
     }
 
 
-    return new ConfigPubsubMessage(body.eventName, body);
+    return new S2sMessage(body.eventName, body);
   }
   }
 
 
 }
 }
 
 
-module.exports = ConfigPubsubMessage;
+module.exports = S2sMessage;

+ 0 - 13
src/server/routes/admin.js

@@ -19,8 +19,6 @@ module.exports = function(crowi, app) {
   const ApiResponse = require('../util/apiResponse');
   const ApiResponse = require('../util/apiResponse');
   const importer = require('../util/importer')(crowi);
   const importer = require('../util/importer')(crowi);
 
 
-  const searchEvent = crowi.event('search');
-
   const MAX_PAGE_LIST = 50;
   const MAX_PAGE_LIST = 50;
   const actions = {};
   const actions = {};
 
 
@@ -84,17 +82,6 @@ module.exports = function(crowi, app) {
     return pager;
     return pager;
   }
   }
 
 
-  // setup websocket event for rebuild index
-  searchEvent.on('addPageProgress', (total, current, skip) => {
-    crowi.getIo().sockets.emit('admin:addPageProgress', { total, current, skip });
-  });
-  searchEvent.on('finishAddPage', (total, current, skip) => {
-    crowi.getIo().sockets.emit('admin:finishAddPage', { total, current, skip });
-  });
-  searchEvent.on('rebuildingFailed', (error) => {
-    crowi.getIo().sockets.emit('admin:rebuildingFailed', { error: error.message });
-  });
-
   actions.index = function(req, res) {
   actions.index = function(req, res) {
     return res.render('admin/index');
     return res.render('admin/index');
   };
   };

+ 2 - 2
src/server/routes/apiv3/app-settings.js

@@ -366,7 +366,7 @@ module.exports = (crowi) => {
     try {
     try {
       const { configManager, mailService } = crowi;
       const { configManager, mailService } = crowi;
 
 
-      // update config without publishing ConfigPubsubMessage
+      // update config without publishing S2sMessage
       await configManager.updateConfigsInTheSameNamespace('crowi', requestMailSettingParams, true);
       await configManager.updateConfigsInTheSameNamespace('crowi', requestMailSettingParams, true);
 
 
       await mailService.initialize();
       await mailService.initialize();
@@ -423,7 +423,7 @@ module.exports = (crowi) => {
     try {
     try {
       const { configManager, mailService } = crowi;
       const { configManager, mailService } = crowi;
 
 
-      // update config without publishing ConfigPubsubMessage
+      // update config without publishing S2sMessage
       await configManager.updateConfigsInTheSameNamespace('crowi', requestAwsSettingParams, true);
       await configManager.updateConfigsInTheSameNamespace('crowi', requestAwsSettingParams, true);
 
 
       await mailService.initialize();
       await mailService.initialize();

+ 4 - 4
src/server/routes/apiv3/export.js

@@ -43,19 +43,19 @@ module.exports = (crowi) => {
   const adminRequired = require('../../middlewares/admin-required')(crowi);
   const adminRequired = require('../../middlewares/admin-required')(crowi);
   const csrf = require('../../middlewares/csrf')(crowi);
   const csrf = require('../../middlewares/csrf')(crowi);
 
 
-  const { exportService } = crowi;
+  const { exportService, socketIoService } = crowi;
 
 
   this.adminEvent = crowi.event('admin');
   this.adminEvent = crowi.event('admin');
 
 
   // setup event
   // setup event
   this.adminEvent.on('onProgressForExport', (data) => {
   this.adminEvent.on('onProgressForExport', (data) => {
-    crowi.getIo().sockets.emit('admin:onProgressForExport', data);
+    socketIoService.getAdminSocket().emit('admin:onProgressForExport', data);
   });
   });
   this.adminEvent.on('onStartZippingForExport', (data) => {
   this.adminEvent.on('onStartZippingForExport', (data) => {
-    crowi.getIo().sockets.emit('admin:onStartZippingForExport', data);
+    socketIoService.getAdminSocket().emit('admin:onStartZippingForExport', data);
   });
   });
   this.adminEvent.on('onTerminateForExport', (data) => {
   this.adminEvent.on('onTerminateForExport', (data) => {
-    crowi.getIo().sockets.emit('admin:onTerminateForExport', data);
+    socketIoService.getAdminSocket().emit('admin:onTerminateForExport', data);
   });
   });
 
 
 
 

+ 4 - 4
src/server/routes/apiv3/import.js

@@ -60,7 +60,7 @@ const generateOverwriteParams = (collectionName, req, options) => {
 };
 };
 
 
 module.exports = (crowi) => {
 module.exports = (crowi) => {
-  const { growiBridgeService, importService } = crowi;
+  const { growiBridgeService, importService, socketIoService } = crowi;
   const accessTokenParser = require('../../middlewares/access-token-parser')(crowi);
   const accessTokenParser = require('../../middlewares/access-token-parser')(crowi);
   const loginRequired = require('../../middlewares/login-required')(crowi);
   const loginRequired = require('../../middlewares/login-required')(crowi);
   const adminRequired = require('../../middlewares/admin-required')(crowi);
   const adminRequired = require('../../middlewares/admin-required')(crowi);
@@ -70,13 +70,13 @@ module.exports = (crowi) => {
 
 
   // setup event
   // setup event
   this.adminEvent.on('onProgressForImport', (data) => {
   this.adminEvent.on('onProgressForImport', (data) => {
-    crowi.getIo().sockets.emit('admin:onProgressForImport', data);
+    socketIoService.getAdminSocket().emit('admin:onProgressForImport', data);
   });
   });
   this.adminEvent.on('onTerminateForImport', (data) => {
   this.adminEvent.on('onTerminateForImport', (data) => {
-    crowi.getIo().sockets.emit('admin:onTerminateForImport', data);
+    socketIoService.getAdminSocket().emit('admin:onTerminateForImport', data);
   });
   });
   this.adminEvent.on('onErrorForImport', (data) => {
   this.adminEvent.on('onErrorForImport', (data) => {
-    crowi.getIo().sockets.emit('admin:onErrorForImport', data);
+    socketIoService.getAdminSocket().emit('admin:onErrorForImport', data);
   });
   });
 
 
   const uploads = multer({
   const uploads = multer({

+ 1 - 1
src/server/routes/apiv3/security-setting.js

@@ -327,7 +327,7 @@ module.exports = (crowi) => {
   async function updateAndReloadStrategySettings(authId, params) {
   async function updateAndReloadStrategySettings(authId, params) {
     const { configManager, passportService } = crowi;
     const { configManager, passportService } = crowi;
 
 
-    // update config without publishing ConfigPubsubMessage
+    // update config without publishing S2sMessage
     await configManager.updateConfigsInTheSameNamespace('crowi', params, true);
     await configManager.updateConfigsInTheSameNamespace('crowi', params, true);
 
 
     await passportService.setupStrategyById(authId);
     await passportService.setupStrategyById(authId);

+ 0 - 5
src/server/routes/hackmd.js

@@ -40,11 +40,6 @@ module.exports = function(crowi, app) {
   let agentScriptContentTpl;
   let agentScriptContentTpl;
   let stylesScriptContentTpl;
   let stylesScriptContentTpl;
 
 
-  // init 'saveOnHackmd' event
-  pageEvent.on('saveOnHackmd', (page) => {
-    crowi.getIo().sockets.emit('page:editingWithHackmd', { page });
-  });
-
   /**
   /**
    * GET /_hackmd/load-agent
    * GET /_hackmd/load-agent
    *
    *

+ 3 - 35
src/server/routes/page.js

@@ -149,42 +149,10 @@ module.exports = function(crowi, app) {
   const { slackNotificationService, configManager } = crowi;
   const { slackNotificationService, configManager } = crowi;
   const interceptorManager = crowi.getInterceptorManager();
   const interceptorManager = crowi.getInterceptorManager();
   const globalNotificationService = crowi.getGlobalNotificationService();
   const globalNotificationService = crowi.getGlobalNotificationService();
+  const pageService = crowi.pageService;
 
 
   const actions = {};
   const actions = {};
 
 
-  // register page events
-
-  const pageEvent = crowi.event('page');
-  pageEvent.on('create', (page, user, socketClientId) => {
-    page = serializeToObj(page); // eslint-disable-line no-param-reassign
-    crowi.getIo().sockets.emit('page:create', { page, user, socketClientId });
-  });
-  pageEvent.on('update', (page, user, socketClientId) => {
-    page = serializeToObj(page); // eslint-disable-line no-param-reassign
-    crowi.getIo().sockets.emit('page:update', { page, user, socketClientId });
-  });
-  pageEvent.on('delete', (page, user, socketClientId) => {
-    page = serializeToObj(page); // eslint-disable-line no-param-reassign
-    crowi.getIo().sockets.emit('page:delete', { page, user, socketClientId });
-  });
-
-
-  function serializeToObj(page) {
-    const returnObj = page.toObject();
-    if (page.revisionHackmdSynced != null && page.revisionHackmdSynced._id != null) {
-      returnObj.revisionHackmdSynced = page.revisionHackmdSynced._id;
-    }
-
-    if (page.lastUpdateUser != null && page.lastUpdateUser instanceof User) {
-      returnObj.lastUpdateUser = page.lastUpdateUser.toObject();
-    }
-    if (page.creator != null && page.creator instanceof User) {
-      returnObj.creator = page.creator.toObject();
-    }
-
-    return returnObj;
-  }
-
   function getPathFromRequest(req) {
   function getPathFromRequest(req) {
     return pathUtils.normalizePath(req.params[0] || '');
     return pathUtils.normalizePath(req.params[0] || '');
   }
   }
@@ -742,7 +710,7 @@ module.exports = function(crowi, app) {
       savedTags = await PageTagRelation.listTagNamesByPage(createdPage.id);
       savedTags = await PageTagRelation.listTagNamesByPage(createdPage.id);
     }
     }
 
 
-    const result = { page: serializeToObj(createdPage), tags: savedTags };
+    const result = { page: pageService.serializeToObj(createdPage), tags: savedTags };
     res.json(ApiResponse.success(result));
     res.json(ApiResponse.success(result));
 
 
     // update scopes for descendants
     // update scopes for descendants
@@ -871,7 +839,7 @@ module.exports = function(crowi, app) {
       savedTags = await PageTagRelation.listTagNamesByPage(pageId);
       savedTags = await PageTagRelation.listTagNamesByPage(pageId);
     }
     }
 
 
-    const result = { page: serializeToObj(page), tags: savedTags };
+    const result = { page: pageService.serializeToObj(page), tags: savedTags };
     res.json(ApiResponse.success(result));
     res.json(ApiResponse.success(result));
 
 
     // update scopes for descendants
     // update scopes for descendants

+ 17 - 16
src/server/service/app.js

@@ -2,27 +2,27 @@ const logger = require('@alias/logger')('growi:service:AppService'); // eslint-d
 const { pathUtils } = require('growi-commons');
 const { pathUtils } = require('growi-commons');
 
 
 
 
-const ConfigPubsubMessage = require('../models/vo/config-pubsub-message');
-const ConfigPubsubMessageHandlable = require('./config-pubsub/handlable');
+const S2sMessage = require('../models/vo/s2s-message');
+const S2sMessageHandlable = require('./s2s-messaging/handlable');
 
 
 /**
 /**
  * the service class of AppService
  * the service class of AppService
  */
  */
-class AppService extends ConfigPubsubMessageHandlable {
+class AppService extends S2sMessageHandlable {
 
 
   constructor(crowi) {
   constructor(crowi) {
     super();
     super();
 
 
     this.crowi = crowi;
     this.crowi = crowi;
     this.configManager = crowi.configManager;
     this.configManager = crowi.configManager;
-    this.configPubsub = crowi.configPubsub;
+    this.s2sMessagingService = crowi.s2sMessagingService;
   }
   }
 
 
   /**
   /**
    * @inheritdoc
    * @inheritdoc
    */
    */
-  shouldHandleConfigPubsubMessage(configPubsubMessage) {
-    const { eventName } = configPubsubMessage;
+  shouldHandleS2sMessage(s2sMessage) {
+    const { eventName } = s2sMessage;
     if (eventName !== 'systemInstalled') {
     if (eventName !== 'systemInstalled') {
       return false;
       return false;
     }
     }
@@ -35,10 +35,10 @@ class AppService extends ConfigPubsubMessageHandlable {
   /**
   /**
    * @inheritdoc
    * @inheritdoc
    */
    */
-  async handleConfigPubsubMessage(configPubsubMessage) {
+  async handleS2sMessage(s2sMessage) {
     logger.info('Invoke post installation process by pubsub notification');
     logger.info('Invoke post installation process by pubsub notification');
 
 
-    const { crowi, configManager, configPubsub } = this;
+    const { crowi, configManager, s2sMessagingService } = this;
 
 
     // load config and setup
     // load config and setup
     await configManager.loadConfigs();
     await configManager.loadConfigs();
@@ -48,26 +48,27 @@ class AppService extends ConfigPubsubMessageHandlable {
       crowi.setupAfterInstall();
       crowi.setupAfterInstall();
 
 
       // remove message handler
       // remove message handler
-      configPubsub.removeMessageHandler(this);
+      s2sMessagingService.removeMessageHandler(this);
     }
     }
   }
   }
 
 
   async publishPostInstallationMessage() {
   async publishPostInstallationMessage() {
-    const { configPubsub } = this;
+    const { s2sMessagingService } = this;
 
 
-    if (configPubsub != null) {
-      const configPubsubMessage = new ConfigPubsubMessage('systemInstalled');
+    if (s2sMessagingService != null) {
+      const s2sMessage = new S2sMessage('systemInstalled');
 
 
       try {
       try {
-        await configPubsub.publish(configPubsubMessage);
+        await s2sMessagingService.publish(s2sMessage);
       }
       }
       catch (e) {
       catch (e) {
-        logger.error('Failed to publish post installation message with configPubsub: ', e.message);
+        logger.error('Failed to publish post installation message with S2sMessagingService: ', e.message);
       }
       }
+
+      // remove message handler
+      s2sMessagingService.removeMessageHandler(this);
     }
     }
 
 
-    // remove message handler
-    configPubsub.removeMessageHandler(this);
   }
   }
 
 
   getAppTitle() {
   getAppTitle() {

+ 14 - 8
src/server/service/config-loader.js

@@ -107,6 +107,12 @@ const ENV_VAR_NAME_TO_CONFIG_INFO = {
   //   type:    ,
   //   type:    ,
   //   default:
   //   default:
   // },
   // },
+  DRAWIO_URI: {
+    ns:      'crowi',
+    key:     'app:drawioUri',
+    type:    TYPES.STRING,
+    default: null,
+  },
   NCHAN_URI: {
   NCHAN_URI: {
     ns:      'crowi',
     ns:      'crowi',
     key:     'app:nchanUri',
     key:     'app:nchanUri',
@@ -125,27 +131,27 @@ const ENV_VAR_NAME_TO_CONFIG_INFO = {
     type:    TYPES.BOOLEAN,
     type:    TYPES.BOOLEAN,
     default: false,
     default: false,
   },
   },
-  CONFIG_PUBSUB_SERVER_TYPE: {
+  S2SMSG_PUBSUB_SERVER_TYPE: {
     ns:      'crowi',
     ns:      'crowi',
-    key:     'configPubsub:serverType',
+    key:     's2sMessagingPubsub:serverType',
     type:    TYPES.STRING,
     type:    TYPES.STRING,
     default: null,
     default: null,
   },
   },
-  CONFIG_PUBSUB_NCHAN_PUBLISH_PATH: {
+  S2SMSG_PUBSUB_NCHAN_PUBLISH_PATH: {
     ns:      'crowi',
     ns:      'crowi',
-    key:     'configPubsub:nchan:publishPath',
+    key:     's2sMessagingPubsub:nchan:publishPath',
     type:    TYPES.STRING,
     type:    TYPES.STRING,
     default: '/pubsub',
     default: '/pubsub',
   },
   },
-  CONFIG_PUBSUB_NCHAN_SUBSCRIBE_PATH: {
+  S2SMSG_PUBSUB_NCHAN_SUBSCRIBE_PATH: {
     ns:      'crowi',
     ns:      'crowi',
-    key:     'configPubsub:nchan:subscribePath',
+    key:     's2sMessagingPubsub:nchan:subscribePath',
     type:    TYPES.STRING,
     type:    TYPES.STRING,
     default: '/pubsub',
     default: '/pubsub',
   },
   },
-  CONFIG_PUBSUB_NCHAN_CHANNEL_ID: {
+  S2SMSG_PUBSUB_NCHAN_CHANNEL_ID: {
     ns:      'crowi',
     ns:      'crowi',
-    key:     'configPubsub:nchan:channelId',
+    key:     's2sMessagingPubsub:nchan:channelId',
     type:    TYPES.STRING,
     type:    TYPES.STRING,
     default: null,
     default: null,
   },
   },

+ 18 - 16
src/server/service/config-manager.js

@@ -1,7 +1,9 @@
 const logger = require('@alias/logger')('growi:service:ConfigManager');
 const logger = require('@alias/logger')('growi:service:ConfigManager');
 
 
-const ConfigPubsubMessage = require('../models/vo/config-pubsub-message');
-const ConfigPubsubMessageHandlable = require('./config-pubsub/handlable');
+const parseISO = require('date-fns/parseISO');
+
+const S2sMessage = require('../models/vo/s2s-message');
+const S2sMessageHandlable = require('./s2s-messaging/handlable');
 
 
 const ConfigLoader = require('./config-loader');
 const ConfigLoader = require('./config-loader');
 
 
@@ -22,7 +24,7 @@ const KEYS_FOR_SAML_USE_ONLY_ENV_OPTION = [
   'security:passport-saml:ABLCRule',
   'security:passport-saml:ABLCRule',
 ];
 ];
 
 
-class ConfigManager extends ConfigPubsubMessageHandlable {
+class ConfigManager extends S2sMessageHandlable {
 
 
   constructor(configModel) {
   constructor(configModel) {
     super();
     super();
@@ -50,11 +52,11 @@ class ConfigManager extends ConfigPubsubMessageHandlable {
   }
   }
 
 
   /**
   /**
-   * Set ConfigPubsubDelegator instance
-   * @param {ConfigPubsubDelegator} configPubsub
+   * Set S2sMessagingServiceDelegator instance
+   * @param {S2sMessagingServiceDelegator} s2sMessagingService
    */
    */
-  async setPubsub(configPubsub) {
-    this.configPubsub = configPubsub;
+  async setS2sMessagingService(s2sMessagingService) {
+    this.s2sMessagingService = s2sMessagingService;
   }
   }
 
 
   /**
   /**
@@ -180,7 +182,7 @@ class ConfigManager extends ConfigPubsubMessageHandlable {
    *  );
    *  );
    * ```
    * ```
    */
    */
-  async updateConfigsInTheSameNamespace(namespace, configs, withoutPublishingConfigPubsubMessage) {
+  async updateConfigsInTheSameNamespace(namespace, configs, withoutPublishingS2sMessage) {
     const queries = [];
     const queries = [];
     for (const key of Object.keys(configs)) {
     for (const key of Object.keys(configs)) {
       queries.push({
       queries.push({
@@ -196,7 +198,7 @@ class ConfigManager extends ConfigPubsubMessageHandlable {
     await this.loadConfigs();
     await this.loadConfigs();
 
 
     // publish updated date after reloading
     // publish updated date after reloading
-    if (this.configPubsub != null && !withoutPublishingConfigPubsubMessage) {
+    if (this.s2sMessagingService != null && !withoutPublishingS2sMessage) {
       this.publishUpdateMessage();
       this.publishUpdateMessage();
     }
     }
   }
   }
@@ -309,32 +311,32 @@ class ConfigManager extends ConfigPubsubMessageHandlable {
   }
   }
 
 
   async publishUpdateMessage() {
   async publishUpdateMessage() {
-    const configPubsubMessage = new ConfigPubsubMessage('configUpdated', { updatedAt: new Date() });
+    const s2sMessage = new S2sMessage('configUpdated', { updatedAt: new Date() });
 
 
     try {
     try {
-      await this.configPubsub.publish(configPubsubMessage);
+      await this.s2sMessagingService.publish(s2sMessage);
     }
     }
     catch (e) {
     catch (e) {
-      logger.error('Failed to publish update message with configPubsub: ', e.message);
+      logger.error('Failed to publish update message with S2sMessagingService: ', e.message);
     }
     }
   }
   }
 
 
   /**
   /**
    * @inheritdoc
    * @inheritdoc
    */
    */
-  shouldHandleConfigPubsubMessage(configPubsubMessage) {
-    const { eventName, updatedAt } = configPubsubMessage;
+  shouldHandleS2sMessage(s2sMessage) {
+    const { eventName, updatedAt } = s2sMessage;
     if (eventName !== 'configUpdated' || updatedAt == null) {
     if (eventName !== 'configUpdated' || updatedAt == null) {
       return false;
       return false;
     }
     }
 
 
-    return this.lastLoadedAt == null || this.lastLoadedAt < new Date(configPubsubMessage.updatedAt);
+    return this.lastLoadedAt == null || this.lastLoadedAt < parseISO(s2sMessage.updatedAt);
   }
   }
 
 
   /**
   /**
    * @inheritdoc
    * @inheritdoc
    */
    */
-  async handleConfigPubsubMessage(configPubsubMessage) {
+  async handleS2sMessage(s2sMessage) {
     logger.info('Reload configs by pubsub notification');
     logger.info('Reload configs by pubsub notification');
     return this.loadConfigs();
     return this.loadConfigs();
   }
   }

+ 0 - 14
src/server/service/config-pubsub/handlable.js

@@ -1,14 +0,0 @@
-// TODO: make interface with TS
-class ConfigPubsubMessageHandlable {
-
-  shouldHandleConfigPubsubMessage(configPubsubMessage) {
-    throw new Error('implement this');
-  }
-
-  async handleConfigPubsubMessage(configPubsubMessage) {
-    throw new Error('implement this');
-  }
-
-}
-
-module.exports = ConfigPubsubMessageHandlable;

+ 0 - 195
src/server/service/config-pubsub/nchan.js

@@ -1,195 +0,0 @@
-const logger = require('@alias/logger')('growi:service:config-pubsub:nchan');
-
-const path = require('path');
-const axios = require('axios');
-const WebSocketClient = require('websocket').client;
-
-const ConfigPubsubMessage = require('../../models/vo/config-pubsub-message');
-const ConfigPubsubDelegator = require('./base');
-
-
-class NchanDelegator extends ConfigPubsubDelegator {
-
-  constructor(uri, publishPath, subscribePath, channelId) {
-    super(uri);
-
-    this.publishPath = publishPath;
-    this.subscribePath = subscribePath;
-
-    this.channelId = channelId;
-    this.isConnecting = false;
-
-    /**
-     * A list of ConfigPubsubHandler instance
-     */
-    this.handlableList = [];
-
-    this.client = null;
-    this.connection = null;
-  }
-
-  /**
-   * @inheritdoc
-   */
-  shouldResubscribe() {
-    if (this.connection != null && this.connection.connected) {
-      return false;
-    }
-
-    return !this.isConnecting;
-  }
-
-  /**
-   * @inheritdoc
-   */
-  subscribe(forceReconnect = false) {
-    if (forceReconnect) {
-      if (this.connection != null && this.connection.connected) {
-        this.connection.close();
-      }
-    }
-
-    if (this.client != null && this.shouldResubscribe()) {
-      logger.info('The connection to config pubsub server is offline. Try to reconnect...');
-    }
-
-    // init client
-    if (this.client == null) {
-      this.initClient();
-    }
-
-    // connect
-    this.isConnecting = true;
-    const url = this.constructUrl(this.subscribePath).toString();
-    logger.debug(`Subscribe to ${url}`);
-    this.client.connect(url.toString());
-  }
-
-  /**
-   * @inheritdoc
-   */
-  async publish(configPubsubMessage) {
-    await super.publish(configPubsubMessage);
-
-    const url = this.constructUrl(this.publishPath).toString();
-
-    logger.debug('Publish message', configPubsubMessage, `to ${url}`);
-
-    return axios.post(url, JSON.stringify(configPubsubMessage));
-  }
-
-  /**
-   * @inheritdoc
-   */
-  addMessageHandler(handlable) {
-    super.addMessageHandler(handlable);
-    this.registerMessageHandlerToConnection(handlable);
-  }
-
-  /**
-   * @inheritdoc
-   */
-  removeMessageHandler(handlable) {
-    super.removeMessageHandler(handlable);
-    this.subscribe(true);
-  }
-
-  registerMessageHandlerToConnection(handlable) {
-    if (this.connection != null) {
-      this.connection.on('message', (messageObj) => {
-        this.handleMessage(messageObj, handlable);
-      });
-    }
-  }
-
-  constructUrl(basepath) {
-    const pathname = this.channelId == null
-      ? basepath //                                 /pubsub
-      : path.join(basepath, this.channelId); //     /pubsub/my-channel-id
-
-    return new URL(pathname, this.uri);
-  }
-
-  initClient() {
-    const client = new WebSocketClient();
-
-    client.on('connectFailed', (error) => {
-      logger.warn(`Connect Error: ${error.toString()}`);
-      this.isConnecting = false;
-    });
-
-    client.on('connect', (connection) => {
-      this.isConnecting = false;
-      this.connection = connection;
-
-      logger.info('WebSocket client connected');
-
-      connection.on('error', (error) => {
-        this.isConnecting = false;
-        logger.error(`Connection Error: ${error.toString()}`);
-      });
-      connection.on('close', () => {
-        logger.info('WebSocket connection closed');
-      });
-
-      // register all message handlers
-      this.handlableList.forEach(handler => this.registerMessageHandlerToConnection(handler));
-    });
-
-    this.client = client;
-  }
-
-  /**
-   * Handle message string with the specified ConfigPubsubHandler
-   *
-   * @see https://github.com/theturtle32/WebSocket-Node/blob/1f7ffba2f7a6f9473bcb39228264380ce2772ba7/docs/WebSocketConnection.md#message
-   *
-   * @param {object} message WebSocket-Node message object
-   * @param {ConfigPubsubHandler} handlable
-   */
-  handleMessage(message, handlable) {
-    if (message.type !== 'utf8') {
-      logger.warn('Only utf8 message is supported.');
-    }
-
-    try {
-      const configPubsubMessage = ConfigPubsubMessage.parse(message.utf8Data);
-
-      // check uid
-      if (configPubsubMessage.publisherUid === this.uid) {
-        logger.debug(`Skip processing by ${handlable.constructor.name} because this message is sent by the publisher itself:`, `from ${this.uid}`);
-        return;
-      }
-
-      // check shouldHandleConfigPubsubMessage
-      const shouldHandle = handlable.shouldHandleConfigPubsubMessage(configPubsubMessage);
-      logger.debug(`${handlable.constructor.name}.shouldHandleConfigPubsubMessage(`, configPubsubMessage, `) => ${shouldHandle}`);
-
-      if (shouldHandle) {
-        handlable.handleConfigPubsubMessage(configPubsubMessage);
-      }
-    }
-    catch (err) {
-      logger.warn('Could not handle a message: ', err.message);
-    }
-  }
-
-}
-
-module.exports = function(crowi) {
-  const { configManager } = crowi;
-
-  const uri = configManager.getConfig('crowi', 'app:nchanUri');
-
-  // when nachan server URI is not set
-  if (uri == null) {
-    logger.warn('NCHAN_URI is not specified.');
-    return;
-  }
-
-  const publishPath = configManager.getConfig('crowi', 'configPubsub:nchan:publishPath');
-  const subscribePath = configManager.getConfig('crowi', 'configPubsub:nchan:subscribePath');
-  const channelId = configManager.getConfig('crowi', 'configPubsub:nchan:channelId');
-
-  return new NchanDelegator(uri, publishPath, subscribePath, channelId);
-};

+ 13 - 13
src/server/service/customize.js

@@ -3,20 +3,20 @@ const logger = require('@alias/logger')('growi:service:CustomizeService');
 
 
 const DevidedPagePath = require('@commons/models/devided-page-path');
 const DevidedPagePath = require('@commons/models/devided-page-path');
 
 
-const ConfigPubsubMessage = require('../models/vo/config-pubsub-message');
-const ConfigPubsubMessageHandlable = require('./config-pubsub/handlable');
+const S2sMessage = require('../models/vo/s2s-message');
+const S2sMessageHandlable = require('./s2s-messaging/handlable');
 
 
 
 
 /**
 /**
  * the service class of CustomizeService
  * the service class of CustomizeService
  */
  */
-class CustomizeService extends ConfigPubsubMessageHandlable {
+class CustomizeService extends S2sMessageHandlable {
 
 
   constructor(crowi) {
   constructor(crowi) {
     super();
     super();
 
 
     this.configManager = crowi.configManager;
     this.configManager = crowi.configManager;
-    this.configPubsub = crowi.configPubsub;
+    this.s2sMessagingService = crowi.s2sMessagingService;
     this.appService = crowi.appService;
     this.appService = crowi.appService;
     this.xssService = crowi.xssService;
     this.xssService = crowi.xssService;
 
 
@@ -26,19 +26,19 @@ class CustomizeService extends ConfigPubsubMessageHandlable {
   /**
   /**
    * @inheritdoc
    * @inheritdoc
    */
    */
-  shouldHandleConfigPubsubMessage(configPubsubMessage) {
-    const { eventName, updatedAt } = configPubsubMessage;
+  shouldHandleS2sMessage(s2sMessage) {
+    const { eventName, updatedAt } = s2sMessage;
     if (eventName !== 'customizeServiceUpdated' || updatedAt == null) {
     if (eventName !== 'customizeServiceUpdated' || updatedAt == null) {
       return false;
       return false;
     }
     }
 
 
-    return this.lastLoadedAt == null || this.lastLoadedAt < new Date(configPubsubMessage.updatedAt);
+    return this.lastLoadedAt == null || this.lastLoadedAt < new Date(s2sMessage.updatedAt);
   }
   }
 
 
   /**
   /**
    * @inheritdoc
    * @inheritdoc
    */
    */
-  async handleConfigPubsubMessage(configPubsubMessage) {
+  async handleS2sMessage(s2sMessage) {
     const { configManager } = this;
     const { configManager } = this;
 
 
     logger.info('Reset customized value by pubsub notification');
     logger.info('Reset customized value by pubsub notification');
@@ -48,16 +48,16 @@ class CustomizeService extends ConfigPubsubMessageHandlable {
   }
   }
 
 
   async publishUpdatedMessage() {
   async publishUpdatedMessage() {
-    const { configPubsub } = this;
+    const { s2sMessagingService } = this;
 
 
-    if (configPubsub != null) {
-      const configPubsubMessage = new ConfigPubsubMessage('customizeServiceUpdated', { updatedAt: new Date() });
+    if (s2sMessagingService != null) {
+      const s2sMessage = new S2sMessage('customizeServiceUpdated', { updatedAt: new Date() });
 
 
       try {
       try {
-        await configPubsub.publish(configPubsubMessage);
+        await s2sMessagingService.publish(s2sMessage);
       }
       }
       catch (e) {
       catch (e) {
-        logger.error('Failed to publish update message with configPubsub: ', e.message);
+        logger.error('Failed to publish update message with S2sMessagingService: ', e.message);
       }
       }
     }
     }
   }
   }

+ 13 - 13
src/server/service/mail.js

@@ -4,18 +4,18 @@ const nodemailer = require('nodemailer');
 const swig = require('swig-templates');
 const swig = require('swig-templates');
 
 
 
 
-const ConfigPubsubMessage = require('../models/vo/config-pubsub-message');
-const ConfigPubsubMessageHandlable = require('./config-pubsub/handlable');
+const S2sMessage = require('../models/vo/s2s-message');
+const S2sMessageHandlable = require('./s2s-messaging/handlable');
 
 
 
 
-class MailService extends ConfigPubsubMessageHandlable {
+class MailService extends S2sMessageHandlable {
 
 
   constructor(crowi) {
   constructor(crowi) {
     super();
     super();
 
 
     this.appService = crowi.appService;
     this.appService = crowi.appService;
     this.configManager = crowi.configManager;
     this.configManager = crowi.configManager;
-    this.configPubsub = crowi.configPubsub;
+    this.s2sMessagingService = crowi.s2sMessagingService;
 
 
     this.mailConfig = {};
     this.mailConfig = {};
     this.mailer = {};
     this.mailer = {};
@@ -26,19 +26,19 @@ class MailService extends ConfigPubsubMessageHandlable {
   /**
   /**
    * @inheritdoc
    * @inheritdoc
    */
    */
-  shouldHandleConfigPubsubMessage(configPubsubMessage) {
-    const { eventName, updatedAt } = configPubsubMessage;
+  shouldHandleS2sMessage(s2sMessage) {
+    const { eventName, updatedAt } = s2sMessage;
     if (eventName !== 'mailServiceUpdated' || updatedAt == null) {
     if (eventName !== 'mailServiceUpdated' || updatedAt == null) {
       return false;
       return false;
     }
     }
 
 
-    return this.lastLoadedAt == null || this.lastLoadedAt < new Date(configPubsubMessage.updatedAt);
+    return this.lastLoadedAt == null || this.lastLoadedAt < new Date(s2sMessage.updatedAt);
   }
   }
 
 
   /**
   /**
    * @inheritdoc
    * @inheritdoc
    */
    */
-  async handleConfigPubsubMessage(configPubsubMessage) {
+  async handleS2sMessage(s2sMessage) {
     const { configManager } = this;
     const { configManager } = this;
 
 
     logger.info('Initialize mail settings by pubsub notification');
     logger.info('Initialize mail settings by pubsub notification');
@@ -47,16 +47,16 @@ class MailService extends ConfigPubsubMessageHandlable {
   }
   }
 
 
   async publishUpdatedMessage() {
   async publishUpdatedMessage() {
-    const { configPubsub } = this;
+    const { s2sMessagingService } = this;
 
 
-    if (configPubsub != null) {
-      const configPubsubMessage = new ConfigPubsubMessage('mailServiceUpdated', { updatedAt: new Date() });
+    if (s2sMessagingService != null) {
+      const s2sMessage = new S2sMessage('mailServiceUpdated', { updatedAt: new Date() });
 
 
       try {
       try {
-        await configPubsub.publish(configPubsubMessage);
+        await s2sMessagingService.publish(s2sMessage);
       }
       }
       catch (e) {
       catch (e) {
-        logger.error('Failed to publish update message with configPubsub: ', e.message);
+        logger.error('Failed to publish update message with S2sMessagingService: ', e.message);
       }
       }
     }
     }
   }
   }

+ 29 - 0
src/server/service/page.js

@@ -0,0 +1,29 @@
+class PageService {
+
+  constructor(crowi) {
+    this.crowi = crowi;
+  }
+
+  serializeToObj(page) {
+    const { User } = this.crowi.models;
+
+    const returnObj = page.toObject();
+
+    // set the ObjectID to revisionHackmdSynced
+    if (page.revisionHackmdSynced != null && page.revisionHackmdSynced._id != null) {
+      returnObj.revisionHackmdSynced = page.revisionHackmdSynced._id;
+    }
+
+    if (page.lastUpdateUser != null && page.lastUpdateUser instanceof User) {
+      returnObj.lastUpdateUser = page.lastUpdateUser.toObject();
+    }
+    if (page.creator != null && page.creator instanceof User) {
+      returnObj.creator = page.creator.toObject();
+    }
+
+    return returnObj;
+  }
+
+}
+
+module.exports = PageService;

+ 13 - 13
src/server/service/passport.js

@@ -13,13 +13,13 @@ const SamlStrategy = require('passport-saml').Strategy;
 const OIDCIssuer = require('openid-client').Issuer;
 const OIDCIssuer = require('openid-client').Issuer;
 const BasicStrategy = require('passport-http').BasicStrategy;
 const BasicStrategy = require('passport-http').BasicStrategy;
 
 
-const ConfigPubsubMessage = require('../models/vo/config-pubsub-message');
-const ConfigPubsubMessageHandlable = require('./config-pubsub/handlable');
+const S2sMessage = require('../models/vo/s2s-message');
+const S2sMessageHandlable = require('./s2s-messaging/handlable');
 
 
 /**
 /**
  * the service class of Passport
  * the service class of Passport
  */
  */
-class PassportService extends ConfigPubsubMessageHandlable {
+class PassportService extends S2sMessageHandlable {
 
 
   // see '/lib/form/login.js'
   // see '/lib/form/login.js'
   static get USERNAME_FIELD() { return 'loginForm[username]' }
   static get USERNAME_FIELD() { return 'loginForm[username]' }
@@ -129,21 +129,21 @@ class PassportService extends ConfigPubsubMessageHandlable {
   /**
   /**
    * @inheritdoc
    * @inheritdoc
    */
    */
-  shouldHandleConfigPubsubMessage(configPubsubMessage) {
-    const { eventName, updatedAt, strategyId } = configPubsubMessage;
+  shouldHandleS2sMessage(s2sMessage) {
+    const { eventName, updatedAt, strategyId } = s2sMessage;
     if (eventName !== 'passportServiceUpdated' || updatedAt == null || strategyId == null) {
     if (eventName !== 'passportServiceUpdated' || updatedAt == null || strategyId == null) {
       return false;
       return false;
     }
     }
 
 
-    return this.lastLoadedAt == null || this.lastLoadedAt < new Date(configPubsubMessage.updatedAt);
+    return this.lastLoadedAt == null || this.lastLoadedAt < new Date(s2sMessage.updatedAt);
   }
   }
 
 
   /**
   /**
    * @inheritdoc
    * @inheritdoc
    */
    */
-  async handleConfigPubsubMessage(configPubsubMessage) {
+  async handleS2sMessage(s2sMessage) {
     const { configManager } = this.crowi;
     const { configManager } = this.crowi;
-    const { strategyId } = configPubsubMessage;
+    const { strategyId } = s2sMessage;
 
 
     logger.info('Reset strategy by pubsub notification');
     logger.info('Reset strategy by pubsub notification');
     await configManager.loadConfigs();
     await configManager.loadConfigs();
@@ -151,19 +151,19 @@ class PassportService extends ConfigPubsubMessageHandlable {
   }
   }
 
 
   async publishUpdatedMessage(strategyId) {
   async publishUpdatedMessage(strategyId) {
-    const { configPubsub } = this.crowi;
+    const { s2sMessagingService } = this.crowi;
 
 
-    if (configPubsub != null) {
-      const configPubsubMessage = new ConfigPubsubMessage('passportStrategyReloaded', {
+    if (s2sMessagingService != null) {
+      const s2sMessage = new S2sMessage('passportStrategyReloaded', {
         updatedAt: new Date(),
         updatedAt: new Date(),
         strategyId,
         strategyId,
       });
       });
 
 
       try {
       try {
-        await configPubsub.publish(configPubsubMessage);
+        await s2sMessagingService.publish(s2sMessage);
       }
       }
       catch (e) {
       catch (e) {
-        logger.error('Failed to publish update message with configPubsub: ', e.message);
+        logger.error('Failed to publish update message with S2sMessagingService: ', e.message);
       }
       }
     }
     }
   }
   }

+ 13 - 11
src/server/service/config-pubsub/base.js → src/server/service/s2s-messaging/base.js

@@ -1,8 +1,8 @@
-const logger = require('@alias/logger')('growi:service:config-pubsub:base');
+const logger = require('@alias/logger')('growi:service:s2s-messaging:base');
 
 
-const ConfigPubsubMessageHandlable = require('../config-pubsub/handlable');
+const S2sMessageHandlable = require('./handlable');
 
 
-class ConfigPubsubDelegator {
+class S2sMessagingServiceDelegator {
 
 
   constructor(uri) {
   constructor(uri) {
     this.uid = Math.floor(Math.random() * 100000);
     this.uid = Math.floor(Math.random() * 100000);
@@ -11,6 +11,8 @@ class ConfigPubsubDelegator {
     if (uri == null) {
     if (uri == null) {
       throw new Error('uri must be set');
       throw new Error('uri must be set');
     }
     }
+
+    this.handlableList = [];
   }
   }
 
 
   shouldResubscribe() {
   shouldResubscribe() {
@@ -23,18 +25,18 @@ class ConfigPubsubDelegator {
 
 
   /**
   /**
    * Publish message
    * Publish message
-   * @param {ConfigPubsubMessage} configPubsubMessage
+   * @param {S2sMessage} s2sMessage
    */
    */
-  async publish(configPubsubMessage) {
-    configPubsubMessage.setPublisherUid(this.uid);
+  async publish(s2sMessage) {
+    s2sMessage.setPublisherUid(this.uid);
   }
   }
 
 
   /**
   /**
    * Add message handler
    * Add message handler
-   * @param {ConfigPubsubMessageHandlable} handlable
+   * @param {S2sMessageHandlable} handlable
    */
    */
   addMessageHandler(handlable) {
   addMessageHandler(handlable) {
-    if (!(handlable instanceof ConfigPubsubMessageHandlable)) {
+    if (!(handlable instanceof S2sMessageHandlable)) {
       logger.warn('Unsupported instance');
       logger.warn('Unsupported instance');
       logger.debug('Unsupported instance: ', handlable);
       logger.debug('Unsupported instance: ', handlable);
       return;
       return;
@@ -45,10 +47,10 @@ class ConfigPubsubDelegator {
 
 
   /**
   /**
    * Remove message handler
    * Remove message handler
-   * @param {ConfigPubsubMessageHandlable} handlable
+   * @param {S2sMessageHandlable} handlable
    */
    */
   removeMessageHandler(handlable) {
   removeMessageHandler(handlable) {
-    if (!(handlable instanceof ConfigPubsubMessageHandlable)) {
+    if (!(handlable instanceof S2sMessageHandlable)) {
       logger.warn('Unsupported instance');
       logger.warn('Unsupported instance');
       logger.debug('Unsupported instance: ', handlable);
       logger.debug('Unsupported instance: ', handlable);
       return;
       return;
@@ -59,4 +61,4 @@ class ConfigPubsubDelegator {
 
 
 }
 }
 
 
-module.exports = ConfigPubsubDelegator;
+module.exports = S2sMessagingServiceDelegator;

+ 18 - 0
src/server/service/s2s-messaging/handlable.js

@@ -0,0 +1,18 @@
+// TODO: make interface with TS
+
+/**
+ * The interface to handle server-to-server message
+ */
+class S2sMessageHandlable {
+
+  shouldHandleS2sMessage(s2sMessage) {
+    throw new Error('implement this');
+  }
+
+  async handleS2sMessage(s2sMessage) {
+    throw new Error('implement this');
+  }
+
+}
+
+module.exports = S2sMessageHandlable;

+ 7 - 4
src/server/service/config-pubsub/index.js → src/server/service/s2s-messaging/index.js

@@ -1,14 +1,17 @@
-const logger = require('@alias/logger')('growi:service:ConfigPubsubFactory');
+const logger = require('@alias/logger')('growi:service:s2s-messaging:S2sMessagingServiceFactory');
 
 
 const envToModuleMappings = {
 const envToModuleMappings = {
   redis:   'redis',
   redis:   'redis',
   nchan:   'nchan',
   nchan:   'nchan',
 };
 };
 
 
-class ConfigPubsubFactory {
+/**
+ * Instanciate server-to-server messaging service
+ */
+class S2sMessagingServiceFactory {
 
 
   initializeDelegator(crowi) {
   initializeDelegator(crowi) {
-    const type = crowi.configManager.getConfig('crowi', 'configPubsub:serverType');
+    const type = crowi.configManager.getConfig('crowi', 's2sMessagingPubsub:serverType');
 
 
     if (type == null) {
     if (type == null) {
       logger.info('Config pub/sub server is not defined.');
       logger.info('Config pub/sub server is not defined.');
@@ -36,7 +39,7 @@ class ConfigPubsubFactory {
 
 
 }
 }
 
 
-const factory = new ConfigPubsubFactory();
+const factory = new S2sMessagingServiceFactory();
 
 
 module.exports = (crowi) => {
 module.exports = (crowi) => {
   return factory.getDelegator(crowi);
   return factory.getDelegator(crowi);

+ 194 - 0
src/server/service/s2s-messaging/nchan.js

@@ -0,0 +1,194 @@
+const logger = require('@alias/logger')('growi:service:s2s-messaging:nchan');
+
+const path = require('path');
+const axios = require('axios');
+const WebSocket = require('ws');
+const ReconnectingWebSocket = require('reconnecting-websocket');
+
+const S2sMessage = require('../../models/vo/s2s-message');
+const S2sMessagingServiceDelegator = require('./base');
+
+
+class NchanDelegator extends S2sMessagingServiceDelegator {
+
+  constructor(uri, publishPath, subscribePath, channelId) {
+    super(uri);
+
+    this.publishPath = publishPath;
+    this.subscribePath = subscribePath;
+
+    this.channelId = channelId;
+
+    /**
+     * A list of S2sMessageHandlable instance
+     */
+    this.handlableToEventListenerMap = {};
+
+    this.socket = null;
+  }
+
+  /**
+   * @inheritdoc
+   */
+  shouldResubscribe() {
+    return this.socket.readyState === ReconnectingWebSocket.CLOSED;
+  }
+
+  /**
+   * @inheritdoc
+   */
+  subscribe(forceReconnect = false) {
+    if (forceReconnect) {
+      logger.info('Force reconnecting is requested. Try to reconnect...');
+    }
+    else if (this.socket != null && this.shouldResubscribe()) {
+      logger.info('The connection to config pubsub server is offline. Try to reconnect...');
+    }
+
+    // init client
+    if (this.socket == null) {
+      this.initClient();
+    }
+
+    // connect
+    if (forceReconnect || this.shouldResubscribe()) {
+      this.socket.reconnect();
+    }
+  }
+
+  /**
+   * @inheritdoc
+   */
+  async publish(s2sMessage) {
+    await super.publish(s2sMessage);
+
+    const url = this.constructUrl(this.publishPath).toString();
+
+    logger.debug('Publish message', s2sMessage, `to ${url}`);
+
+    return axios.post(url, s2sMessage);
+  }
+
+  /**
+   * @inheritdoc
+   */
+  addMessageHandler(handlable) {
+    if (this.socket == null) {
+      logger.error('socket has not initialized yet.');
+      return;
+    }
+
+    super.addMessageHandler(handlable);
+    this.registerMessageHandlerToSocket(handlable);
+  }
+
+  /**
+   * @inheritdoc
+   */
+  removeMessageHandler(handlable) {
+    if (this.socket == null) {
+      logger.error('socket has not initialized yet.');
+      return;
+    }
+
+    super.removeMessageHandler(handlable);
+
+    const eventListener = this.handlableToEventListenerMap[handlable];
+    if (eventListener != null) {
+      this.socket.removeEventListener('message', eventListener);
+      delete this.handlableToEventListenerMap[handlable];
+    }
+  }
+
+  registerMessageHandlerToSocket(handlable) {
+    const eventListener = (messageObj) => {
+      this.handleMessage(messageObj, handlable);
+    };
+
+    this.socket.addEventListener('message', eventListener);
+    this.handlableToEventListenerMap[handlable] = eventListener;
+  }
+
+  constructUrl(basepath) {
+    const pathname = this.channelId == null
+      ? basepath //                                 /pubsub
+      : path.join(basepath, this.channelId); //     /pubsub/my-channel-id
+
+    return new URL(pathname, this.uri);
+  }
+
+  initClient() {
+    // const client = new WebSocketClient();
+    const url = this.constructUrl(this.publishPath).toString();
+    const socket = new ReconnectingWebSocket(url, [], {
+      WebSocket,
+      maxRetries: 3,
+      startClosed: true,
+    });
+
+    socket.addEventListener('close', () => {
+      logger.info('WebSocket client disconnected');
+    });
+    socket.addEventListener('error', (error) => {
+      logger.error('WebSocket error occured:', error.message);
+    });
+
+    socket.addEventListener('open', () => {
+      logger.info('WebSocket client connected.');
+    });
+
+    this.handlableList.forEach(handlable => this.registerMessageHandlerToSocket(handlable));
+
+    this.socket = socket;
+  }
+
+  /**
+   * Handle message string with the specified S2sMessageHandlable
+   *
+   * @see https://github.com/theturtle32/WebSocket-Node/blob/1f7ffba2f7a6f9473bcb39228264380ce2772ba7/docs/WebSocketConnection.md#message
+   *
+   * @param {object} message WebSocket-Node message object
+   * @param {S2sMessageHandlable} handlable
+   */
+  handleMessage(message, handlable) {
+    try {
+      const s2sMessage = S2sMessage.parse(message.data);
+
+      // check uid
+      if (s2sMessage.publisherUid === this.uid) {
+        logger.debug(`Skip processing by ${handlable.constructor.name} because this message is sent by the publisher itself:`, `from ${this.uid}`);
+        return;
+      }
+
+      // check shouldHandleS2sMessage
+      const shouldHandle = handlable.shouldHandleS2sMessage(s2sMessage);
+      logger.debug(`${handlable.constructor.name}.shouldHandleS2sMessage(`, s2sMessage, `) => ${shouldHandle}`);
+
+      if (shouldHandle) {
+        handlable.handleS2sMessage(s2sMessage);
+      }
+    }
+    catch (err) {
+      logger.warn('Could not handle a message: ', err.message);
+    }
+  }
+
+}
+
+module.exports = function(crowi) {
+  const { configManager } = crowi;
+
+  const uri = configManager.getConfig('crowi', 'app:nchanUri');
+
+  // when nachan server URI is not set
+  if (uri == null) {
+    logger.warn('NCHAN_URI is not specified.');
+    return;
+  }
+
+  const publishPath = configManager.getConfig('crowi', 's2sMessagingPubsub:nchan:publishPath');
+  const subscribePath = configManager.getConfig('crowi', 's2sMessagingPubsub:nchan:subscribePath');
+  const channelId = configManager.getConfig('crowi', 's2sMessagingPubsub:nchan:channelId');
+
+  return new NchanDelegator(uri, publishPath, subscribePath, channelId);
+};

+ 1 - 1
src/server/service/config-pubsub/redis.js → src/server/service/s2s-messaging/redis.js

@@ -1,4 +1,4 @@
-const logger = require('@alias/logger')('growi:service:config-pubsub:redis');
+const logger = require('@alias/logger')('growi:service:s2s-messaging:redis');
 
 
 module.exports = function(crowi) {
 module.exports = function(crowi) {
   logger.warn('Config pub/sub with Redis has not implemented yet.');
   logger.warn('Config pub/sub with Redis has not implemented yet.');

+ 7 - 7
src/server/service/search-delegator/elasticsearch.js

@@ -17,9 +17,9 @@ const BULK_REINDEX_SIZE = 100;
 
 
 class ElasticsearchDelegator {
 class ElasticsearchDelegator {
 
 
-  constructor(configManager, searchEvent) {
+  constructor(configManager, socketIoService) {
     this.configManager = configManager;
     this.configManager = configManager;
-    this.searchEvent = searchEvent;
+    this.socketIoService = socketIoService;
 
 
     this.client = null;
     this.client = null;
 
 
@@ -225,8 +225,8 @@ class ElasticsearchDelegator {
     catch (error) {
     catch (error) {
       logger.warn('An error occured while \'rebuildIndex\', normalize indices anyway.');
       logger.warn('An error occured while \'rebuildIndex\', normalize indices anyway.');
 
 
-      const { searchEvent } = this;
-      searchEvent.emit('rebuildingFailed', error);
+      const socket = this.socketIoService.getAdminSocket();
+      socket.emit('rebuildingFailed', { error: error.message });
 
 
       throw error;
       throw error;
     }
     }
@@ -360,7 +360,7 @@ class ElasticsearchDelegator {
     const Bookmark = mongoose.model('Bookmark');
     const Bookmark = mongoose.model('Bookmark');
     const PageTagRelation = mongoose.model('PageTagRelation');
     const PageTagRelation = mongoose.model('PageTagRelation');
 
 
-    const { searchEvent } = this;
+    const socket = this.socketIoService.getAdminSocket();
 
 
     // prepare functions invoked from custom streams
     // prepare functions invoked from custom streams
     const prepareBodyForCreate = this.prepareBodyForCreate.bind(this);
     const prepareBodyForCreate = this.prepareBodyForCreate.bind(this);
@@ -458,7 +458,7 @@ class ElasticsearchDelegator {
           logger.info(`Adding pages progressing: (count=${count}, errors=${res.errors}, took=${res.took}ms)`);
           logger.info(`Adding pages progressing: (count=${count}, errors=${res.errors}, took=${res.took}ms)`);
 
 
           if (isEmittingProgressEvent) {
           if (isEmittingProgressEvent) {
-            searchEvent.emit('addPageProgress', totalCount, count, skipped);
+            socket.emit('addPageProgress', { totalCount, count, skipped });
           }
           }
         }
         }
         catch (err) {
         catch (err) {
@@ -471,7 +471,7 @@ class ElasticsearchDelegator {
         logger.info(`Adding pages has completed: (totalCount=${totalCount}, skipped=${skipped})`);
         logger.info(`Adding pages has completed: (totalCount=${totalCount}, skipped=${skipped})`);
 
 
         if (isEmittingProgressEvent) {
         if (isEmittingProgressEvent) {
-          searchEvent.emit('finishAddPage', totalCount, count, skipped);
+          socket.emit('finishAddPage', { totalCount, count, skipped });
         }
         }
         callback();
         callback();
       },
       },

+ 2 - 4
src/server/service/search.js

@@ -42,17 +42,15 @@ class SearchService {
   initDelegator() {
   initDelegator() {
     logger.info('Initializing search delegator');
     logger.info('Initializing search delegator');
 
 
-    const searchEvent = this.crowi.event('search');
-
     if (this.isSearchboxEnabled) {
     if (this.isSearchboxEnabled) {
       logger.info('Searchbox is enabled');
       logger.info('Searchbox is enabled');
       const SearchboxDelegator = require('./search-delegator/searchbox.js');
       const SearchboxDelegator = require('./search-delegator/searchbox.js');
-      return new SearchboxDelegator(this.configManager, searchEvent);
+      return new SearchboxDelegator(this.configManager, this.crowi.socketIoService);
     }
     }
     if (this.isElasticsearchEnabled) {
     if (this.isElasticsearchEnabled) {
       logger.info('Elasticsearch (not Searchbox) is enabled');
       logger.info('Elasticsearch (not Searchbox) is enabled');
       const ElasticsearchDelegator = require('./search-delegator/elasticsearch.js');
       const ElasticsearchDelegator = require('./search-delegator/elasticsearch.js');
-      return new ElasticsearchDelegator(this.configManager, searchEvent);
+      return new ElasticsearchDelegator(this.configManager, this.crowi.socketIoService);
     }
     }
 
 
   }
   }

+ 37 - 0
src/server/service/socket-io.js

@@ -0,0 +1,37 @@
+const socketIo = require('socket.io');
+
+/**
+ * Serve socket.io for server-to-client messaging
+ */
+class SocketIoService {
+
+  get isInitialized() {
+    return (this.io != null);
+  }
+
+  attachServer(server) {
+    this.io = socketIo(server, {
+      transports: ['websocket'],
+    });
+
+    // create namespace for admin
+    this.adminNamespace = this.io.of('/admin');
+  }
+
+  getDefaultSocket() {
+    if (this.io == null) {
+      throw new Error('Http server has not attached yet.');
+    }
+    return this.io.sockets;
+  }
+
+  getAdminSocket() {
+    if (this.io == null) {
+      throw new Error('Http server has not attached yet.');
+    }
+    return this.adminNamespace;
+  }
+
+}
+
+module.exports = SocketIoService;

+ 110 - 0
src/server/service/system-events/sync-page-status.js

@@ -0,0 +1,110 @@
+const logger = require('@alias/logger')('growi:service:system-events:SyncPageStatusService');
+
+const S2sMessage = require('../../models/vo/s2s-message');
+const S2sMessageHandlable = require('../s2s-messaging/handlable');
+
+/**
+ * This service notify page status
+ *  to clients who are connecting to this server
+ *  and also notify to clients connecting to other GROWI servers
+ *
+ * Sequence:
+ *  1. A client A1 connecting to GROWI server A updates a page
+ *  2. GROWI server A notify the information
+ *    2.1 -- to client A2, A3, ... with SocketIoService
+ *    2.2 -- to other GROWI servers with S2sMessagingService
+ *  3. GROWI server B, C, ... relay the information to clients B1, B2, .. C1, C2, ... with SocketIoService
+ *
+ */
+class SyncPageStatusService extends S2sMessageHandlable {
+
+  constructor(crowi, s2sMessagingService, socketIoService) {
+    super();
+
+    this.crowi = crowi;
+    this.s2sMessagingService = s2sMessagingService;
+    this.socketIoService = socketIoService;
+
+    this.emitter = crowi.events.page;
+
+    this.initSystemEventListeners();
+  }
+
+  /**
+   * @inheritdoc
+   */
+  shouldHandleS2sMessage(s2sMessage) {
+    const { eventName } = s2sMessage;
+    if (eventName !== 'pageStatusUpdated') {
+      return false;
+    }
+
+    return true;
+  }
+
+  /**
+   * @inheritdoc
+   */
+  async handleS2sMessage(s2sMessage) {
+    const { socketIoEventName, page, user } = s2sMessage;
+    const { socketIoService } = this;
+
+    // emit the updated information to clients
+    if (socketIoService.isInitialized) {
+      socketIoService.getDefaultSocket().emit(socketIoEventName, { page, user });
+    }
+  }
+
+  async publishToOtherServers(socketIoEventName, page, user) {
+    const { s2sMessagingService } = this;
+
+    if (s2sMessagingService != null) {
+      const s2sMessage = new S2sMessage('pageStatusUpdated', { socketIoEventName, page, user });
+
+      try {
+        await s2sMessagingService.publish(s2sMessage);
+      }
+      catch (e) {
+        logger.error('Failed to publish update message with S2sMessagingService: ', e.message);
+      }
+    }
+  }
+
+  initSystemEventListeners() {
+    const { socketIoService } = this;
+    const { pageService } = this.crowi;
+
+    // register events
+    this.emitter.on('create', (page, user, socketClientId) => {
+      logger.debug('\'create\' event emitted.');
+
+      page = pageService.serializeToObj(page); // eslint-disable-line no-param-reassign
+      socketIoService.getDefaultSocket().emit('page:create', { page, user, socketClientId });
+
+      this.publishToOtherServers('page:create', page, user);
+    });
+    this.emitter.on('update', (page, user, socketClientId) => {
+      logger.debug('\'update\' event emitted.');
+
+      page = pageService.serializeToObj(page); // eslint-disable-line no-param-reassign
+      socketIoService.getDefaultSocket().emit('page:update', { page, user, socketClientId });
+
+      this.publishToOtherServers('page:update', page, user);
+    });
+    this.emitter.on('delete', (page, user, socketClientId) => {
+      logger.debug('\'delete\' event emitted.');
+
+      page = pageService.serializeToObj(page); // eslint-disable-line no-param-reassign
+      socketIoService.getDefaultSocket().emit('page:delete', { page, user, socketClientId });
+
+      this.publishToOtherServers('page:delete', page, user);
+    });
+    this.emitter.on('saveOnHackmd', (page) => {
+      socketIoService.getDefaultSocket().emit('page:editingWithHackmd', { page });
+      this.publishToOtherServers('page:editingWithHackmd', page);
+    });
+  }
+
+}
+
+module.exports = SyncPageStatusService;

+ 2 - 0
src/server/views/layout/layout.html

@@ -23,6 +23,8 @@
     {% include '../widget/headers/mathjax.html' %}
     {% include '../widget/headers/mathjax.html' %}
   {% endif %}
   {% endif %}
 
 
+  {% include '../widget/headers/drawio.html' %}
+
   {% include '../widget/headers/scripts-for-dev.html' %}
   {% include '../widget/headers/scripts-for-dev.html' %}
 
 
   <script src="{{ webpack_asset('js/boot.js') }}"></script>
   <script src="{{ webpack_asset('js/boot.js') }}"></script>

+ 2 - 0
src/server/views/search.html

@@ -12,6 +12,8 @@
 {% endblock %}
 {% endblock %}
 
 
 {% block layout_main %}
 {% block layout_main %}
+<div id="grw-fav-sticky-trigger" class="sticky-top"></div>
+
 <div class="container-fluid">
 <div class="container-fluid">
 
 
   <div class="row">
   <div class="row">

+ 31 - 0
src/server/views/widget/headers/drawio.html

@@ -0,0 +1,31 @@
+<!-- draw.io -->
+{% if getConfig('crowi', 'app:drawioUri') %}
+<script type="text/javascript">
+  // refs: https://github.com/jgraph/drawio/blob/v13.4.3/etc/build/build.xml#L35-L38
+  let url = new URL("{{ getConfig('crowi', 'app:drawioUri') }}");
+  let origin = url.origin;
+  window.DRAWIO_BASE_URL = origin;
+  window.DRAWIO_LIGHTBOX_URL = origin;
+  window.STENCIL_PATH = [origin, 'stencils'].join('/');
+  window.SHAPES_PATH = [origin, 'shapes'].join('/');
+  window.mxBasePath = [origin, 'mxgraph'].join('/');
+</script>
+{% endif %}
+
+<script type="text/javascript">
+  // define callback function invoked by viewer.min.js of draw.io
+  // refs: https://github.com/jgraph/drawio/blob/v12.9.1/etc/build/build.xml#L219-L232
+  window.onDrawioViewerLoad = function() {
+    const DrawioViewer = window.GraphViewer;
+
+    if (DrawioViewer != null) {
+      // disable useResizeSensor and checkVisibleState
+      //   for preventing resize event by viewer.min.js
+      DrawioViewer.useResizeSensor = false;
+      DrawioViewer.prototype.checkVisibleState = false;
+
+      // initialize
+      DrawioViewer.processElements();
+    }
+  };
+</script>

+ 17 - 12
src/server/views/widget/headers/mathjax.html

@@ -1,16 +1,21 @@
 <!-- Mathjax -->
 <!-- Mathjax -->
-<script type="text/x-mathjax-config" async>
-  MathJax.Hub.Config({
-    skipStartupTypeset: true,
-    extensions: ["tex2jax.js"],
-    jax: ["input/TeX", "output/SVG"],
-    tex2jax: {
-      processEscapes: true
+<script type="text/javascript">
+  window.MathJax = {
+    startup: {
+      typeset: false
     },
     },
-    showMathMenu: false,
-    showMathMenuMSIE: false,
-    showProcessingMessages: false,
-    messageStyle: "none"
-  });
+    tex: {
+      processEscapes: true,
+      inlineMath: [['$', '$'], ['\\(', '\\)']]
+    },
+    options: {
+      renderActions: {
+        addMenu: [],
+        checkLoading: []
+      },
+      ignoreHtmlClass: 'tex2jax_ignore',
+      processHtmlClass: 'tex2jax_process'
+    }
+  };
 </script>
 </script>
 {{ cdnScriptTag('mathjax') }}
 {{ cdnScriptTag('mathjax') }}

+ 1 - 1
src/server/views/widget/page_list.html

@@ -8,7 +8,7 @@
 {% endif %}
 {% endif %}
 
 
 <li>
 <li>
-  <img src="{{ listPage.lastUpdateUser.imageUrlCached }}" class="picture rounded-circle">
+  <img src="{{ listPage.lastUpdateUser.imageUrlCached|default('/images/icons/user.svg') }}" class="picture rounded-circle">
   <a href="{{ encodeURI(listPage.path) }}" class="text-break ml-1">
   <a href="{{ encodeURI(listPage.path) }}" class="text-break ml-1">
     {{ listPage.path | preventXss }}
     {{ listPage.path | preventXss }}
   </a>
   </a>

+ 125 - 0
src/test/models/page.test.js

@@ -6,6 +6,7 @@ let testUser0;
 let testUser1;
 let testUser1;
 let testUser2;
 let testUser2;
 let testGroup0;
 let testGroup0;
+let parentPage;
 
 
 describe('Page', () => {
 describe('Page', () => {
   // eslint-disable-next-line no-unused-vars
   // eslint-disable-next-line no-unused-vars
@@ -61,6 +62,12 @@ describe('Page', () => {
         grantedUsers: [testUser0],
         grantedUsers: [testUser0],
         creator: testUser0,
         creator: testUser0,
       },
       },
+      {
+        path: '/grant',
+        grant: Page.GRANT_PUBLIC,
+        grantedUsers: [testUser0],
+        creator: testUser0,
+      },
       {
       {
         path: '/grant/public',
         path: '/grant/public',
         grant: Page.GRANT_PUBLIC,
         grant: Page.GRANT_PUBLIC,
@@ -115,6 +122,8 @@ describe('Page', () => {
       },
       },
     ]);
     ]);
 
 
+    parentPage = await Page.findOne({ path: '/grant' });
+
     done();
     done();
   });
   });
 
 
@@ -374,4 +383,120 @@ describe('Page', () => {
       expect(pagePaths).toContainEqual('/page2');
       expect(pagePaths).toContainEqual('/page2');
     });
     });
   });
   });
+
+  describe('.findListWithDescendants', () => {
+    test('can retrieve all pages with testUser0', async() => {
+      const result = await Page.findListWithDescendants('/grant', testUser0);
+      const { pages } = result;
+
+      // assert totalCount
+      expect(pages.length).toEqual(5);
+
+      // assert paths
+      const pagePaths = await pages.map((page) => { return page.path });
+      expect(pagePaths).toContainEqual('/grant/groupacl');
+      expect(pagePaths).toContainEqual('/grant/specified');
+      expect(pagePaths).toContainEqual('/grant/owner');
+      expect(pagePaths).toContainEqual('/grant/public');
+      expect(pagePaths).toContainEqual('/grant');
+    });
+
+    test('can retrieve all pages with testUser1', async() => {
+      const result = await Page.findListWithDescendants('/grant', testUser1);
+      const { pages } = result;
+
+      // assert totalCount
+      expect(pages.length).toEqual(5);
+
+      // assert paths
+      const pagePaths = await pages.map((page) => { return page.path });
+      expect(pagePaths).toContainEqual('/grant/groupacl');
+      expect(pagePaths).toContainEqual('/grant/specified');
+      expect(pagePaths).toContainEqual('/grant/owner');
+      expect(pagePaths).toContainEqual('/grant/public');
+      expect(pagePaths).toContainEqual('/grant');
+    });
+
+    test('can retrieve all pages with testUser2', async() => {
+      const result = await Page.findListWithDescendants('/grant', testUser2);
+      const { pages } = result;
+
+      // assert totalCount
+      expect(pages.length).toEqual(5);
+
+      // assert paths
+      const pagePaths = await pages.map((page) => { return page.path });
+      expect(pagePaths).toContainEqual('/grant/groupacl');
+      expect(pagePaths).toContainEqual('/grant/specified');
+      expect(pagePaths).toContainEqual('/grant/owner');
+      expect(pagePaths).toContainEqual('/grant/public');
+      expect(pagePaths).toContainEqual('/grant');
+    });
+
+    test('can retrieve all pages without user', async() => {
+      const result = await Page.findListWithDescendants('/grant', null);
+      const { pages } = result;
+
+      // assert totalCount
+      expect(pages.length).toEqual(5);
+
+      // assert paths
+      const pagePaths = await pages.map((page) => { return page.path });
+      expect(pagePaths).toContainEqual('/grant/groupacl');
+      expect(pagePaths).toContainEqual('/grant/specified');
+      expect(pagePaths).toContainEqual('/grant/owner');
+      expect(pagePaths).toContainEqual('/grant/public');
+      expect(pagePaths).toContainEqual('/grant');
+    });
+  });
+
+  describe('.findManageableListWithDescendants', () => {
+    test('can retrieve all pages with testUser0', async() => {
+      const pages = await Page.findManageableListWithDescendants(parentPage, testUser0);
+
+      // assert totalCount
+      expect(pages.length).toEqual(5);
+
+      // assert paths
+      const pagePaths = await pages.map((page) => { return page.path });
+      expect(pagePaths).toContainEqual('/grant/groupacl');
+      expect(pagePaths).toContainEqual('/grant/specified');
+      expect(pagePaths).toContainEqual('/grant/owner');
+      expect(pagePaths).toContainEqual('/grant/public');
+      expect(pagePaths).toContainEqual('/grant');
+    });
+
+    test('can retrieve group page and public page which starts with testUser1', async() => {
+      const pages = await Page.findManageableListWithDescendants(parentPage, testUser1);
+
+      // assert totalCount
+      expect(pages.length).toEqual(3);
+
+      // assert paths
+      const pagePaths = await pages.map((page) => { return page.path });
+      expect(pagePaths).toContainEqual('/grant/groupacl');
+      expect(pagePaths).toContainEqual('/grant/public');
+      expect(pagePaths).toContainEqual('/grant');
+    });
+
+    test('can retrieve only public page which starts with testUser2', async() => {
+      const pages = await Page.findManageableListWithDescendants(parentPage, testUser2);
+
+      // assert totalCount
+      expect(pages.length).toEqual(2);
+
+      // assert paths
+      const pagePaths = await pages.map((page) => { return page.path });
+      expect(pagePaths).toContainEqual('/grant/public');
+      expect(pagePaths).toContainEqual('/grant');
+    });
+
+    test('can retrieve only public page which starts without user', async() => {
+      const pages = await Page.findManageableListWithDescendants(parentPage, null);
+
+      // assert totalCount
+      expect(pages).toBeNull();
+    });
+  });
+
 });
 });

+ 1 - 1
src/test/service/config-manager.test.js

@@ -18,7 +18,7 @@ describe('ConfigManager test', () => {
     const configModelMock = {};
     const configModelMock = {};
 
 
     beforeEach(async(done) => {
     beforeEach(async(done) => {
-      configManager.configPubsub = {};
+      configManager.s2sMessagingService = {};
 
 
       // prepare mocks for updateConfigsInTheSameNamespace method
       // prepare mocks for updateConfigsInTheSameNamespace method
       configManager.configModel = configModelMock;
       configManager.configModel = configModelMock;

+ 106 - 144
yarn.lock

@@ -1668,6 +1668,11 @@
   resolved "https://registry.yarnpkg.com/@kaishuu0123/markdown-it-fence/-/markdown-it-fence-0.2.0.tgz#f46722bfce4ab7eb3e051def5090dcae1bd6e36b"
   resolved "https://registry.yarnpkg.com/@kaishuu0123/markdown-it-fence/-/markdown-it-fence-0.2.0.tgz#f46722bfce4ab7eb3e051def5090dcae1bd6e36b"
   integrity sha512-mdqKA+bXfJPl7gAg9tis8fGlea2oppBM068YbMDSXKWM6H18nVSZLrVKPHXpPWBgSv1ceeKkoWj8K1ntpIHlrw==
   integrity sha512-mdqKA+bXfJPl7gAg9tis8fGlea2oppBM068YbMDSXKWM6H18nVSZLrVKPHXpPWBgSv1ceeKkoWj8K1ntpIHlrw==
 
 
+"@kaishuu0123/markdown-it-fence@^1.0.0":
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/@kaishuu0123/markdown-it-fence/-/markdown-it-fence-1.0.0.tgz#07525441b731e9ba518d886e203da2557e533f0e"
+  integrity sha512-4e+1JVCN3Qg2KvZkyvlmaay929bfeqf3MyA9agyx49gXKYhp5fvFQD0/0moBP52Kj/u0LCdVEnNWXn1s8Zi5sQ==
+
 "@lykmapipo/common@>=0.34.2", "@lykmapipo/common@>=0.34.3":
 "@lykmapipo/common@>=0.34.2", "@lykmapipo/common@>=0.34.3":
   version "0.34.3"
   version "0.34.3"
   resolved "https://registry.yarnpkg.com/@lykmapipo/common/-/common-0.34.3.tgz#eb74fa4af14f2f1e59ddd42491f05ab69f96bd71"
   resolved "https://registry.yarnpkg.com/@lykmapipo/common/-/common-0.34.3.tgz#eb74fa4af14f2f1e59ddd42491f05ab69f96bd71"
@@ -2166,13 +2171,6 @@ abort-controller@^3.0.0:
   dependencies:
   dependencies:
     event-target-shim "^5.0.0"
     event-target-shim "^5.0.0"
 
 
-accepts@1.3.3:
-  version "1.3.3"
-  resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.3.tgz#c3ca7434938648c3e0d9c1e328dd68b622c284ca"
-  dependencies:
-    mime-types "~2.1.11"
-    negotiator "0.6.1"
-
 accepts@~1.3.4:
 accepts@~1.3.4:
   version "1.3.4"
   version "1.3.4"
   resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.4.tgz#86246758c7dd6d21a6474ff084a4740ec05eb21f"
   resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.4.tgz#86246758c7dd6d21a6474ff084a4740ec05eb21f"
@@ -2890,6 +2888,11 @@ base64id@1.0.0:
   version "1.0.0"
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/base64id/-/base64id-1.0.0.tgz#47688cb99bb6804f0e06d3e763b1c32e57d8e6b6"
   resolved "https://registry.yarnpkg.com/base64id/-/base64id-1.0.0.tgz#47688cb99bb6804f0e06d3e763b1c32e57d8e6b6"
 
 
+base64id@2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6"
+  integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==
+
 base64url@^3.0.0, base64url@^3.0.1:
 base64url@^3.0.0, base64url@^3.0.1:
   version "3.0.1"
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d"
   resolved "https://registry.yarnpkg.com/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d"
@@ -3031,6 +3034,11 @@ blob@0.0.4:
   version "0.0.4"
   version "0.0.4"
   resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.4.tgz#bcf13052ca54463f30f9fc7e95b9a47630a94921"
   resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.4.tgz#bcf13052ca54463f30f9fc7e95b9a47630a94921"
 
 
+blob@0.0.5:
+  version "0.0.5"
+  resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.5.tgz#d680eeef25f8cd91ad533f5b01eed48e64caf683"
+  integrity sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==
+
 block-stream@*:
 block-stream@*:
   version "0.0.9"
   version "0.0.9"
   resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a"
   resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a"
@@ -4001,6 +4009,11 @@ component-emitter@1.2.1, component-emitter@^1.2.1, component-emitter@~1.2.0:
   version "1.2.1"
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6"
   resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6"
 
 
+component-emitter@~1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
+  integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==
+
 component-ie@^1.0.0:
 component-ie@^1.0.0:
   version "1.0.0"
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/component-ie/-/component-ie-1.0.0.tgz#0f9582ccb078a687592cc29eb46b3186e6fe637f"
   resolved "https://registry.yarnpkg.com/component-ie/-/component-ie-1.0.0.tgz#0f9582ccb078a687592cc29eb46b3186e6fe637f"
@@ -4624,14 +4637,6 @@ cyclist@~0.2.2:
   version "0.2.2"
   version "0.2.2"
   resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640"
   resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640"
 
 
-d@1, d@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a"
-  integrity sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==
-  dependencies:
-    es5-ext "^0.10.50"
-    type "^1.0.1"
-
 dashdash@^1.12.0, dashdash@^1.14.0:
 dashdash@^1.12.0, dashdash@^1.14.0:
   version "1.14.1"
   version "1.14.1"
   resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
   resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
@@ -4691,7 +4696,7 @@ debounce@^1.0.0:
   version "1.1.0"
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.1.0.tgz#6a1a4ee2a9dc4b7c24bb012558dbcdb05b37f408"
   resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.1.0.tgz#6a1a4ee2a9dc4b7c24bb012558dbcdb05b37f408"
 
 
-debug@2, debug@2.6.9, debug@^2.0.0, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9, debug@~2.6.4, debug@~2.6.6, debug@~2.6.9:
+debug@2, debug@2.6.9, debug@^2.0.0, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9:
   version "2.6.9"
   version "2.6.9"
   resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
   resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
   dependencies:
   dependencies:
@@ -4711,7 +4716,7 @@ debug@^3.1.0, debug@^3.2.6:
   dependencies:
   dependencies:
     ms "^2.1.1"
     ms "^2.1.1"
 
 
-debug@^4.0.1, debug@^4.1.0, debug@^4.1.1:
+debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@~4.1.0:
   version "4.1.1"
   version "4.1.1"
   resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
   resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
   dependencies:
   dependencies:
@@ -5185,13 +5190,13 @@ end-of-stream@~1.1.0:
   dependencies:
   dependencies:
     once "~1.3.0"
     once "~1.3.0"
 
 
-engine.io-client@~3.1.0:
-  version "3.1.4"
-  resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-3.1.4.tgz#4fcf1370b47163bd2ce9be2733972430350d4ea1"
+engine.io-client@~3.2.0:
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-3.2.1.tgz#6f54c0475de487158a1a7c77d10178708b6add36"
   dependencies:
   dependencies:
     component-emitter "1.2.1"
     component-emitter "1.2.1"
     component-inherit "0.0.3"
     component-inherit "0.0.3"
-    debug "~2.6.9"
+    debug "~3.1.0"
     engine.io-parser "~2.1.1"
     engine.io-parser "~2.1.1"
     has-cors "1.1.0"
     has-cors "1.1.0"
     indexof "0.0.1"
     indexof "0.0.1"
@@ -5201,9 +5206,9 @@ engine.io-client@~3.1.0:
     xmlhttprequest-ssl "~1.5.4"
     xmlhttprequest-ssl "~1.5.4"
     yeast "0.1.2"
     yeast "0.1.2"
 
 
-engine.io-client@~3.2.0:
-  version "3.2.1"
-  resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-3.2.1.tgz#6f54c0475de487158a1a7c77d10178708b6add36"
+engine.io-client@~3.3.1:
+  version "3.3.2"
+  resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-3.3.2.tgz#04e068798d75beda14375a264bb3d742d7bc33aa"
   dependencies:
   dependencies:
     component-emitter "1.2.1"
     component-emitter "1.2.1"
     component-inherit "0.0.3"
     component-inherit "0.0.3"
@@ -5213,18 +5218,19 @@ engine.io-client@~3.2.0:
     indexof "0.0.1"
     indexof "0.0.1"
     parseqs "0.0.5"
     parseqs "0.0.5"
     parseuri "0.0.5"
     parseuri "0.0.5"
-    ws "~3.3.1"
+    ws "~6.1.0"
     xmlhttprequest-ssl "~1.5.4"
     xmlhttprequest-ssl "~1.5.4"
     yeast "0.1.2"
     yeast "0.1.2"
 
 
-engine.io-client@~3.3.1:
-  version "3.3.2"
-  resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-3.3.2.tgz#04e068798d75beda14375a264bb3d742d7bc33aa"
+engine.io-client@~3.4.0:
+  version "3.4.3"
+  resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-3.4.3.tgz#192d09865403e3097e3575ebfeb3861c4d01a66c"
+  integrity sha512-0NGY+9hioejTEJCaSJZfWZLk4FPI9dN+1H1C4+wj2iuFba47UgZbJzfWs4aNFajnX/qAaYKbe2lLTfEEWzCmcw==
   dependencies:
   dependencies:
-    component-emitter "1.2.1"
+    component-emitter "~1.3.0"
     component-inherit "0.0.3"
     component-inherit "0.0.3"
-    debug "~3.1.0"
-    engine.io-parser "~2.1.1"
+    debug "~4.1.0"
+    engine.io-parser "~2.2.0"
     has-cors "1.1.0"
     has-cors "1.1.0"
     indexof "0.0.1"
     indexof "0.0.1"
     parseqs "0.0.5"
     parseqs "0.0.5"
@@ -5243,18 +5249,16 @@ engine.io-parser@~2.1.0, engine.io-parser@~2.1.1:
     blob "0.0.4"
     blob "0.0.4"
     has-binary2 "~1.0.2"
     has-binary2 "~1.0.2"
 
 
-engine.io@~3.1.0:
-  version "3.1.4"
-  resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-3.1.4.tgz#3d0211b70a552ce841ffc7da8627b301a9a4162e"
+engine.io-parser@~2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-2.2.0.tgz#312c4894f57d52a02b420868da7b5c1c84af80ed"
+  integrity sha512-6I3qD9iUxotsC5HEMuuGsKA0cXerGz+4uGcXQEkfBidgKf0amsjrrtwcbwK/nzpZBxclXlV7gGl9dgWvu4LF6w==
   dependencies:
   dependencies:
-    accepts "1.3.3"
-    base64id "1.0.0"
-    cookie "0.3.1"
-    debug "~2.6.9"
-    engine.io-parser "~2.1.0"
-    ws "~3.3.1"
-  optionalDependencies:
-    uws "~0.14.4"
+    after "0.8.2"
+    arraybuffer.slice "~0.0.7"
+    base64-arraybuffer "0.1.5"
+    blob "0.0.5"
+    has-binary2 "~1.0.2"
 
 
 engine.io@~3.2.0:
 engine.io@~3.2.0:
   version "3.2.1"
   version "3.2.1"
@@ -5267,6 +5271,18 @@ engine.io@~3.2.0:
     engine.io-parser "~2.1.0"
     engine.io-parser "~2.1.0"
     ws "~3.3.1"
     ws "~3.3.1"
 
 
+engine.io@~3.4.0:
+  version "3.4.2"
+  resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-3.4.2.tgz#8fc84ee00388e3e228645e0a7d3dfaeed5bd122c"
+  integrity sha512-b4Q85dFkGw+TqgytGPrGgACRUhsdKc9S9ErRAXpPGy/CXKs4tYoHDkvIRdsseAF7NjfVwjRFIn6KTnbw7LwJZg==
+  dependencies:
+    accepts "~1.3.4"
+    base64id "2.0.0"
+    cookie "0.3.1"
+    debug "~4.1.0"
+    engine.io-parser "~2.2.0"
+    ws "^7.1.2"
+
 enhanced-resolve@4.1.0, enhanced-resolve@^4.1.0:
 enhanced-resolve@4.1.0, enhanced-resolve@^4.1.0:
   version "4.1.0"
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz#41c7e0bfdfe74ac1ffe1e57ad6a5c6c9f3742a7f"
   resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz#41c7e0bfdfe74ac1ffe1e57ad6a5c6c9f3742a7f"
@@ -5408,24 +5424,6 @@ es-to-primitive@^1.2.0:
     is-date-object "^1.0.1"
     is-date-object "^1.0.1"
     is-symbol "^1.0.2"
     is-symbol "^1.0.2"
 
 
-es5-ext@^0.10.35, es5-ext@^0.10.50:
-  version "0.10.53"
-  resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.53.tgz#93c5a3acfdbef275220ad72644ad02ee18368de1"
-  integrity sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==
-  dependencies:
-    es6-iterator "~2.0.3"
-    es6-symbol "~3.1.3"
-    next-tick "~1.0.0"
-
-es6-iterator@~2.0.3:
-  version "2.0.3"
-  resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7"
-  integrity sha1-p96IkUGgWpSwhUQDstCg+/qY87c=
-  dependencies:
-    d "1"
-    es5-ext "^0.10.35"
-    es6-symbol "^3.1.1"
-
 es6-object-assign@^1.1.0:
 es6-object-assign@^1.1.0:
   version "1.1.0"
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/es6-object-assign/-/es6-object-assign-1.1.0.tgz#c2c3582656247c39ea107cb1e6652b6f9f24523c"
   resolved "https://registry.yarnpkg.com/es6-object-assign/-/es6-object-assign-1.1.0.tgz#c2c3582656247c39ea107cb1e6652b6f9f24523c"
@@ -5452,14 +5450,6 @@ es6-promisify@^5.0.0:
   dependencies:
   dependencies:
     es6-promise "^4.0.3"
     es6-promise "^4.0.3"
 
 
-es6-symbol@^3.1.1, es6-symbol@~3.1.3:
-  version "3.1.3"
-  resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18"
-  integrity sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==
-  dependencies:
-    d "^1.0.1"
-    ext "^1.1.2"
-
 esa-nodejs@^0.0.7:
 esa-nodejs@^0.0.7:
   version "0.0.7"
   version "0.0.7"
   resolved "https://registry.yarnpkg.com/esa-nodejs/-/esa-nodejs-0.0.7.tgz#c4749412605ad430d5da17aa4928291927561b42"
   resolved "https://registry.yarnpkg.com/esa-nodejs/-/esa-nodejs-0.0.7.tgz#c4749412605ad430d5da17aa4928291927561b42"
@@ -5939,13 +5929,6 @@ express@^4.16.3:
     utils-merge "1.0.1"
     utils-merge "1.0.1"
     vary "~1.1.2"
     vary "~1.1.2"
 
 
-ext@^1.1.2:
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/ext/-/ext-1.4.0.tgz#89ae7a07158f79d35517882904324077e4379244"
-  integrity sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==
-  dependencies:
-    type "^2.0.0"
-
 extend-shallow@^2.0.1:
 extend-shallow@^2.0.1:
   version "2.0.1"
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f"
   resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f"
@@ -9117,12 +9100,12 @@ markdown-it-blockdiag@^1.1.1:
     url-join "^4.0.0"
     url-join "^4.0.0"
     utf8-bytes "0.0.1"
     utf8-bytes "0.0.1"
 
 
-markdown-it-drawio-viewer@^1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/markdown-it-drawio-viewer/-/markdown-it-drawio-viewer-1.2.0.tgz#d47648c039f12e4c5ca706ed4d0f5dc19400c9a2"
-  integrity sha512-Hu9jxqKLVfFhk2T8J4ayaVbuoW2RSugRrXIsREMW7MMWFDciBgs9C8ADKaTav7JITY5fp7q6KJU7pqP/5dMRnA==
+markdown-it-drawio-viewer@^1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/markdown-it-drawio-viewer/-/markdown-it-drawio-viewer-1.3.0.tgz#bd70b5df7655080afbbe83a2d3bc9ac10f4e433e"
+  integrity sha512-aGm1sa9kWsuSDXwRMMSma6c026GRqcsKyFldO7hRv3vywE3SSDWFXUWDJ6j7kU5nXbQTd1LtcBokCyfn6JyunQ==
   dependencies:
   dependencies:
-    "@kaishuu0123/markdown-it-fence" "^0.2.0"
+    "@kaishuu0123/markdown-it-fence" "^1.0.0"
     xmldoc "^1.1.2"
     xmldoc "^1.1.2"
 
 
 markdown-it-emoji@^1.4.0:
 markdown-it-emoji@^1.4.0:
@@ -9413,7 +9396,7 @@ mime-types@^2.1.3, mime-types@~2.1.18:
   dependencies:
   dependencies:
     mime-db "~1.33.0"
     mime-db "~1.33.0"
 
 
-mime-types@~2.1.11, mime-types@~2.1.15, mime-types@~2.1.16, mime-types@~2.1.17:
+mime-types@~2.1.15, mime-types@~2.1.16, mime-types@~2.1.17:
   version "2.1.17"
   version "2.1.17"
   resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a"
   resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a"
   dependencies:
   dependencies:
@@ -9792,11 +9775,6 @@ neo-async@^2.6.1:
   resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c"
   resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c"
   integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==
   integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==
 
 
-next-tick@~1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c"
-  integrity sha1-yobR/ogoFpsBICCOPchCS524NCw=
-
 nice-try@^1.0.4:
 nice-try@^1.0.4:
   version "1.0.4"
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.4.tgz#d93962f6c52f2c1558c0fbda6d512819f1efe1c4"
   resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.4.tgz#d93962f6c52f2c1558c0fbda6d512819f1efe1c4"
@@ -12223,6 +12201,11 @@ realpath-native@^1.1.0:
   dependencies:
   dependencies:
     util.promisify "^1.0.0"
     util.promisify "^1.0.0"
 
 
+reconnecting-websocket@^4.4.0:
+  version "4.4.0"
+  resolved "https://registry.yarnpkg.com/reconnecting-websocket/-/reconnecting-websocket-4.4.0.tgz#3b0e5b96ef119e78a03135865b8bb0af1b948783"
+  integrity sha512-D2E33ceRPga0NvTDhJmphEgJ7FUYF0v4lr1ki0csq06OdlxKfugGzN0dSkxM/NfqCxYELK4KcaTOUOjTV6Dcng==
+
 redent@^1.0.0:
 redent@^1.0.0:
   version "1.0.0"
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde"
   resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde"
@@ -13315,41 +13298,43 @@ socket.io-adapter@~1.1.0:
   version "1.1.1"
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz#2a805e8a14d6372124dd9159ad4502f8cb07f06b"
   resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz#2a805e8a14d6372124dd9159ad4502f8cb07f06b"
 
 
-socket.io-client@2.0.4, socket.io-client@^2.0.3:
-  version "2.0.4"
-  resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-2.0.4.tgz#0918a552406dc5e540b380dcd97afc4a64332f8e"
+socket.io-client@2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-2.1.1.tgz#dcb38103436ab4578ddb026638ae2f21b623671f"
   dependencies:
   dependencies:
     backo2 "1.0.2"
     backo2 "1.0.2"
     base64-arraybuffer "0.1.5"
     base64-arraybuffer "0.1.5"
     component-bind "1.0.0"
     component-bind "1.0.0"
     component-emitter "1.2.1"
     component-emitter "1.2.1"
-    debug "~2.6.4"
-    engine.io-client "~3.1.0"
+    debug "~3.1.0"
+    engine.io-client "~3.2.0"
+    has-binary2 "~1.0.2"
     has-cors "1.1.0"
     has-cors "1.1.0"
     indexof "0.0.1"
     indexof "0.0.1"
     object-component "0.0.3"
     object-component "0.0.3"
     parseqs "0.0.5"
     parseqs "0.0.5"
     parseuri "0.0.5"
     parseuri "0.0.5"
-    socket.io-parser "~3.1.1"
+    socket.io-parser "~3.2.0"
     to-array "0.1.4"
     to-array "0.1.4"
 
 
-socket.io-client@2.1.1:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-2.1.1.tgz#dcb38103436ab4578ddb026638ae2f21b623671f"
+socket.io-client@2.3.0, socket.io-client@^2.3.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-2.3.0.tgz#14d5ba2e00b9bcd145ae443ab96b3f86cbcc1bb4"
+  integrity sha512-cEQQf24gET3rfhxZ2jJ5xzAOo/xhZwK+mOqtGRg5IowZsMgwvHwnf/mCRapAAkadhM26y+iydgwsXGObBB5ZdA==
   dependencies:
   dependencies:
     backo2 "1.0.2"
     backo2 "1.0.2"
     base64-arraybuffer "0.1.5"
     base64-arraybuffer "0.1.5"
     component-bind "1.0.0"
     component-bind "1.0.0"
     component-emitter "1.2.1"
     component-emitter "1.2.1"
-    debug "~3.1.0"
-    engine.io-client "~3.2.0"
+    debug "~4.1.0"
+    engine.io-client "~3.4.0"
     has-binary2 "~1.0.2"
     has-binary2 "~1.0.2"
     has-cors "1.1.0"
     has-cors "1.1.0"
     indexof "0.0.1"
     indexof "0.0.1"
     object-component "0.0.3"
     object-component "0.0.3"
     parseqs "0.0.5"
     parseqs "0.0.5"
     parseuri "0.0.5"
     parseuri "0.0.5"
-    socket.io-parser "~3.2.0"
+    socket.io-parser "~3.3.0"
     to-array "0.1.4"
     to-array "0.1.4"
 
 
 socket.io-client@^2.0.4:
 socket.io-client@^2.0.4:
@@ -13371,15 +13356,6 @@ socket.io-client@^2.0.4:
     socket.io-parser "~3.3.0"
     socket.io-parser "~3.3.0"
     to-array "0.1.4"
     to-array "0.1.4"
 
 
-socket.io-parser@~3.1.1:
-  version "3.1.2"
-  resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.1.2.tgz#dbc2282151fc4faebbe40aeedc0772eba619f7f2"
-  dependencies:
-    component-emitter "1.2.1"
-    debug "~2.6.4"
-    has-binary2 "~1.0.2"
-    isarray "2.0.1"
-
 socket.io-parser@~3.2.0:
 socket.io-parser@~3.2.0:
   version "3.2.0"
   version "3.2.0"
   resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.2.0.tgz#e7c6228b6aa1f814e6148aea325b51aa9499e077"
   resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.2.0.tgz#e7c6228b6aa1f814e6148aea325b51aa9499e077"
@@ -13396,6 +13372,15 @@ socket.io-parser@~3.3.0:
     debug "~3.1.0"
     debug "~3.1.0"
     isarray "2.0.1"
     isarray "2.0.1"
 
 
+socket.io-parser@~3.4.0:
+  version "3.4.1"
+  resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.4.1.tgz#b06af838302975837eab2dc980037da24054d64a"
+  integrity sha512-11hMgzL+WCLWf1uFtHSNvliI++tcRUWdoeYuwIl+Axvwy9z2gQM+7nJyN3STj1tLj5JyIUH8/gpDGxzAlDdi0A==
+  dependencies:
+    component-emitter "1.2.1"
+    debug "~4.1.0"
+    isarray "2.0.1"
+
 socket.io@2.1.1:
 socket.io@2.1.1:
   version "2.1.1"
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-2.1.1.tgz#a069c5feabee3e6b214a75b40ce0652e1cfb9980"
   resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-2.1.1.tgz#a069c5feabee3e6b214a75b40ce0652e1cfb9980"
@@ -13407,15 +13392,17 @@ socket.io@2.1.1:
     socket.io-client "2.1.1"
     socket.io-client "2.1.1"
     socket.io-parser "~3.2.0"
     socket.io-parser "~3.2.0"
 
 
-socket.io@^2.0.3:
-  version "2.0.4"
-  resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-2.0.4.tgz#c1a4590ceff87ecf13c72652f046f716b29e6014"
+socket.io@^2.3.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-2.3.0.tgz#cd762ed6a4faeca59bc1f3e243c0969311eb73fb"
+  integrity sha512-2A892lrj0GcgR/9Qk81EaY2gYhCBxurV0PfmmESO6p27QPrUK1J3zdns+5QPqvUYK2q657nSj0guoIil9+7eFg==
   dependencies:
   dependencies:
-    debug "~2.6.6"
-    engine.io "~3.1.0"
+    debug "~4.1.0"
+    engine.io "~3.4.0"
+    has-binary2 "~1.0.2"
     socket.io-adapter "~1.1.0"
     socket.io-adapter "~1.1.0"
-    socket.io-client "2.0.4"
-    socket.io-parser "~3.1.1"
+    socket.io-client "2.3.0"
+    socket.io-parser "~3.4.0"
 
 
 sort-keys@^1.0.0:
 sort-keys@^1.0.0:
   version "1.1.2"
   version "1.1.2"
@@ -14593,16 +14580,6 @@ type-is@~1.6.16:
     media-typer "0.3.0"
     media-typer "0.3.0"
     mime-types "~2.1.18"
     mime-types "~2.1.18"
 
 
-type@^1.0.1:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0"
-  integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==
-
-type@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/type/-/type-2.0.0.tgz#5f16ff6ef2eb44f260494dae271033b29c09a9c3"
-  integrity sha512-KBt58xCHry4Cejnc2ISQAF7QY+ORngsWfxezO68+12hKV6lQY8P/psIkcbjeHWn7MqcgciWJyCCevFMJdIXpow==
-
 typed-styles@^0.0.7:
 typed-styles@^0.0.7:
   version "0.0.7"
   version "0.0.7"
   resolved "https://registry.yarnpkg.com/typed-styles/-/typed-styles-0.0.7.tgz#93392a008794c4595119ff62dde6809dbc40a3d9"
   resolved "https://registry.yarnpkg.com/typed-styles/-/typed-styles-0.0.7.tgz#93392a008794c4595119ff62dde6809dbc40a3d9"
@@ -14973,10 +14950,6 @@ uuid@^3.3.2:
   resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
   resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
   integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
   integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
 
 
-uws@~0.14.4:
-  version "0.14.5"
-  resolved "https://registry.yarnpkg.com/uws/-/uws-0.14.5.tgz#67aaf33c46b2a587a5f6666d00f7691328f149dc"
-
 v8-compile-cache@2.0.3:
 v8-compile-cache@2.0.3:
   version "2.0.3"
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz#00f7494d2ae2b688cfe2899df6ed2c54bef91dbe"
   resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz#00f7494d2ae2b688cfe2899df6ed2c54bef91dbe"
@@ -15250,17 +15223,6 @@ webpack@^4.39.3:
     watchpack "^1.6.0"
     watchpack "^1.6.0"
     webpack-sources "^1.4.1"
     webpack-sources "^1.4.1"
 
 
-websocket@^1.0.31:
-  version "1.0.31"
-  resolved "https://registry.yarnpkg.com/websocket/-/websocket-1.0.31.tgz#e5d0f16c3340ed87670e489ecae6144c79358730"
-  integrity sha512-VAouplvGKPiKFDTeCCO65vYHsyay8DqoBSlzIO3fayrfOgU94lQN5a1uWVnFrMLceTJw/+fQXR5PGbUVRaHshQ==
-  dependencies:
-    debug "^2.2.0"
-    es5-ext "^0.10.50"
-    nan "^2.14.0"
-    typedarray-to-buffer "^3.1.5"
-    yaeti "^0.0.6"
-
 whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.5:
 whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.5:
   version "1.0.5"
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0"
   resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0"
@@ -15435,6 +15397,11 @@ ws@^7.0.0:
   resolved "https://registry.yarnpkg.com/ws/-/ws-7.2.1.tgz#03ed52423cd744084b2cf42ed197c8b65a936b8e"
   resolved "https://registry.yarnpkg.com/ws/-/ws-7.2.1.tgz#03ed52423cd744084b2cf42ed197c8b65a936b8e"
   integrity sha512-sucePNSafamSKoOqoNfBd8V0StlkzJKL2ZAhGQinCfNQ+oacw+Pk7lcdAElecBF2VkLNZRiIb5Oi1Q5lVUVt2A==
   integrity sha512-sucePNSafamSKoOqoNfBd8V0StlkzJKL2ZAhGQinCfNQ+oacw+Pk7lcdAElecBF2VkLNZRiIb5Oi1Q5lVUVt2A==
 
 
+ws@^7.1.2, ws@^7.3.1:
+  version "7.3.1"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-7.3.1.tgz#d0547bf67f7ce4f12a72dfe31262c68d7dc551c8"
+  integrity sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==
+
 ws@~3.3.1:
 ws@~3.3.1:
   version "3.3.3"
   version "3.3.3"
   resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.3.tgz#f1cf84fe2d5e901ebce94efaece785f187a228f2"
   resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.3.tgz#f1cf84fe2d5e901ebce94efaece785f187a228f2"
@@ -15581,11 +15548,6 @@ y18n@^3.2.1:
   version "4.0.0"
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"
   resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"
 
 
-yaeti@^0.0.6:
-  version "0.0.6"
-  resolved "https://registry.yarnpkg.com/yaeti/-/yaeti-0.0.6.tgz#f26f484d72684cf42bedfb76970aa1608fbf9577"
-  integrity sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc=
-
 yallist@^2.1.2:
 yallist@^2.1.2:
   version "2.1.2"
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
   resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"